├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── .vscodeignore ├── .watchmanconfig ├── CHANGELOG.md ├── LICENSE.md ├── MODULE_REPORT.md ├── README.md ├── addon ├── .gitkeep ├── adapters │ └── ls-adapter.js ├── index.js └── serializers │ └── ls-serializer.js ├── app ├── .gitkeep ├── adapters │ └── ls-adapter.js └── serializers │ └── ls-serializer.js ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── testem.js ├── tests ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ └── .gitkeep │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ ├── application.hbs │ │ │ └── components │ │ │ └── .gitkeep │ ├── config │ │ ├── environment.js │ │ └── targets.js │ └── public │ │ └── robots.txt ├── helpers │ ├── .gitkeep │ ├── fixtures.js │ ├── owner.js │ └── store.js ├── index.html ├── integration │ ├── .gitkeep │ ├── adapters │ │ └── ls-adapter-test.js │ └── serializers │ │ └── ls-serializer-test.js ├── test-helper.js └── unit │ └── .gitkeep └── vendor └── .gitkeep /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2017, 5 | sourceType: 'module' 6 | }, 7 | plugins: [ 8 | 'ember' 9 | ], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:ember/recommended' 13 | ], 14 | env: { 15 | browser: true 16 | }, 17 | rules: { 18 | }, 19 | overrides: [ 20 | // node files 21 | { 22 | files: [ 23 | 'index.js', 24 | 'testem.js', 25 | 'ember-cli-build.js', 26 | 'config/**/*.js', 27 | 'tests/dummy/config/**/*.js' 28 | ], 29 | excludedFiles: [ 30 | 'app/**', 31 | 'addon/**', 32 | 'tests/dummy/app/**' 33 | ], 34 | parserOptions: { 35 | sourceType: 'script', 36 | ecmaVersion: 2015 37 | }, 38 | env: { 39 | browser: false, 40 | node: true 41 | }, 42 | plugins: ['node'], 43 | rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { 44 | // add your custom rules and overrides for node files here 45 | }) 46 | } 47 | ] 48 | }; 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log* 17 | yarn-error.log 18 | testem.log 19 | 20 | # ember-try 21 | .node_modules.ember-try/ 22 | bower.json.ember-try 23 | package.json.ember-try 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .eslintrc.js 11 | .gitignore 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | 18 | # ember-try 19 | .node_modules.ember-try/ 20 | bower.json.ember-try 21 | package.json.ember-try 22 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "12" 5 | 6 | dist: xenial 7 | 8 | addons: 9 | chrome: stable 10 | 11 | env: 12 | global: 13 | # See https://git.io/vdao3 for details. 14 | - JOBS=1 15 | 16 | before_install: 17 | - npm config set spin false 18 | - npm --version 19 | 20 | script: 21 | - npm run lint:js 22 | - npm test 23 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | dist/** 2 | tmp/** 3 | build/** 4 | cache/** 5 | node_modules/** 6 | bower_components/** 7 | .sass-cache/** 8 | connect.lock/** 9 | coverage/*/** 10 | libpeerconnection.log -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ember-localstorage-adapter 2 | 3 | ## Master 4 | 5 | ### 0.3.2 (June 27, 2014) 6 | 7 | * `#find` rejects its promise if no record is found. 8 | * `#findQuery` rejects its promise if no records are found. 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012 Ryan Florence 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /MODULE_REPORT.md: -------------------------------------------------------------------------------- 1 | ## Module Report 2 | ### Unknown Global 3 | 4 | **Global**: `Ember._RegistryProxyMixin` 5 | 6 | **Location**: `tests/helpers/owner.js` at line 5 7 | 8 | ```js 9 | let Owner; 10 | 11 | if (Ember._RegistryProxyMixin && Ember._ContainerProxyMixin) { 12 | Owner = Ember.Object.extend(Ember._RegistryProxyMixin, Ember._ContainerProxyMixin); 13 | } else { 14 | ``` 15 | 16 | ### Unknown Global 17 | 18 | **Global**: `Ember._ContainerProxyMixin` 19 | 20 | **Location**: `tests/helpers/owner.js` at line 5 21 | 22 | ```js 23 | let Owner; 24 | 25 | if (Ember._RegistryProxyMixin && Ember._ContainerProxyMixin) { 26 | Owner = Ember.Object.extend(Ember._RegistryProxyMixin, Ember._ContainerProxyMixin); 27 | } else { 28 | ``` 29 | 30 | ### Unknown Global 31 | 32 | **Global**: `Ember._RegistryProxyMixin` 33 | 34 | **Location**: `tests/helpers/owner.js` at line 6 35 | 36 | ```js 37 | 38 | if (Ember._RegistryProxyMixin && Ember._ContainerProxyMixin) { 39 | Owner = Ember.Object.extend(Ember._RegistryProxyMixin, Ember._ContainerProxyMixin); 40 | } else { 41 | Owner = Ember.Object.extend(); 42 | ``` 43 | 44 | ### Unknown Global 45 | 46 | **Global**: `Ember._ContainerProxyMixin` 47 | 48 | **Location**: `tests/helpers/owner.js` at line 6 49 | 50 | ```js 51 | 52 | if (Ember._RegistryProxyMixin && Ember._ContainerProxyMixin) { 53 | Owner = Ember.Object.extend(Ember._RegistryProxyMixin, Ember._ContainerProxyMixin); 54 | } else { 55 | Owner = Ember.Object.extend(); 56 | ``` 57 | 58 | ### Unknown Global 59 | 60 | **Global**: `Ember.Registry` 61 | 62 | **Location**: `tests/helpers/store.js` at line 11 63 | 64 | ```js 65 | options = options || {}; 66 | 67 | if (Ember.Registry) { 68 | registry = env.registry = new Ember.Registry(); 69 | owner = Owner.create({ 70 | ``` 71 | 72 | ### Unknown Global 73 | 74 | **Global**: `Ember.Registry` 75 | 76 | **Location**: `tests/helpers/store.js` at line 12 77 | 78 | ```js 79 | 80 | if (Ember.Registry) { 81 | registry = env.registry = new Ember.Registry(); 82 | owner = Owner.create({ 83 | __registry__: registry 84 | ``` 85 | 86 | ### Unknown Global 87 | 88 | **Global**: `Ember.Container` 89 | 90 | **Location**: `tests/helpers/store.js` at line 19 91 | 92 | ```js 93 | owner.__container__ = container; 94 | } else { 95 | container = env.container = new Ember.Container(); 96 | registry = env.registry = container; 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember-localstorage-adapter 2 | 3 | [![Build 4 | Status](https://travis-ci.org/locks/ember-localstorage-adapter.svg?branch=master)](https://travis-ci.org/locks/ember-localstorage-adapter) 5 | 6 | Store your ember application data in localStorage. 7 | 8 | Compatible with Ember Data 1.13 and above. 9 | 10 | **NOTE**: New versions of the `localStorage` adapter are no longer compatible 11 | with older versions of Ember Data. For older versions, checkout the `pre-beta` 12 | branch. 13 | 14 | Usage 15 | ----- 16 | 17 | Include this addon in your app with `ember install ember-localstorage-adapter` 18 | and then like all adapters and serializers: 19 | 20 | ```js 21 | // app/serializers/application.js 22 | import { LSSerializer } from 'ember-localstorage-adapter'; 23 | 24 | export default LSSerializer.extend(); 25 | 26 | // app/adapters/application.js 27 | import LSAdapter from 'ember-localstorage-adapter'; 28 | 29 | export default LSAdapter.extend({ 30 | namespace: 'yournamespace' 31 | }); 32 | ``` 33 | 34 | ### Local Storage Namespace 35 | 36 | All of your application data lives on a single `localStorage` key, it defaults to `DS.LSAdapter` but if you supply a `namespace` option it will store it there: 37 | 38 | ```js 39 | import LSAdapter from 'ember-localstorage-adapter/adapters/ls-adapter'; 40 | 41 | export default LSAdapter.extend({ 42 | namespace: 'my app' 43 | }); 44 | ``` 45 | 46 | ### Models 47 | 48 | Whenever the adapter returns a record, it'll also return all 49 | relationships, so __do not__ use `{async: true}` in your model definitions. 50 | 51 | #### Namespace 52 | 53 | If your model definition has a `url` property, the adapter will store the data on that namespace. URL is a weird term in this context, but it makes swapping out adapters simpler by not requiring additional properties on your models. 54 | 55 | ```js 56 | const List = DS.Model.extend({ 57 | // ... 58 | }); 59 | List.reopen({ 60 | url: '/some/url' 61 | }); 62 | export default List; 63 | ``` 64 | 65 | ### Quota Exceeded Handler 66 | 67 | Browser's `localStorage` has limited space, if you try to commit application data and the browser is out of space, then the adapter will trigger the `QUOTA_EXCEEDED_ERR` event. 68 | 69 | ```js 70 | import DS from 'ember-data'; 71 | DS.Store.adapter.on('QUOTA_EXCEEDED_ERR', function(records){ 72 | // do stuff 73 | }); 74 | 75 | DS.Store.commit(); 76 | ``` 77 | 78 | ### Local Storage Unavailable 79 | 80 | When `localStorage` is not available (typically because the user has explicitly disabled it), the adapter will keep records in memory. When the adapter first discovers that this is the case, it will trigger a `persistenceUnavailable` event, which the application may use to take any necessary actions. 81 | 82 | ```js 83 | adapter.on('persistenceUnavailable', function() { 84 | // Maybe notify the user that their data won't live past the end of the current session 85 | }); 86 | ``` 87 | 88 | License & Copyright 89 | ------------------- 90 | 91 | Copyright (c) 2012 Ryan Florence 92 | MIT Style license. http://opensource.org/licenses/MIT 93 | 94 | ## Running 95 | 96 | * `ember server` 97 | * Visit your app at http://localhost:4200. 98 | 99 | ## Linting 100 | 101 | * `npm run lint:js` 102 | * `npm run lint:js -- --fix` 103 | 104 | ## Running Tests 105 | 106 | * `npm test` (Runs `ember try:each` to test your addon against multiple Ember versions) 107 | * `ember test` 108 | * `ember test --server` 109 | 110 | ## Building 111 | 112 | * `ember build` 113 | 114 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). 115 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/addon/.gitkeep -------------------------------------------------------------------------------- /addon/adapters/ls-adapter.js: -------------------------------------------------------------------------------- 1 | import { get } from '@ember/object'; 2 | import { reject, resolve } from 'rsvp'; 3 | import { A } from '@ember/array'; 4 | import Evented from '@ember/object/evented'; 5 | import DS from 'ember-data'; 6 | 7 | const DEFAULT_NAMESPACE = 'DS.LSAdapter'; 8 | 9 | const LSAdapter = DS.Adapter.extend(Evented, { 10 | /** 11 | * This governs if promises will be resolved immediately for `findAll` 12 | * requests or if they will wait for the store requests to finish. This matches 13 | * the ember < 2.0 behavior. 14 | * [deprecation id: ds.adapter.should-reload-all-default-behavior] 15 | */ 16 | shouldReloadAll: function(/* modelClass, snapshotArray */) { 17 | return true; 18 | }, 19 | 20 | /** 21 | * Conforms to ember <2.0 behavior, in order to remove deprecation. 22 | * Probably safe to remove if running on ember 2.0 23 | * [deprecation id: ds.model.relationship-changing-to-asynchrounous-by-default] 24 | */ 25 | shouldBackgroundReloadRecord: function(){ 26 | return false; 27 | }, 28 | 29 | /** 30 | This is the main entry point into finding records. The first parameter to 31 | this method is the model's name as a string. 32 | 33 | @method find 34 | @param {DS.Model} type 35 | @param {Object|String|Integer|null} id 36 | */ 37 | findRecord: function(store, type, id, opts) { 38 | var allowRecursive = true; 39 | var namespace = this._namespaceForType(type); 40 | var record = A(namespace.records[id]); 41 | 42 | /** 43 | * In the case where there are relationships, this method is called again 44 | * for each relation. Given the relations have references to the main 45 | * object, we use allowRecursive to avoid going further into infinite 46 | * recursiveness. 47 | * 48 | * Concept from ember-indexdb-adapter 49 | */ 50 | if (opts && typeof opts.allowRecursive !== 'undefined') { 51 | allowRecursive = opts.allowRecursive; 52 | } 53 | 54 | if (!record || !record.hasOwnProperty('id')) { 55 | return reject(new Error("Couldn't find record of" + " type '" + type.modelName + "' for the id '" + id + "'.")); 56 | } 57 | 58 | if (allowRecursive) { 59 | return this.loadRelationships(store, type, record); 60 | } else { 61 | return resolve(record); 62 | } 63 | }, 64 | 65 | findMany: function (store, type, ids, opts) { 66 | var namespace = this._namespaceForType(type); 67 | var allowRecursive = true, 68 | results = A([]), record; 69 | 70 | /** 71 | * In the case where there are relationships, this method is called again 72 | * for each relation. Given the relations have references to the main 73 | * object, we use allowRecursive to avoid going further into infinite 74 | * recursiveness. 75 | * 76 | * Concept from ember-indexdb-adapter 77 | */ 78 | if (opts && typeof opts.allowRecursive !== 'undefined') { 79 | allowRecursive = opts.allowRecursive; 80 | } 81 | 82 | for (var i = 0; i < ids.length; i++) { 83 | record = namespace.records[ids[i]]; 84 | if (!record || !record.hasOwnProperty('id')) { 85 | return reject(new Error("Couldn't find record of type '" + type.modelName + "' for the id '" + ids[i] + "'.")); 86 | } 87 | results.push(Object.assign({}, record)); 88 | } 89 | 90 | if (results.get('length') && allowRecursive) { 91 | return this.loadRelationshipsForMany(store, type, results); 92 | } else { 93 | return resolve(results); 94 | } 95 | }, 96 | 97 | // Supports queries that look like this: 98 | // 99 | // { 100 | // : , 101 | // ... 102 | // } 103 | // 104 | // Every property added to the query is an "AND" query, not "OR" 105 | // 106 | // Example: 107 | // 108 | // match records with "complete: true" and the name "foo" or "bar" 109 | // 110 | // { complete: true, name: /foo|bar/ } 111 | query: function (store, type, query /*recordArray*/) { 112 | var namespace = this._namespaceForType(type); 113 | var results = this._query(namespace.records, query); 114 | 115 | if (results.get('length')) { 116 | return this.loadRelationshipsForMany(store, type, results); 117 | } else { 118 | return resolve(results); 119 | } 120 | }, 121 | 122 | _query: function (records, query) { 123 | var results = A([]), record; 124 | 125 | function recordMatchesQuery(record) { 126 | return Object.keys(query).every(function(property) { 127 | var test = query[property]; 128 | if (Object.prototype.toString.call(test) === '[object RegExp]') { 129 | return test.test(record[property]); 130 | } else { 131 | return record[property] === test; 132 | } 133 | }); 134 | } 135 | 136 | for (var id in records) { 137 | record = records[id]; 138 | if (recordMatchesQuery(record)) { 139 | results.push(Object.assign({}, record)); 140 | } 141 | } 142 | return results; 143 | }, 144 | 145 | findAll: function (store, type) { 146 | var namespace = this._namespaceForType(type), 147 | results = A([]); 148 | 149 | for (var id in namespace.records) { 150 | results.push(Object.assign({}, namespace.records[id])); 151 | } 152 | return resolve(results); 153 | }, 154 | 155 | createRecord: function (store, type, snapshot) { 156 | var namespaceRecords = this._namespaceForType(type); 157 | var serializer = store.serializerFor(type.modelName); 158 | var recordHash = serializer.serialize(snapshot, {includeId: true}); 159 | 160 | namespaceRecords.records[recordHash.id] = recordHash; 161 | 162 | this.persistData(type, namespaceRecords); 163 | return resolve(); 164 | }, 165 | 166 | updateRecord: function (store, type, snapshot) { 167 | var namespaceRecords = this._namespaceForType(type); 168 | var id = snapshot.id; 169 | var serializer = store.serializerFor(type.modelName); 170 | 171 | namespaceRecords.records[id] = serializer.serialize(snapshot, {includeId: true}); 172 | 173 | this.persistData(type, namespaceRecords); 174 | return resolve(); 175 | }, 176 | 177 | deleteRecord: function (store, type, snapshot) { 178 | var namespaceRecords = this._namespaceForType(type); 179 | var id = snapshot.id; 180 | 181 | delete namespaceRecords.records[id]; 182 | 183 | this.persistData(type, namespaceRecords); 184 | return resolve(); 185 | }, 186 | 187 | generateIdForRecord: function () { 188 | return Math.random().toString(32).slice(2).substr(0, 5); 189 | }, 190 | 191 | // private 192 | 193 | adapterNamespace: function () { 194 | return this.get('namespace') || DEFAULT_NAMESPACE; 195 | }, 196 | 197 | loadData: function () { 198 | var storage = this.getLocalStorage().getItem(this.adapterNamespace()); 199 | return storage ? JSON.parse(storage) : {}; 200 | }, 201 | 202 | persistData: function(type, data) { 203 | var modelNamespace = this.modelNamespace(type); 204 | var localStorageData = this.loadData(); 205 | 206 | localStorageData[modelNamespace] = data; 207 | 208 | this.getLocalStorage().setItem(this.adapterNamespace(), JSON.stringify(localStorageData)); 209 | }, 210 | 211 | getLocalStorage: function() { 212 | if (this._localStorage) { return this._localStorage; } 213 | 214 | var storage; 215 | try { 216 | storage = this.getNativeStorage() || this._enableInMemoryStorage(); 217 | } catch (e) { 218 | storage = this._enableInMemoryStorage(e); 219 | } 220 | this._localStorage = storage; 221 | return this._localStorage; 222 | }, 223 | 224 | _enableInMemoryStorage: function(reason) { 225 | this.trigger('persistenceUnavailable', reason); 226 | return { 227 | storage: {}, 228 | getItem: function(name) { 229 | return this.storage[name]; 230 | }, 231 | setItem: function(name, value) { 232 | this.storage[name] = value; 233 | } 234 | }; 235 | }, 236 | 237 | // This exists primarily as a testing extension point 238 | getNativeStorage: function() { 239 | return localStorage; 240 | }, 241 | 242 | _namespaceForType: function (type) { 243 | var namespace = this.modelNamespace(type); 244 | var storage = this.loadData(); 245 | 246 | return storage[namespace] || {records: {}}; 247 | }, 248 | 249 | modelNamespace: function(type) { 250 | return type.url || type.modelName; 251 | }, 252 | 253 | 254 | /** 255 | * This takes a record, then analyzes the model relationships and replaces 256 | * ids with the actual values. 257 | * 258 | * Stolen from ember-indexdb-adapter 259 | * 260 | * Consider the following JSON is entered: 261 | * 262 | * ```js 263 | * { 264 | * "id": 1, 265 | * "title": "Rails Rambo", 266 | * "comments": [1, 2] 267 | * } 268 | * 269 | * This will return: 270 | * 271 | * ```js 272 | * { 273 | * "id": 1, 274 | * "title": "Rails Rambo", 275 | * "comments": [1, 2] 276 | * 277 | * "_embedded": { 278 | * "comment": [{ 279 | * "_id": 1, 280 | * "comment_title": "FIRST" 281 | * }, { 282 | * "_id": 2, 283 | * "comment_title": "Rails is unagi" 284 | * }] 285 | * } 286 | * } 287 | * 288 | * This way, whenever a resource returned, its relationships will be also 289 | * returned. 290 | * 291 | * @method loadRelationships 292 | * @private 293 | * @param {DS.Model} type 294 | * @param {Object} record 295 | */ 296 | loadRelationships: function(store, type, record) { 297 | var adapter = this, 298 | relationshipNames, relationships; 299 | 300 | /** 301 | * Create a chain of promises, so the relationships are 302 | * loaded sequentially. Think of the variable 303 | * `recordPromise` as of the accumulator in a left fold. 304 | */ 305 | var recordPromise = resolve(record); 306 | 307 | relationshipNames = get(type, 'relationshipNames'); 308 | relationships = relationshipNames.belongsTo 309 | .concat(relationshipNames.hasMany); 310 | 311 | relationships.forEach(function(relationName) { 312 | var relationModel = type.typeForRelationship(relationName,store); 313 | var relationEmbeddedId = record[relationName]; 314 | var relationProp = adapter.relationshipProperties(type, relationName); 315 | var relationType = relationProp.kind; 316 | 317 | var opts = {allowRecursive: false}; 318 | 319 | /** 320 | * embeddedIds are ids of relations that are included in the main 321 | * payload, such as: 322 | * 323 | * { 324 | * cart: { 325 | * id: "s85fb", 326 | * customer: "rld9u" 327 | * } 328 | * } 329 | * 330 | * In this case, cart belongsTo customer and its id is present in the 331 | * main payload. We find each of these records and add them to _embedded. 332 | */ 333 | if (relationEmbeddedId && LSAdapter.prototype.isPrototypeOf(adapter)) 334 | { 335 | recordPromise = recordPromise.then(function(recordPayload) { 336 | var promise; 337 | if (relationType === 'belongsTo' || relationType === 'hasOne') { 338 | promise = adapter.findRecord(null, relationModel, relationEmbeddedId, opts); 339 | } else if (relationType === 'hasMany') { 340 | promise = adapter.findMany(null, relationModel, relationEmbeddedId, opts); 341 | } 342 | 343 | return promise.then(function(relationRecord) { 344 | return adapter.addEmbeddedPayload(recordPayload, relationName, relationRecord); 345 | }); 346 | }); 347 | } 348 | }); 349 | 350 | return recordPromise; 351 | }, 352 | 353 | 354 | /** 355 | * Given the following payload, 356 | * 357 | * { 358 | * cart: { 359 | * id: "1", 360 | * customer: "2" 361 | * } 362 | * } 363 | * 364 | * With `relationshipName` being `customer` and `relationshipRecord` 365 | * 366 | * {id: "2", name: "Rambo"} 367 | * 368 | * This method returns the following payload: 369 | * 370 | * { 371 | * cart: { 372 | * id: "1", 373 | * customer: "2" 374 | * }, 375 | * _embedded: { 376 | * customer: { 377 | * id: "2", 378 | * name: "Rambo" 379 | * } 380 | * } 381 | * } 382 | * 383 | * which is then treated by the serializer later. 384 | * 385 | * @method addEmbeddedPayload 386 | * @private 387 | * @param {Object} payload 388 | * @param {String} relationshipName 389 | * @param {Object} relationshipRecord 390 | */ 391 | addEmbeddedPayload: function(payload, relationshipName, relationshipRecord) { 392 | var objectHasId = (relationshipRecord && relationshipRecord.id); 393 | var arrayHasIds = (relationshipRecord.length && relationshipRecord.isEvery("id")); 394 | var isValidRelationship = (objectHasId || arrayHasIds); 395 | 396 | if (isValidRelationship) { 397 | if (!payload._embedded) { 398 | payload._embedded = {}; 399 | } 400 | 401 | payload._embedded[relationshipName] = relationshipRecord; 402 | if (relationshipRecord.length) { 403 | payload[relationshipName] = relationshipRecord.mapBy('id'); 404 | } else { 405 | payload[relationshipName] = relationshipRecord.id; 406 | } 407 | } 408 | 409 | if (this.isArray(payload[relationshipName])) { 410 | payload[relationshipName] = payload[relationshipName].filter(function(id) { 411 | return id; 412 | }); 413 | } 414 | 415 | return payload; 416 | }, 417 | 418 | 419 | isArray: function(value) { 420 | return Object.prototype.toString.call(value) === '[object Array]'; 421 | }, 422 | 423 | /** 424 | * Same as `loadRelationships`, but for an array of records. 425 | * 426 | * @method loadRelationshipsForMany 427 | * @private 428 | * @param {DS.Model} type 429 | * @param {Object} recordsArray 430 | */ 431 | loadRelationshipsForMany: function(store, type, recordsArray) { 432 | var adapter = this, 433 | promise = resolve(A([])); 434 | 435 | /** 436 | * Create a chain of promises, so the records are loaded sequentially. 437 | * Think of the variable promise as of the accumulator in a left fold. 438 | */ 439 | recordsArray.forEach(function(record) { 440 | promise = promise.then(function(records) { 441 | return adapter.loadRelationships(store, type, record) 442 | .then(function(loadedRecord) { 443 | records.push(loadedRecord); 444 | return records; 445 | }); 446 | }); 447 | }); 448 | 449 | return promise; 450 | }, 451 | 452 | 453 | /** 454 | * 455 | * @method relationshipProperties 456 | * @private 457 | * @param {DS.Model} type 458 | * @param {String} relationName 459 | */ 460 | relationshipProperties: function(type, relationName) { 461 | var relationships = get(type, 'relationshipsByName'); 462 | if (relationName) { 463 | return relationships.get(relationName); 464 | } else { 465 | return relationships; 466 | } 467 | } 468 | }); 469 | 470 | export default LSAdapter; 471 | -------------------------------------------------------------------------------- /addon/index.js: -------------------------------------------------------------------------------- 1 | import LSAdapter from 'ember-localstorage-adapter/adapters/ls-adapter'; 2 | import LSSerializer from 'ember-localstorage-adapter/serializers/ls-serializer'; 3 | 4 | export { 5 | LSAdapter, 6 | LSSerializer 7 | }; 8 | 9 | export default LSAdapter; 10 | -------------------------------------------------------------------------------- /addon/serializers/ls-serializer.js: -------------------------------------------------------------------------------- 1 | import { typeOf } from '@ember/utils'; 2 | import { A } from '@ember/array'; 3 | import DS from 'ember-data'; 4 | 5 | export default DS.JSONSerializer.extend({ 6 | /** 7 | * Invokes the new serializer API. 8 | * This should be removed in 2.0 9 | */ 10 | isNewSerializerAPI: true, 11 | 12 | serializeHasMany: function(snapshot, json, relationship) { 13 | var key = relationship.key; 14 | var payloadKey = this.keyForRelationship ? this.keyForRelationship(key, "hasMany") : key; 15 | var relationshipType = snapshot.type.determineRelationshipType(relationship, this.store); 16 | 17 | if (relationshipType === 'manyToNone' || 18 | relationshipType === 'manyToMany' || 19 | relationshipType === 'manyToOne') { 20 | json[payloadKey] = snapshot.hasMany(key, { ids: true }); 21 | // TODO support for polymorphic manyToNone and manyToMany relationships 22 | } 23 | }, 24 | 25 | /** 26 | * Extracts whatever was returned from the adapter. 27 | * 28 | * If the adapter returns relationships in an embedded way, such as follows: 29 | * 30 | * ```js 31 | * { 32 | * "id": 1, 33 | * "title": "Rails Rambo", 34 | * 35 | * "_embedded": { 36 | * "comment": [{ 37 | * "id": 1, 38 | * "comment_title": "FIRST" 39 | * }, { 40 | * "id": 2, 41 | * "comment_title": "Rails is unagi" 42 | * }] 43 | * } 44 | * } 45 | * 46 | * this method will create separated JSON for each resource and then combine 47 | * the data and the embed payload into the JSON.Api spec for included objects 48 | * returning a single object. 49 | * 50 | * @method extractSingle 51 | * @private 52 | * @param {DS.Store} store the returned store 53 | * @param {DS.Model} type the type/model 54 | * @param {Object} payload returned JSON 55 | */ 56 | normalizeSingleResponse: function(store, type, payload) { 57 | var included = A([]); 58 | if (payload && payload._embedded) { 59 | var forEachFunc = (record) => { 60 | included.pushObject(this.normalize(relType,record).data); 61 | }; 62 | 63 | for (var relation in payload._embedded) { 64 | var relType = type.typeForRelationship(relation,store); 65 | var embeddedPayload = payload._embedded[relation]; 66 | 67 | if (embeddedPayload) { 68 | if (typeOf(embeddedPayload) === 'array') { 69 | embeddedPayload.forEach(forEachFunc); 70 | } else { 71 | included.pushObject(this.normalize(relType, embeddedPayload).data); 72 | } 73 | } 74 | } 75 | 76 | delete payload._embedded; 77 | } 78 | 79 | var normalPayload = this.normalize(type, payload); 80 | if(included.length > 0){ 81 | normalPayload.included = included; 82 | } 83 | return normalPayload; 84 | }, 85 | 86 | /** 87 | * This is exactly the same as extractSingle, but used in an array. 88 | * 89 | * @method extractSingle 90 | * @private 91 | * @param {DS.Store} store the returned store 92 | * @param {DS.Model} type the type/model 93 | * @param {Array} payload returned JSONs 94 | */ 95 | normalizeArrayResponse: function(store, type, payload) { 96 | var response = { data: A([]), included: A([]) }; 97 | payload.forEach((json) => { 98 | var normalized = this.normalizeSingleResponse(store, type, json); 99 | response.data.pushObject(normalized.data); 100 | 101 | if(normalized.included){ 102 | normalized.included.forEach(function(included){ 103 | if(!response.included.includes(included.id)){ 104 | response.included.addObject(included); 105 | } 106 | }); 107 | } 108 | }); 109 | 110 | return response; 111 | } 112 | }); 113 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/app/.gitkeep -------------------------------------------------------------------------------- /app/adapters/ls-adapter.js: -------------------------------------------------------------------------------- 1 | import LSAdapter from 'ember-localstorage-adapter/adapters/ls-adapter'; 2 | 3 | export default LSAdapter; 4 | -------------------------------------------------------------------------------- /app/serializers/ls-serializer.js: -------------------------------------------------------------------------------- 1 | import LSSerializer from 'ember-localstorage-adapter/serializers/ls-serializer'; 2 | 3 | export default LSSerializer; 4 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = function() { 6 | return Promise.all([ 7 | getChannelURL('release'), 8 | getChannelURL('beta'), 9 | getChannelURL('canary'), 10 | ]).then((urls) => { 11 | return { 12 | scenarios: [ 13 | { 14 | name: 'ember-lts-2.12', 15 | npm: { 16 | devDependencies: { 17 | 'ember-source': '~2.12.0', 18 | 'ember-data': '~2.12.0' 19 | } 20 | } 21 | }, 22 | { 23 | name: 'ember-lts-2.16', 24 | npm: { 25 | devDependencies: { 26 | 'ember-source': '~2.16.0', 27 | 'ember-data': '~2.16.0' 28 | } 29 | } 30 | }, 31 | { 32 | name: 'ember-lts-2.18', 33 | npm: { 34 | devDependencies: { 35 | 'ember-source': '~2.18.0', 36 | 'ember-data': '~2.18.0' 37 | } 38 | } 39 | }, 40 | { 41 | name: 'ember-lts-3.4', 42 | npm: { 43 | devDependencies: { 44 | 'ember-source': '~3.4.0', 45 | 'ember-data': '~3.4.0' 46 | } 47 | } 48 | }, 49 | { 50 | name: 'ember-release', 51 | npm: { 52 | devDependencies: { 53 | 'ember-source': urls[0] 54 | } 55 | } 56 | }, 57 | { 58 | name: 'ember-beta', 59 | npm: { 60 | devDependencies: { 61 | 'ember-source': urls[1] 62 | } 63 | } 64 | }, 65 | { 66 | name: 'ember-canary', 67 | npm: { 68 | devDependencies: { 69 | 'ember-source': urls[2] 70 | } 71 | } 72 | }, 73 | { 74 | name: 'ember-default', 75 | npm: { 76 | devDependencies: {} 77 | } 78 | } 79 | ] 80 | }; 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: 'ember-localstorage-adapter' 5 | }; 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | {"compilerOptions":{"target":"es6","experimentalDecorators":true},"exclude":["node_modules","bower_components","tmp","vendor",".git","dist"]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-localstorage-adapter", 3 | "version": "1.0.0", 4 | "description": "Store your Ember application data in LocalStorage.", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "license": "MIT", 9 | "author": "Ryan Florence ", 10 | "directories": { 11 | "doc": "doc", 12 | "test": "tests" 13 | }, 14 | "repository": "https://github.com/locks/ember-localstorage-adapter", 15 | "scripts": { 16 | "build": "ember build", 17 | "lint:js": "eslint ./*.js addon app config tests", 18 | "start": "ember serve", 19 | "test": "ember try:each" 20 | }, 21 | "dependencies": { 22 | "ember-cli-babel": "^7.5.0", 23 | "ember-runtime-enumerable-includes-polyfill": "^2.1.0" 24 | }, 25 | "devDependencies": { 26 | "broccoli-asset-rev": "^3.0.0", 27 | "ember-ajax": "^5.0.0", 28 | "ember-cli": "^3.8.1", 29 | "ember-cli-dependency-checker": "^3.1.0", 30 | "ember-cli-eslint": "^5.1.0", 31 | "ember-cli-htmlbars": "^4.2.2", 32 | "ember-cli-htmlbars-inline-precompile": "^3.0.1", 33 | "ember-cli-inject-live-reload": "^2.0.1", 34 | "ember-cli-qunit": "^4.1.1", 35 | "ember-cli-shims": "^1.2.0", 36 | "ember-cli-sri": "^2.1.0", 37 | "ember-cli-uglify": "^3.0.0", 38 | "ember-data": "^3.8.0", 39 | "ember-disable-prototype-extensions": "^1.1.2", 40 | "ember-export-application-global": "^2.0.0", 41 | "ember-load-initializers": "^2.0.0", 42 | "ember-maybe-import-regenerator": "^0.1.6", 43 | "ember-resolver": "^7.0.0", 44 | "ember-source": "^3.8.0", 45 | "ember-source-channel-url": "^2.0.1", 46 | "ember-try": "^1.1.0", 47 | "eslint-plugin-ember": "^7.7.2", 48 | "eslint-plugin-node": "^11.0.0", 49 | "loader.js": "^4.2.3" 50 | }, 51 | "engines": { 52 | "node": ">= 8.*" 53 | }, 54 | "contributors": [ 55 | { 56 | "name": "Alexandre de Oliveira", 57 | "email": "chavedomundo@gmail.com" 58 | }, 59 | { 60 | "name": "Daniel Ochoa", 61 | "email": "dannytenaglias@gmail.com" 62 | } 63 | ], 64 | "bugs": { 65 | "url": "https://github.com/locks/ember-localstorage-adapter/issues" 66 | }, 67 | "homepage": "https://github.com/locks/ember-localstorage-adapter", 68 | "ember-addon": { 69 | "configPath": "tests/dummy/config" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | mode: 'ci', 13 | args: [ 14 | // --no-sandbox is needed when running Chrome inside a container 15 | process.env.TRAVIS ? '--no-sandbox' : null, 16 | 17 | '--disable-gpu', 18 | '--headless', 19 | '--remote-debugging-port=0', 20 | '--window-size=1440,900' 21 | ].filter(Boolean) 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | }); 11 | 12 | export default Router; 13 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

Welcome to Ember

2 | 3 | {{outlet}} -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/helpers/fixtures.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'list': { 3 | records: { 4 | 'l1': { id: 'l1', name: 'one', done: true, items: ['i1', 'i2'] }, 5 | 'l2': { id: 'l2', name: 'two', done: false, items: [] }, 6 | 'l3': { id: 'l3', name: 'three', done: false, items: [] } 7 | } 8 | }, 9 | 10 | 'item': { 11 | records: { 12 | 'i1': { id: 'i1', name: 'one', list: 'l1' }, 13 | 'i2': { id: 'i2', name: 'two', list: 'l1' } 14 | } 15 | }, 16 | 17 | 'order': { 18 | records: { 19 | 'o1': { id: 'o1', name: 'one', done: true, hours: ['h1', 'h2'] }, 20 | 'o2': { id: 'o2', name: 'two', done: false, hours: [] }, 21 | 'o3': { id: 'o3', name: 'three', done: true, hours: ['h3', 'h4'] }, 22 | 'o4': { id: 'o4', name: 'four', done: true, hours: [] } 23 | } 24 | }, 25 | 26 | 'hour': { 27 | records: { 28 | 'h1': { id: 'h1', name: 'one', amount: 4, order: 'o1' }, 29 | 'h2': { id: 'h2', name: 'two', amount: 3, order: 'o1' }, 30 | 'h3': { id: 'h3', name: 'three', amount: 2, order: 'o3' }, 31 | 'h4': { id: 'h4', name: 'four', amount: 1, order: 'o3' } 32 | } 33 | } 34 | }; 35 | 36 | -------------------------------------------------------------------------------- /tests/helpers/owner.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import Ember from 'ember'; 3 | 4 | let Owner; 5 | 6 | if (Ember._RegistryProxyMixin && Ember._ContainerProxyMixin) { 7 | Owner = EmberObject.extend(Ember._RegistryProxyMixin, Ember._ContainerProxyMixin); 8 | } else { 9 | Owner = EmberObject.extend(); 10 | } 11 | 12 | export default Owner; 13 | -------------------------------------------------------------------------------- /tests/helpers/store.js: -------------------------------------------------------------------------------- 1 | import { dasherize } from '@ember/string'; 2 | import Ember from 'ember'; 3 | import DS from 'ember-data'; 4 | import Owner from 'dummy/tests/helpers/owner'; 5 | import LSSerializer from 'ember-localstorage-adapter/serializers/ls-serializer'; 6 | 7 | export default function setupStore(options) { 8 | let container, registry, owner; 9 | let env = {}; 10 | options = options || {}; 11 | 12 | if (Ember.Registry) { 13 | registry = env.registry = new Ember.Registry(); 14 | owner = Owner.create({ 15 | __registry__: registry 16 | }); 17 | container = env.container = registry.container({owner}); 18 | owner.__container__ = container; 19 | } else { 20 | container = env.container = new Ember.Container(); 21 | registry = env.registry = container; 22 | } 23 | 24 | env.replaceContainerNormalize = function replaceContainerNormalize(fn) { 25 | if (env.registry) { 26 | env.registry.normalize = fn; 27 | } else { 28 | env.container.normalize = fn; 29 | } 30 | }; 31 | 32 | let adapter = env.adapter = (options.adapter || '-default'); 33 | delete options.adapter; 34 | 35 | if (typeof adapter !== 'string') { 36 | env.registry.register('adapter:-default', adapter); 37 | adapter = '-default'; 38 | } 39 | 40 | for (var prop in options) { 41 | registry.register(`model:${dasherize(prop)}`, options[prop]); 42 | } 43 | 44 | registry.register('service:store', DS.Store.extend({adapter})); 45 | 46 | registry.optionsForType('serializer', { singleton: false }); 47 | registry.optionsForType('adapter', { singleton: false }); 48 | 49 | registry.register('serializer:-default', LSSerializer); 50 | 51 | env.store = container.lookup('service:store'); 52 | env.serializer = container.lookup('serializer:-default'); 53 | env.adapter = container.lookup('adapter:-default'); 54 | 55 | return env; 56 | } 57 | 58 | const transforms = { 59 | 'boolean': DS.BooleanTransform.create(), 60 | 'date': DS.DateTransform.create(), 61 | 'number': DS.NumberTransform.create(), 62 | 'string': DS.StringTransform.create() 63 | }; 64 | 65 | // Prevent all tests involving serialization to require a container 66 | DS.JSONSerializer.reopen({ 67 | transformFor: function(attributeType) { 68 | return this._super(attributeType, true) || transforms[attributeType]; 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/integration/adapters/ls-adapter-test.js: -------------------------------------------------------------------------------- 1 | /* global localStorage */ 2 | import { all } from 'rsvp'; 3 | 4 | import { isEmpty } from '@ember/utils'; 5 | import { run } from '@ember/runloop'; 6 | import { set, get } from '@ember/object'; 7 | import setupStore from 'dummy/tests/helpers/store'; 8 | import FIXTURES from 'dummy/tests/helpers/fixtures'; 9 | import DS from 'ember-data'; 10 | import LSAdapter from 'ember-localstorage-adapter/adapters/ls-adapter'; 11 | 12 | import { module, test } from 'qunit'; 13 | 14 | let env, store, List, Item, Order, Hour, Person; 15 | 16 | module('integration/adapters/ls-adapter - LSAdapter', { 17 | beforeEach() { 18 | localStorage.setItem('DS.LSAdapter', JSON.stringify(FIXTURES)); 19 | 20 | List = DS.Model.extend({ 21 | name: DS.attr('string'), 22 | done: DS.attr('boolean'), 23 | items: DS.hasMany('item', { async: true }) 24 | }); 25 | 26 | Item = DS.Model.extend({ 27 | name: DS.attr('string'), 28 | list: DS.belongsTo('list', { async: true }) 29 | }); 30 | 31 | Order = DS.Model.extend({ 32 | name: DS.attr('string'), 33 | b: DS.attr('boolean'), 34 | hours: DS.hasMany('hour', { async: true }) 35 | }); 36 | 37 | Hour = DS.Model.extend({ 38 | name: DS.attr('string'), 39 | amount: DS.attr('number'), 40 | order: DS.belongsTo('order', { async: true }) 41 | }); 42 | 43 | Person = DS.Model.extend({ 44 | name: DS.attr('string'), 45 | birthdate: DS.attr('date') 46 | }); 47 | 48 | env = setupStore({ 49 | list: List, 50 | item: Item, 51 | order: Order, 52 | hour: Hour, 53 | person: Person, 54 | adapter: LSAdapter 55 | }); 56 | store = env.store; 57 | }, 58 | 59 | afterEach() { 60 | run(store, 'destroy'); 61 | } 62 | }); 63 | 64 | test('exists through the store', function(assert) { 65 | const lsAdapter = store.adapterFor('-default'); 66 | const lsSerializer = store.serializerFor('-default'); 67 | assert.ok(lsAdapter, 'LSAdapter exists'); 68 | assert.ok(lsSerializer, 'LSSerializer exists'); 69 | }); 70 | 71 | test('find with id', function(assert) { 72 | assert.expect(3); 73 | const done = assert.async(); 74 | run(store, 'findRecord', 'list', 'l1').then(list => { 75 | assert.equal(get(list, 'id'), 'l1', 'id is loaded correctly'); 76 | assert.equal(get(list, 'name'), 'one', 'name is loaded correctly'); 77 | assert.equal(get(list, 'done'), true, 'done is loaded correctly'); 78 | done(); 79 | }); 80 | }); 81 | 82 | test('find rejects promise for non-existing record', function(assert) { 83 | assert.expect(1); 84 | const done = assert.async(); 85 | // using run like on the other tests makes the test fail. 86 | run(() => { 87 | store.findRecord('list', 'unknown').catch(() => { 88 | assert.ok(true); 89 | done(); 90 | }); 91 | }); 92 | }); 93 | 94 | test('query', function(assert) { 95 | assert.expect(2); 96 | const done = assert.async(2); 97 | 98 | run(store, 'query', 'list', { name: /one|two/ }).then(records => { 99 | assert.equal(get(records, 'length'), 2, 'found results for /one|two/'); 100 | done(); 101 | }); 102 | run(store, 'query', 'list', { name: /.+/, id: /l1/ }).then(records => { 103 | assert.equal(get(records, 'length'), 1, 'found results for {name: /.+/, id: /l1/}'); 104 | done(); 105 | }); 106 | }); 107 | 108 | test('query resolves empty when there are no records', function(assert) { 109 | const done = assert.async(); 110 | assert.expect(2); 111 | run(store, 'query', 'list', { name: /unknown/ }).then(list => { 112 | assert.ok(isEmpty(list)); 113 | assert.equal(store.hasRecordForId('list', 'unknown'), false); 114 | done(); 115 | }); 116 | }); 117 | 118 | test('findAll', function(assert) { 119 | assert.expect(4); 120 | const done = assert.async(); 121 | 122 | run(store, 'findAll', 'list').then(records => { 123 | assert.equal(get(records, 'length'), 3, '3 items were found'); 124 | const [firstRecord, secondRecord, thirdRecord] = records.toArray(); 125 | assert.equal(get(firstRecord, 'name'), 'one', 'First item name is one'); 126 | assert.equal(get(secondRecord, 'name'), 'two', 'Second item name is two'); 127 | assert.equal(get(thirdRecord, 'name'), 'three', 'Third item name is three'); 128 | done(); 129 | }); 130 | }); 131 | 132 | test('queryMany', function(assert) { 133 | assert.expect(11); 134 | const done = assert.async(); 135 | run(store, 'query', 'order', { done: true }).then(records => { 136 | const [firstRecord, secondRecord, thirdRecord] = records.toArray(); 137 | assert.equal(get(records, 'length'), 3, '3 orders were found'); 138 | assert.equal(get(firstRecord, 'name'), 'one', 'First\'s order name is one'); 139 | assert.equal(get(secondRecord, 'name'), 'three', 'Second\'s order name is three'); 140 | assert.equal(get(thirdRecord, 'name'), 'four', 'Third\'s order name is four'); 141 | 142 | const firstHours = firstRecord.get('hours'), 143 | secondHours = secondRecord.get('hours'), 144 | thirdHours = thirdRecord.get('hours'); 145 | 146 | assert.equal(get(firstHours, 'length'), 2, 'Order one has two hours'); 147 | assert.equal(get(secondHours, 'length'), 2, 'Order three has two hours'); 148 | assert.equal(get(thirdHours, 'length'), 0, 'Order four has no hours'); 149 | 150 | const [hourOne, hourTwo] = firstHours.toArray(); 151 | const [hourThree, hourFour] = secondHours.toArray(); 152 | assert.equal(get(hourOne, 'amount'), 4, 'Hour one has amount of 4'); 153 | assert.equal(get(hourTwo, 'amount'), 3, 'Hour two has amount of 3'); 154 | assert.equal(get(hourThree, 'amount'), 2, 'Hour three has amount of 2'); 155 | assert.equal(get(hourFour, 'amount'), 1, 'Hour four has amount of 1'); 156 | done(); 157 | }); 158 | }); 159 | 160 | test('createRecord', function(assert) { 161 | assert.expect(5); 162 | const done = assert.async(2); 163 | const list = run(store, 'createRecord', 'list', { name: 'Rambo' }); 164 | 165 | run(list, 'save').then(() => { 166 | store.query('list', { name: 'Rambo' }).then(records => { 167 | let record = records.objectAt(0); 168 | 169 | assert.equal(get(records, 'length'), 1, 'Only Rambo was found'); 170 | assert.equal(get(record, 'name'), 'Rambo', 'Correct name'); 171 | assert.equal(get(record, 'id'), list.id, 'Correct, original id'); 172 | done(); 173 | }); 174 | }); 175 | 176 | run(list, 'save').then(() => { 177 | store.findRecord('list', list.id).then(record => { 178 | assert.equal(get(record, 'name'), 'Rambo', 'Correct name'); 179 | assert.equal(get(record, 'id'), list.id, 'Correct, original id'); 180 | done(); 181 | }); 182 | }); 183 | }); 184 | 185 | test('updateRecords', function(assert) { 186 | assert.expect(3); 187 | const done = assert.async(); 188 | const list = run(store, 'createRecord', 'list', { name: 'Rambo' }); 189 | 190 | run(list, 'save').then(list => { 191 | return store.query('list', { name: 'Rambo' }).then(records => { 192 | let record = records.objectAt(0); 193 | record.set('name', 'Macgyver'); 194 | return record.save(); 195 | }).then(() => { 196 | return store.query('list', { name: 'Macgyver' }).then(records => { 197 | let record = records.objectAt(0); 198 | assert.equal(get(records, 'length'), 1, 'Only one record was found'); 199 | assert.equal(get(record, 'name'), 'Macgyver', 'Updated name shows up'); 200 | assert.equal(get(record, 'id'), list.id, 'Correct, original id'); 201 | done(); 202 | }); 203 | }); 204 | }); 205 | }); 206 | 207 | test('deleteRecord', function(assert) { 208 | assert.expect(2); 209 | const done = assert.async(); 210 | 211 | const assertListIsDeleted = () => { 212 | return store.query('list', { name: 'one' }).then(list => { 213 | assert.ok(isEmpty(list), 'List was deleted'); 214 | done(); 215 | }); 216 | }; 217 | 218 | run(() => { 219 | store.query('list', { name: 'one' }).then(lists => { 220 | const list = lists.objectAt(0); 221 | assert.equal(get(list, 'id'), 'l1', 'Item exists'); 222 | list.deleteRecord(); 223 | list.on('didDelete', assertListIsDeleted); 224 | list.save(); 225 | }); 226 | }); 227 | }); 228 | 229 | test('changes in bulk', function(assert) { 230 | assert.expect(3); 231 | const done = assert.async(); 232 | let promises; 233 | 234 | let listToUpdate = run(store, 'findRecord', 'list', 'l1'), 235 | listToDelete = run(store, 'findRecord', 'list', 'l2'), 236 | listToCreate = run(store, 'createRecord', 'list', { name: 'Rambo' }); 237 | 238 | const updateList = (list) => { 239 | set(list, 'name', 'updatedName'); 240 | return list; 241 | }; 242 | 243 | const deleteList = (list) => { 244 | run(list, 'deleteRecord'); 245 | return list; 246 | }; 247 | 248 | promises = [ 249 | listToCreate, 250 | listToUpdate.then(updateList), 251 | listToDelete.then(deleteList) 252 | ]; 253 | 254 | all(promises).then(lists => { 255 | return lists.map(list => { 256 | return list.save(); 257 | }); 258 | }).then(() => { 259 | 260 | let createdList = store.query('list', { name: 'Rambo' }).then(lists => { 261 | return assert.equal(get(lists, 'length'), 1, 'Record was created successfully'); 262 | }); 263 | let deletedList = store.findRecord('list', 'l2').then(list => { 264 | return assert.equal(get(list, 'length'), undefined, 'Record was deleted successfully'); 265 | }); 266 | let updatedList = store.findRecord('list', 'l1').then(list => { 267 | return assert.equal(get(list, 'name'), 'updatedName', 'Record was updated successfully'); 268 | }); 269 | 270 | return all([createdList, deletedList, updatedList]).then(done); 271 | }); 272 | }); 273 | 274 | test('load hasMany association', function(assert) { 275 | assert.expect(4); 276 | const done = assert.async(); 277 | 278 | run(store, 'findRecord', 'list', 'l1').then(list => { 279 | let items = get(list, 'items'); 280 | 281 | let firstItem = get(items, 'firstObject'), 282 | lastItem = get(items, 'lastObject'); 283 | 284 | assert.equal(get(firstItem, 'id'), 'i1', 'first item id is loaded correctly'); 285 | assert.equal(get(firstItem, 'name'), 'one', 'first item name is loaded correctly'); 286 | assert.equal(get(lastItem, 'id'), 'i2', 'last item id is loaded correctly'); 287 | assert.equal(get(lastItem, 'name'), 'two', 'last item name is loaded correctly'); 288 | done(); 289 | }); 290 | }); 291 | 292 | test('load belongsTo association', function(assert) { 293 | assert.expect(2); 294 | const done = assert.async(); 295 | 296 | run(store, 'findRecord', 'item', 'i1').then(item => { 297 | return get(item, 'list'); 298 | }).then(list => { 299 | assert.equal(get(list, 'id'), 'l1', 'id is loaded correctly'); 300 | assert.equal(get(list, 'name'), 'one', 'name is loaded correctly'); 301 | done(); 302 | }); 303 | }); 304 | 305 | test('saves belongsTo', function(assert) { 306 | assert.expect(2); 307 | let item, listId = 'l2'; 308 | const done = assert.async(); 309 | 310 | run(store, 'findRecord', 'list', listId).then(list => { 311 | item = store.createRecord('item', { name: 'three thousand' }); 312 | set(item, 'list', list); 313 | 314 | return all([list.save(), item.save()]); 315 | 316 | }).then(([, item]) => { 317 | 318 | store.unloadAll('item'); 319 | return store.findRecord('item', get(item, 'id')); 320 | }).then(item => { 321 | let list = get(item, 'list'); 322 | assert.ok(get(item, 'list'), 'list is present'); 323 | assert.equal(get(list, 'id'), listId, 'list is retrieved correctly'); 324 | done(); 325 | }); 326 | }); 327 | 328 | test('saves hasMany', function(assert) { 329 | assert.expect(1); 330 | let listId = 'l2'; 331 | const done = assert.async(); 332 | 333 | let list = run(store, 'findRecord', 'list', listId); 334 | let item = run(store, 'createRecord', 'item', { name: 'three thousand' }); 335 | 336 | return all([list, item]).then(([list, item]) => { 337 | get(list, 'items').pushObject(item); 338 | return all([list.save(), item.save()]); 339 | }).then(() => { 340 | store.unloadAll('list'); 341 | return store.findRecord('list', listId); 342 | }).then(list => { 343 | let item = get(list, 'items').objectAt(0); 344 | assert.equal(get(item, 'name'), 'three thousand', 'item is saved'); 345 | done(); 346 | }); 347 | }); 348 | 349 | test('date is loaded correctly', function(assert) { 350 | assert.expect(2); 351 | const done = assert.async(); 352 | 353 | const date = new Date(1982, 5, 18); 354 | const person = run(store, 'createRecord', 'person', { 355 | name: 'Dan', birthdate: date 356 | }); 357 | 358 | return run(person, 'save').then(() => { 359 | return store.query('person', { name: 'Dan' }).then(records => { 360 | const loadedPerson = get(records, 'firstObject'); 361 | const birthdate = get(loadedPerson, 'birthdate'); 362 | assert.ok((birthdate instanceof Date), 'Date should be loaded as an instance of Date'); 363 | assert.equal(birthdate.getTime(), date.getTime(), 'Date content should be loaded correctly'); 364 | done(); 365 | }); 366 | }); 367 | }); 368 | 369 | test('handles localStorage being unavailable', function(assert) { 370 | assert.expect(3); 371 | const done = assert.async(); 372 | 373 | let calledGetnativeStorage = false; 374 | const handler = () => { 375 | calledGetnativeStorage = true; 376 | }; 377 | var adapter = store.get('defaultAdapter'); 378 | 379 | // We can't actually disable localStorage in PhantomJS, so emulate as closely as possible by 380 | // causing a wrapper method on the adapter to throw. 381 | adapter.getNativeStorage = function() { throw new Error('Nope.'); }; 382 | adapter.on('persistenceUnavailable', handler); 383 | 384 | var person = run(store, 'createRecord', 'person', { id: 'tom', name: 'Tom' }); 385 | assert.notOk(calledGetnativeStorage, 'Should not trigger `persistenceUnavailable` until actually trying to persist'); 386 | 387 | run(person, 'save').then(() => { 388 | assert.ok(calledGetnativeStorage, 'Saving a record without local storage should trigger `persistenceUnavailable`'); 389 | store.unloadRecord(person); 390 | return store.findRecord('person', 'tom'); 391 | }).then((reloadedPerson) => { 392 | assert.equal(get(reloadedPerson, 'name'), 'Tom', 'Records should still persist in-memory without local storage'); 393 | done(); 394 | }); 395 | }); 396 | -------------------------------------------------------------------------------- /tests/integration/serializers/ls-serializer-test.js: -------------------------------------------------------------------------------- 1 | /* global localStorage */ 2 | import { all } from 'rsvp'; 3 | 4 | import { run } from '@ember/runloop'; 5 | import setupStore from 'dummy/tests/helpers/store'; 6 | import FIXTURES from 'dummy/tests/helpers/fixtures'; 7 | import DS from 'ember-data'; 8 | import LSAdapter from 'ember-localstorage-adapter/adapters/ls-adapter'; 9 | 10 | import { module, test } from 'qunit'; 11 | 12 | let env, store, registry, List, Item; 13 | 14 | module('integration/serializers/ls-serializer - LSSerializer', { 15 | beforeEach() { 16 | localStorage.setItem('DS.LSAdapter', JSON.stringify(FIXTURES)); 17 | 18 | List = DS.Model.extend({ 19 | name: DS.attr('string'), 20 | done: DS.attr('boolean'), 21 | items: DS.hasMany('item', {async: true}) 22 | }); 23 | 24 | Item = DS.Model.extend({ 25 | name: DS.attr('string'), 26 | list: DS.belongsTo('list', {async: true}) 27 | }); 28 | 29 | env = setupStore({ 30 | list: List, 31 | item: Item, 32 | adapter: LSAdapter 33 | }); 34 | store = env.store; 35 | registry = env.registry; 36 | }, 37 | 38 | afterEach() { 39 | run(store, 'destroy'); 40 | } 41 | }); 42 | 43 | test('serializeHasMany respects keyForRelationship', function(assert) { 44 | assert.expect(1); 45 | const done = assert.async(); 46 | store.serializerFor('list').reopen({ 47 | keyForRelationship(key /*type*/) { 48 | return key.toUpperCase(); 49 | } 50 | }); 51 | 52 | const list = run(store, 'createRecord', 'list', {name: 'Rails is omakase', id: 1}); 53 | const comment = run(store, 'createRecord', 'item', {name: 'Omakase is delicious', list, id: 1}); 54 | 55 | return all([list, comment]).then(() => { 56 | let json = {}; 57 | const snapshot = list._createSnapshot(); 58 | store.serializerFor('list').serializeHasMany(snapshot, json, { 59 | key: 'items', options: {} 60 | }); 61 | assert.deepEqual(json, {ITEMS: ['1']}); 62 | 63 | registry.unregister('serializer:list'); 64 | done(); 65 | }); 66 | }); 67 | 68 | test('normalizeArrayResponse calls normalizeSingleResponse', function(assert) { 69 | assert.expect(1); 70 | const done = assert.async(); 71 | let callCount = 0; 72 | 73 | store.serializerFor('list').reopen({ 74 | normalizeSingleResponse: function(store, type, payload) { 75 | callCount++; 76 | return this.normalize(type, payload); 77 | } 78 | }); 79 | 80 | run(store, 'findAll', 'list').then(() => { 81 | assert.equal(callCount, 3); 82 | done(); 83 | }); 84 | 85 | registry.unregister('serializer:list'); 86 | }); 87 | 88 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/tests/unit/.gitkeep -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locks/ember-localstorage-adapter/cf42731c5765ccdb6992f42b2dc8175897c6e4ff/vendor/.gitkeep --------------------------------------------------------------------------------