import utils from './utils' import Component from './Component' import { belongsToType, hasManyType, hasOneType } from './decorators' import Mapper from './Mapper' const DOMAIN = 'Container' const toProxy = [ /** * Proxy for {@link Mapper#count}. * * @name Container#count * @method * @param {string} name - Name of the {@link Mapper} to target. * @param {Object} [query] - Passed to {@link Model.count}. * @param {Object} [opts] - Passed to {@link Model.count}. * @return {Promise} */ 'count', /** * Proxy for {@link Mapper#create}. * * @name Container#create * @method * @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. * @return {Promise} */ 'create', /** * Proxy for {@link Mapper#createMany}. * * @name Container#createMany * @method * @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. * @return {Promise} */ 'createMany', /** * Proxy for {@link Mapper#createRecord}. * * @name Container#createRecord * @method * @param {string} name Name of the {@link Mapper} to target. * @param {Object} props Passed to {@link Mapper#createRecord}. * @param {Object} [opts] Passed to {@link Mapper#createRecord}. See * {@link Mapper#createRecord} for configuration options. * @return {Promise} */ 'createRecord', /** * Proxy for {@link Mapper#dbg}. * * @name Container#dbg * @method * @param {string} name - Name of the {@link Mapper} to target. * @param {...*} args - Passed to {@link Mapper#dbg}. */ 'dbg', /** * Proxy for {@link Mapper#destroy}. * * @name Container#destroy * @method * @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. * @return {Promise} */ 'destroy', /** * Proxy for {@link Mapper#destroyAll}. * * @name Container#destroyAll * @method * @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. * @return {Promise} */ 'destroyAll', /** * Proxy for {@link Mapper#find}. * * @name Container#find * @method * @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}. * @return {Promise} */ 'find', /** * Proxy for {@link Mapper#createRecord}. * * @name Container#findAll * @method * @param {string} name - Name of the {@link Mapper} to target. * @param {Object} [query] - Passed to {@link Model.findAll}. * @param {Object} [opts] - Passed to {@link Model.findAll}. * @return {Promise} */ 'findAll', /** * Proxy for {@link Mapper#is}. * * @name Container#getSchema * @method * @param {string} name - Name of the {@link Mapper} to target. */ 'getSchema', /** * Proxy for {@link Mapper#is}. * * @name Container#is * @method * @param {string} name - Name of the {@link Mapper} to target. * @param {...*} args - Passed to {@link Mapper#is}. */ 'is', /** * Proxy for {@link Mapper#log}. * * @name Container#log * @method * @param {string} name - Name of the {@link Mapper} to target. * @param {...*} args - Passed to {@link Mapper#log}. */ 'log', /** * Proxy for {@link Mapper#sum}. * * @name Container#sum * @method * @param {string} name - Name of the {@link Mapper} to target. * @param {string} field - Passed to {@link Model.sum}. * @param {Object} [query] - Passed to {@link Model.sum}. * @param {Object} [opts] - Passed to {@link Model.sum}. * @return {Promise} */ 'sum', /** * Proxy for {@link Mapper#toJSON}. * * @name Container#toJSON * @method * @param {string} name - Name of the {@link Mapper} to target. * @param {...*} args - Passed to {@link Mapper#toJSON}. */ 'toJSON', /** * Proxy for {@link Mapper#update}. * * @name Container#update * @method * @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. * @return {Promise} */ 'update', /** * Proxy for {@link Mapper#updateAll}. * * @name Container#updateAll * @method * @param {string} name - Name of the {@link Mapper} to target. * @param {Object?} query - Passed to {@link Model.updateAll}. * @param {Object} props - Passed to {@link Model.updateAll}. * @param {Object} [opts] - Passed to {@link Model.updateAll}. See * {@link Model.updateAll} for more configuration options. * @return {Promise} */ 'updateAll', /** * Proxy for {@link Mapper#updateMany}. * * @name Container#updateMany * @method * @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. * @return {Promise} */ 'updateMany' ] const props = { constructor: function Container (opts) { const self = this utils.classCallCheck(self, Container) Container.__super__.call(self) opts || (opts = {}) // Apply options provided by the user utils.fillIn(self, opts) /** * Defaults options to pass to {@link Container#mapperClass} when creating a * new mapper. * * @name Container#mapperDefaults * @type {Object} */ self.mapperDefaults = self.mapperDefaults || {} /** * Constructor function to use in {@link Container#defineMapper} to create a * new mapper. * * @name Container#mapperClass * @type {Function} */ self.mapperClass = self.mapperClass || Mapper // Initilize private data // Holds the adapters, shared by all mappers in this container self._adapters = {} // The the mappers in this container self._mappers = {} }, /** * Register a new event listener on this Container. * * Proxy for {@link Component#on}. If an event was emitted by a Mapper in the * Container, then the name of the Mapper will be prepended to the arugments * passed to the listener. * * @name Container#on * @method * @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 mappers in this container. * * @name Container#_onMapperEvent * @method * @private * @param {string} name Name of the mapper that emitted the event. * @param {...*} [args] Args passed to {@link Mapper#emit}. */ _onMapperEvent (name, ...args) { const type = args.shift() this.emit(type, name, ...args) }, /** * Create a new mapper and register it in this container. * * @example * import {Container} from 'js-data' * const container = new Container({ * mapperDefaults: { foo: 'bar' } * }) * const userMapper = container.defineMapper('user') * userMapper.foo // "bar" * * @name Container#defineMapper * @method * @param {string} name Name under which to register the new {@link Mapper}. * {@link Mapper#name} will be set to this value. * @param {Object} [opts] Configuration options. Passed to * {@link Container#mapperClass} when creating the new {@link Mapper}. * @return {Mapper} */ defineMapper (name, opts) { const self = this // For backwards compatibility with defineResource if (utils.isObject(name)) { opts = name name = opts.name } if (!utils.isString(name)) { throw utils.err(`${DOMAIN}#defineMapper`, 'name')(400, 'string', name) } // Default values for arguments opts || (opts = {}) // Set Mapper#name opts.name = name opts.relations || (opts.relations = {}) // Check if the user is overriding the datastore's default mapperClass const mapperClass = opts.mapperClass || self.mapperClass delete opts.mapperClass // Apply the datastore's defaults to the options going into the mapper utils.fillIn(opts, self.mapperDefaults) // Instantiate a mapper const mapper = self._mappers[name] = new mapperClass(opts) // eslint-disable-line mapper.relations || (mapper.relations = {}) // Make sure the mapper's name is set mapper.name = name // All mappers in this datastore will share adapters mapper._adapters = self.getAdapters() mapper.datastore = self mapper.on('all', function (...args) { self._onMapperEvent(name, ...args) }) // Setup the mapper's relations, including generating Mapper#relationList // and Mapper#relationFields utils.forOwn(mapper.relations, function (group, type) { utils.forOwn(group, function (relations, _name) { if (utils.isObject(relations)) { relations = [relations] } relations.forEach(function (def) { def.getRelation = function () { return self.getMapper(_name) } const relatedMapper = self._mappers[_name] || _name if (type === belongsToType) { mapper.belongsTo(relatedMapper, def) } else if (type === hasOneType) { mapper.hasOne(relatedMapper, def) } else if (type === hasManyType) { mapper.hasMany(relatedMapper, def) } }) }) }) return mapper }, defineResource (name, opts) { return this.defineMapper(name, opts) }, /** * Return the registered adapter with the given name or the default adapter if * no name is provided. * * @name Container#getAdapter * @method * @param {string} [name] The name of the adapter to retrieve. * @return {Adapter} The adapter. */ getAdapter (name) { const self = this const adapter = self.getAdapterName(name) if (!adapter) { throw utils.err(`${DOMAIN}#getAdapter`, 'name')(400, 'string', name) } return self.getAdapters()[adapter] }, /** * Return the name of a registered adapter based on the given name or options, * or the name of the default adapter if no name provided. * * @name Container#getAdapterName * @method * @param {(Object|string)} [opts] The name of an adapter or options, if any. * @return {string} The name of the adapter. */ getAdapterName (opts) { opts || (opts = {}) if (utils.isString(opts)) { opts = { adapter: opts } } return opts.adapter || this.mapperDefaults.defaultAdapter }, /** * Return the registered adapters of this container. * * @name Container#getAdapters * @method * @return {Adapter} */ getAdapters () { return this._adapters }, /** * Return the mapper registered under the specified name. * * @example * import {Container} from 'js-data' * const container = new Container() * const userMapper = container.defineMapper('user') * userMapper === container.getMapper('user') // true * * @name Container#getMapper * @method * @param {string} name {@link Mapper#name}. * @return {Mapper} */ getMapper (name) { const mapper = this._mappers[name] if (!mapper) { throw utils.err(`${DOMAIN}#getMapper`, name)(404, 'mapper') } return mapper }, /** * Register an adapter on this container under the given name. Adapters * registered on a container are shared by all mappers in the container. * * @example * import {Container} from 'js-data' * import HttpAdapter from 'js-data-http' * const container = new Container() * container.registerAdapter('http', new HttpAdapter, { default: true }) * * @name Container#registerAdapter * @method * @param {string} name The name of the adapter to register. * @param {Adapter} adapter The adapter to register. * @param {Object} [opts] Configuration options. * @param {boolean} [opts.default=false] Whether to make the adapter the * default adapter for all Mappers in this container. */ registerAdapter (name, adapter, opts) { const self = this opts || (opts = {}) self.getAdapters()[name] = adapter // Optionally make it the default adapter for the target. if (opts === true || opts.default) { self.mapperDefaults.defaultAdapter = name utils.forOwn(self._mappers, function (mapper) { mapper.defaultAdapter = name }) } } } toProxy.forEach(function (method) { props[method] = function (name, ...args) { return this.getMapper(name)[method](...args) } }) /** * ```javascript * import {Container} from 'js-data' * ``` * * The `Container` class is a place to store {@link Mapper} instances. * * Without a container, you need to manage mappers yourself, including resolving * circular dependencies among relations. All mappers in a container share the * same adapters, so you don't have to add each adapter to all of your mappers. * * @example <caption>Without Container</caption> * import {Mapper} from 'js-data' * import HttpAdapter from 'js-data-http' * const adapter = new HttpAdapter() * const userMapper = new Mapper({ name: 'user' }) * userMapper.registerAdapter('http', adapter, { default: true }) * const commentMapper = new Mapper({ name: 'comment' }) * commentMapper.registerAdapter('http', adapter, { default: true }) * * // This might be more difficult if the mappers were defined in different * // modules. * userMapper.hasMany(commentMapper, { * localField: 'comments', * foreignKey: 'userId' * }) * commentMapper.belongsTo(userMapper, { * localField: 'user', * foreignKey: 'userId' * }) * * @example <caption>With Container</caption> * import {Container} from 'js-data' * import HttpAdapter from 'js-data-http' * const container = new Container() * // All mappers in container share adapters * container.registerAdapter('http', new HttpAdapter(), { default: true }) * * // These could be defined in separate modules without a problem. * container.defineMapper('user', { * relations: { * hasMany: { * comment: { * localField: 'comments', * foreignKey: 'userId' * } * } * } * }) * container.defineMapper('comment', { * relations: { * belongsTo: { * user: { * localField: 'user', * foreignKey: 'userId' * } * } * } * }) * * @class Container * @extends Component * @param {Object} [opts] Configuration options. * @param {Function} [opts.mapperClass] Constructor function to use in * {@link Container#defineMapper} to create a new mapper. * @param {Object} [opts.mapperDefaults] Defaults options to pass to * {@link Container#mapperClass} when creating a new mapper. * @return {Container} */ export default Component.extend(props)