From ea3d20f9ca2126c2642ef89f16f2d61e31d8d64a Mon Sep 17 00:00:00 2001 From: David Fahlander Date: Mon, 9 May 2016 17:36:05 +0200 Subject: [PATCH 1/5] Including unit tests in the checkin into 'releases' branch. This can be handy when unit testing from various browsers by going directly to https://rawgit.com/dfahlander/Dexie.js/v1.4.0-beta.3/test/run-unit-tests.html from a safari browser in browser stack. --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index 7a513676f..394a01af5 100644 --- a/tools/release.sh +++ b/tools/release.sh @@ -90,7 +90,7 @@ printf "Browserstack tests passed.\n" rm -rf dist/*.gz git add -A --no-ignore-removal -f dist/ 2>/dev/null git add -A --no-ignore-removal -f addons/*/dist/ 2>/dev/null - +git add -A --no-ignore-removal -f test/bundle.js 2>/dev/null # Commit all changes (still locally) git commit -am "Build output" 2>/dev/null From eb75db054c4372061c23700d4d4ddc0ff414efd9 Mon Sep 17 00:00:00 2001 From: David Fahlander Date: Tue, 10 May 2016 01:53:09 +0200 Subject: [PATCH 2/5] Code restructuring only. --- src/Dexie.js | 413 +++++++++++++++++++++++++++------------------------ 1 file changed, 215 insertions(+), 198 deletions(-) diff --git a/src/Dexie.js b/src/Dexie.js index eeb798efc..1d4b1faa0 100644 --- a/src/Dexie.js +++ b/src/Dexie.js @@ -54,7 +54,8 @@ import { } from './chaining-functions'; import * as Debug from './debug'; -var maxString = String.fromCharCode(65535), +var DEXIE_VERSION = '{version}', + maxString = String.fromCharCode(65535), // maxKey is an Array if indexedDB implementations supports array keys (not supported by IE,Edge or Safari at the moment) // Otherwise maxKey is maxString. This is handy when needing an open upper border without limit. maxKey = (function(){try {IDBKeyRange.only([[]]);return [[]];}catch(e){return maxString;}})(), @@ -2876,201 +2877,230 @@ function TableSchema(name, primKey, indexes, instanceTemplate) { this.idxByName = arrayToObject(indexes, index => [index.name, index]); } -// -// Static delete() method. -// -Dexie.delete = function (databaseName) { - var db = new Dexie(databaseName), - promise = db.delete(); - promise.onblocked = function (fn) { - db.on("blocked", fn); - return this; - }; - return promise; -}; +// Used in when defining dependencies later... +// (If IndexedDBShim is loaded, prefer it before standard indexedDB) +var idbshim = _global.idbModules && _global.idbModules.shimIndexedDB ? _global.idbModules : {}; -// -// Static exists() method. -// -Dexie.exists = function(name) { - return new Dexie(name).open().then(db=>{ - db.close(); - return true; - }).catch(Dexie.NoSuchDatabaseError, () => false); -}; +function safariMultiStoreFix(storeNames) { + return storeNames.length === 1 ? storeNames[0] : storeNames; +} + +function getNativeGetDatabaseNamesFn(indexedDB) { + var fn = indexedDB && (indexedDB.getDatabaseNames || indexedDB.webkitGetDatabaseNames); + return fn && fn.bind(indexedDB); +} + +// Export Error classes +props(Dexie, fullNameExceptions); // Dexie.XXXError = class XXXError {...}; // -// Static method for retrieving a list of all existing databases at current host. -// -Dexie.getDatabaseNames = function (cb) { - return new Promise(function (resolve, reject) { - var getDatabaseNames = getNativeGetDatabaseNamesFn(indexedDB); - if (getDatabaseNames) { // In case getDatabaseNames() becomes standard, let's prepare to support it: - var req = getDatabaseNames(); - req.onsuccess = function (event) { - resolve(slice(event.target.result, 0)); // Converst DOMStringList to Array - }; - req.onerror = eventRejectHandler(reject); - } else { - globalDatabaseList(function (val) { - resolve(val); - return false; - }); +// Static methods and properties +// +props(Dexie, { + + // + // Static delete() method. + // + delete: function (databaseName) { + var db = new Dexie(databaseName), + promise = db.delete(); + promise.onblocked = function (fn) { + db.on("blocked", fn); + return this; + }; + return promise; + }, + + // + // Static exists() method. + // + exists: function(name) { + return new Dexie(name).open().then(db=>{ + db.close(); + return true; + }).catch(Dexie.NoSuchDatabaseError, () => false); + }, + + // + // Static method for retrieving a list of all existing databases at current host. + // + getDatabaseNames: function (cb) { + return new Promise(function (resolve, reject) { + var getDatabaseNames = getNativeGetDatabaseNamesFn(indexedDB); + if (getDatabaseNames) { // In case getDatabaseNames() becomes standard, let's prepare to support it: + var req = getDatabaseNames(); + req.onsuccess = function (event) { + resolve(slice(event.target.result, 0)); // Converst DOMStringList to Array + }; + req.onerror = eventRejectHandler(reject); + } else { + globalDatabaseList(function (val) { + resolve(val); + return false; + }); + } + }).then(cb); + }, + + defineClass: function (structure) { + /// + /// Create a javascript constructor based on given template for which properties to expect in the class. + /// Any property that is a constructor function will act as a type. So {name: String} will be equal to {name: new String()}. + /// + /// Helps IDE code completion by knowing the members that objects contain and not just the indexes. Also + /// know what type each member has. Example: {name: String, emailAddresses: [String], properties: {shoeSize: Number}} + + // Default constructor able to copy given properties into this object. + function Class(properties) { + /// Properties to initialize object with. + /// + properties ? extend(this, properties) : fake && applyStructure(this, structure); } - }).then(cb); -}; - -Dexie.defineClass = function (structure) { - /// - /// Create a javascript constructor based on given template for which properties to expect in the class. - /// Any property that is a constructor function will act as a type. So {name: String} will be equal to {name: new String()}. - /// - /// Helps IDE code completion by knowing the members that objects contain and not just the indexes. Also - /// know what type each member has. Example: {name: String, emailAddresses: [String], properties: {shoeSize: Number}} - - // Default constructor able to copy given properties into this object. - function Class(properties) { - /// Properties to initialize object with. - /// - properties ? extend(this, properties) : fake && applyStructure(this, structure); - } - return Class; -}; - -Dexie.applyStructure = applyStructure; - -Dexie.ignoreTransaction = function (scopeFunc) { - // In case caller is within a transaction but needs to create a separate transaction. - // Example of usage: - // - // Let's say we have a logger function in our app. Other application-logic should be unaware of the - // logger function and not need to include the 'logentries' table in all transaction it performs. - // The logging should always be done in a separate transaction and not be dependant on the current - // running transaction context. Then you could use Dexie.ignoreTransaction() to run code that starts a new transaction. - // - // Dexie.ignoreTransaction(function() { - // db.logentries.add(newLogEntry); - // }); - // - // Unless using Dexie.ignoreTransaction(), the above example would try to reuse the current transaction - // in current Promise-scope. - // - // An alternative to Dexie.ignoreTransaction() would be setImmediate() or setTimeout(). The reason we still provide an - // API for this because - // 1) The intention of writing the statement could be unclear if using setImmediate() or setTimeout(). - // 2) setTimeout() would wait unnescessary until firing. This is however not the case with setImmediate(). - // 3) setImmediate() is not supported in the ES standard. - // 4) You might want to keep other PSD state that was set in a parent PSD, such as PSD.letThrough. - return PSD.trans ? - usePSD(PSD.transless, scopeFunc) : // Use the closest parent that was non-transactional. - scopeFunc(); // No need to change scope because there is no ongoing transaction. -}; - -Dexie.vip = function (fn) { - // To be used by subscribers to the on('ready') event. - // This will let caller through to access DB even when it is blocked while the db.ready() subscribers are firing. - // This would have worked automatically if we were certain that the Provider was using Dexie.Promise for all asyncronic operations. The promise PSD - // from the provider.connect() call would then be derived all the way to when provider would call localDatabase.applyChanges(). But since - // the provider more likely is using non-promise async APIs or other thenable implementations, we cannot assume that. - // Note that this method is only useful for on('ready') subscribers that is returning a Promise from the event. If not using vip() - // the database could deadlock since it wont open until the returned Promise is resolved, and any non-VIPed operation started by - // the caller will not resolve until database is opened. - return newScope(function () { - PSD.letThrough = true; // Make sure we are let through if still blocking db due to onready is firing. - return fn(); - }); -}; + return Class; + }, + + applyStructure: applyStructure, + + ignoreTransaction: function (scopeFunc) { + // In case caller is within a transaction but needs to create a separate transaction. + // Example of usage: + // + // Let's say we have a logger function in our app. Other application-logic should be unaware of the + // logger function and not need to include the 'logentries' table in all transaction it performs. + // The logging should always be done in a separate transaction and not be dependant on the current + // running transaction context. Then you could use Dexie.ignoreTransaction() to run code that starts a new transaction. + // + // Dexie.ignoreTransaction(function() { + // db.logentries.add(newLogEntry); + // }); + // + // Unless using Dexie.ignoreTransaction(), the above example would try to reuse the current transaction + // in current Promise-scope. + // + // An alternative to Dexie.ignoreTransaction() would be setImmediate() or setTimeout(). The reason we still provide an + // API for this because + // 1) The intention of writing the statement could be unclear if using setImmediate() or setTimeout(). + // 2) setTimeout() would wait unnescessary until firing. This is however not the case with setImmediate(). + // 3) setImmediate() is not supported in the ES standard. + // 4) You might want to keep other PSD state that was set in a parent PSD, such as PSD.letThrough. + return PSD.trans ? + usePSD(PSD.transless, scopeFunc) : // Use the closest parent that was non-transactional. + scopeFunc(); // No need to change scope because there is no ongoing transaction. + }, + + vip: function (fn) { + // To be used by subscribers to the on('ready') event. + // This will let caller through to access DB even when it is blocked while the db.ready() subscribers are firing. + // This would have worked automatically if we were certain that the Provider was using Dexie.Promise for all asyncronic operations. The promise PSD + // from the provider.connect() call would then be derived all the way to when provider would call localDatabase.applyChanges(). But since + // the provider more likely is using non-promise async APIs or other thenable implementations, we cannot assume that. + // Note that this method is only useful for on('ready') subscribers that is returning a Promise from the event. If not using vip() + // the database could deadlock since it wont open until the returned Promise is resolved, and any non-VIPed operation started by + // the caller will not resolve until database is opened. + return newScope(function () { + PSD.letThrough = true; // Make sure we are let through if still blocking db due to onready is firing. + return fn(); + }); + }, + + async: function (generatorFn) { + return function () { + try { + var rv = awaitIterator(generatorFn.apply(this, arguments)); + if (!rv || typeof rv.then !== 'function') + return Promise.resolve(rv); + return rv; + } catch (e) { + return rejection (e); + } + }; + }, -Dexie.async = function (generatorFn) { - return function () { + spawn: function (generatorFn, args, thiz) { try { - var rv = awaitIterator(generatorFn.apply(this, arguments)); + var rv = awaitIterator(generatorFn.apply(thiz, args || [])); if (!rv || typeof rv.then !== 'function') return Promise.resolve(rv); return rv; } catch (e) { - return rejection (e); + return rejection(e); } - }; -}; - -Dexie.spawn = function (generatorFn, args, thiz) { - try { - var rv = awaitIterator(generatorFn.apply(thiz, args || [])); - if (!rv || typeof rv.then !== 'function') - return Promise.resolve(rv); - return rv; - } catch (e) { - return rejection(e); - } -}; - -// Dexie.currentTransaction property. Only applicable for transactions entered using the new "transact()" method. -setProp(Dexie, "currentTransaction", { - get: function () { - /// - return PSD.trans || null; - } -}); - -function safariMultiStoreFix(storeNames) { - return storeNames.length === 1 ? storeNames[0] : storeNames; -} - -// Export our Promise implementation since it can be handy as a standalone Promise implementation -Dexie.Promise = Promise; -// Dexie.debug proptery: -// Dexie.debug = false -// Dexie.debug = true -// Dexie.debug = "dexie" - don't hide dexie's stack frames. -setProp(Dexie, "debug", { - get: ()=>Debug.debug, - set: value => { - Debug.setDebug(value, value === 'dexie' ? ()=>true : dexieStackFrameFilter); - } + }, + + // Dexie.currentTransaction property + currentTransaction: { + get: () => PSD.trans || null + }, + + // Export our Promise implementation since it can be handy as a standalone Promise implementation + Promise: Promise, + + // Dexie.debug proptery: + // Dexie.debug = false + // Dexie.debug = true + // Dexie.debug = "dexie" - don't hide dexie's stack frames. + debug: { + get: () => Debug.debug, + set: value => { + Debug.setDebug(value, value === 'dexie' ? ()=>true : dexieStackFrameFilter); + } + }, + + // Export our derive/extend/override methodology + derive: derive, + extend: extend, + props: props, + override: override, + // Export our Events() function - can be handy as a toolkit + Events: Events, + events: Events, // Backward compatible lowercase version. Deprecate. + // Utilities + getByKeyPath: getByKeyPath, + setByKeyPath: setByKeyPath, + delByKeyPath: delByKeyPath, + shallowClone: shallowClone, + deepClone: deepClone, + fakeAutoComplete: fakeAutoComplete, + asap: asap, + maxKey: maxKey, + // Addon registry + addons: [], + // Global DB connection list + connections: connections, + + MultiModifyError: exceptions.Modify, // Backward compatibility 0.9.8. Deprecate. + errnames: errnames, + + // Export other static classes + IndexSpec: IndexSpec, + TableSchema: TableSchema, + + // + // Dependencies + // + // These will automatically work in browsers with indexedDB support, or where an indexedDB polyfill has been included. + // + // In node.js, however, these properties must be set "manually" before instansiating a new Dexie(). + // For node.js, you need to require indexeddb-js or similar and then set these deps. + // + dependencies: { + // Required: + indexedDB: idbshim.shimIndexedDB || _global.indexedDB || _global.mozIndexedDB || _global.webkitIndexedDB || _global.msIndexedDB, + IDBKeyRange: idbshim.IDBKeyRange || _global.IDBKeyRange || _global.webkitIDBKeyRange + }, + + // API Version Number: Type Number, make sure to always set a version number that can be comparable correctly. Example: 0.9, 0.91, 0.92, 1.0, 1.01, 1.1, 1.2, 1.21, etc. + semVer: DEXIE_VERSION, + version: DEXIE_VERSION.split('.') + .map(n => parseInt(n)) + .reduce((p,c,i) => p + (c/Math.pow(10,i*2))), + + // https://github.com/dfahlander/Dexie.js/issues/186 + // typescript compiler tsc in mode ts-->es5 & commonJS, will expect require() to return + // x.default. Workaround: Set Dexie.default = Dexie. + default: Dexie }); -Promise.rejectionMapper = mapError; -// Export our derive/extend/override methodology -Dexie.derive = derive; -Dexie.extend = extend; -Dexie.props = props; -Dexie.override = override; -// Export our Events() function - can be handy as a toolkit -Dexie.Events = Dexie.events = Events; // Backward compatible lowercase version. -// Utilities -Dexie.getByKeyPath = getByKeyPath; -Dexie.setByKeyPath = setByKeyPath; -Dexie.delByKeyPath = delByKeyPath; -Dexie.shallowClone = shallowClone; -Dexie.deepClone = deepClone; -Dexie.addons = []; -Dexie.fakeAutoComplete = fakeAutoComplete; -Dexie.asap = asap; -Dexie.maxKey = maxKey; -Dexie.connections = connections; - -// Export Error classes -extend(Dexie, fullNameExceptions); // Dexie.XXXError = class XXXError {...}; -Dexie.MultiModifyError = Dexie.ModifyError; // Backward compatibility 0.9.8 -Dexie.errnames = errnames; -// Export other static classes -Dexie.IndexSpec = IndexSpec; -Dexie.TableSchema = TableSchema; - -// -// Dependencies -// -// These will automatically work in browsers with indexedDB support, or where an indexedDB polyfill has been included. -// -// In node.js, however, these properties must be set "manually" before instansiating a new Dexie(). For node.js, you need to require indexeddb-js or similar and then set these deps. -// -var idbshim = _global.idbModules && _global.idbModules.shimIndexedDB ? _global.idbModules : {}; -Dexie.dependencies = { - // Required: - indexedDB: idbshim.shimIndexedDB || _global.indexedDB || _global.mozIndexedDB || _global.webkitIndexedDB || _global.msIndexedDB, - IDBKeyRange: idbshim.IDBKeyRange || _global.IDBKeyRange || _global.webkitIDBKeyRange -}; tryCatch(()=>{ // Optional dependencies // localStorage @@ -3078,24 +3108,11 @@ tryCatch(()=>{ ((typeof chrome !== "undefined" && chrome !== null ? chrome.storage : void 0) != null ? null : _global.localStorage); }); -// API Version Number: Type Number, make sure to always set a version number that can be comparable correctly. Example: 0.9, 0.91, 0.92, 1.0, 1.01, 1.1, 1.2, 1.21, etc. -Dexie.semVer = "{version}"; -Dexie.version = Dexie.semVer.split('.') - .map(n => parseInt(n)) - .reduce((p,c,i) => p + (c/Math.pow(10,i*2))); - -function getNativeGetDatabaseNamesFn(indexedDB) { - var fn = indexedDB && (indexedDB.getDatabaseNames || indexedDB.webkitGetDatabaseNames); - return fn && fn.bind(indexedDB); -} +// Map DOMErrors and DOMExceptions to corresponding Dexie errors. May change in Dexie v2.0. +Promise.rejectionMapper = mapError; // Fool IDE to improve autocomplete. Tested with Visual Studio 2013 and 2015. doFakeAutoComplete(function() { Dexie.fakeAutoComplete = fakeAutoComplete = doFakeAutoComplete; Dexie.fake = fake = true; }); - -// https://github.com/dfahlander/Dexie.js/issues/186 -// typescript compiler tsc in mode ts-->es5 & commonJS, will expect require() to return -// x.default. Workaround: Set Dexie.default = Dexie. -Dexie.default = Dexie; From 0235203cdc5e14860e041ad1b106349d9ddecf1c Mon Sep 17 00:00:00 2001 From: David Fahlander Date: Tue, 10 May 2016 14:13:18 +0200 Subject: [PATCH 3/5] Reproducing Issue #248 --- test/tests-misc.js | 67 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/test/tests-misc.js b/test/tests-misc.js index f65693a2a..f5752014e 100644 --- a/test/tests-misc.js +++ b/test/tests-misc.js @@ -1,6 +1,6 @@ import Dexie from 'dexie'; import {module, stop, start, asyncTest, equal, ok} from 'QUnit'; -import {resetDatabase} from './dexie-unittest-utils'; +import {resetDatabase, spawnedTest} from './dexie-unittest-utils'; const async = Dexie.async; @@ -8,6 +8,7 @@ var db = new Dexie("TestIssuesDB"); db.version(1).stores({ users: "id,first,last,&username,*&email,*pets", keyless: ",name", + foo: "id" // If required for your test, add more tables here }); @@ -56,3 +57,67 @@ asyncTest("#102 Passing an empty array to anyOf throws exception", async(functio start(); } })); + +spawnedTest("#248 'modifications' object in 'updating' hook can be bizarre", function*() { + var numCreating = 0, + numUpdating = 0; + function CustomDate (realDate) { + this._year = new Date(realDate).getFullYear(); + this._month = new Date(realDate).getMonth(); + this._day = new Date(realDate).getDate(); + this._millisec = new Date(realDate).getTime(); + //... + } + + function creatingHook (primKey, obj) { + ++numCreating; + var date = obj.date; + if (date && date instanceof CustomDate) { + obj.date = new Date(date._year, date._month, date._day); + } + } + function updatingHook (modifications, primKey, obj) { + ++numUpdating; + var date = modifications.date; + if (date && date instanceof CustomDate) { + return {date: new Date(date._year, date._month, date._day)}; + } + } + function readingHook (obj) { + if (obj.date && obj.date instanceof Date) { + obj.date = new CustomDate(obj.date); + } + return obj; + } + + db.foo.hook('creating', creatingHook); + db.foo.hook('reading', readingHook); + db.foo.hook('updating', updatingHook); + var testDate = new CustomDate(new Date(2016, 5, 11)); + equal(testDate._year, 2016, "CustomDate has year 2016"); + equal(testDate._month, 5, "CustomDate has month 5"); + equal(testDate._day, 11, "CustomDate has day 11"); + var testDate2 = new CustomDate(new Date(2016, 5, 12)); + try { + db.foo.add ({id: 1, date: testDate}); + + var retrieved = yield db.foo.get(1); + + ok(retrieved.date instanceof CustomDate, "Got a CustomDate object when retrieving object"); + equal (retrieved.date._day, 11, "The CustomDate is on day 11"); + db.foo.put ({id: 1, date: testDate2}); + + retrieved = yield db.foo.get(1); + + ok(retrieved.date.constructor === CustomDate, "Got a CustomDate object when retrieving object"); + equal (retrieved.date._day, 12, "The CustomDate is now on day 12"); + + // Check that hooks has been called expected number of times + equal(numCreating, 1, "creating hook called once"); + equal(numUpdating, 1, "updating hook called once"); + } finally { + db.foo.hook('creating').unsubscribe(creatingHook); + db.foo.hook('reading').unsubscribe(readingHook); + db.foo.hook('updating').unsubscribe(updatingHook); + } +}); From 8868e5bb7ed3f6583f062e4280f5b3aa4abeba8c Mon Sep 17 00:00:00 2001 From: David Fahlander Date: Tue, 10 May 2016 14:21:14 +0200 Subject: [PATCH 4/5] Corrected getObjectDiff() so that recursive diffs only occur when objects are of exact same class. Otherwise consider it as a change on first level property. Closes #248 --- src/utils.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/utils.js b/src/utils.js index 4e93de448..c9df6ebce 100644 --- a/src/utils.js +++ b/src/utils.js @@ -217,21 +217,26 @@ export function getObjectDiff(a, b, rv, prfx) { // Compares objects a and b and produces a diff object. rv = rv || {}; prfx = prfx || ''; - for (var prop in a) if (hasOwn(a, prop)) { + keys(a).forEach(prop => { if (!hasOwn(b, prop)) rv[prfx+prop] = undefined; // Property removed else { var ap = a[prop], bp = b[prop]; - if (typeof ap === 'object' && typeof bp === 'object') - getObjectDiff(ap, bp, rv, prfx + prop + "."); + if (typeof ap === 'object' && typeof bp === 'object' && + ap && bp && + ap.constructor === bp.constructor) + // Same type of object but its properties may have changed + getObjectDiff (ap, bp, rv, prfx + prop + "."); else if (ap !== bp) rv[prfx + prop] = b[prop];// Primitive value changed } - } - for (prop in b) if (hasOwn(b, prop) && !hasOwn(a, prop)) { - rv[prfx+prop] = b[prop]; // Property added - } + }); + keys(b).forEach(prop => { + if (!hasOwn(a, prop)) { + rv[prfx+prop] = b[prop]; // Property added + } + }); return rv; } From 9a935c334dae62e8d2b28a7ee52a11b4cfbc17f1 Mon Sep 17 00:00:00 2001 From: David Fahlander Date: Tue, 10 May 2016 14:22:07 +0200 Subject: [PATCH 5/5] Exporting getObjectDiff() function on Dexie object. --- src/Dexie.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Dexie.js b/src/Dexie.js index 1d4b1faa0..ae1dbe0b3 100644 --- a/src/Dexie.js +++ b/src/Dexie.js @@ -3060,7 +3060,7 @@ props(Dexie, { delByKeyPath: delByKeyPath, shallowClone: shallowClone, deepClone: deepClone, - fakeAutoComplete: fakeAutoComplete, + getObjectDiff: getObjectDiff, asap: asap, maxKey: maxKey, // Addon registry @@ -3094,6 +3094,7 @@ props(Dexie, { version: DEXIE_VERSION.split('.') .map(n => parseInt(n)) .reduce((p,c,i) => p + (c/Math.pow(10,i*2))), + fakeAutoComplete: fakeAutoComplete, // https://github.com/dfahlander/Dexie.js/issues/186 // typescript compiler tsc in mode ts-->es5 & commonJS, will expect require() to return