import utils from './utils'
import {
belongsToType,
hasManyType,
hasOneType
} from './decorators'
import {proxiedMapperMethods, Container} from './Container'
import LinkedCollection from './LinkedCollection'
const DOMAIN = 'DataStore'
const proxiedCollectionMethods = [
/**
* Wrapper for {@link LinkedCollection#add}.
*
* @example <caption>DataStore#add</caption>
* // Normally you would do: import {DataStore} from 'js-data'
* const JSData = require('js-data@3.0.0-beta.7')
* const {DataStore} = JSData
* console.log('Using JSData v' + JSData.version.full)
*
* const store = new DataStore()
* 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 DataStore#add
* @method DataStore#add
* @param {(string|number)} name Name of the {@link Mapper} to target.
* @param {(Object|Object[]|Record|Record[])} data See {@link LinkedCollection#add}.
* @param {Object} [opts] Configuration options. See {@link LinkedCollection#add}.
* @returns {(Object|Object[]|Record|Record[])} See {@link LinkedCollection#add}.
* @see LinkedCollection#add
* @see Collection#add
* @since 3.0.0
*/
'add',
/**
* Wrapper for {@link LinkedCollection#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 DataStore#between
* @param {(string|number)} name Name of the {@link Mapper} to target.
* @param {Array} leftKeys See {@link LinkedCollection#between}.
* @param {Array} rightKeys See {@link LinkedCollection#between}.
* @param {Object} [opts] Configuration options. See {@link LinkedCollection#between}.
* @returns {Object[]|Record[]} See {@link LinkedCollection#between}.
* @see LinkedCollection#between
* @see Collection#between
* @since 3.0.0
*/
'between',
/**
* Wrapper for {@link LinkedCollection#createIndex}.
*
* @example
* // Index users by age
* store.createIndex('user', 'age')
*
* @example
* // Index users by status and role
* store.createIndex('user', 'statusAndRole', ['status', 'role'])
*
* @method DataStore#createIndex
* @param {(string|number)} name Name of the {@link Mapper} to target.
* @param {string} name See {@link LinkedCollection#createIndex}.
* @param {string[]} [fieldList] See {@link LinkedCollection#createIndex}.
* @see LinkedCollection#createIndex
* @see Collection#createIndex
* @since 3.0.0
*/
'createIndex',
/**
* Wrapper for {@link LinkedCollection#filter}.
*
* @example <caption>DataStore#filter</caption>
* // import {DataStore} from 'js-data'
* const JSData = require('js-data@3.0.0-beta.7')
* const {DataStore} = JSData
* console.log('Using JSData v' + JSData.version.full)
*
* const store = new DataStore()
* 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', (post) => post.id % 2 === 0)
*
* @method DataStore#filter
* @param {(string|number)} name Name of the {@link Mapper} to target.
* @param {(Object|Function)} [queryOrFn={}] See {@link LinkedCollection#filter}.
* @param {Object} [thisArg] See {@link LinkedCollection#filter}.
* @returns {Array} See {@link LinkedCollection#filter}.
* @see LinkedCollection#filter
* @see Collection#filter
* @since 3.0.0
*/
'filter',
/**
* Wrapper for {@link LinkedCollection#get}.
*
* @example <caption>DataStore#get</caption>
* // import {DataStore} from 'js-data'
* const JSData = require('js-data@3.0.0-beta.7')
* const {DataStore} = JSData
* console.log('Using JSData v' + JSData.version.full)
*
* const store = new DataStore()
* 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 DataStore#get
* @param {(string|number)} name Name of the {@link Mapper} to target.
* @param {(string|number)} id See {@link LinkedCollection#get}.
* @returns {(Object|Record)} See {@link LinkedCollection#get}.
* @see LinkedCollection#get
* @see Collection#get
* @since 3.0.0
*/
'get',
/**
* Wrapper for {@link LinkedCollection#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 DataStore#getAll
* @param {(string|number)} name Name of the {@link Mapper} to target.
* @param {...Array} [keyList] See {@link LinkedCollection#getAll}.
* @param {Object} [opts] See {@link LinkedCollection#getAll}.
* @returns {Array} See {@link LinkedCollection#getAll}.
* @see LinkedCollection#getAll
* @see Collection#getAll
* @since 3.0.0
*/
'getAll',
/**
* Wrapper for {@link LinkedCollection#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 DataStore#query
* @param {(string|number)} name Name of the {@link Mapper} to target.
* @returns {Query} See {@link LinkedCollection#query}.
* @see LinkedCollection#query
* @see Collection#query
* @since 3.0.0
*/
'query',
/**
* Wrapper for {@link LinkedCollection#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 DataStore#toJSON
* @param {(string|number)} name Name of the {@link Mapper} to target.
* @param {Object} [opts] See {@link LinkedCollection#toJSON}.
* @returns {Array} See {@link LinkedCollection#toJSON}.
* @see LinkedCollection#toJSON
* @see Collection#toJSON
* @since 3.0.0
*/
'toJSON'
]
const ownMethodsForScoping = [
'addToCache',
'cachedFind',
'cachedFindAll',
'cacheFind',
'cacheFindAll',
'hashQuery'
]
const safeSetProp = function (record, field, value) {
if (record && record._set) {
record._set(`props.${field}`, value)
} else {
utils.set(record, field, value)
}
}
const safeSetLink = function (record, field, value) {
if (record && record._set) {
record._set(`links.${field}`, value)
} else {
utils.set(record, field, value)
}
}
const cachedFn = function (name, hashOrId, opts) {
const cached = this._completedQueries[name][hashOrId]
if (utils.isFunction(cached)) {
return cached(name, hashOrId, opts)
}
return cached
}
const DATASTORE_DEFAULTS = {
/**
* Whether in-memory relations should be unlinked from records after they are
* destroyed.
*
* @default true
* @name DataStore#unlinkOnDestroy
* @since 3.0.0
*/
unlinkOnDestroy: true
}
/**
* The `DataStore` class is an extension of {@link Container}. Not only does
* `DataStore` manage mappers, but also collections. `DataStore` implements the
* asynchronous {@link Mapper} methods, such as {@link Mapper#find} and
* {@link Mapper#create}. If you use the asynchronous `DataStore` 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 `DataStore` 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 {DataStore} from 'js-data'
* ```
*
* @example
* import {DataStore} from 'js-data'
* import HttpAdapter from 'js-data-http'
* const store = new DataStore()
*
* // DataStore#defineMapper returns a direct reference to the newly created
* // Mapper.
* const UserMapper = store.defineMapper('user')
*
* // DataStore#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 DataStore)
* 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 DataStore
* @extends Container
* @param {Object} [opts] Configuration options. See {@link Container}.
* @param {boolean} [opts.collectionClass={@link LinkedCollection}] See {@link DataStore#collectionClass}.
* @param {boolean} [opts.debug=false] See {@link Component#debug}.
* @param {boolean} [opts.unlinkOnDestroy=true] See {@link DataStore#unlinkOnDestroy}.
* @returns {DataStore}
* @see Container
* @since 3.0.0
* @tutorial ["http://www.js-data.io/v3.0/docs/components-of-jsdata#datastore","Components of JSData: DataStore"]
* @tutorial ["http://www.js-data.io/v3.0/docs/working-with-the-datastore","Working with the DataStore"]
* @tutorial ["http://www.js-data.io/v3.0/docs/jsdata-and-the-browser","Notes on using JSData in the Browser"]
*/
function DataStore (opts) {
utils.classCallCheck(this, DataStore)
opts || (opts = {})
// Fill in any missing options with the defaults
utils.fillIn(opts, utils.plainCopy(DATASTORE_DEFAULTS))
Container.call(this, opts)
this.collectionClass = this.collectionClass || LinkedCollection
this._collections = {}
this._pendingQueries = {}
this._completedQueries = {}
}
const props = {
constructor: DataStore,
_callSuper (method, ...args) {
return this.constructor.__super__.prototype[method].apply(this, args)
},
/**
* Internal method used to handle Mapper responses.
*
* @method DataStore#_end
* @private
* @param {string} name Name of the {@link LinkedCollection} 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 DataStore.
*
* Proxy for {@link Container#on}. If an event was emitted by a Mapper or
* Collection in the DataStore, 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 DataStore
* 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 DataStore#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 DataStore#_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 DataStore#find},
* {@link DataStore#findAll}, {@link DataStore#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 DataStore({
* 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 DataStore.prototype.addToCache.call(this, mapperName, data, opts)
* }
* })
*
* @example
* // Extend using ES2015 class syntax.
* class MyStore extends DataStore {
* 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 DataStore#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>DataStore.as</caption>
* // Normally you would do: import {DataStore} from 'js-data'
* const JSData = require('js-data@3.0.0-beta.7')
* const {DataStore} = JSData
* console.log('Using JSData v' + JSData.version.full)
*
* const store = new DataStore()
* 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 DataStore#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 DataStore#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 DataStore#find} will immediately resolve with
* the return value of this method.
*
* When using {@link DataStore} in the browser, you can override this method
* to implement your own cache-busting strategy.
*
* @example
* const store = new DataStore({
* 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 DataStore.prototype.cachedFind.call(this, mapperName, id, opts)
* }
* })
*
* @example
* // Extend using ES2015 class syntax.
* class MyStore extends DataStore {
* 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 DataStore#cachedFind
* @param {string} name The `name` argument passed to {@link DataStore#find}.
* @param {(string|number)} id The `id` argument passed to {@link DataStore#find}.
* @param {Object} opts The `opts` argument passed to {@link DataStore#find}.
* @since 3.0.0
*/
cachedFind: cachedFn,
/**
* Retrieve a cached `findAll` result, if any. This method is called during
* {@link DataStore#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 DataStore#findAll} will immediately resolve with
* the return value of this method.
*
* When using {@link DataStore} in the browser, you can override this method
* to implement your own cache-busting strategy.
*
* @example
* const store = new DataStore({
* 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 DataStore.prototype.cachedFindAll.call(this, mapperName, hash, opts)
* }
* })
*
* @example
* // Extend using ES2015 class syntax.
* class MyStore extends DataStore {
* 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 DataStore#cachedFindAll
* @param {string} name The `name` argument passed to {@link DataStore#findAll}.
* @param {string} hash The result of calling {@link DataStore#hashQuery} on
* the `query` argument passed to {@link DataStore#findAll}.
* @param {Object} opts The `opts` argument passed to {@link DataStore#findAll}.
* @since 3.0.0
*/
cachedFindAll: cachedFn,
/**
* Mark a {@link Mapper#find} result as cached by adding an entry to
* {@link DataStore#_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 DataStore#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 DataStore({
* 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 DataStore#_completedQueries
* return
* }
* // Otherwise perform default behavior
* return DataStore.prototype.cacheFind.call(this, mapperName, data, id, opts)
* }
* })
*
* @example
* // Extend using ES2015 class syntax.
* class MyStore extends DataStore {
* 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 DataStore#_completedQueries
* return
* }
* // Otherwise perform default behavior
* return super.cacheFind(mapperName, data, id, opts)
* }
* }
* const store = new MyStore()
*
* @method DataStore#cacheFind
* @param {string} name The `name` argument passed to {@link DataStore#find}.
* @param {*} data The result to cache.
* @param {(string|number)} id The `id` argument passed to {@link DataStore#find}.
* @param {Object} opts The `opts` argument passed to {@link DataStore#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 DataStore#_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 DataStore#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 DataStore({
* 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 DataStore#_completedQueries
* return
* }
* // Otherwise perform default behavior.
* return DataStore.prototype.cachedFindAll.call(this, mapperName, data, hash, opts)
* }
* })
*
* @example
* // Extend using ES2015 class syntax.
* class MyStore extends DataStore {
* 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 DataStore#_completedQueries
* return
* }
* // Otherwise perform default behavior.
* return super.cachedFindAll(mapperName, data, hash, opts)
* }
* }
* const store = new MyStore()
*
* @method DataStore#cacheFindAll
* @param {string} name The `name` argument passed to {@link DataStore#findAll}.
* @param {*} data The result to cache.
* @param {string} hash The result of calling {@link DataStore#hashQuery} on
* the `query` argument passed to {@link DataStore#findAll}.
* @param {Object} opts The `opts` argument passed to {@link DataStore#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 DataStore#_completedQueries}.
*
* @method DataStore#clear
* @returns {Object} Object containing all records that were in the store.
* @see DataStore#remove
* @see DataStore#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 DataStore#create}. See
* {@link DataStore~beforeCreateListener} for how to listen for this event.
*
* @event DataStore#beforeCreate
* @see DataStore~beforeCreateListener
* @see DataStore#create
*/
/**
* Callback signature for the {@link DataStore#event:beforeCreate} event.
*
* @example
* function onBeforeCreate (mapperName, props, opts) {
* // do something
* }
* store.on('beforeCreate', onBeforeCreate)
*
* @callback DataStore~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 DataStore#event:beforeCreate
* @see DataStore#create
* @since 3.0.0
*/
/**
* Fired during {@link DataStore#create}. See
* {@link DataStore~afterCreateListener} for how to listen for this event.
*
* @event DataStore#afterCreate
* @see DataStore~afterCreateListener
* @see DataStore#create
*/
/**
* Callback signature for the {@link DataStore#event:afterCreate} event.
*
* @example
* function onAfterCreate (mapperName, props, opts, result) {
* // do something
* }
* store.on('afterCreate', onAfterCreate)
*
* @callback DataStore~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 DataStore#event:afterCreate
* @see DataStore#create
* @since 3.0.0
*/
/**
* Wrapper for {@link Mapper#create}. Adds the created record to the store.
*
* @example
* import {DataStore} from 'js-data'
* import {HttpAdapter} from 'js-data-http'
*
* const store = new DataStore()
* 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 DataStore#beforeCreate
* @fires DataStore#afterCreate
* @fires DataStore#add
* @method DataStore#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 this._callSuper('create', name, record, opts)
.then((result) => this._end(name, result, opts))
},
/**
* Fired during {@link DataStore#createMany}. See
* {@link DataStore~beforeCreateManyListener} for how to listen for this event.
*
* @event DataStore#beforeCreateMany
* @see DataStore~beforeCreateManyListener
* @see DataStore#createMany
*/
/**
* Callback signature for the {@link DataStore#event:beforeCreateMany} event.
*
* @example
* function onBeforeCreateMany (mapperName, records, opts) {
* // do something
* }
* store.on('beforeCreateMany', onBeforeCreateMany)
*
* @callback DataStore~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 DataStore#event:beforeCreateMany
* @see DataStore#createMany
* @since 3.0.0
*/
/**
* Fired during {@link DataStore#createMany}. See
* {@link DataStore~afterCreateManyListener} for how to listen for this event.
*
* @event DataStore#afterCreateMany
* @see DataStore~afterCreateManyListener
* @see DataStore#createMany
*/
/**
* Callback signature for the {@link DataStore#event:afterCreateMany} event.
*
* @example
* function onAfterCreateMany (mapperName, records, opts, result) {
* // do something
* }
* store.on('afterCreateMany', onAfterCreateMany)
*
* @callback DataStore~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 DataStore#event:afterCreateMany
* @see DataStore#createMany
* @since 3.0.0
*/
/**
* Wrapper for {@link Mapper#createMany}. Adds the created records to the
* store.
*
* @example
* import {DataStore} from 'js-data'
* import {HttpAdapter} from 'js-data-http'
*
* const store = new DataStore()
* 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 DataStore#beforeCreateMany
* @fires DataStore#afterCreateMany
* @fires DataStore#add
* @method DataStore#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 this._callSuper('createMany', name, records, opts)
.then((result) => this._end(name, result, opts))
},
defineMapper (name, opts) {
// Complexity of this method is beyond simply using => functions to bind context
const self = this
const mapper = utils.getSuper(self).prototype.defineMapper.call(self, name, opts)
self._pendingQueries[name] = {}
self._completedQueries[name] = {}
mapper.relationList || Object.defineProperty(mapper, 'relationList', { value: [] })
// The datastore uses a subclass of Collection that is "datastore-aware"
const collection = self._collections[name] = new self.collectionClass(null, { // eslint-disable-line
// Make sure the collection has somewhere to store "added" timestamps
_added: {},
// Give the collection a reference to this datastore
datastore: self,
// The mapper tied to the collection
mapper
})
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)
})
const idAttribute = mapper.idAttribute
mapper.relationList.forEach(function (def) {
const relation = def.relation
const localField = def.localField
const path = `links.${localField}`
const foreignKey = def.foreignKey
const type = def.type
const updateOpts = { index: foreignKey }
let descriptor
const getter = function () { return this._get(path) }
if (type === belongsToType) {
if (!collection.indexes[foreignKey]) {
collection.createIndex(foreignKey)
}
descriptor = {
get: getter,
// e.g. profile.user = someUser
// or comment.post = somePost
set (record) {
const _self = this
// e.g. const otherUser = profile.user
const current = this._get(path)
// e.g. profile.user === someUser
if (record === current) {
return current
}
const id = utils.get(_self, idAttribute)
const inverseDef = def.getInverse(mapper)
// e.g. profile.user !== someUser
// or comment.post !== somePost
if (current) {
// e.g. otherUser.profile = undefined
if (inverseDef.type === hasOneType) {
safeSetLink(current, inverseDef.localField, undefined)
} else if (inverseDef.type === hasManyType) {
// e.g. remove comment from otherPost.comments
const children = utils.get(current, inverseDef.localField)
utils.remove(children, function (_record) {
return id === utils.get(_record, idAttribute)
})
}
}
if (record) {
// e.g. profile.user = someUser
const relatedIdAttribute = def.getRelation().idAttribute
const relatedId = utils.get(record, relatedIdAttribute)
// Prefer store record
if (!utils.isUndefined(relatedId)) {
record = self.get(relation, relatedId) || record
}
// Set locals
// e.g. profile.user = someUser
// or comment.post = somePost
_self._set(path, record)
safeSetProp(_self, foreignKey, relatedId)
collection.updateIndex(_self, updateOpts)
// Update (set) inverse relation
if (inverseDef.type === hasOneType) {
// e.g. someUser.profile = profile
safeSetLink(record, inverseDef.localField, _self)
} else if (inverseDef.type === hasManyType) {
// e.g. add comment to somePost.comments
const children = utils.get(record, inverseDef.localField)
utils.noDupeAdd(children, _self, function (_record) {
return id === utils.get(_record, idAttribute)
})
}
} else {
// Unset locals
// e.g. profile.user = undefined
// or comment.post = undefined
_self._set(path, undefined)
safeSetProp(_self, foreignKey, undefined)
collection.updateIndex(_self, updateOpts)
}
return record
}
}
let foreignKeyDescriptor = Object.getOwnPropertyDescriptor(mapper.recordClass.prototype, foreignKey)
if (!foreignKeyDescriptor) {
foreignKeyDescriptor = {
enumerable: true
}
}
const originalGet = foreignKeyDescriptor.get
foreignKeyDescriptor.get = function () {
if (originalGet) {
return originalGet.call(this)
}
return this._get(`props.${foreignKey}`)
}
const originalSet = foreignKeyDescriptor.set
foreignKeyDescriptor.set = function (value) {
if (originalSet) {
originalSet.call(this, value)
}
if (utils.isUndefined(value)) {
// Unset locals
utils.set(this, localField, undefined)
} else {
safeSetProp(this, foreignKey, value)
let storeRecord = self.get(relation, value)
if (storeRecord) {
utils.set(this, localField, storeRecord)
}
}
}
Object.defineProperty(mapper.recordClass.prototype, foreignKey, foreignKeyDescriptor)
} else if (type === hasManyType) {
const localKeys = def.localKeys
const foreignKeys = def.foreignKeys
// TODO: Handle case when belongsTo relation isn't ever defined
if (self._collections[relation] && foreignKey && !self.getCollection(relation).indexes[foreignKey]) {
self.getCollection(relation).createIndex(foreignKey)
}
descriptor = {
get () {
const _self = this
let current = getter.call(_self)
if (!current) {
_self._set(path, [])
}
return getter.call(_self)
},
// e.g. post.comments = someComments
// or user.groups = someGroups
// or group.users = someUsers
set (records) {
const _self = this
records || (records = [])
if (records && !utils.isArray(records)) {
records = [records]
}
const id = utils.get(_self, idAttribute)
const relatedIdAttribute = def.getRelation().idAttribute
const inverseDef = def.getInverse(mapper)
const inverseLocalField = inverseDef.localField
const current = _self._get(path) || []
const linked = []
const toLink = {}
records.forEach(function (record) {
// e.g. comment.id
const relatedId = utils.get(record, relatedIdAttribute)
if (!utils.isUndefined(relatedId)) {
// Prefer store record
record = self.get(relation, relatedId) || record
// e.g. toLink[comment.id] = comment
toLink[relatedId] = record
const _localField = utils.get(record, inverseLocalField)
if (_localField) {
const __localField = utils.get(_localField, localField)
// e.g. somePost.comments.remove(comment)
utils.remove(__localField, function (_record) {
return relatedId === utils.get(_record, relatedIdAttribute)
})
}
}
linked.push(record)
})
// e.g. post.comments = someComments
if (foreignKey) {
current.forEach(function (record) {
// e.g. comment.id
const relatedId = utils.get(record, relatedIdAttribute)
if (!utils.isUndefined(relatedId) && !(relatedId in toLink)) {
// Update (unset) inverse relation
// e.g. comment.post_id = undefined
safeSetProp(record, foreignKey, undefined)
// e.g. CommentCollection.updateIndex(comment, { index: 'post_id' })
self.getCollection(relation).updateIndex(record, updateOpts)
// e.g. comment.post = undefined
safeSetLink(record, inverseLocalField, undefined)
}
})
linked.forEach(function (record) {
// Update (set) inverse relation
// e.g. comment.post_id = post.id
safeSetProp(record, foreignKey, id)
// e.g. CommentCollection.updateIndex(comment, { index: 'post_id' })
self.getCollection(relation).updateIndex(record, updateOpts)
// e.g. comment.post = post
safeSetLink(record, inverseLocalField, _self)
})
} else if (localKeys) {
// Update locals
// e.g. group.users = someUsers
const _localKeys = linked.map(function (record) {
// Update (set) inverse relation
// safeSetLink(record, inverseLocalField, _self)
return utils.get(record, relatedIdAttribute)
})
// e.g. group.user_ids = [1,2,3,...]
utils.set(_self, localKeys, _localKeys)
// Update (unset) inverse relation
if (inverseDef.foreignKeys) {
current.forEach(function (record) {
const relatedId = utils.get(record, relatedIdAttribute)
if (!utils.isUndefined(relatedId) && !(relatedId in toLink)) {
// Update inverse relation
// safeSetLink(record, inverseLocalField, undefined)
const _localField = utils.get(record, inverseLocalField) || []
// e.g. someUser.groups.remove(group)
utils.remove(_localField, function (_record) {
return id === utils.get(_record, idAttribute)
})
}
})
linked.forEach(function (record) {
// Update (set) inverse relation
const _localField = utils.get(record, inverseLocalField) || []
// e.g. someUser.groups.push(group)
utils.noDupeAdd(_localField, _self, function (_record) {
return id === utils.get(_record, idAttribute)
})
})
}
} else if (foreignKeys) {
// e.g. user.groups = someGroups
// Update (unset) inverse relation
current.forEach(function (record) {
const _localKeys = utils.get(record, foreignKeys) || []
// e.g. someGroup.user_ids.remove(user.id)
utils.remove(_localKeys, function (_key) {
return id === _key
})
const _localField = utils.get(record, inverseLocalField) || []
// e.g. someGroup.users.remove(user)
utils.remove(_localField, function (_record) {
return id === utils.get(_record, idAttribute)
})
})
// Update (set) inverse relation
linked.forEach(function (record) {
const _localKeys = utils.get(record, foreignKeys) || []
utils.noDupeAdd(_localKeys, id, function (_key) {
return id === _key
})
const _localField = utils.get(record, inverseLocalField) || []
utils.noDupeAdd(_localField, _self, function (_record) {
return id === utils.get(_record, idAttribute)
})
})
}
_self._set(path, linked)
return linked
}
}
} else if (type === hasOneType) {
// TODO: Handle case when belongsTo relation isn't ever defined
if (self._collections[relation] && foreignKey && !self.getCollection(relation).indexes[foreignKey]) {
self.getCollection(relation).createIndex(foreignKey)
}
descriptor = {
get: getter,
// e.g. user.profile = someProfile
set (record) {
const _self = this
const current = this._get(path)
if (record === current) {
return current
}
const inverseLocalField = def.getInverse(mapper).localField
// Update (unset) inverse relation
if (current) {
safeSetProp(current, foreignKey, undefined)
self.getCollection(relation).updateIndex(current, updateOpts)
safeSetLink(current, inverseLocalField, undefined)
}
if (record) {
const relatedId = utils.get(record, def.getRelation().idAttribute)
// Prefer store record
if (!utils.isUndefined(relatedId)) {
record = self.get(relation, relatedId) || record
}
// Set locals
_self._set(path, record)
// Update (set) inverse relation
safeSetProp(record, foreignKey, utils.get(_self, idAttribute))
self.getCollection(relation).updateIndex(record, updateOpts)
safeSetLink(record, inverseLocalField, _self)
} else {
// Set locals
_self._set(path, undefined)
}
return record
}
}
}
if (descriptor) {
descriptor.enumerable = utils.isUndefined(def.enumerable) ? false : def.enumerable
if (def.get) {
let origGet = descriptor.get
descriptor.get = function () {
return def.get(def, this, (...args) => origGet.apply(this, args))
}
}
if (def.set) {
let origSet = descriptor.set
descriptor.set = function (related) {
return def.set(def, this, related, (value) => origSet.call(this, value === undefined ? related : value))
}
}
Object.defineProperty(mapper.recordClass.prototype, localField, descriptor)
}
})
return mapper
},
/**
* Fired during {@link DataStore#destroy}. See
* {@link DataStore~beforeDestroyListener} for how to listen for this event.
*
* @event DataStore#beforeDestroy
* @see DataStore~beforeDestroyListener
* @see DataStore#destroy
*/
/**
* Callback signature for the {@link DataStore#event:beforeDestroy} event.
*
* @example
* function onBeforeDestroy (mapperName, id, opts) {
* // do something
* }
* store.on('beforeDestroy', onBeforeDestroy)
*
* @callback DataStore~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 DataStore#event:beforeDestroy
* @see DataStore#destroy
* @since 3.0.0
*/
/**
* Fired during {@link DataStore#destroy}. See
* {@link DataStore~afterDestroyListener} for how to listen for this event.
*
* @event DataStore#afterDestroy
* @see DataStore~afterDestroyListener
* @see DataStore#destroy
*/
/**
* Callback signature for the {@link DataStore#event:afterDestroy} event.
*
* @example
* function onAfterDestroy (mapperName, id, opts, result) {
* // do something
* }
* store.on('afterDestroy', onAfterDestroy)
*
* @callback DataStore~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 DataStore#event:afterDestroy
* @see DataStore#destroy
* @since 3.0.0
*/
/**
* Wrapper for {@link Mapper#destroy}. Removes any destroyed record from the
* in-memory store. Clears out any {@link DataStore#_completedQueries} entries
* associated with the provided `id`.
*
* @example
* import {DataStore} from 'js-data'
* import {HttpAdapter} from 'js-data-http'
*
* const store = new DataStore()
* 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 DataStore#beforeDestroy
* @fires DataStore#afterDestroy
* @fires DataStore#remove
* @method DataStore#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 this._callSuper('destroy', name, id, opts).then((result) => {
const record = this.getCollection(name).remove(id, opts)
if (record && this.unlinkOnDestroy) {
const _opts = utils.plainCopy(opts)
_opts.withAll = true
utils.forEachRelation(this.getMapper(name), _opts, (def) => {
utils.set(record, def.localField, undefined)
})
}
if (opts.raw) {
result.data = record
} else {
result = record
}
delete this._pendingQueries[name][id]
delete this._completedQueries[name][id]
return result
})
},
/**
* Fired during {@link DataStore#destroyAll}. See
* {@link DataStore~beforeDestroyAllListener} for how to listen for this event.
*
* @event DataStore#beforeDestroyAll
* @see DataStore~beforeDestroyAllListener
* @see DataStore#destroyAll
*/
/**
* Callback signature for the {@link DataStore#event:beforeDestroyAll} event.
*
* @example
* function onBeforeDestroyAll (mapperName, query, opts) {
* // do something
* }
* store.on('beforeDestroyAll', onBeforeDestroyAll)
*
* @callback DataStore~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 DataStore#event:beforeDestroyAll
* @see DataStore#destroyAll
* @since 3.0.0
*/
/**
* Fired during {@link DataStore#destroyAll}. See
* {@link DataStore~afterDestroyAllListener} for how to listen for this event.
*
* @event DataStore#afterDestroyAll
* @see DataStore~afterDestroyAllListener
* @see DataStore#destroyAll
*/
/**
* Callback signature for the {@link DataStore#event:afterDestroyAll} event.
*
* @example
* function onAfterDestroyAll (mapperName, query, opts, result) {
* // do something
* }
* store.on('afterDestroyAll', onAfterDestroyAll)
*
* @callback DataStore~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 DataStore#event:afterDestroyAll
* @see DataStore#destroyAll
* @since 3.0.0
*/
/**
* Wrapper for {@link Mapper#destroyAll}. Removes any destroyed records from
* the in-memory store.
*
* @example
* import {DataStore} from 'js-data'
* import {HttpAdapter} from 'js-data-http'
*
* const store = new DataStore()
* 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 DataStore#beforeDestroyAll
* @fires DataStore#afterDestroyAll
* @fires DataStore#remove
* @method DataStore#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 this._callSuper('destroyAll', name, query, opts).then((result) => {
const records = this.getCollection(name).removeAll(query, opts)
if (records && records.length && this.unlinkOnDestroy) {
const _opts = utils.plainCopy(opts)
_opts.withAll = true
utils.forEachRelation(this.getMapper(name), _opts, (def) => {
records.forEach((record) => {
utils.set(record, def.localField, undefined)
})
})
}
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 DataStore#find}. See
* {@link DataStore~beforeFindListener} for how to listen for this event.
*
* @event DataStore#beforeFind
* @see DataStore~beforeFindListener
* @see DataStore#find
*/
/**
* Callback signature for the {@link DataStore#event:beforeFind} event.
*
* @example
* function onBeforeFind (mapperName, id, opts) {
* // do something
* }
* store.on('beforeFind', onBeforeFind)
*
* @callback DataStore~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 DataStore#event:beforeFind
* @see DataStore#find
* @since 3.0.0
*/
/**
* Fired during {@link DataStore#find}. See
* {@link DataStore~afterFindListener} for how to listen for this event.
*
* @event DataStore#afterFind
* @see DataStore~afterFindListener
* @see DataStore#find
*/
/**
* Callback signature for the {@link DataStore#event:afterFind} event.
*
* @example
* function onAfterFind (mapperName, id, opts, result) {
* // do something
* }
* store.on('afterFind', onAfterFind)
*
* @callback DataStore~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 DataStore#event:afterFind
* @see DataStore#find
* @since 3.0.0
*/
/**
* Wrapper for {@link Mapper#find}. Adds any found record to the store.
*
* @example
* import {DataStore} from 'js-data'
* import {HttpAdapter} from 'js-data-http'
*
* const store = new DataStore()
* 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 DataStore#beforeFind
* @fires DataStore#afterFind
* @fires DataStore#add
* @method DataStore#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}.
* @returns {Promise} Resolves with the result, if any.
* @since 3.0.0
*/
find (name, id, opts) {
opts || (opts = {})
const pendingQuery = this._pendingQueries[name][id]
utils.fillIn(opts, this.getMapper(name))
if (pendingQuery) {
return pendingQuery
}
const item = this.cachedFind(name, id, opts)
let promise
if (opts.force || !item) {
promise = this._pendingQueries[name][id] = this._callSuper('find', name, id, opts).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)
})
} else {
promise = utils.resolve(item)
}
return promise
},
/**
* Fired during {@link DataStore#findAll}. See
* {@link DataStore~beforeFindAllListener} for how to listen for this event.
*
* @event DataStore#beforeFindAll
* @see DataStore~beforeFindAllListener
* @see DataStore#findAll
*/
/**
* Callback signature for the {@link DataStore#event:beforeFindAll} event.
*
* @example
* function onBeforeFindAll (mapperName, query, opts) {
* // do something
* }
* store.on('beforeFindAll', onBeforeFindAll)
*
* @callback DataStore~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 DataStore#event:beforeFindAll
* @see DataStore#findAll
* @since 3.0.0
*/
/**
* Fired during {@link DataStore#findAll}. See
* {@link DataStore~afterFindAllListener} for how to listen for this event.
*
* @event DataStore#afterFindAll
* @see DataStore~afterFindAllListener
* @see DataStore#findAll
*/
/**
* Callback signature for the {@link DataStore#event:afterFindAll} event.
*
* @example
* function onAfterFindAll (mapperName, query, opts, result) {
* // do something
* }
* store.on('afterFindAll', onAfterFindAll)
*
* @callback DataStore~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 DataStore#event:afterFindAll
* @see DataStore#findAll
* @since 3.0.0
*/
/**
* Wrapper for {@link Mapper#findAll}. Adds any found records to the store.
*
* @example
* import {DataStore} from 'js-data'
* import {HttpAdapter} from 'js-data-http'
*
* const store = new DataStore()
* 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 DataStore#beforeFindAll
* @fires DataStore#afterFindAll
* @fires DataStore#add
* @method DataStore#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}.
* @returns {Promise} Resolves with the result, if any.
* @since 3.0.0
*/
findAll (name, query, opts) {
opts || (opts = {})
const hash = this.hashQuery(name, query, opts)
const pendingQuery = this._pendingQueries[name][hash]
utils.fillIn(opts, this.getMapper(name))
if (pendingQuery) {
return pendingQuery
}
const items = this.cachedFindAll(name, hash, opts)
let promise
if (opts.force || !items) {
promise = this._pendingQueries[name][hash] = this._callSuper('findAll', name, query, opts).then((result) => {
delete this._pendingQueries[name][hash]
result = this._end(name, result, opts)
this.cacheFindAll(name, result, hash, opts)
return result
}, (err) => {
delete this._pendingQueries[name][hash]
return utils.reject(err)
})
} else {
promise = utils.resolve(items)
}
return promise
},
/**
* Return the {@link LinkedCollection} with the given name, if for some
* reason you need a direct reference to the collection.
*
* @method DataStore#getCollection
* @param {string} name Name of the {@link LinkedCollection} to retrieve.
* @returns {LinkedCollection}
* @since 3.0.0
* @throws {Error} Thrown if the specified {@link LinkedCollection} does not
* exist.
*/
getCollection (name) {
const collection = this._collections[name]
if (!collection) {
throw utils.err(`${DOMAIN}#getCollection`, name)(404, 'collection')
}
return collection
},
/**
* Hashing function used to cache {@link DataStore#find} and
* {@link DataStore#findAll} requests. This method simply JSONifies the
* `query` argument passed to {@link DataStore#find} or
* {@link DataStore#findAll}.
*
* Override this method for custom hashing behavior.
* @method DataStore#hashQuery
* @param {string} name The `name` argument passed to {@link DataStore#find}
* or {@link DataStore#findAll}.
* @param {Object} query The `query` argument passed to {@link DataStore#find}
* or {@link DataStore#findAll}.
* @returns {string} The JSONified `query`.
* @since 3.0.0
*/
hashQuery (name, query, opts) {
return utils.toJson(query)
},
inject (name, records, opts) {
console.warn('DEPRECATED: "inject" is deprecated, use "add" instead')
return this.add(name, records, opts)
},
/**
* Wrapper for {@link LinkedCollection#remove}. Removes the specified
* {@link Record} from the store.
*
* @example <caption>DataStore#remove</caption>
* // Normally you would do: import {DataStore} from 'js-data'
* const JSData = require('js-data@3.0.0-beta.7')
* const {DataStore} = JSData
* console.log('Using JSData v' + JSData.version.full)
*
* const store = new DataStore()
* store.defineMapper('book')
* console.log(store.getAll('book').length)
* store.add('book', { id: 1234 })
* console.log(store.getAll('book').length)
* store.remove('book', 1234)
* console.log(store.getAll('book').length)
*
* @fires DataStore#remove
* @method DataStore#remove
* @param {string} name The name of the {@link LinkedCollection} to target.
* @param {string|number} id The primary key of the {@link Record} to remove.
* @param {Object} [opts] Configuration options.
* @param {string[]} [opts.with] Relations of the {@link Record} to also
* remove from the store.
* @returns {Record} The removed {@link Record}, if any.
* @see LinkedCollection#add
* @see Collection#add
* @since 3.0.0
*/
remove (name, id, opts) {
const record = this.getCollection(name).remove(id, opts)
if (record) {
this.removeRelated(name, [record], opts)
}
return record
},
/**
* Wrapper for {@link LinkedCollection#removeAll}. Removes the selected
* {@link Record}s from the store.
*
* @example <caption>DataStore#removeAll</caption>
* // Normally you would do: import {DataStore} from 'js-data'
* const JSData = require('js-data@3.0.0-beta.7')
* const {DataStore} = JSData
* console.log('Using JSData v' + JSData.version.full)
*
* const store = new DataStore()
* store.defineMapper('movie')
* console.log(store.getAll('movie').length)
* store.add('movie', [{ id: 3, rating: 'R' }, { id: 4, rating: 'PG-13' })
* console.log(store.getAll('movie').length)
* store.removeAll('movie', { rating: 'R' })
* console.log(store.getAll('movie').length)
*
* @fires DataStore#remove
* @method DataStore#removeAll
* @param {string} name The name of the {@link LinkedCollection} to target.
* @param {Object} [query={}] Selection query. See {@link query}.
* @param {Object} [query.where] See {@link query.where}.
* @param {number} [query.offset] See {@link query.offset}.
* @param {number} [query.limit] See {@link query.limit}.
* @param {string|Array[]} [query.orderBy] See {@link query.orderBy}.
* @param {Object} [opts] Configuration options.
* @param {string[]} [opts.with] Relations of the {@link Record} to also
* remove from the store.
* @returns {Record} The removed {@link Record}s, if any.
* @see LinkedCollection#add
* @see Collection#add
* @since 3.0.0
*/
removeAll (name, query, opts) {
const records = this.getCollection(name).removeAll(query, opts)
if (records.length) {
this.removeRelated(name, records, opts)
}
return records
},
/**
* Remove from the store {@link Record}s that are related to the provided
* {@link Record}(s).
*
* @fires DataStore#remove
* @method DataStore#removeRelated
* @param {string} name The name of the {@link LinkedCollection} to target.
* @param {Record|Record[]} records {@link Record}s whose relations are to be
* removed.
* @param {Object} [opts] Configuration options.
* @param {string[]} [opts.with] Relations of the {@link Record}(s) to remove
* from the store.
* @since 3.0.0
*/
removeRelated (name, records, opts) {
if (!utils.isArray(records)) {
records = [records]
}
utils.forEachRelation(this.getMapper(name), opts, (def, optsCopy) => {
records.forEach((record) => {
let relatedData
let query
if (def.foreignKey && (def.type === hasOneType || def.type === hasManyType)) {
query = { [def.foreignKey]: def.getForeignKey(record) }
} else if (def.type === hasManyType && def.localKeys) {
query = {
where: {
[def.getRelation().idAttribute]: {
'in': utils.get(record, def.localKeys)
}
}
}
} else if (def.type === hasManyType && def.foreignKeys) {
query = {
where: {
[def.foreignKeys]: {
'contains': def.getForeignKey(record)
}
}
}
} else if (def.type === belongsToType) {
relatedData = this.remove(def.relation, def.getForeignKey(record), optsCopy)
}
if (query) {
relatedData = this.removeAll(def.relation, query, optsCopy)
}
if (relatedData) {
if (utils.isArray(relatedData) && !relatedData.length) {
return
}
if (def.type === hasOneType) {
relatedData = relatedData[0]
}
def.setLocalField(record, relatedData)
}
})
})
},
/**
* Fired during {@link DataStore#update}. See
* {@link DataStore~beforeUpdateListener} for how to listen for this event.
*
* @event DataStore#beforeUpdate
* @see DataStore~beforeUpdateListener
* @see DataStore#update
*/
/**
* Callback signature for the {@link DataStore#event:beforeUpdate} event.
*
* @example
* function onBeforeUpdate (mapperName, id, props, opts) {
* // do something
* }
* store.on('beforeUpdate', onBeforeUpdate)
*
* @callback DataStore~beforeUpdateListener
* @param {string} name The `name` argument received by {@link Mapper#beforeUpdate}.
* @param {string|number} id The `id` argument received by {@link Mapper#beforeUpdate}.
* @param {Object} props The `props` argument received by {@link Mapper#beforeUpdate}.
* @param {Object} opts The `opts` argument received by {@link Mapper#beforeUpdate}.
* @see DataStore#event:beforeUpdate
* @see DataStore#update
* @since 3.0.0
*/
/**
* Fired during {@link DataStore#update}. See
* {@link DataStore~afterUpdateListener} for how to listen for this event.
*
* @event DataStore#afterUpdate
* @see DataStore~afterUpdateListener
* @see DataStore#update
*/
/**
* Callback signature for the {@link DataStore#event:afterUpdate} event.
*
* @example
* function onAfterUpdate (mapperName, id, props, opts, result) {
* // do something
* }
* store.on('afterUpdate', onAfterUpdate)
*
* @callback DataStore~afterUpdateListener
* @param {string} name The `name` argument received by {@link Mapper#afterUpdate}.
* @param {string|number} id The `id` argument received by {@link Mapper#afterUpdate}.
* @param {Object} props The `props` argument received by {@link Mapper#afterUpdate}.
* @param {Object} opts The `opts` argument received by {@link Mapper#afterUpdate}.
* @param {Object} result The `result` argument received by {@link Mapper#afterUpdate}.
* @see DataStore#event:afterUpdate
* @see DataStore#update
* @since 3.0.0
*/
/**
* Wrapper for {@link Mapper#update}. Adds the updated {@link Record} to the
* store.
*
* @example
* import {DataStore} from 'js-data'
* import {HttpAdapter} from 'js-data-http'
*
* const store = new DataStore()
* store.registerAdapter('http', new HttpAdapter(), { default: true })
*
* store.defineMapper('post')
*
* // Since this example uses the http adapter, we'll get something like:
* //
* // PUT /post/1234 {"status":"published"}
* store.update('post', 1, { status: 'published' }).then((post) => {
* // The post record has also been updated in the in-memory store
* console.log(store.get('post', 1234))
* })
*
* @fires DataStore#beforeUpdate
* @fires DataStore#afterUpdate
* @fires DataStore#add
* @method DataStore#update
* @param {string} name Name of the {@link Mapper} to target.
* @param {(string|number)} id Passed to {@link Mapper#update}.
* @param {Object} record Passed to {@link Mapper#update}.
* @param {Object} [opts] Passed to {@link Mapper#update}. See
* {@link Mapper#update} for more configuration options.
* @returns {Promise} Resolves with the result of the update.
* @since 3.0.0
*/
update (name, id, record, opts) {
opts || (opts = {})
return this._callSuper('update', name, id, record, opts)
.then((result) => this._end(name, result, opts))
},
/**
* Fired during {@link DataStore#updateAll}. See
* {@link DataStore~beforeUpdateAllListener} for how to listen for this event.
*
* @event DataStore#beforeUpdateAll
* @see DataStore~beforeUpdateAllListener
* @see DataStore#updateAll
*/
/**
* Callback signature for the {@link DataStore#event:beforeUpdateAll} event.
*
* @example
* function onBeforeUpdateAll (mapperName, props, query, opts) {
* // do something
* }
* store.on('beforeUpdateAll', onBeforeUpdateAll)
*
* @callback DataStore~beforeUpdateAllListener
* @param {string} name The `name` argument received by {@link Mapper#beforeUpdateAll}.
* @param {Object} props The `props` argument received by {@link Mapper#beforeUpdateAll}.
* @param {Object} query The `query` argument received by {@link Mapper#beforeUpdateAll}.
* @param {Object} opts The `opts` argument received by {@link Mapper#beforeUpdateAll}.
* @see DataStore#event:beforeUpdateAll
* @see DataStore#updateAll
* @since 3.0.0
*/
/**
* Fired during {@link DataStore#updateAll}. See
* {@link DataStore~afterUpdateAllListener} for how to listen for this event.
*
* @event DataStore#afterUpdateAll
* @see DataStore~afterUpdateAllListener
* @see DataStore#updateAll
*/
/**
* Callback signature for the {@link DataStore#event:afterUpdateAll} event.
*
* @example
* function onAfterUpdateAll (mapperName, props, query, opts, result) {
* // do something
* }
* store.on('afterUpdateAll', onAfterUpdateAll)
*
* @callback DataStore~afterUpdateAllListener
* @param {string} name The `name` argument received by {@link Mapper#afterUpdateAll}.
* @param {Object} props The `props` argument received by {@link Mapper#afterUpdateAll}.
* @param {Object} query The `query` argument received by {@link Mapper#afterUpdateAll}.
* @param {Object} opts The `opts` argument received by {@link Mapper#afterUpdateAll}.
* @param {Object} result The `result` argument received by {@link Mapper#afterUpdateAll}.
* @see DataStore#event:afterUpdateAll
* @see DataStore#updateAll
* @since 3.0.0
*/
/**
* Wrapper for {@link Mapper#updateAll}. Adds the updated {@link Record}s to
* the store.
*
* @example
* import {DataStore} from 'js-data'
* import {HttpAdapter} from 'js-data-http'
*
* const store = new DataStore()
* store.registerAdapter('http', new HttpAdapter(), { default: true })
*
* store.defineMapper('post')
*
* // Since this example uses the http adapter, we'll get something like:
* //
* // PUT /post?author_id=1234 {"status":"published"}
* store.updateAll('post', { author_id: 1234 }, { status: 'published' }).then((posts) => {
* // The post records have also been updated in the in-memory store
* console.log(store.filter('posts', { author_id: 1234 }))
* })
*
* @fires DataStore#beforeUpdateAll
* @fires DataStore#afterUpdateAll
* @fires DataStore#add
* @method DataStore#updateAll
* @param {string} name Name of the {@link Mapper} to target.
* @param {Object} props Passed to {@link Mapper#updateAll}.
* @param {Object} [query] Passed to {@link Mapper#updateAll}.
* @param {Object} [opts] Passed to {@link Mapper#updateAll}. See
* {@link Mapper#updateAll} for more configuration options.
* @returns {Promise} Resolves with the result of the update.
* @since 3.0.0
*/
updateAll (name, props, query, opts) {
opts || (opts = {})
return this._callSuper('updateAll', name, query, props, opts)
.then((result) => this._end(name, result, opts))
},
/**
* Fired during {@link DataStore#updateMany}. See
* {@link DataStore~beforeUpdateManyListener} for how to listen for this event.
*
* @event DataStore#beforeUpdateMany
* @see DataStore~beforeUpdateManyListener
* @see DataStore#updateMany
*/
/**
* Callback signature for the {@link DataStore#event:beforeUpdateMany} event.
*
* @example
* function onBeforeUpdateMany (mapperName, records, opts) {
* // do something
* }
* store.on('beforeUpdateMany', onBeforeUpdateMany)
*
* @callback DataStore~beforeUpdateManyListener
* @param {string} name The `name` argument received by {@link Mapper#beforeUpdateMany}.
* @param {Object} records The `records` argument received by {@link Mapper#beforeUpdateMany}.
* @param {Object} opts The `opts` argument received by {@link Mapper#beforeUpdateMany}.
* @see DataStore#event:beforeUpdateMany
* @see DataStore#updateMany
* @since 3.0.0
*/
/**
* Fired during {@link DataStore#updateMany}. See
* {@link DataStore~afterUpdateManyListener} for how to listen for this event.
*
* @event DataStore#afterUpdateMany
* @see DataStore~afterUpdateManyListener
* @see DataStore#updateMany
*/
/**
* Callback signature for the {@link DataStore#event:afterUpdateMany} event.
*
* @example
* function onAfterUpdateMany (mapperName, records, opts, result) {
* // do something
* }
* store.on('afterUpdateMany', onAfterUpdateMany)
*
* @callback DataStore~afterUpdateManyListener
* @param {string} name The `name` argument received by {@link Mapper#afterUpdateMany}.
* @param {Object} records The `records` argument received by {@link Mapper#afterUpdateMany}.
* @param {Object} opts The `opts` argument received by {@link Mapper#afterUpdateMany}.
* @param {Object} result The `result` argument received by {@link Mapper#afterUpdateMany}.
* @see DataStore#event:afterUpdateMany
* @see DataStore#updateMany
* @since 3.0.0
*/
/**
* Wrapper for {@link Mapper#updateMany}. Adds the updated {@link Record}s to
* the store.
*
* @example
* import {DataStore} from 'js-data'
* import {HttpAdapter} from 'js-data-http'
*
* const store = new DataStore()
* store.registerAdapter('http', new HttpAdapter(), { default: true })
*
* store.defineMapper('post')
*
* // Since this example uses the http adapter, we'll get something like:
* //
* // PUT /post [{"id":3,status":"published"},{"id":4,status":"published"}]
* store.updateMany('post', [
* { id: 3, status: 'published' },
* { id: 4, status: 'published' }
* ]).then((posts) => {
* // The post records have also been updated in the in-memory store
* console.log(store.getAll('post', 3, 4))
* })
*
* @fires DataStore#beforeUpdateMany
* @fires DataStore#afterUpdateMany
* @fires DataStore#add
* @method DataStore#updateMany
* @param {string} name Name of the {@link Mapper} to target.
* @param {(Object[]|Record[])} records Passed to {@link Mapper#updateMany}.
* @param {Object} [opts] Passed to {@link Mapper#updateMany}. See
* {@link Mapper#updateMany} for more configuration options.
* @returns {Promise} Resolves with the result of the update.
* @since 3.0.0
*/
updateMany (name, records, opts) {
opts || (opts = {})
return this._callSuper('updateMany', name, records, opts)
.then((result) => this._end(name, result, opts))
}
}
proxiedCollectionMethods.forEach(function (method) {
props[method] = function (name, ...args) {
return this.getCollection(name)[method](...args)
}
})
export default Container.extend(props)
/**
* Fired when a record changes. Only works for records that have tracked fields.
* See {@link DataStore~changeListener} on how to listen for this event.
*
* @event DataStore#change
* @see DataStore~changeListener
*/
/**
* Callback signature for the {@link DataStore#event:change} event.
*
* @example
* function onChange (mapperName, record, changes) {
* // do something
* }
* store.on('change', onChange)
*
* @callback DataStore~changeListener
* @param {string} name The name of the associated {@link Mapper}.
* @param {Record} The Record that changed.
* @param {Object} The changes.
* @see DataStore#event:change
* @since 3.0.0
*/
/**
* Fired when one or more records are added to the in-memory store. See
* {@link DataStore~addListener} on how to listen for this event.
*
* @event DataStore#add
* @see DataStore~addListener
* @see DataStore#event:add
* @see DataStore#add
* @see DataStore#create
* @see DataStore#createMany
* @see DataStore#find
* @see DataStore#findAll
* @see DataStore#update
* @see DataStore#updateAll
* @see DataStore#updateMany
*/
/**
* Callback signature for the {@link DataStore#event:add} event.
*
* @example
* function onAdd (mapperName, recordOrRecords) {
* // do something
* }
* store.on('add', onAdd)
*
* @callback DataStore~addListener
* @param {string} name The name of the associated {@link Mapper}.
* @param {Record|Record[]} The Record or Records that were added.
* @see DataStore#event:add
* @see DataStore#add
* @see DataStore#create
* @see DataStore#createMany
* @see DataStore#find
* @see DataStore#findAll
* @see DataStore#update
* @see DataStore#updateAll
* @see DataStore#updateMany
* @since 3.0.0
*/
/**
* Fired when one or more records are removed from the in-memory store. See
* {@link DataStore~removeListener} for how to listen for this event.
*
* @event DataStore#remove
* @see DataStore~removeListener
* @see DataStore#event:remove
* @see DataStore#clear
* @see DataStore#destroy
* @see DataStore#destroyAll
* @see DataStore#remove
* @see DataStore#removeAll
*/
/**
* Callback signature for the {@link DataStore#event:remove} event.
*
* @example
* function onRemove (mapperName, recordsOrRecords) {
* // do something
* }
* store.on('remove', onRemove)
*
* @callback DataStore~removeListener
* @param {string} name The name of the associated {@link Mapper}.
* @param {Record|Record[]} Record or Records that were removed.
* @see DataStore#event:remove
* @see DataStore#clear
* @see DataStore#destroy
* @see DataStore#destroyAll
* @see DataStore#remove
* @see DataStore#removeAll
* @since 3.0.0
*/
/**
* Create a subclass of this DataStore:
* @example <caption>DataStore.extend</caption>
* // Normally you would do: import {DataStore} from 'js-data'
* const JSData = require('js-data@3.0.0-beta.7')
* const {DataStore} = JSData
* console.log('Using JSData v' + JSData.version.full)
*
* // Extend the class using ES2015 class syntax.
* class CustomDataStoreClass extends DataStore {
* foo () { return 'bar' }
* static beep () { return 'boop' }
* }
* const customDataStore = new CustomDataStoreClass()
* console.log(customDataStore.foo())
* console.log(CustomDataStoreClass.beep())
*
* // Extend the class using alternate method.
* const OtherDataStoreClass = DataStore.extend({
* foo () { return 'bar' }
* }, {
* beep () { return 'boop' }
* })
* const otherDataStore = new OtherDataStoreClass()
* console.log(otherDataStore.foo())
* console.log(OtherDataStoreClass.beep())
*
* // Extend the class, providing a custom constructor.
* function AnotherDataStoreClass () {
* DataStore.call(this)
* this.created_at = new Date().getTime()
* }
* DataStore.extend({
* constructor: AnotherDataStoreClass,
* foo () { return 'bar' }
* }, {
* beep () { return 'boop' }
* })
* const anotherDataStore = new AnotherDataStoreClass()
* console.log(anotherDataStore.created_at)
* console.log(anotherDataStore.foo())
* console.log(AnotherDataStoreClass.beep())
*
* @method DataStore.extend
* @param {Object} [props={}] Properties to add to the prototype of the
* subclass.
* @param {Object} [props.constructor] Provide a custom constructor function
* to be used as the subclass itself.
* @param {Object} [classProps={}] Static properties to add to the subclass.
* @returns {Constructor} Subclass of this DataStore class.
* @since 3.0.0
*/