UNPKG

js-data

Version:

Robust, framework-agnostic in-memory data store.

1,421 lines (1,368 loc) 72.5 kB
import utils from './utils' import { belongsToType, hasManyType, hasOneType } from './decorators' import { proxiedMapperMethods, Container } from './Container' import Collection from './Collection' const DOMAIN = 'SimpleStore' const proxiedCollectionMethods = [ /** * Wrapper for {@link Collection#add}. * * @example <caption>SimpleStore#add</caption> * const JSData = require('js-data'); * const { SimpleStore } = JSData; * console.log('Using JSData v' + JSData.version.full); * * const store = new SimpleStore(); * store.defineMapper('book'); * * // Add one book to the in-memory store: * store.add('book', { id: 1, title: 'Respect your Data' }); * // Add multiple books to the in-memory store: * store.add('book', [ * { id: 2, title: 'Easy data recipes' }, * { id: 3, title: 'Active Record 101' } * ]); * * @fires SimpleStore#add * @method SimpleStore#add * @param {(string|number)} name Name of the {@link Mapper} to target. * @param {(Object|Object[]|Record|Record[])} data See {@link Collection#add}. * @param {object} [opts] Configuration options. See {@link Collection#add}. * @returns {(Object|Object[]|Record|Record[])} See {@link Collection#add}. * @see Collection#add * @see Collection#add * @since 3.0.0 */ 'add', /** * Wrapper for {@link Collection#between}. * * @example * // Get all users ages 18 to 30 * const users = store.between('user', 18, 30, { index: 'age' }); * * @example * // Same as above * const users = store.between('user', [18], [30], { index: 'age' }); * * @method SimpleStore#between * @param {(string|number)} name Name of the {@link Mapper} to target. * @param {array} leftKeys See {@link Collection#between}. * @param {array} rightKeys See {@link Collection#between}. * @param {object} [opts] Configuration options. See {@link Collection#between}. * @returns {Object[]|Record[]} See {@link Collection#between}. * @see Collection#between * @see Collection#between * @since 3.0.0 */ 'between', /** * Wrapper for {@link Collection#createIndex}. * * @example * // Index users by age * store.createIndex('user', 'age'); * * @example * // Index users by status and role * store.createIndex('user', 'statusAndRole', ['status', 'role']); * * @method SimpleStore#createIndex * @param {(string|number)} name Name of the {@link Mapper} to target. * @param {string} name See {@link Collection#createIndex}. * @param {string[]} [fieldList] See {@link Collection#createIndex}. * @see Collection#createIndex * @see Collection#createIndex * @since 3.0.0 */ 'createIndex', /** * Wrapper for {@link Collection#filter}. * * @example <caption>SimpleStore#filter</caption> * const JSData = require('js-data'); * const { SimpleStore } = JSData; * console.log('Using JSData v' + JSData.version.full); * * const store = new SimpleStore(); * store.defineMapper('post'); * store.add('post', [ * { id: 1, status: 'draft', created_at_timestamp: new Date().getTime() } * ]); * * // Get the draft posts created less than three months ago * let posts = store.filter('post', { * where: { * status: { * '==': 'draft' * }, * created_at_timestamp: { * '>=': (new Date().getTime() - (1000 \* 60 \* 60 \* 24 \* 30 \* 3)) // 3 months ago * } * } * }); * console.log(posts); * * // Use a custom filter function * posts = store.filter('post', function (post) { return post.id % 2 === 0 }); * * @method SimpleStore#filter * @param {(string|number)} name Name of the {@link Mapper} to target. * @param {(Object|Function)} [queryOrFn={}] See {@link Collection#filter}. * @param {object} [thisArg] See {@link Collection#filter}. * @returns {Array} See {@link Collection#filter}. * @see Collection#filter * @see Collection#filter * @since 3.0.0 */ 'filter', /** * Wrapper for {@link Collection#get}. * * @example <caption>SimpleStore#get</caption> * const JSData = require('js-data'); * const { SimpleStore } = JSData; * console.log('Using JSData v' + JSData.version.full); * * const store = new SimpleStore(); * store.defineMapper('post'); * store.add('post', [ * { id: 1, status: 'draft', created_at_timestamp: new Date().getTime() } * ]); * * console.log(store.get('post', 1)); // {...} * console.log(store.get('post', 2)); // undefined * * @method SimpleStore#get * @param {(string|number)} name Name of the {@link Mapper} to target. * @param {(string|number)} id See {@link Collection#get}. * @returns {(Object|Record)} See {@link Collection#get}. * @see Collection#get * @see Collection#get * @since 3.0.0 */ 'get', /** * Wrapper for {@link Collection#getAll}. * * @example * // Get the posts where "status" is "draft" or "inReview" * const posts = store.getAll('post', 'draft', 'inReview', { index: 'status' }); * * @example * // Same as above * const posts = store.getAll('post', ['draft'], ['inReview'], { index: 'status' }); * * @method SimpleStore#getAll * @param {(string|number)} name Name of the {@link Mapper} to target. * @param {...Array} [keyList] See {@link Collection#getAll}. * @param {object} [opts] See {@link Collection#getAll}. * @returns {Array} See {@link Collection#getAll}. * @see Collection#getAll * @see Collection#getAll * @since 3.0.0 */ 'getAll', /** * Wrapper for {@link Collection#prune}. * * @method SimpleStore#prune * @param {object} [opts] See {@link Collection#prune}. * @returns {Array} See {@link Collection#prune}. * @see Collection#prune * @see Collection#prune * @since 3.0.0 */ 'prune', /** * Wrapper for {@link Collection#query}. * * @example * // Grab page 2 of users between ages 18 and 30 * store.query('user') * .between(18, 30, { index: 'age' }) // between ages 18 and 30 * .skip(10) // second page * .limit(10) // page size * .run(); * * @method SimpleStore#query * @param {(string|number)} name Name of the {@link Mapper} to target. * @returns {Query} See {@link Collection#query}. * @see Collection#query * @see Collection#query * @since 3.0.0 */ 'query', /** * Wrapper for {@link Collection#toJSON}. * * @example * store.defineMapper('post', { * schema: { * properties: { * id: { type: 'number' }, * title: { type: 'string' } * } * } * }); * store.add('post', [ * { id: 1, status: 'published', title: 'Respect your Data' }, * { id: 2, status: 'draft', title: 'Connecting to a data source' } * ]); * console.log(store.toJSON('post')); * const draftsJSON = store.query('post') * .filter({ status: 'draft' }) * .mapCall('toJSON') * .run(); * * @method SimpleStore#toJSON * @param {(string|number)} name Name of the {@link Mapper} to target. * @param {object} [opts] See {@link Collection#toJSON}. * @returns {Array} See {@link Collection#toJSON}. * @see Collection#toJSON * @see Collection#toJSON * @since 3.0.0 */ 'toJSON', /** * Wrapper for {@link Collection#unsaved}. * * @method SimpleStore#unsaved * @returns {Array} See {@link Collection#unsaved}. * @see Collection#unsaved * @see Collection#unsaved * @since 3.0.0 */ 'unsaved' ] const ownMethodsForScoping = [ 'addToCache', 'cachedFind', 'cachedFindAll', 'cacheFind', 'cacheFindAll', 'hashQuery' ] const cachedFn = function (name, hashOrId, opts) { const cached = this._completedQueries[name][hashOrId] if (utils.isFunction(cached)) { return cached(name, hashOrId, opts) } return cached } const SIMPLESTORE_DEFAULTS = { /** * Whether to use the pending query if a `find` request for the specified * record is currently underway. Can be set to `true`, `false`, or to a * function that returns `true` or `false`. * * @default true * @name SimpleStore#usePendingFind * @since 3.0.0 * @type {boolean|Function} */ usePendingFind: true, /** * Whether to use the pending query if a `findAll` request for the given query * is currently underway. Can be set to `true`, `false`, or to a function that * returns `true` or `false`. * * @default true * @name SimpleStore#usePendingFindAll * @since 3.0.0 * @type {boolean|Function} */ usePendingFindAll: true } /** * The `SimpleStore` class is an extension of {@link Container}. Not only does * `SimpleStore` manage mappers, but also collections. `SimpleStore` implements the * asynchronous {@link Mapper} methods, such as {@link Mapper#find} and * {@link Mapper#create}. If you use the asynchronous `SimpleStore` methods * instead of calling them directly on the mappers, then the results of the * method calls will be inserted into the store's collections. You can think of * a `SimpleStore` as an [Identity Map](https://en.wikipedia.org/wiki/Identity_map_pattern) * for the [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) * (the Mappers). * * ```javascript * import { SimpleStore } from 'js-data'; * ``` * * @example * import { SimpleStore } from 'js-data'; * import { HttpAdapter } from 'js-data-http'; * const store = new SimpleStore(); * * // SimpleStore#defineMapper returns a direct reference to the newly created * // Mapper. * const UserMapper = store.defineMapper('user'); * * // SimpleStore#as returns the store scoped to a particular Mapper. * const UserStore = store.as('user'); * * // Call "find" on "UserMapper" (Stateless ORM) * UserMapper.find(1).then((user) => { * // retrieved a "user" record via the http adapter, but that's it * * // Call "find" on "store" targeting "user" (Stateful SimpleStore) * return store.find('user', 1); // same as "UserStore.find(1)" * }).then((user) => { * // not only was a "user" record retrieved, but it was added to the * // store's "user" collection * const cachedUser = store.getCollection('user').get(1); * console.log(user === cachedUser); // true * }); * * @class SimpleStore * @extends Container * @param {object} [opts] Configuration options. See {@link Container}. * @param {boolean} [opts.collectionClass={@link Collection}] See {@link SimpleStore#collectionClass}. * @param {boolean} [opts.debug=false] See {@link Component#debug}. * @param {boolean|Function} [opts.usePendingFind=true] See {@link SimpleStore#usePendingFind}. * @param {boolean|Function} [opts.usePendingFindAll=true] See {@link SimpleStore#usePendingFindAll}. * @returns {SimpleStore} * @see Container * @since 3.0.0 * @tutorial ["http://www.js-data.io/v3.0/docs/components-of-jsdata#SimpleStore","Components of JSData: SimpleStore"] * @tutorial ["http://www.js-data.io/v3.0/docs/working-with-the-SimpleStore","Working with the SimpleStore"] * @tutorial ["http://www.js-data.io/v3.0/docs/jsdata-and-the-browser","Notes on using JSData in the Browser"] */ function SimpleStore (opts) { utils.classCallCheck(this, SimpleStore) opts || (opts = {}) // Fill in any missing options with the defaults utils.fillIn(opts, SIMPLESTORE_DEFAULTS) Container.call(this, opts) this.collectionClass = this.collectionClass || Collection this._collections = {} this._pendingQueries = {} this._completedQueries = {} } const props = { constructor: SimpleStore, /** * Internal method used to handle Mapper responses. * * @method SimpleStore#_end * @private * @param {string} name Name of the {@link Collection} to which to * add the data. * @param {object} result The result from a Mapper. * @param {object} [opts] Configuration options. * @returns {(Object|Array)} Result. */ _end (name, result, opts) { let data = opts.raw ? result.data : result if (data && utils.isFunction(this.addToCache)) { data = this.addToCache(name, data, opts) if (opts.raw) { result.data = data } else { result = data } } return result }, /** * Register a new event listener on this SimpleStore. * * Proxy for {@link Container#on}. If an event was emitted by a Mapper or * Collection in the SimpleStore, then the name of the Mapper or Collection will * be prepended to the arugments passed to the provided event handler. * * @example * // Listen for all "afterCreate" events in a SimpleStore * store.on('afterCreate', (mapperName, props, opts, result) => { * console.log(mapperName); // "post" * console.log(props.id); // undefined * console.log(result.id); // 1234 * }); * store.create('post', { title: 'Modeling your data' }).then((post) => { * console.log(post.id); // 1234 * }); * * @example * // Listen for the "add" event on a collection * store.on('add', (mapperName, records) => { * console.log(records); // [...] * }); * * @example * // Listen for "change" events on a record * store.on('change', (mapperName, record, changes) => { * console.log(changes); // { changed: { title: 'Modeling your data' } } * }); * post.title = 'Modeling your data'; * * @method SimpleStore#on * @param {string} event Name of event to subsribe to. * @param {Function} listener Listener function to handle the event. * @param {*} [ctx] Optional content in which to invoke the listener. */ /** * Used to bind to events emitted by collections in this store. * * @method SimpleStore#_onCollectionEvent * @private * @param {string} name Name of the collection that emitted the event. * @param {...*} [args] Args passed to {@link Collection#emit}. */ _onCollectionEvent (name, ...args) { const type = args.shift() this.emit(type, name, ...args) }, /** * This method takes the data received from {@link SimpleStore#find}, * {@link SimpleStore#findAll}, {@link SimpleStore#update}, etc., and adds the * data to the store. _You don't need to call this method directly._ * * If you're using the http adapter and your response data is in an unexpected * format, you may need to override this method so the right data gets added * to the store. * * @example * const store = new SimpleStore({ * addToCache (mapperName, data, opts) { * // Let's say for a particular Resource, response data is in a weird format * if (name === 'comment') { * // Re-assign the variable to add the correct records into the stores * data = data.items; * } * // Now perform default behavior * return SimpleStore.prototype.addToCache.call(this, mapperName, data, opts); * } * }); * * @example * // Extend using ES2015 class syntax. * class MyStore extends SimpleStore { * addToCache (mapperName, data, opts) { * // Let's say for a particular Resource, response data is in a weird format * if (name === 'comment') { * // Re-assign the variable to add the correct records into the stores * data = data.items; * } * // Now perform default behavior * return super.addToCache(mapperName, data, opts); * } * } * const store = new MyStore(); * * @method SimpleStore#addToCache * @param {string} name Name of the {@link Mapper} to target. * @param {*} data Data from which data should be selected for add. * @param {object} [opts] Configuration options. */ addToCache (name, data, opts) { return this.getCollection(name).add(data, opts) }, /** * Return the store scoped to a particular mapper/collection pair. * * @example <caption>SimpleStore.as</caption> * const JSData = require('js-data'); * const { SimpleStore } = JSData; * console.log('Using JSData v' + JSData.version.full); * * const store = new SimpleStore(); * const UserMapper = store.defineMapper('user'); * const UserStore = store.as('user'); * * const user1 = store.createRecord('user', { name: 'John' }); * const user2 = UserStore.createRecord({ name: 'John' }); * const user3 = UserMapper.createRecord({ name: 'John' }); * console.log(user1 === user2); * console.log(user2 === user3); * console.log(user1 === user3); * * @method SimpleStore#as * @param {string} name Name of the {@link Mapper}. * @returns {Object} The store, scoped to a particular Mapper/Collection pair. * @since 3.0.0 */ as (name) { const props = {} const original = this const methods = ownMethodsForScoping .concat(proxiedMapperMethods) .concat(proxiedCollectionMethods) methods.forEach(function (method) { props[method] = { writable: true, value (...args) { return original[method](name, ...args) } } }) props.getMapper = { writable: true, value () { return original.getMapper(name) } } props.getCollection = { writable: true, value () { return original.getCollection(name) } } return Object.create(this, props) }, /** * Retrieve a cached `find` result, if any. This method is called during * {@link SimpleStore#find} to determine if {@link Mapper#find} needs to be * called. If this method returns `undefined` then {@link Mapper#find} will * be called. Otherwise {@link SimpleStore#find} will immediately resolve with * the return value of this method. * * When using {@link SimpleStore} in the browser, you can override this method * to implement your own cache-busting strategy. * * @example * const store = new SimpleStore({ * cachedFind (mapperName, id, opts) { * // Let's say for a particular Resource, we always want to pull fresh from the server * if (mapperName === 'schedule') { * // Return undefined to trigger a Mapper#find call * return; * } * // Otherwise perform default behavior * return SimpleStore.prototype.cachedFind.call(this, mapperName, id, opts); * } * }); * * @example * // Extend using ES2015 class syntax. * class MyStore extends SimpleStore { * cachedFind (mapperName, id, opts) { * // Let's say for a particular Resource, we always want to pull fresh from the server * if (mapperName === 'schedule') { * // Return undefined to trigger a Mapper#find call * return; * } * // Otherwise perform default behavior * return super.cachedFind(mapperName, id, opts); * } * } * const store = new MyStore(); * * @method SimpleStore#cachedFind * @param {string} name The `name` argument passed to {@link SimpleStore#find}. * @param {(string|number)} id The `id` argument passed to {@link SimpleStore#find}. * @param {object} opts The `opts` argument passed to {@link SimpleStore#find}. * @since 3.0.0 */ cachedFind: cachedFn, /** * Retrieve a cached `findAll` result, if any. This method is called during * {@link SimpleStore#findAll} to determine if {@link Mapper#findAll} needs to be * called. If this method returns `undefined` then {@link Mapper#findAll} will * be called. Otherwise {@link SimpleStore#findAll} will immediately resolve with * the return value of this method. * * When using {@link SimpleStore} in the browser, you can override this method * to implement your own cache-busting strategy. * * @example * const store = new SimpleStore({ * cachedFindAll (mapperName, hash, opts) { * // Let's say for a particular Resource, we always want to pull fresh from the server * if (mapperName === 'schedule') { * // Return undefined to trigger a Mapper#findAll call * return undefined; * } * // Otherwise perform default behavior * return SimpleStore.prototype.cachedFindAll.call(this, mapperName, hash, opts); * } * }); * * @example * // Extend using ES2015 class syntax. * class MyStore extends SimpleStore { * cachedFindAll (mapperName, hash, opts) { * // Let's say for a particular Resource, we always want to pull fresh from the server * if (mapperName === 'schedule') { * // Return undefined to trigger a Mapper#findAll call * return undefined; * } * // Otherwise perform default behavior * return super.cachedFindAll(mapperName, hash, opts); * } * } * const store = new MyStore(); * * @method SimpleStore#cachedFindAll * @param {string} name The `name` argument passed to {@link SimpleStore#findAll}. * @param {string} hash The result of calling {@link SimpleStore#hashQuery} on * the `query` argument passed to {@link SimpleStore#findAll}. * @param {object} opts The `opts` argument passed to {@link SimpleStore#findAll}. * @since 3.0.0 */ cachedFindAll: cachedFn, /** * Mark a {@link Mapper#find} result as cached by adding an entry to * {@link SimpleStore#_completedQueries}. By default, once a `find` entry is * added it means subsequent calls to the same Resource with the same `id` * argument will immediately resolve with the result of calling * {@link SimpleStore#get} instead of delegating to {@link Mapper#find}. * * As part of implementing your own caching strategy, you may choose to * override this method. * * @example * const store = new SimpleStore({ * cacheFind (mapperName, data, id, opts) { * // Let's say for a particular Resource, we always want to pull fresh from the server * if (mapperName === 'schedule') { * // Return without saving an entry to SimpleStore#_completedQueries * return; * } * // Otherwise perform default behavior * return SimpleStore.prototype.cacheFind.call(this, mapperName, data, id, opts); * } * }); * * @example * // Extend using ES2015 class syntax. * class MyStore extends SimpleStore { * cacheFind (mapperName, data, id, opts) { * // Let's say for a particular Resource, we always want to pull fresh from the server * if (mapperName === 'schedule') { * // Return without saving an entry to SimpleStore#_completedQueries * return; * } * // Otherwise perform default behavior * return super.cacheFind(mapperName, data, id, opts); * } * } * const store = new MyStore(); * * @method SimpleStore#cacheFind * @param {string} name The `name` argument passed to {@link SimpleStore#find}. * @param {*} data The result to cache. * @param {(string|number)} id The `id` argument passed to {@link SimpleStore#find}. * @param {object} opts The `opts` argument passed to {@link SimpleStore#find}. * @since 3.0.0 */ cacheFind (name, data, id, opts) { this._completedQueries[name][id] = (name, id, opts) => this.get(name, id) }, /** * Mark a {@link Mapper#findAll} result as cached by adding an entry to * {@link SimpleStore#_completedQueries}. By default, once a `findAll` entry is * added it means subsequent calls to the same Resource with the same `query` * argument will immediately resolve with the result of calling * {@link SimpleStore#filter} instead of delegating to {@link Mapper#findAll}. * * As part of implementing your own caching strategy, you may choose to * override this method. * * @example * const store = new SimpleStore({ * cachedFindAll (mapperName, data, hash, opts) { * // Let's say for a particular Resource, we always want to pull fresh from the server * if (mapperName === 'schedule') { * // Return without saving an entry to SimpleStore#_completedQueries * return; * } * // Otherwise perform default behavior. * return SimpleStore.prototype.cachedFindAll.call(this, mapperName, data, hash, opts); * } * }); * * @example * // Extend using ES2015 class syntax. * class MyStore extends SimpleStore { * cachedFindAll (mapperName, data, hash, opts) { * // Let's say for a particular Resource, we always want to pull fresh from the server * if (mapperName === 'schedule') { * // Return without saving an entry to SimpleStore#_completedQueries * return; * } * // Otherwise perform default behavior. * return super.cachedFindAll(mapperName, data, hash, opts); * } * } * const store = new MyStore(); * * @method SimpleStore#cacheFindAll * @param {string} name The `name` argument passed to {@link SimpleStore#findAll}. * @param {*} data The result to cache. * @param {string} hash The result of calling {@link SimpleStore#hashQuery} on * the `query` argument passed to {@link SimpleStore#findAll}. * @param {object} opts The `opts` argument passed to {@link SimpleStore#findAll}. * @since 3.0.0 */ cacheFindAll (name, data, hash, opts) { this._completedQueries[name][hash] = (name, hash, opts) => this.filter(name, utils.fromJson(hash)) }, /** * Remove __all__ records from the in-memory store and reset * {@link SimpleStore#_completedQueries}. * * @method SimpleStore#clear * @returns {Object} Object containing all records that were in the store. * @see SimpleStore#remove * @see SimpleStore#removeAll * @since 3.0.0 */ clear () { const removed = {} utils.forOwn(this._collections, (collection, name) => { removed[name] = collection.removeAll() this._completedQueries[name] = {} }) return removed }, /** * Fired during {@link SimpleStore#create}. See * {@link SimpleStore~beforeCreateListener} for how to listen for this event. * * @event SimpleStore#beforeCreate * @see SimpleStore~beforeCreateListener * @see SimpleStore#create */ /** * Callback signature for the {@link SimpleStore#event:beforeCreate} event. * * @example * function onBeforeCreate (mapperName, props, opts) { * // do something * } * store.on('beforeCreate', onBeforeCreate); * * @callback SimpleStore~beforeCreateListener * @param {string} name The `name` argument received by {@link Mapper#beforeCreate}. * @param {object} props The `props` argument received by {@link Mapper#beforeCreate}. * @param {object} opts The `opts` argument received by {@link Mapper#beforeCreate}. * @see SimpleStore#event:beforeCreate * @see SimpleStore#create * @since 3.0.0 */ /** * Fired during {@link SimpleStore#create}. See * {@link SimpleStore~afterCreateListener} for how to listen for this event. * * @event SimpleStore#afterCreate * @see SimpleStore~afterCreateListener * @see SimpleStore#create */ /** * Callback signature for the {@link SimpleStore#event:afterCreate} event. * * @example * function onAfterCreate (mapperName, props, opts, result) { * // do something * } * store.on('afterCreate', onAfterCreate); * * @callback SimpleStore~afterCreateListener * @param {string} name The `name` argument received by {@link Mapper#afterCreate}. * @param {object} props The `props` argument received by {@link Mapper#afterCreate}. * @param {object} opts The `opts` argument received by {@link Mapper#afterCreate}. * @param {object} result The `result` argument received by {@link Mapper#afterCreate}. * @see SimpleStore#event:afterCreate * @see SimpleStore#create * @since 3.0.0 */ /** * Wrapper for {@link Mapper#create}. Adds the created record to the store. * * @example * import { SimpleStore } from 'js-data'; * import { HttpAdapter } from 'js-data-http'; * * const store = new SimpleStore(); * store.registerAdapter('http', new HttpAdapter(), { default: true }); * * store.defineMapper('book'); * * // Since this example uses the http adapter, we'll get something like: * // * // POST /book {"author_id":1234,...} * store.create('book', { * author_id: 1234, * edition: 'First Edition', * title: 'Respect your Data' * }).then((book) => { * console.log(book.id); // 120392 * console.log(book.title); // "Respect your Data" * }); * * @fires SimpleStore#beforeCreate * @fires SimpleStore#afterCreate * @fires SimpleStore#add * @method SimpleStore#create * @param {string} name Name of the {@link Mapper} to target. * @param {object} record Passed to {@link Mapper#create}. * @param {object} [opts] Passed to {@link Mapper#create}. See * {@link Mapper#create} for more configuration options. * @returns {Promise} Resolves with the result of the create. * @since 3.0.0 */ create (name, record, opts) { opts || (opts = {}) return Container.prototype.create.call(this, name, record, opts) .then((result) => this._end(name, result, opts)) }, /** * Fired during {@link SimpleStore#createMany}. See * {@link SimpleStore~beforeCreateManyListener} for how to listen for this event. * * @event SimpleStore#beforeCreateMany * @see SimpleStore~beforeCreateManyListener * @see SimpleStore#createMany */ /** * Callback signature for the {@link SimpleStore#event:beforeCreateMany} event. * * @example * function onBeforeCreateMany (mapperName, records, opts) { * // do something * } * store.on('beforeCreateMany', onBeforeCreateMany); * * @callback SimpleStore~beforeCreateManyListener * @param {string} name The `name` argument received by {@link Mapper#beforeCreateMany}. * @param {object} records The `records` argument received by {@link Mapper#beforeCreateMany}. * @param {object} opts The `opts` argument received by {@link Mapper#beforeCreateMany}. * @see SimpleStore#event:beforeCreateMany * @see SimpleStore#createMany * @since 3.0.0 */ /** * Fired during {@link SimpleStore#createMany}. See * {@link SimpleStore~afterCreateManyListener} for how to listen for this event. * * @event SimpleStore#afterCreateMany * @see SimpleStore~afterCreateManyListener * @see SimpleStore#createMany */ /** * Callback signature for the {@link SimpleStore#event:afterCreateMany} event. * * @example * function onAfterCreateMany (mapperName, records, opts, result) { * // do something * } * store.on('afterCreateMany', onAfterCreateMany); * * @callback SimpleStore~afterCreateManyListener * @param {string} name The `name` argument received by {@link Mapper#afterCreateMany}. * @param {object} records The `records` argument received by {@link Mapper#afterCreateMany}. * @param {object} opts The `opts` argument received by {@link Mapper#afterCreateMany}. * @param {object} result The `result` argument received by {@link Mapper#afterCreateMany}. * @see SimpleStore#event:afterCreateMany * @see SimpleStore#createMany * @since 3.0.0 */ /** * Wrapper for {@link Mapper#createMany}. Adds the created records to the * store. * * @example * import { SimpleStore } from 'js-data'; * import { HttpAdapter } from 'js-data-http'; * * const store = new SimpleStore(); * store.registerAdapter('http', new HttpAdapter(), { default: true }); * * store.defineMapper('book'); * * // Since this example uses the http adapter, we'll get something like: * // * // POST /book [{"author_id":1234,...},{...}] * store.createMany('book', [{ * author_id: 1234, * edition: 'First Edition', * title: 'Respect your Data' * }, { * author_id: 1234, * edition: 'Second Edition', * title: 'Respect your Data' * }]).then((books) => { * console.log(books[0].id); // 142394 * console.log(books[0].title); // "Respect your Data" * }); * * @fires SimpleStore#beforeCreateMany * @fires SimpleStore#afterCreateMany * @fires SimpleStore#add * @method SimpleStore#createMany * @param {string} name Name of the {@link Mapper} to target. * @param {array} records Passed to {@link Mapper#createMany}. * @param {object} [opts] Passed to {@link Mapper#createMany}. See * {@link Mapper#createMany} for more configuration options. * @returns {Promise} Resolves with the result of the create. * @since 3.0.0 */ createMany (name, records, opts) { opts || (opts = {}) return Container.prototype.createMany.call(this, name, records, opts) .then((result) => this._end(name, result, opts)) }, defineMapper (name, opts) { const self = this const mapper = Container.prototype.defineMapper.call(self, name, opts) self._pendingQueries[name] = {} self._completedQueries[name] = {} mapper.relationList || Object.defineProperty(mapper, 'relationList', { value: [] }) const collectionOpts = { // Make sure the collection has somewhere to store "added" timestamps _added: {}, // Give the collection a reference to this SimpleStore datastore: self, // The mapper tied to the collection mapper } if (opts && ('onConflict' in opts)) { collectionOpts.onConflict = opts.onConflict } // The SimpleStore uses a subclass of Collection that is "SimpleStore-aware" const collection = self._collections[name] = new self.collectionClass(null, collectionOpts) // eslint-disable-line const schema = mapper.schema || {} const properties = schema.properties || {} // TODO: Make it possible index nested properties? utils.forOwn(properties, function (opts, prop) { if (opts.indexed) { collection.createIndex(prop) } }) // Create a secondary index on the "added" timestamps of records in the // collection collection.createIndex('addedTimestamps', ['$'], { fieldGetter (obj) { return collection._added[collection.recordId(obj)] } }) collection.on('all', function (...args) { self._onCollectionEvent(name, ...args) }) return mapper }, /** * Fired during {@link SimpleStore#destroy}. See * {@link SimpleStore~beforeDestroyListener} for how to listen for this event. * * @event SimpleStore#beforeDestroy * @see SimpleStore~beforeDestroyListener * @see SimpleStore#destroy */ /** * Callback signature for the {@link SimpleStore#event:beforeDestroy} event. * * @example * function onBeforeDestroy (mapperName, id, opts) { * // do something * } * store.on('beforeDestroy', onBeforeDestroy); * * @callback SimpleStore~beforeDestroyListener * @param {string} name The `name` argument received by {@link Mapper#beforeDestroy}. * @param {string|number} id The `id` argument received by {@link Mapper#beforeDestroy}. * @param {object} opts The `opts` argument received by {@link Mapper#beforeDestroy}. * @see SimpleStore#event:beforeDestroy * @see SimpleStore#destroy * @since 3.0.0 */ /** * Fired during {@link SimpleStore#destroy}. See * {@link SimpleStore~afterDestroyListener} for how to listen for this event. * * @event SimpleStore#afterDestroy * @see SimpleStore~afterDestroyListener * @see SimpleStore#destroy */ /** * Callback signature for the {@link SimpleStore#event:afterDestroy} event. * * @example * function onAfterDestroy (mapperName, id, opts, result) { * // do something * } * store.on('afterDestroy', onAfterDestroy); * * @callback SimpleStore~afterDestroyListener * @param {string} name The `name` argument received by {@link Mapper#afterDestroy}. * @param {string|number} id The `id` argument received by {@link Mapper#afterDestroy}. * @param {object} opts The `opts` argument received by {@link Mapper#afterDestroy}. * @param {object} result The `result` argument received by {@link Mapper#afterDestroy}. * @see SimpleStore#event:afterDestroy * @see SimpleStore#destroy * @since 3.0.0 */ /** * Wrapper for {@link Mapper#destroy}. Removes any destroyed record from the * in-memory store. Clears out any {@link SimpleStore#_completedQueries} entries * associated with the provided `id`. * * @example * import { SimpleStore } from 'js-data'; * import { HttpAdapter } from 'js-data-http'; * * const store = new SimpleStore(); * store.registerAdapter('http', new HttpAdapter(), { default: true }); * * store.defineMapper('book'); * * store.add('book', { id: 1234, title: 'Data Management is Hard' }); * * // Since this example uses the http adapter, we'll get something like: * // * // DELETE /book/1234 * store.destroy('book', 1234).then(() => { * // The book record is no longer in the in-memory store * console.log(store.get('book', 1234)); // undefined * * return store.find('book', 1234); * }).then((book) { * // The book was deleted from the database too * console.log(book); // undefined * }); * * @fires SimpleStore#beforeDestroy * @fires SimpleStore#afterDestroy * @fires SimpleStore#remove * @method SimpleStore#destroy * @param {string} name Name of the {@link Mapper} to target. * @param {(string|number)} id Passed to {@link Mapper#destroy}. * @param {object} [opts] Passed to {@link Mapper#destroy}. See * {@link Mapper#destroy} for more configuration options. * @returns {Promise} Resolves when the destroy operation completes. * @since 3.0.0 */ destroy (name, id, opts) { opts || (opts = {}) return Container.prototype.destroy.call(this, name, id, opts).then((result) => { const record = this.getCollection(name).remove(id, opts) if (opts.raw) { result.data = record } else { result = record } delete this._pendingQueries[name][id] delete this._completedQueries[name][id] return result }) }, /** * Fired during {@link SimpleStore#destroyAll}. See * {@link SimpleStore~beforeDestroyAllListener} for how to listen for this event. * * @event SimpleStore#beforeDestroyAll * @see SimpleStore~beforeDestroyAllListener * @see SimpleStore#destroyAll */ /** * Callback signature for the {@link SimpleStore#event:beforeDestroyAll} event. * * @example * function onBeforeDestroyAll (mapperName, query, opts) { * // do something * } * store.on('beforeDestroyAll', onBeforeDestroyAll); * * @callback SimpleStore~beforeDestroyAllListener * @param {string} name The `name` argument received by {@link Mapper#beforeDestroyAll}. * @param {object} query The `query` argument received by {@link Mapper#beforeDestroyAll}. * @param {object} opts The `opts` argument received by {@link Mapper#beforeDestroyAll}. * @see SimpleStore#event:beforeDestroyAll * @see SimpleStore#destroyAll * @since 3.0.0 */ /** * Fired during {@link SimpleStore#destroyAll}. See * {@link SimpleStore~afterDestroyAllListener} for how to listen for this event. * * @event SimpleStore#afterDestroyAll * @see SimpleStore~afterDestroyAllListener * @see SimpleStore#destroyAll */ /** * Callback signature for the {@link SimpleStore#event:afterDestroyAll} event. * * @example * function onAfterDestroyAll (mapperName, query, opts, result) { * // do something * } * store.on('afterDestroyAll', onAfterDestroyAll); * * @callback SimpleStore~afterDestroyAllListener * @param {string} name The `name` argument received by {@link Mapper#afterDestroyAll}. * @param {object} query The `query` argument received by {@link Mapper#afterDestroyAll}. * @param {object} opts The `opts` argument received by {@link Mapper#afterDestroyAll}. * @param {object} result The `result` argument received by {@link Mapper#afterDestroyAll}. * @see SimpleStore#event:afterDestroyAll * @see SimpleStore#destroyAll * @since 3.0.0 */ /** * Wrapper for {@link Mapper#destroyAll}. Removes any destroyed records from * the in-memory store. * * @example * import { SimpleStore } from 'js-data'; * import { HttpAdapter } from 'js-data-http'; * * const store = new SimpleStore(); * store.registerAdapter('http', new HttpAdapter(), { default: true }); * * store.defineMapper('book'); * * store.add('book', { id: 1234, title: 'Data Management is Hard' }); * * // Since this example uses the http adapter, we'll get something like: * // * // DELETE /book/1234 * store.destroy('book', 1234).then(() => { * // The book record is gone from the in-memory store * console.log(store.get('book', 1234)); // undefined * return store.find('book', 1234); * }).then((book) { * // The book was deleted from the database too * console.log(book); // undefined * }); * * @fires SimpleStore#beforeDestroyAll * @fires SimpleStore#afterDestroyAll * @fires SimpleStore#remove * @method SimpleStore#destroyAll * @param {string} name Name of the {@link Mapper} to target. * @param {object} [query] Passed to {@link Mapper#destroyAll}. * @param {object} [opts] Passed to {@link Mapper#destroyAll}. See * {@link Mapper#destroyAll} for more configuration options. * @returns {Promise} Resolves when the delete completes. * @since 3.0.0 */ destroyAll (name, query, opts) { opts || (opts = {}) return Container.prototype.destroyAll.call(this, name, query, opts).then((result) => { const records = this.getCollection(name).removeAll(query, opts) if (opts.raw) { result.data = records } else { result = records } const hash = this.hashQuery(name, query, opts) delete this._pendingQueries[name][hash] delete this._completedQueries[name][hash] return result }) }, eject (name, id, opts) { console.warn('DEPRECATED: "eject" is deprecated, use "remove" instead') return this.remove(name, id, opts) }, ejectAll (name, query, opts) { console.warn('DEPRECATED: "ejectAll" is deprecated, use "removeAll" instead') return this.removeAll(name, query, opts) }, /** * Fired during {@link SimpleStore#find}. See * {@link SimpleStore~beforeFindListener} for how to listen for this event. * * @event SimpleStore#beforeFind * @see SimpleStore~beforeFindListener * @see SimpleStore#find */ /** * Callback signature for the {@link SimpleStore#event:beforeFind} event. * * @example * function onBeforeFind (mapperName, id, opts) { * // do something * } * store.on('beforeFind', onBeforeFind); * * @callback SimpleStore~beforeFindListener * @param {string} name The `name` argument received by {@link Mapper#beforeFind}. * @param {string|number} id The `id` argument received by {@link Mapper#beforeFind}. * @param {object} opts The `opts` argument received by {@link Mapper#beforeFind}. * @see SimpleStore#event:beforeFind * @see SimpleStore#find * @since 3.0.0 */ /** * Fired during {@link SimpleStore#find}. See * {@link SimpleStore~afterFindListener} for how to listen for this event. * * @event SimpleStore#afterFind * @see SimpleStore~afterFindListener * @see SimpleStore#find */ /** * Callback signature for the {@link SimpleStore#event:afterFind} event. * * @example * function onAfterFind (mapperName, id, opts, result) { * // do something * } * store.on('afterFind', onAfterFind); * * @callback SimpleStore~afterFindListener * @param {string} name The `name` argument received by {@link Mapper#afterFind}. * @param {string|number} id The `id` argument received by {@link Mapper#afterFind}. * @param {object} opts The `opts` argument received by {@link Mapper#afterFind}. * @param {object} result The `result` argument received by {@link Mapper#afterFind}. * @see SimpleStore#event:afterFind * @see SimpleStore#find * @since 3.0.0 */ /** * Wrapper for {@link Mapper#find}. Adds any found record to the store. * * @example * import { SimpleStore } from 'js-data'; * import { HttpAdapter } from 'js-data-http'; * * const store = new SimpleStore(); * store.registerAdapter('http', new HttpAdapter(), { default: true }); * * store.defineMapper('book'); * * // Since this example uses the http adapter, we'll get something like: * // * // GET /book/1234 * store.find('book', 1234).then((book) => { * // The book record is now in the in-memory store * console.log(store.get('book', 1234) === book); // true * }); * * @fires SimpleStore#beforeFind * @fires SimpleStore#afterFind * @fires SimpleStore#add * @method SimpleStore#find * @param {string} name Name of the {@link Mapper} to target. * @param {(string|number)} id Passed to {@link Mapper#find}. * @param {object} [opts] Passed to {@link Mapper#find}. * @param {boolean} [opts.force] Bypass cacheFind * @param {boolean|Function} [opts.usePendingFind] See {@link SimpleStore#usePendingFind} * @returns {Promise} Resolves with the result, if any. * @since 3.0.0 */ find (name, id, opts) { opts || (opts = {}) const mapper = this.getMapper(name) const pendingQuery = this._pendingQueries[name][id] const usePendingFind = opts.usePendingFind === undefined ? this.usePendingFind : opts.usePendingFind utils._(opts, mapper) if (pendingQuery && (utils.isFunction(usePendingFind) ? usePendingFind.call(this, name, id, opts) : usePendingFind)) { return pendingQuery } const item = this.cachedFind(name, id, opts) if (opts.force || !item) { const promise = this._pendingQueries[name][id] = Container.prototype.find.call(this, name, id, opts) return promise .then((result) => { delete this._pendingQueries[name][id] result = this._end(name, result, opts) this.cacheFind(name, result, id, opts) return result }, (err) => { delete this._pendingQueries[name][id] return utils.reject(err) }) } return utils.resolve(item) }, /** * Fired during {@link SimpleStore#findAll}. See * {@link SimpleStore~beforeFindAllListener} for how to listen for this event. * * @event SimpleStore#beforeFindAll * @see SimpleStore~beforeFindAllListener * @see SimpleStore#findAll */ /** * Callback signature for the {@link SimpleStore#event:beforeFindAll} event. * * @example * function onBeforeFindAll (mapperName, query, opts) { * // do something * } * store.on('beforeFindAll', onBeforeFindAll); * * @callback SimpleStore~beforeFindAllListener * @param {string} name The `name` argument received by {@link Mapper#beforeFindAll}. * @param {object} query The `query` argument received by {@link Mapper#beforeFindAll}. * @param {object} opts The `opts` argument received by {@link Mapper#beforeFindAll}. * @see SimpleStore#event:beforeFindAll * @see SimpleStore#findAll * @since 3.0.0 */ /** * Fired during {@link SimpleStore#findAll}. See * {@link SimpleStore~afterFindAllListener} for how to listen for this event. * * @event SimpleStore#afterFindAll * @see SimpleStore~afterFindAllListener * @see SimpleStore#findAll */ /** * Callback signature for the {@link SimpleStore#event:afterFindAll} event. * * @example * function onAfterFindAll (mapperName, query, opts, result) { * // do something * } * store.on('afterFindAll', onAfterFindAll); * * @callback SimpleStore~afterFindAllListener * @param {string} name The `name` argument received by {@link Mapper#afterFindAll}. * @param {object} query The `query` argument received by {@link Mapper#afterFindAll}. * @param {object} opts The `opts` argument received by {@link Mapper#afterFindAll}. * @param {object} result The `result` argument received by {@link Mapper#afterFindAll}. * @see SimpleStore#event:afterFindAll * @see SimpleStore#findAll * @since 3.0.0 */ /** * Wrapper for {@link Mapper#findAll}. Adds any found records to the store. * * @example * import { SimpleStore } from 'js-data'; * import { HttpAdapter } from 'js-data-http'; * * const store = new SimpleStore(); * store.registerAdapter('http', new HttpAdapter(), { default: true }); * * store.defineMapper('movie'); * * // Since this example uses the http adapter, we'll get something like: * // * // GET /movie?rating=PG * store.find('movie', { rating: 'PG' }).then((movies) => { * // The movie records are now in the in-memory store * console.log(store.filter('movie')); * }); * * @fires SimpleStore#beforeFindAll * @fires SimpleStore#afterFindAll * @fires SimpleStore#add * @method SimpleStore#findAll * @param {string} name Name of the {@link Mapper} to target. * @param {object} [query] Passed to {@link Mapper.findAll}. * @param {object} [opts] Passed to {@link Mapper.findAll}. * @param {boolean} [opts.force] Bypass cacheFindAll * @param {boolean|Function} [opts.usePendingFindAll] See {@link SimpleStore#usePendingFindAll} * @returns {Promise} Resolves with the result, if any. * @since 3.0.0 */ findAll (name, query, opts) { opts || (opts = {}) const mapper = this.getMapper(name) const hash = this.hashQuery(name, query, opts) const pendingQuery = this._pendingQueries[name][hash] const usePendingFindAll = opts.usePendingFindAll === undefined ? this.usePendingFindAll : opts.usePendingFindAll utils._(opts, mapper) if (pendingQuery && (utils.isFunction(usePendingFindAll) ? usePendingFindAll.call(this, name, query, opts) : usePendingFindAll)) { return pendingQuery } const items = this.cachedFindAll(name, hash, opts) if (opts.force || !items) { const promise = this._pendingQueries[name][has