diff --git a/README.md b/README.md index 665798b2..b4c0463e 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ See the [CHANGELOG](CHANGELOG.md) ## Usage +* [Configuration](#configuration) * [Object & Array](#object--array) * [Object](#object) * [Array](#array) @@ -42,6 +43,33 @@ See the [CHANGELOG](CHANGELOG.md) * [.query() & .queryRecord()](#query--queryrecord) * [Import & Export of localStorage records](#export--import) +### Configuration + +#### Namespace & keyDelimiter + +In you apps `config/environment.js` you can set a `namespace` and a `keyDelimiter`. For backward compatibility this is a opt-in feature. + +**Important:** Don't turn this feature on for existing apps. You will lose access to existing keys. + +To activate it there are the following options: + +- `namespace` can be `true` or a string. If set to `true` it will use `modulePrefix` as the namespace +- `keyDelimiter` is a string. The default is `:` + +```js +// config/environment.js +module.exports = function() { + var ENV = { + modulePrefix: 'my-app', + 'ember-local-storage': { + namespace: true, // will use the modulePrefix e.g. 'my-app' + namespace: 'customNamespace', // will use 'customNamespace' + keyDelimiter: '/' // will use / as a delimiter - the default is : + } + } +}; +``` + ### Object & Array #### Object @@ -165,7 +193,7 @@ export default Ember.Component.extend({ `model` Optional string - The dependent property. Must be an ember data model or an object with `modelName` and `id` properties. (It is still experimental) `options` are: -- `legacyKey` String +- `legacyKey` String - **Deprecated see [Deprecations](#deprecations)** #### Methods @@ -456,6 +484,15 @@ module('basic acceptance test', function(hooks) { ``` +## Deprecations + +### storageFor - legacyKey +until: 2.0.0 + +id: ember-local-storage.storageFor.options.legacyKey + +Using `legacyKey` has been deprecated and will be removed in version 2.0.0. You should migrate your key to the new format. For `storageFor('settings')` that would be `storage:settings`. + ## Running * `ember serve` diff --git a/addon/adapters/base.js b/addon/adapters/base.js index 2010db67..10eeaa48 100644 --- a/addon/adapters/base.js +++ b/addon/adapters/base.js @@ -1,6 +1,7 @@ import Ember from 'ember'; import DS from 'ember-data'; import ImportExportMixin from '../mixins/adapters/import-export'; +import { _buildKey } from '../helpers/storage'; const keys = Object.keys || Ember.keys; @@ -323,7 +324,7 @@ export default JSONAPIAdapter.extend(ImportExportMixin, { }, _storageKey(type, id) { - return type + '-' + id; + return _buildKey(this, type + '-' + id); }, // Should be overwriten diff --git a/addon/adapters/local.js b/addon/adapters/local.js index 5ab99590..0e2a7057 100644 --- a/addon/adapters/local.js +++ b/addon/adapters/local.js @@ -1,6 +1,6 @@ import Ember from 'ember'; import BaseAdapter from './base'; -import { getStorage } from '../helpers/storage'; +import { getStorage, _buildKey } from '../helpers/storage'; import StorageArray from '../local/array'; const { @@ -14,8 +14,10 @@ export default BaseAdapter.extend({ const indices = get(this, '_indices'); if (!indices[type]) { + let storageKey = _buildKey(this, 'index-' + type); + indices[type] = StorageArray - .extend({ _storageKey: 'index-' + type }) + .extend({ _storageKey: storageKey }) .create(); } diff --git a/addon/adapters/session.js b/addon/adapters/session.js index 21ff8de0..26e944ef 100644 --- a/addon/adapters/session.js +++ b/addon/adapters/session.js @@ -1,6 +1,6 @@ import Ember from 'ember'; import BaseAdapter from './base'; -import { getStorage } from '../helpers/storage'; +import { getStorage, _buildKey } from '../helpers/storage'; import StorageArray from '../session/array'; const { @@ -14,8 +14,10 @@ export default BaseAdapter.extend({ const indices = get(this, '_indices'); if (!indices[type]) { + let storageKey = _buildKey(this, 'index-' + type); + indices[type] = StorageArray - .extend({ _storageKey: 'index-' + type }) + .extend({ _storageKey: storageKey }) .create(); } diff --git a/addon/helpers/storage.js b/addon/helpers/storage.js index 286f82af..39fe0181 100644 --- a/addon/helpers/storage.js +++ b/addon/helpers/storage.js @@ -6,7 +6,8 @@ const { getOwner, String: { dasherize - } + }, + deprecate } = Ember; const assign = Ember.assign || Ember.merge; @@ -38,6 +39,7 @@ function getStorage(name) { let storages = {}; +// TODO: v2.0 - Remove options function storageFor(key, modelName, options = {}) { if (arguments.length === 2 && typeof modelName === 'object') { options = modelName; @@ -85,6 +87,7 @@ function storageFor(key, modelName, options = {}) { * Looks up the storage factory on the container and sets initial state * on the instance if desired. */ +// TODO: v2.0 - Remove options and legacyKey function createStorage(context, key, modelKey, options) { const owner = getOwner(context); const factoryType = 'storage'; @@ -95,11 +98,19 @@ function createStorage(context, key, modelKey, options) { owner.registerOptionsForType(factoryType, { instantiate: false }); if (options.legacyKey) { + deprecate('Using legacyKey has been deprecated and will be removed in version 2.0.0', false, { + id: 'ember-local-storage.storageFor.options.legacyKey', + until: '2.0.0', + url: 'https://github.com/funkensturm/ember-local-storage#deprecations' + }); + storageKey = options.legacyKey; } else { storageKey = modelKey ? `${storageFactory}:${modelKey}` : storageFactory; } + storageKey = _buildKey(context, storageKey); + const initialState = {}, defaultState = { _storageKey: storageKey @@ -136,6 +147,29 @@ function _modelKey(model) { return `${modelName}:${id}`; } +// TODO: v2.0 - Make modulePrefix the default +function _getNamespace(appConfig, addonConfig) { + // For backward compatibility this is a opt-in feature + let namespace = addonConfig.namespace; + + // Shortcut for modulePrefix + if (namespace === true) { + namespace = appConfig.modulePrefix + } + + return namespace; +} + +// TODO: Add migration helper +function _buildKey(context, key) { + let appConfig = getOwner(context).resolveRegistration('config:environment'); + let addonConfig = appConfig && appConfig['ember-local-storage'] || {}; + let namespace = _getNamespace(appConfig, addonConfig); + let delimiter = addonConfig.keyDelimiter || ':'; + + return namespace ? `${namespace}${delimiter}${key}` : key; +} + // Testing helper function _resetStorages() { storages = {}; @@ -145,5 +179,6 @@ export { tryStorage, getStorage, storageFor, - _resetStorages + _resetStorages, + _buildKey }; diff --git a/tests/helpers/storage.js b/tests/helpers/storage.js index 2c51b091..ac23b974 100644 --- a/tests/helpers/storage.js +++ b/tests/helpers/storage.js @@ -8,7 +8,24 @@ function storageDeepEqual(assert, actual, expected, message) { assert.deepEqual(actual, expected, message); } +function registerConfigEnvironment(context) { + let environment = { + modulePrefix: 'my-app', + 'ember-local-storage': {} + }; + + context.register('config:environment', environment, { instantiate: false }); +} + +function setConfigEnvironment(context, key, value) { + let appConfig = context.container.lookup('config:environment'); + let addonConfig = appConfig['ember-local-storage'] || {}; + addonConfig[key] = value; +} + export { storageEqual, - storageDeepEqual + storageDeepEqual, + registerConfigEnvironment, + setConfigEnvironment }; diff --git a/tests/unit/adapters/indices-test.js b/tests/unit/adapters/indices-test.js index 0c75246e..18edeb13 100644 --- a/tests/unit/adapters/indices-test.js +++ b/tests/unit/adapters/indices-test.js @@ -1,13 +1,17 @@ import { moduleFor, test } from 'ember-qunit'; import { storageEqual, - storageDeepEqual + storageDeepEqual, + registerConfigEnvironment, + setConfigEnvironment } from '../../helpers/storage'; moduleFor('adapter:application', 'Unit | Adapter | indices', { // Specify the other units that are required for this test. // needs: ['serializer:foo'] beforeEach: function() { + registerConfigEnvironment(this); + window.localStorage.clear(); window.sessionStorage.clear(); } @@ -34,7 +38,7 @@ test('it does not persists duplicates to index', function(assert) { storageDeepEqual(assert, window.localStorage['index-projects'], ['1234']); }); -test('it removes ids from index', function(assert) { +test('it removes ids from index (namespace not set)', function(assert) { assert.expect(3); var adapter = this.subject(); @@ -46,3 +50,64 @@ test('it removes ids from index', function(assert) { adapter._removeFromIndex('projects', '1234'); storageDeepEqual(assert, window.localStorage['index-projects'], []); }); + +test('it removes ids from index (namespace: true)', function(assert) { + assert.expect(3); + + setConfigEnvironment(this, 'namespace', true); + + var adapter = this.subject(); + + storageEqual(assert, window.localStorage['index-projects'], undefined); + + adapter._addToIndex('projects', '1234'); + storageDeepEqual( + assert, + window.localStorage['my-app:index-projects'], + ['1234'] + ); + + adapter._removeFromIndex('projects', '1234'); + storageDeepEqual(assert, window.localStorage['my-app:index-projects'], []); +}); + +test('it removes ids from index (namespace: "custom")', function(assert) { + assert.expect(3); + + setConfigEnvironment(this, 'namespace', 'custom'); + + var adapter = this.subject(); + + storageEqual(assert, window.localStorage['index-projects'], undefined); + + adapter._addToIndex('projects', '1234'); + storageDeepEqual( + assert, + window.localStorage['custom:index-projects'], + ['1234'] + ); + + adapter._removeFromIndex('projects', '1234'); + storageDeepEqual(assert, window.localStorage['custom:index-projects'], []); +}); + +test('it removes ids from index (keyDelimiter: "/")', function(assert) { + assert.expect(3); + + setConfigEnvironment(this, 'namespace', true); + setConfigEnvironment(this, 'keyDelimiter', '/'); + + var adapter = this.subject(); + + storageEqual(assert, window.localStorage['index-projects'], undefined); + + adapter._addToIndex('projects', '1234'); + storageDeepEqual( + assert, + window.localStorage['my-app/index-projects'], + ['1234'] + ); + + adapter._removeFromIndex('projects', '1234'); + storageDeepEqual(assert, window.localStorage['my-app/index-projects'], []); +}); diff --git a/tests/unit/models/post-test.js b/tests/unit/models/post-test.js index f9d8f3f4..0f15b9d9 100644 --- a/tests/unit/models/post-test.js +++ b/tests/unit/models/post-test.js @@ -1,6 +1,10 @@ import Ember from 'ember'; import DS from 'ember-data'; import { moduleForModel, test } from 'ember-qunit'; +import { + registerConfigEnvironment, + setConfigEnvironment +} from '../../helpers/storage'; const { get, @@ -19,6 +23,8 @@ moduleForModel('post', 'Unit | Model | post', { 'model:book-publication' ], beforeEach: function() { + registerConfigEnvironment(this); + window.localStorage.clear(); window.sessionStorage.clear(); } @@ -202,3 +208,123 @@ test('queryRecord empty store', function(assert) { done(); }); }); + +test('create a record (namespace: true)', function(assert) { + assert.expect(1); + + setConfigEnvironment(this, 'namespace', true); + + const done = assert.async(); + const store = this.store(); + + run(function() { + store.createRecord('post', { name: 'Just a Name' }).save(); + }); + + store.findAll('post') + .then(function(posts) { + assert.equal(get(posts, 'length'), 1); + done(); + }); +}); + +test('create a record (namespace: "custom")', function(assert) { + assert.expect(1); + + setConfigEnvironment(this, 'namespace', 'custom'); + + const done = assert.async(); + const store = this.store(); + + run(function() { + store.createRecord('post', { name: 'Just a Name' }).save(); + }); + + store.findAll('post') + .then(function(posts) { + assert.equal(get(posts, 'length'), 1); + done(); + }); +}); + +test('create a record (keyDelimiter: "/")', function(assert) { + assert.expect(1); + + setConfigEnvironment(this, 'namespace', 'custom'); + setConfigEnvironment(this, 'keyDelimiter', '/'); + + const done = assert.async(); + const store = this.store(); + + run(function() { + store.createRecord('post', { name: 'Just a Name' }).save(); + }); + + store.findAll('post') + .then(function(posts) { + assert.equal(get(posts, 'length'), 1); + done(); + }); +}); + +test('push a record (namespace: true)', function(assert) { + assert.expect(2); + + setConfigEnvironment(this, 'namespace', true); + + const done = assert.async(); + const store = this.store(); + + let posts = store.findAll('post'); + + assert.equal(get(posts, 'length'), 0); + + run(function() { + store.push({data: [ + { + id: '1', + type: 'post', + attributes: {name: 'Super Name'} + }, + { + id: '2', + type: 'post', + attributes: {name: 'Totally rad'} + } + ]}); + }); + + store.findAll('post') + .then(function(posts) { + assert.equal(get(posts, 'length'), 2); + done(); + }); +}); + +test('find a single record (namespace: true)', function(assert) { + assert.expect(2); + + setConfigEnvironment(this, 'namespace', true); + + const done = assert.async(); + const store = this.store(); + + let newPost; + + run(function() { + newPost = store.createRecord('post', { + name: 'Ember.js: 10 most common mistakes' + }); + + newPost.save(); + }); + + run(function() { + store.find('post', get(newPost, 'id')) + .then(function(post) { + assert.equal(get(post, 'id'), get(newPost, 'id')); + assert.equal(get(post, 'name'), 'Ember.js: 10 most common mistakes'); + done(); + }); + }); +}); diff --git a/tests/unit/storage-for-test.js b/tests/unit/storage-for-test.js index d756c84a..ecdc2fd0 100644 --- a/tests/unit/storage-for-test.js +++ b/tests/unit/storage-for-test.js @@ -1,7 +1,9 @@ import Ember from 'ember'; import { moduleFor, test } from 'ember-qunit'; import { - storageDeepEqual + storageDeepEqual, + registerConfigEnvironment, + setConfigEnvironment } from '../helpers/storage'; import StorageObject from 'ember-local-storage/local/object'; @@ -10,8 +12,12 @@ import { _resetStorages } from 'ember-local-storage/helpers/storage'; -moduleFor('router:main', 'legacy - config', { +let subject; + +moduleFor('router:main', 'storageFor', { beforeEach() { + registerConfigEnvironment(this); + let mockStorage = StorageObject.extend(); mockStorage.reopenClass({ @@ -24,6 +30,18 @@ moduleFor('router:main', 'legacy - config', { this.register('storage:settings', mockStorage); this.register('storage:options', mockStorage); + + let post = Ember.Object.extend({ + modelName: 'post', + id: '123' + }).create(); + + this.register('object:test', Ember.Object.extend({ + post: post, + settings: storageFor('settings', 'post'), + options: storageFor('options', 'post') + })); + subject = this.container.lookup('object:test'); }, afterEach() { window.localStorage.clear(); @@ -31,21 +49,9 @@ moduleFor('router:main', 'legacy - config', { } }); -test('it has the correct key', function(assert) { +test('it has the correct key (namespace not set)', function(assert) { assert.expect(4); - let post = Ember.Object.extend({ - modelName: 'post', - id: '123' - }).create(); - - this.register('object:test', Ember.Object.extend({ - post: post, - settings: storageFor('settings', 'post'), - options: storageFor('options', 'post') - })); - let subject = this.container.lookup('object:test'); - assert.equal( subject.get('settings._storageKey'), 'storage:settings:post:123' @@ -64,3 +70,95 @@ test('it has the correct key', function(assert) { perPage: 10 }); }); + +test('it has the correct key (namespace: true)', function(assert) { + assert.expect(4); + + setConfigEnvironment(this, 'namespace', true); + + assert.equal( + subject.get('settings._storageKey'), + 'my-app:storage:settings:post:123' + ); + + assert.equal( + subject.get('options._storageKey'), + 'my-app:storage:options:post:123' + ); + + storageDeepEqual(assert, + window.localStorage['my-app:storage:settings:post:123'], + { + perPage: 10 + } + ); + + storageDeepEqual(assert, + window.localStorage['my-app:storage:options:post:123'], + { + perPage: 10 + } + ); +}); + +test('it has the correct key (namespace: "custom")', function(assert) { + assert.expect(4); + + setConfigEnvironment(this, 'namespace', 'custom'); + + assert.equal( + subject.get('settings._storageKey'), + 'custom:storage:settings:post:123' + ); + + assert.equal( + subject.get('options._storageKey'), + 'custom:storage:options:post:123' + ); + + storageDeepEqual(assert, + window.localStorage['custom:storage:settings:post:123'], + { + perPage: 10 + } + ); + + storageDeepEqual(assert, + window.localStorage['custom:storage:options:post:123'], + { + perPage: 10 + } + ); +}); + +test('it has the correct key (keyDelimiter: "/")', function(assert) { + assert.expect(4); + + setConfigEnvironment(this, 'namespace', true); + setConfigEnvironment(this, 'keyDelimiter', '/'); + + assert.equal( + subject.get('settings._storageKey'), + 'my-app/storage:settings:post:123' + ); + + assert.equal( + subject.get('options._storageKey'), + 'my-app/storage:options:post:123' + ); + + storageDeepEqual(assert, + window.localStorage['my-app/storage:settings:post:123'], + { + perPage: 10 + } + ); + + storageDeepEqual(assert, + window.localStorage['my-app/storage:options:post:123'], + { + perPage: 10 + } + ); +}); +