diff --git a/addon-test-support/helpers/indexed-db.js b/addon-test-support/helpers/indexed-db.js index 32154ca..bdf2f6c 100644 --- a/addon-test-support/helpers/indexed-db.js +++ b/addon-test-support/helpers/indexed-db.js @@ -37,6 +37,7 @@ export function setupIndexedDb(hooks) { await wait(); await run(() => indexedDb.waitForQueueTask.perform()); await run(() => indexedDb.waitForQueueTask.perform()); + await indexedDb.dropDatabaseTask.perform(); }); } diff --git a/addon/index.js b/addon/index.js index 00fb3a8..d8d8f85 100644 --- a/addon/index.js +++ b/addon/index.js @@ -41,15 +41,15 @@ import IndexedDbConfiguration from './services/indexed-db-configuration'; import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; - export default Ember.Route.extend({ - indexedDb: service(), + export default class ApplicationRoute extends Route { + @service indexedDb; beforeModel() { this._super(...arguments); return this.indexedDb.setupTask.perform(); } -}); +} ``` This returns a promise that is ready once the database is setup. Note that this will reject if IndexedDB is not available - you need to handle this case accordingly. @@ -69,8 +69,8 @@ import IndexedDbConfiguration from './services/indexed-db-configuration'; ```js import IndexedDbConfigurationService from 'ember-indexeddb/services/indexed-db-configuration'; - export default IndexedDbConfigurationService.extend({ - currentVersion: 1, + export default class ExtendedIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; version1: { stores: { @@ -78,7 +78,7 @@ import IndexedDbConfiguration from './services/indexed-db-configuration'; 'model-two': '&id,*status,*modelOne,[status+modelOne]' } } -}); +} ``` Please consult the Dexie Documentation on [details about configuring your database](https://github.com/dfahlander/Dexie.js/wiki/Version.stores()). @@ -88,6 +88,8 @@ import IndexedDbConfiguration from './services/indexed-db-configuration'; You can add as many version as you want, and Dexie will handle the upgrading for you. Note that you cannot downgrade a version. There needs to be a `versionX` property per version, starting at 1. So if you have a `currentVersion` of 3, you need to have `version1`, `version2` and `version3` properties. + You do not need to keep old versionX configurations unless they contain an upgrade. + All of these migrations are automatically run when running `this.indexedDb.setup();`. In addition to the store configuration, you also need to define a `mapTable`. @@ -96,25 +98,23 @@ import IndexedDbConfiguration from './services/indexed-db-configuration'; For the above example, this should look something like this: ```js - mapTable: computed(function() { - return { - 'model-one': (item) => { - return { - id: this._toString(item.id), - json: this._cleanupObject(item), - isNew: this._toZeroOne(item.isNew) - }; - }, - 'model-two': (item) => { - return { - id: this._toString(item.id), - json: this._cleanupObject(item), - modelOne: this._toString(item.relationships.modelOne?.data?.id), - status: item.attribtues.status - }; - } - }; -}) + mapTable: { + 'model-one': (item) => { + return { + id: this._toString(item.id), + json: this._cleanupObject(item), + isNew: this._toZeroOne(item.isNew) + }; + }, + 'model-two': (item) => { + return { + id: this._toString(item.id), + json: this._cleanupObject(item), + modelOne: this._toString(item.relationships.modelOne?.data?.id), + status: item.attribtues.status + }; + } +} ``` Things to note here: @@ -173,7 +173,7 @@ import IndexedDbConfiguration from './services/indexed-db-configuration'; ```js import IndexedDbAdapter from 'ember-indexeddb/adapters/indexed-db'; - export default IndexedDbAdapter.extend(); + export default class ApplicationAdapter extends IndexedDbAdapter {} ``` The next step is to setup your database for your Ember Data models. @@ -185,18 +185,16 @@ import IndexedDbConfiguration from './services/indexed-db-configuration'; import IndexedDbConfigurationService from 'ember-indexeddb/services/indexed-db-configuration'; import { computed, get } from '@ember/object'; - export default IndexedDbConfigurationService.extend({ - currentVersion: 1, + export default class ExtendedIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; - version1: computed(function() { - return { - stores: { - 'model-a': '&id', - 'model-b': '&id' - } - }; - }) -}); + version1 = { + stores: { + 'model-a': '&id', + 'model-b': '&id' + } + }; +} ``` Now, you can simply use the normal ember-data store with functions like `store.query('item', { isNew: true })`. diff --git a/addon/services/indexed-db-configuration.js b/addon/services/indexed-db-configuration.js index 4f2a0cf..f040e10 100644 --- a/addon/services/indexed-db-configuration.js +++ b/addon/services/indexed-db-configuration.js @@ -26,21 +26,26 @@ export default class IndexedDbConfigurationService extends Service { * * upgrade is a function that gets a transaction as parameter, which can be used to run database migrations. * See https://github.com/dfahlander/Dexie.js/wiki/Version.upgrade() for detailed options/examples. + * + * Note that in newer versions of Dexie, you do not need to keep old version definitions anymnore, unless they contain upgrade instructions. + * Instead, each version has to contain the full, current schema (not just the updates to the last version). * * An example would be: * * ```js - * version1: { + * // You can delete this safely when adding version2, as it does not contain an upgrade + * version1 = { * stores: { * 'task': '&id*,isRead', * 'task-item': '&id' * } * }, * - * version2: { - * stores: { + * version2 = { + * stores: { + * 'task': '&id*,isRead', * 'task-item': '&id,*isNew' - * }, + * } * upgrade: (transaction) => { * transaction['task-item'].each((taskItem, cursor) => { taskItem.isNew = 0; @@ -150,7 +155,7 @@ export default class IndexedDbConfigurationService extends Service { * @public */ setupDatabase(db) { - let currentVersion = this.currentVersion; + let { currentVersion } = this; assert( 'You need to override services/indexed-db-configuration.js and provide at least one version.', @@ -159,7 +164,13 @@ export default class IndexedDbConfigurationService extends Service { for (let v = 1; v <= currentVersion; v++) { let versionName = `version${v}`; - let { stores, upgrade } = this[versionName]; + let versionDefinition = this[versionName]; + + if (!versionDefinition) { + continue; + } + + let { stores, upgrade } = versionDefinition; if (stores && upgrade) { db.version(v).stores(stores).upgrade(upgrade); diff --git a/addon/services/indexed-db.js b/addon/services/indexed-db.js index 47a59d9..96cc479 100644 --- a/addon/services/indexed-db.js +++ b/addon/services/indexed-db.js @@ -43,6 +43,17 @@ export default class IndexedDbService extends Service { */ databaseName = 'ember-indexeddb'; + /** + * If set to true, it will output which indecies are used for queries. + * This can be used to debug your indecies. + * + * @property _shouldLogQuery + * @type {Boolean} + * @default false + * @private + */ + _shouldLogQuery = false; + /** * This is an object with an array per model type. * It holds all the objects per model type that should be bulk saved. @@ -167,6 +178,7 @@ export default class IndexedDbService extends Service { */ queryRecord(type, query) { let queryPromise = this._buildQuery(type, query); + let promise = new Promise( (resolve, reject) => queryPromise.first().then(resolve, reject), 'indexedDb/queryRecord' @@ -543,6 +555,13 @@ export default class IndexedDbService extends Service { return Promise.all(promises, 'indexedDb/_bulkSave'); } + _logQuery(str, query) { + if (this._shouldLogQuery) { + // eslint-disable-next-line + console.log(`[QUERY]: ${str}`, query); + } + } + /** * Build a query for Dexie. * @@ -564,29 +583,30 @@ export default class IndexedDbService extends Service { _buildQuery(type, query) { let { db, _supportsCompoundIndices: supportsCompoundIndices } = this; - let promise = null; let keys = Object.keys(query); - // Convert boolean queries to 1/0 - for (let i in query) { - if (getTypeOf(query[i]) === 'boolean') { - query[i] = query[i] ? 1 : 0; - } - } + // Order of query params is important! + let { schema } = db[type]; + let { indexes } = schema; - // Only one, then do a simple where + // Only one, try to find a simple index if (keys.length === 1) { let key = keys[0]; - return db[type].where(key).equals(query[key]); - } + let index = indexes.find((index) => { + let { keyPath } = index; + return keyPath === key; + }); - // Order of query params is important! - let { schema } = db[type]; - let { indexes } = schema; + if (index) { + this._logQuery(`Using index "${key}"`, query); + let value = normalizeValue(query[key]); + return db[type].where(key).equals(value); + } + } // try to find a fitting multi index // only if the client supports compound indices! - if (supportsCompoundIndices) { + if (keys.length > 1 && supportsCompoundIndices) { let multiIndex = indexes.find((index) => { let { keyPath } = index; @@ -609,23 +629,50 @@ export default class IndexedDbService extends Service { let compareValues = array(); keyPath.forEach((key) => { - compareValues.push(query[key]); + let value = normalizeValue(query[key]); + compareValues.push(value); }); + this._logQuery(`Using compound index "${keyPath}"`, query); return db[type].where(keyName).equals(compareValues); } } // Else, filter manually - Object.keys(query).forEach((i) => { - if (!promise) { - promise = db[type].where(i).equals(query[i]); - } else { - promise = promise.and((item) => item[i] === query[i]); - } + // Try to find at least a single actual index, if possible... + let whereKey = keys.find((key) => { + return indexes.some((index) => { + let { keyPath } = index; + return keyPath === key; + }); }); - return promise; + let whereKeyValue = whereKey ? normalizeValue(query[whereKey]) : null; + let vanillaFilterKeys = keys.filter((key) => !whereKey || key !== whereKey); + + let collection = whereKey + ? db[type].where(whereKey).equals(whereKeyValue) + : db[type]; + + if (whereKey) { + this._logQuery( + `Using index "${whereKey}" and vanilla filtering for ${vanillaFilterKeys.join( + ', ' + )}`, + query + ); + } else { + this._logQuery(`Using vanilla filtering`, query); + } + + return collection.filter((item) => { + return vanillaFilterKeys.every((key) => { + return ( + normalizeValue(item.json.attributes[key]) === + normalizeValue(query[key]) + ); + }); + }); } _mapItem(type, item) { @@ -685,3 +732,11 @@ async function closeDb(db) { } return db; } + +function normalizeValue(value) { + if (typeof value === 'boolean') { + return value ? 1 : 0; + } + + return value; +} diff --git a/blueprints/ember-indexeddb/files/__root__/services/indexed-db-configuration.js b/blueprints/ember-indexeddb/files/__root__/services/indexed-db-configuration.js index 42c2941..297d2ee 100644 --- a/blueprints/ember-indexeddb/files/__root__/services/indexed-db-configuration.js +++ b/blueprints/ember-indexeddb/files/__root__/services/indexed-db-configuration.js @@ -15,38 +15,36 @@ import { computed } from '@ember/object'; * A full example configuration after some time of use could look like this: * * ``` - * export default IndexedDbConfigurationService.extend({ - currentVersion: 2, - version1: computed(function() { - return { - stores: { - 'project': '&id', - 'todo': '&id' - } - }; - }), - version2: computed(function() { - return { - stores: { - 'tag': '&id' - } - }; - }) - }); + * export default class ExtendedIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 2; + + version1: { + stores: { + project: '&id', + todo: '&id' + } + }, + + version2: { + stores: { + project: '&id', + todo: '&id,title', + tag: '&id' + } + } + } * ``` * * For more information, please see https://mydea.github.io/ember-indexeddb/docs/modules/Configuring%20your%20database.html */ -export default IndexedDbConfigurationService.extend({ - currentVersion: 1, +export default class ExtendedIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; - version1: computed(function () { - return { - stores: { - // Add your tables here, like this: 'item': '&id' - // When using the ember data adapter, add one entry per model, where the key is your model name - // For example, if you have a model named "my-item", add an entry: `'my-item': '&id' - }, - }; - }), -}); + version1 = { + stores: { + // Add your tables here, like this: 'item': '&id' + // When using the ember data adapter, add one entry per model, where the key is your model name + // For example, if you have a model named "my-item", add an entry: `'my-item': '&id' + }, + }; +} diff --git a/tests/unit/services/indexed-db-test.js b/tests/unit/services/indexed-db-test.js index b813622..39fcb05 100644 --- a/tests/unit/services/indexed-db-test.js +++ b/tests/unit/services/indexed-db-test.js @@ -4,6 +4,8 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { setupIndexedDb } from 'ember-indexeddb/test-support/helpers/indexed-db'; import { run } from '@ember/runloop'; +import IndexedDbService from 'ember-indexeddb/services/indexed-db'; +import IndexedDbConfigurationService from 'ember-indexeddb/services/indexed-db-configuration'; const createMockDb = function () { return { @@ -33,117 +35,935 @@ const createMockDb = function () { module('Unit | Service | indexed-db', function (hooks) { setupTest(hooks); - setupIndexedDb(hooks); - // service.setup() is hard to test as it relies on the global Dexie + module('mocked', function (hooks) { + setupIndexedDb(hooks); - test('add works', function (assert) { - let done = assert.async(); - assert.expect(6); - let db = createMockDb(); + // service.setup() is hard to test as it relies on the global Dexie - let putItems = []; - let promises = []; + test('add works', function (assert) { + let done = assert.async(); + assert.expect(6); + let db = createMockDb(); - db.items = { - bulkPut(items) { - putItems.push(items); - let promise = RSVP.Promise.resolve(items); - promises.push(promise); - return promise; - }, - }; + let putItems = []; + let promises = []; - let service = this.owner.factoryFor('service:indexed-db').create({ - db, - }); + db.items = { + bulkPut(items) { + putItems.push(items); + let promise = RSVP.Promise.resolve(items); + promises.push(promise); + return promise; + }, + }; - let item = { - id: 'TEST-1', - type: 'items', - }; - let promise1 = service.add('items', item); + let service = this.owner.factoryFor('service:indexed-db').create({ + db, + }); - let response1 = [ - { + let item = { id: 'TEST-1', - json: { + type: 'items', + }; + let promise1 = service.add('items', item); + + let response1 = [ + { id: 'TEST-1', - attributes: {}, + json: { + id: 'TEST-1', + attributes: {}, + relationships: {}, + type: 'items', + }, + }, + ]; + assert.deepEqual(putItems, [response1], 'adding one items works'); + + promise1.then((data) => { + assert.deepEqual( + data, + response1, + 'promise 1 resolves with array of data' + ); + }); + + putItems = []; + let item2 = { + id: 'TEST-2', + type: 'items', + }; + let promise2 = service.add('items', [item, item2]); + + let response2 = [ + { + id: 'TEST-1', + json: { + id: 'TEST-1', + attributes: {}, + relationships: {}, + type: 'items', + }, + }, + { + id: 'TEST-2', + json: { + id: 'TEST-2', + attributes: {}, + relationships: {}, + type: 'items', + }, + }, + ]; + assert.deepEqual(putItems, [response2], 'adding two items works'); + + let promiseQueue = service._promiseQueue; + assert.equal( + promiseQueue.length, + 2, + 'there are two items in the promise queue' + ); + + promise2.then((data) => { + assert.deepEqual( + data, + response2, + 'promise 2 resolves with array of data' + ); + }); + + RSVP.all([promise1, promise2]).then(() => { + assert.equal( + get(promiseQueue, 'length'), + 0, + 'promise queue is cleared' + ); + done(); + }); + }); + + test('it does not open multiple db instances on setup', async function (assert) { + let service = this.owner.lookup('service:indexed-db'); + + await run(() => service.setup()); + let { db } = service; + assert.ok(db, 'database is setup'); + + await run(() => service.setup()); + let db2 = service.db; + assert.equal(db2, db, 'db is not overwritten'); + }); + }); + + module('DB migration', function (hooks) { + hooks.afterEach(async function () { + let service = this.owner.lookup('service:indexed-db'); + + // Cleanup + await service.dropDatabaseTask.perform(); + }); + + test('it works with just one version', async function (assert) { + class TestIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; + + version1 = { + stores: { + 'item-1': '&id', + }, + }; + } + + class TestIndexedDbService extends IndexedDbService { + databaseName = 'test-db-migration-1'; + } + + this.owner.register('service:indexed-db', TestIndexedDbService); + this.owner.register( + 'service:indexed-db-configuration', + TestIndexedDbConfigurationService + ); + + let service = this.owner.lookup('service:indexed-db'); + + await service.setupTask.perform(); + + // Try interacting with it + await service.add('item-1', { + id: '1', + type: 'item-1', + attributes: { name: 'hello' }, + }); + + let item = await service.queryRecord('item-1', { name: 'hello' }); + + assert.deepEqual(item, { + id: '1', + json: { + attributes: { + name: 'hello', + }, + id: '1', relationships: {}, - type: 'items', + type: 'item-1', }, - }, - ]; - assert.deepEqual(putItems, [response1], 'adding one items works'); + }); + }); - promise1.then((data) => { - assert.deepEqual( - data, - response1, - 'promise 1 resolves with array of data' + test('it works with an upgrade', async function (assert) { + class TestIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; + + version1 = { + stores: { + 'item-1': '&id', + }, + }; + } + + class TestIndexedDbService extends IndexedDbService { + databaseName = 'test-db-migration-1'; + } + + this.owner.register('service:indexed-db', TestIndexedDbService); + this.owner.register( + 'service:indexed-db-configuration', + TestIndexedDbConfigurationService ); + + let service = this.owner.lookup('service:indexed-db'); + let configService = this.owner.lookup('service:indexed-db-configuration'); + + await service.setupTask.perform(); + + // Try interacting with it + await service.add('item-1', { + id: '1', + type: 'item-1', + attributes: { name: 'hello' }, + }); + + let item = await service.queryRecord('item-1', { name: 'hello' }); + + assert.deepEqual(item, { + id: '1', + json: { + attributes: { + name: 'hello', + }, + id: '1', + relationships: {}, + type: 'item-1', + }, + }); + + // Now run an upgrade... + configService.currentVersion = 2; + configService.version2 = { + stores: { + 'item-1': '&id,isNew', + }, + upgrade: (transaction) => { + transaction['item-1'].each((taskItem, cursor) => { + taskItem.isNew = 1; + cursor.update(taskItem); + }); + }, + }; + + await service.db.close(); + service.db = null; + await service.setupTask.perform(); + + let newItem = await service.queryRecord('item-1', { name: 'hello' }); + + assert.deepEqual(newItem, { + id: '1', + isNew: 1, + json: { + attributes: { + name: 'hello', + }, + id: '1', + relationships: {}, + type: 'item-1', + }, + }); }); - putItems = []; - let item2 = { - id: 'TEST-2', - type: 'items', - }; - let promise2 = service.add('items', [item, item2]); + test('it works without an upgrade', async function (assert) { + class TestIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 5; - let response2 = [ - { - id: 'TEST-1', + version5 = { + stores: { + 'item-1': '&id,name', + }, + }; + + mapTable = { + 'item-1': (item) => { + return { + id: this._toString(item.id), + name: item.attributes.name, + json: this._cleanObject(item), + }; + }, + }; + } + + class TestIndexedDbService extends IndexedDbService { + databaseName = 'test-db-migration-1'; + } + + this.owner.register('service:indexed-db', TestIndexedDbService); + this.owner.register( + 'service:indexed-db-configuration', + TestIndexedDbConfigurationService + ); + + let service = this.owner.lookup('service:indexed-db'); + let configService = this.owner.lookup('service:indexed-db-configuration'); + + await service.setupTask.perform(); + + // Try interacting with it + await service.add('item-1', { + id: '1', + type: 'item-1', + attributes: { name: 'hello' }, + }); + + let item = await service.queryRecord('item-1', { name: 'hello' }); + + assert.deepEqual(item, { + id: '1', + name: 'hello', json: { - id: 'TEST-1', - attributes: {}, + attributes: { + name: 'hello', + }, + id: '1', relationships: {}, - type: 'items', + type: 'item-1', }, - }, - { - id: 'TEST-2', + }); + + // Now run an upgrade... + configService.currentVersion = 6; + delete configService.version5; + configService.version6 = { + stores: { + 'item-1': '&id,name,isNew', + }, + }; + configService.mapTable = { + 'item-1': (item) => { + return { + id: configService._toString(item.id), + name: item.attributes.name, + isNew: configService._toZeroOne(item.attributes.isNew), + json: configService._cleanObject(item), + }; + }, + }; + + await service.db.close(); + service.db = null; + await service.setupTask.perform(); + + let oldItem = await service.queryRecord('item-1', { name: 'hello' }); + + assert.deepEqual(oldItem, { + id: '1', + name: 'hello', json: { - id: 'TEST-2', - attributes: {}, + attributes: { + name: 'hello', + }, + id: '1', + relationships: {}, + type: 'item-1', + }, + }); + + // Add another new one + await service.add('item-1', { + id: '2', + type: 'item-1', + attributes: { name: 'hello2', isNew: true }, + }); + + let newItem = await service.queryRecord('item-1', { isNew: true }); + + assert.deepEqual(newItem, { + id: '2', + name: 'hello2', + isNew: 1, + json: { + attributes: { + name: 'hello2', + isNew: true, + }, + id: '2', + relationships: {}, + type: 'item-1', + }, + }); + }); + + test('it works with just one higher version', async function (assert) { + class TestIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 11; + + version11 = { + stores: { + 'item-1': '&id', + }, + }; + } + + class TestIndexedDbService extends IndexedDbService { + databaseName = 'test-db-migration-1'; + } + + this.owner.register('service:indexed-db', TestIndexedDbService); + this.owner.register( + 'service:indexed-db-configuration', + TestIndexedDbConfigurationService + ); + + let service = this.owner.lookup('service:indexed-db'); + + await service.setupTask.perform(); + + // Try interacting with it + await service.add('item-1', { + id: '1', + type: 'item-1', + attributes: { name: 'hello' }, + }); + + let item = await service.queryRecord('item-1', { name: 'hello' }); + + assert.deepEqual(item, { + id: '1', + json: { + attributes: { + name: 'hello', + }, + id: '1', relationships: {}, - type: 'items', + type: 'item-1', }, - }, - ]; - assert.deepEqual(putItems, [response2], 'adding two items works'); + }); + }); + }); + + module('querying', function (hooks) { + hooks.afterEach(async function () { + let service = this.owner.lookup('service:indexed-db'); + + // Cleanup + await service.dropDatabaseTask.perform(); + }); + + test('it allows to filter for an indexed field', async function (assert) { + class TestIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; + + version1 = { + stores: { + 'item-1': '&id,name', + }, + }; + + mapTable = { + 'item-1': (item) => { + return { + id: this._toString(item.id), + name: item.attributes.name, + json: this._cleanObject(item), + }; + }, + }; + } + + class TestIndexedDbService extends IndexedDbService { + databaseName = 'test-db-migration-1'; + } + + this.owner.register('service:indexed-db', TestIndexedDbService); + this.owner.register( + 'service:indexed-db-configuration', + TestIndexedDbConfigurationService + ); + + let service = this.owner.lookup('service:indexed-db'); + + await service.setupTask.perform(); - let promiseQueue = service._promiseQueue; - assert.equal( - promiseQueue.length, - 2, - 'there are two items in the promise queue' - ); + // Try interacting with it + await service.add('item-1', { + id: '1', + type: 'item-1', + attributes: { name: 'hello', isNew: true }, + }); + await service.add('item-1', { + id: '2', + type: 'item-1', + attributes: { name: 'hello', isNew: false }, + }); + await service.add('item-1', { + id: '3', + type: 'item-1', + attributes: { name: 'hello2', isNew: true }, + }); + + let items = await service.query('item-1', { + name: 'hello', + }); - promise2.then((data) => { assert.deepEqual( - data, - response2, - 'promise 2 resolves with array of data' + items, + [ + { + id: '1', + json: { + attributes: { + isNew: true, + name: 'hello', + }, + id: '1', + relationships: {}, + type: 'item-1', + }, + name: 'hello', + }, + { + id: '2', + json: { + attributes: { + isNew: false, + name: 'hello', + }, + id: '2', + relationships: {}, + type: 'item-1', + }, + name: 'hello', + }, + ], + 'indexed search works' ); }); - RSVP.all([promise1, promise2]).then(() => { - assert.equal(get(promiseQueue, 'length'), 0, 'promise queue is cleared'); - done(); + test('it allows to filter for an unindexed fields', async function (assert) { + class TestIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; + + version1 = { + stores: { + 'item-1': '&id,name', + }, + }; + + mapTable = { + 'item-1': (item) => { + return { + id: this._toString(item.id), + name: item.attributes.name, + json: this._cleanObject(item), + }; + }, + }; + } + + class TestIndexedDbService extends IndexedDbService { + databaseName = 'test-db-migration-1'; + } + + this.owner.register('service:indexed-db', TestIndexedDbService); + this.owner.register( + 'service:indexed-db-configuration', + TestIndexedDbConfigurationService + ); + + let service = this.owner.lookup('service:indexed-db'); + + await service.setupTask.perform(); + + // Try interacting with it + await service.add('item-1', { + id: '1', + type: 'item-1', + attributes: { name: 'hello', isNew: true }, + }); + await service.add('item-1', { + id: '2', + type: 'item-1', + attributes: { name: 'hello', isNew: false }, + }); + await service.add('item-1', { + id: '3', + type: 'item-1', + attributes: { name: 'hello2', isNew: true }, + }); + + let items = await service.query('item-1', { + isNew: true, + }); + + assert.deepEqual( + items, + [ + { + id: '1', + json: { + attributes: { + isNew: true, + name: 'hello', + }, + id: '1', + relationships: {}, + type: 'item-1', + }, + name: 'hello', + }, + { + id: '3', + json: { + attributes: { + isNew: true, + name: 'hello2', + }, + id: '3', + relationships: {}, + type: 'item-1', + }, + name: 'hello2', + }, + ], + 'non-indexed search works' + ); }); - }); - test('it does not open multiple db instances on setup', async function (assert) { - let service = this.owner.lookup('service:indexed-db'); + test('it allows to combine indexed & unindexed fields', async function (assert) { + class TestIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; + + version1 = { + stores: { + 'item-1': '&id,name', + }, + }; + + mapTable = { + 'item-1': (item) => { + return { + id: this._toString(item.id), + name: item.attributes.name, + json: this._cleanObject(item), + }; + }, + }; + } + + class TestIndexedDbService extends IndexedDbService { + databaseName = 'test-db-migration-1'; + } + + this.owner.register('service:indexed-db', TestIndexedDbService); + this.owner.register( + 'service:indexed-db-configuration', + TestIndexedDbConfigurationService + ); + + let service = this.owner.lookup('service:indexed-db'); - await run(() => service.setup()); - let { db } = service; - assert.ok(db, 'database is setup'); + await service.setupTask.perform(); - await run(() => service.setup()); - let db2 = service.db; - assert.equal(db2, db, 'db is not overwritten'); + // Try interacting with it + await service.add('item-1', { + id: '1', + type: 'item-1', + attributes: { name: 'hello', isNew: true }, + }); + await service.add('item-1', { + id: '2', + type: 'item-1', + attributes: { name: 'hello', isNew: false }, + }); + await service.add('item-1', { + id: '3', + type: 'item-1', + attributes: { name: 'hello2', isNew: true }, + }); + + let items = await service.query('item-1', { + name: 'hello', + isNew: true, + }); + + assert.deepEqual( + items, + [ + { + id: '1', + json: { + attributes: { + isNew: true, + name: 'hello', + }, + id: '1', + relationships: {}, + type: 'item-1', + }, + name: 'hello', + }, + ], + 'combi search works' + ); + }); + + test('it allows to use a compound index', async function (assert) { + class TestIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; + + version1 = { + stores: { + 'item-1': '&id,[name+isNew]', + }, + }; + + mapTable = { + 'item-1': (item) => { + return { + id: this._toString(item.id), + name: item.attributes.name, + isNew: this._toZeroOne(item.attributes.isNew), + json: this._cleanObject(item), + }; + }, + }; + } + + class TestIndexedDbService extends IndexedDbService { + databaseName = 'test-db-migration-1'; + } + + this.owner.register('service:indexed-db', TestIndexedDbService); + this.owner.register( + 'service:indexed-db-configuration', + TestIndexedDbConfigurationService + ); + + let service = this.owner.lookup('service:indexed-db'); + + await service.setupTask.perform(); + + await service.add('item-1', { + id: '1', + type: 'item-1', + attributes: { name: 'hello', isNew: true }, + }); + await service.add('item-1', { + id: '2', + type: 'item-1', + attributes: { name: 'hello', isNew: false }, + }); + await service.add('item-1', { + id: '3', + type: 'item-1', + attributes: { name: 'hello2', isNew: true }, + }); + + let items = await service.query('item-1', { + name: 'hello', + isNew: true, + }); + + assert.deepEqual( + items, + [ + { + id: '1', + json: { + attributes: { + isNew: true, + name: 'hello', + }, + id: '1', + relationships: {}, + type: 'item-1', + }, + name: 'hello', + isNew: 1, + }, + ], + 'combi search works' + ); + }); + + test('it allows to provide an empty query object', async function (assert) { + class TestIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; + + version1 = { + stores: { + 'item-1': '&id,name', + }, + }; + + mapTable = { + 'item-1': (item) => { + return { + id: this._toString(item.id), + name: item.attributes.name, + json: this._cleanObject(item), + }; + }, + }; + } + + class TestIndexedDbService extends IndexedDbService { + databaseName = 'test-db-migration-1'; + } + + this.owner.register('service:indexed-db', TestIndexedDbService); + this.owner.register( + 'service:indexed-db-configuration', + TestIndexedDbConfigurationService + ); + + let service = this.owner.lookup('service:indexed-db'); + + await service.setupTask.perform(); + + // Try interacting with it + await service.add('item-1', { + id: '1', + type: 'item-1', + attributes: { name: 'hello', isNew: true }, + }); + await service.add('item-1', { + id: '2', + type: 'item-1', + attributes: { name: 'hello', isNew: false }, + }); + await service.add('item-1', { + id: '3', + type: 'item-1', + attributes: { name: 'hello2', isNew: true }, + }); + + let items = await service.query('item-1', {}); + + assert.deepEqual( + items, + [ + { + id: '1', + json: { + attributes: { + isNew: true, + name: 'hello', + }, + id: '1', + relationships: {}, + type: 'item-1', + }, + name: 'hello', + }, + { + id: '2', + json: { + attributes: { + isNew: false, + name: 'hello', + }, + id: '2', + relationships: {}, + type: 'item-1', + }, + name: 'hello', + }, + { + id: '3', + json: { + attributes: { + isNew: true, + name: 'hello2', + }, + id: '3', + relationships: {}, + type: 'item-1', + }, + name: 'hello2', + }, + ], + 'empty query search works' + ); + }); + + test('it handles empty results', async function (assert) { + class TestIndexedDbConfigurationService extends IndexedDbConfigurationService { + currentVersion = 1; + + version1 = { + stores: { + 'item-1': '&id,name', + }, + }; + + mapTable = { + 'item-1': (item) => { + return { + id: this._toString(item.id), + name: item.attributes.name, + json: this._cleanObject(item), + }; + }, + }; + } + + class TestIndexedDbService extends IndexedDbService { + databaseName = 'test-db-migration-1'; + } + + this.owner.register('service:indexed-db', TestIndexedDbService); + this.owner.register( + 'service:indexed-db-configuration', + TestIndexedDbConfigurationService + ); + + let service = this.owner.lookup('service:indexed-db'); + + await service.setupTask.perform(); + + // Try interacting with it + await service.add('item-1', { + id: '1', + type: 'item-1', + attributes: { name: 'hello', isNew: true }, + }); + await service.add('item-1', { + id: '2', + type: 'item-1', + attributes: { name: 'hello', isNew: false }, + }); + await service.add('item-1', { + id: '3', + type: 'item-1', + attributes: { name: 'hello2', isNew: true }, + }); + + let items = await service.query('item-1', { + name: 'blub', + }); + + assert.deepEqual(items, [], 'not-found search works'); + }); }); });