UNPKG

js-data

Version:

Robust, framework-agnostic in-memory data store.

1,499 lines (1,405 loc) 87.7 kB
import utils from './utils' import Component from './Component' import Record from './Record' import Schema from './Schema' import { Relation } from './relations' import { belongsTo, belongsToType, hasMany, hasManyType, hasOne, hasOneType } from './decorators' const DOMAIN = 'Mapper' const applyDefaultsHooks = [ 'beforeCreate', 'beforeCreateMany' ] const validatingHooks = [ 'beforeCreate', 'beforeCreateMany', 'beforeUpdate', 'beforeUpdateAll', 'beforeUpdateMany' ] const makeNotify = function (num) { return function (...args) { const opts = args[args.length - num] const op = opts.op this.dbg(op, ...args) if (applyDefaultsHooks.indexOf(op) !== -1 && opts.applyDefaults !== false) { const schema = this.getSchema() if (schema && schema.applyDefaults) { let toProcess = args[0] if (!utils.isArray(toProcess)) { toProcess = [toProcess] } toProcess.forEach((record) => { schema.applyDefaults(record) }) } } // Automatic validation if (validatingHooks.indexOf(op) !== -1 && !opts.noValidate) { // Save current value of option const originalExistingOnly = opts.existingOnly // For updates, ignore required fields if they aren't present if (op.indexOf('beforeUpdate') === 0 && opts.existingOnly === undefined) { opts.existingOnly = true } const errors = this.validate(args[op === 'beforeUpdate' ? 1 : 0], utils.pick(opts, ['existingOnly'])) // Restore option opts.existingOnly = originalExistingOnly // Abort lifecycle due to validation errors if (errors) { const err = new Error('validation failed') err.errors = errors return utils.reject(err) } } // Emit lifecycle event if (opts.notify || (opts.notify === undefined && this.notify)) { setTimeout(() => { this.emit(op, ...args) }) } } } // These are the default implementations of all of the lifecycle hooks const notify = makeNotify(1) const notify2 = makeNotify(2) // This object provides meta information used by Mapper#crud to actually // execute each lifecycle method const LIFECYCLE_METHODS = { count: { defaults: [{}, {}], skip: true, types: [] }, destroy: { defaults: [{}, {}], skip: true, types: [] }, destroyAll: { defaults: [{}, {}], skip: true, types: [] }, find: { defaults: [undefined, {}], types: [] }, findAll: { defaults: [{}, {}], types: [] }, sum: { defaults: [undefined, {}, {}], skip: true, types: [] }, update: { adapterArgs (mapper, id, props, opts) { return [id, mapper.toJSON(props, opts), opts] }, beforeAssign: 1, defaults: [undefined, {}, {}], types: [] }, updateAll: { adapterArgs (mapper, props, query, opts) { return [mapper.toJSON(props, opts), query, opts] }, beforeAssign: 0, defaults: [{}, {}, {}], types: [] }, updateMany: { adapterArgs (mapper, records, opts) { return [records.map((record) => mapper.toJSON(record, opts)), opts] }, beforeAssign: 0, defaults: [[], {}], types: [] } } const MAPPER_DEFAULTS = { /** * Hash of registered adapters. Don't modify directly. Use * {@link Mapper#registerAdapter} instead. * * @default {} * @name Mapper#_adapters * @since 3.0.0 * @tutorial ["http://www.js-data.io/v3.0/docs/connecting-to-a-data-source","Connecting to a data source"] */ _adapters: {}, /** * Whether {@link Mapper#beforeCreate} and {@link Mapper#beforeCreateMany} * should automatically receive default values according to the Mapper's schema. * * @default true * @name Mapper#applyDefaults * @since 3.0.0 * @type {boolean} */ applyDefaults: true, /** * Whether to augment {@link Mapper#recordClass} with ES5 getters and setters * according to the properties defined in {@link Mapper#schema}. This makes * possible validation and change tracking on individual properties * when using the dot (e.g. `user.name = "Bob"`) operator to modify a * property, and is `true` by default. * * @default true * @name Mapper#applySchema * @since 3.0.0 * @type {boolean} */ applySchema: true, /** * The name of the registered adapter that this Mapper should used by default. * * @default "http" * @name Mapper#defaultAdapter * @since 3.0.0 * @tutorial ["http://www.js-data.io/v3.0/docs/connecting-to-a-data-source","Connecting to a data source"] * @type {string} */ defaultAdapter: 'http', /** * The field used as the unique identifier on records handled by this Mapper. * * @default id * @name Mapper#idAttribute * @since 3.0.0 * @type {string} */ idAttribute: 'id', /** * Whether records created from this mapper keep changeHistory on property changes. * * @default true * @name Mapper#keepChangeHistory * @since 3.0.0 * @type {boolean} */ keepChangeHistory: true, /** * Whether this Mapper should emit operational events. * * @default true * @name Mapper#notify * @since 3.0.0 * @type {boolean} */ notify: true, /** * Whether to skip validation when the Record instances are created. * * @default false * @name Mapper#noValidate * @since 3.0.0 * @type {boolean} */ noValidate: false, /** * Whether {@link Mapper#create}, {@link Mapper#createMany}, * {@link Mapper#update}, {@link Mapper#updateAll}, {@link Mapper#updateMany}, * {@link Mapper#find}, {@link Mapper#findAll}, {@link Mapper#destroy}, * {@link Mapper#destroyAll}, {@link Mapper#count}, and {@link Mapper#sum} * should return a raw result object that contains both the instance data * returned by the adapter _and_ metadata about the operation. * * The default is to NOT return the result object, and instead return just the * instance data. * * @default false * @name Mapper#raw * @since 3.0.0 * @type {boolean} */ raw: false, /** * Whether records created from this mapper automatically validate their properties * when their properties are modified. * * @default true * @name Mapper#validateOnSet * @since 3.0.0 * @type {boolean} */ validateOnSet: true } /** * The core of JSData's [ORM/ODM][orm] implementation. Given a minimum amout of * meta information about a resource, a Mapper can perform generic CRUD * operations against that resource. Apart from its configuration, a Mapper is * stateless. The particulars of various persistence layers have been abstracted * into adapters, which a Mapper uses to perform its operations. * * The term "Mapper" comes from the [Data Mapper Pattern][pattern] described in * Martin Fowler's [Patterns of Enterprise Application Architecture][book]. A * Data Mapper moves data between [in-memory object instances][record] and a * relational or document-based database. JSData's Mapper can work with any * persistence layer you can write an adapter for. * * _("Model" is a heavily overloaded term and is avoided in this documentation * to prevent confusion.)_ * * [orm]: https://en.wikipedia.org/wiki/Object-relational_mapping * * @example * [pattern]: https://en.wikipedia.org/wiki/Data_mapper_pattern * [book]: http://martinfowler.com/books/eaa.html * [record]: Record.html * // Import and instantiate * import { Mapper } from 'js-data'; * const UserMapper = new Mapper({ name: 'user' }); * * @example * // Define a Mapper using the Container component * import { Container } from 'js-data'; * const store = new Container(); * store.defineMapper('user'); * * @class Mapper * @extends Component * @param {object} opts Configuration options. * @param {boolean} [opts.applySchema=true] See {@link Mapper#applySchema}. * @param {boolean} [opts.debug=false] See {@link Component#debug}. * @param {string} [opts.defaultAdapter=http] See {@link Mapper#defaultAdapter}. * @param {string} [opts.idAttribute=id] See {@link Mapper#idAttribute}. * @param {object} [opts.methods] See {@link Mapper#methods}. * @param {string} opts.name See {@link Mapper#name}. * @param {boolean} [opts.notify] See {@link Mapper#notify}. * @param {boolean} [opts.raw=false] See {@link Mapper#raw}. * @param {Function|boolean} [opts.recordClass] See {@link Mapper#recordClass}. * @param {Object|Schema} [opts.schema] See {@link Mapper#schema}. * @returns {Mapper} A new {@link Mapper} instance. * @see http://www.js-data.io/v3.0/docs/components-of-jsdata#mapper * @since 3.0.0 * @tutorial ["http://www.js-data.io/v3.0/docs/components-of-jsdata#mapper","Components of JSData: Mapper"] * @tutorial ["http://www.js-data.io/v3.0/docs/modeling-your-data","Modeling your data"] */ function Mapper (opts) { utils.classCallCheck(this, Mapper) Component.call(this) opts || (opts = {}) // Prepare certain properties to be non-enumerable Object.defineProperties(this, { _adapters: { value: undefined, writable: true }, /** * The {@link Container} that holds this Mapper. __Do not modify.__ * * @name Mapper#lifecycleMethods * @since 3.0.0 * @type {Object} */ datastore: { value: undefined, writable: true }, /** * The meta information describing this Mapper's available lifecycle * methods. __Do not modify.__ * * @name Mapper#lifecycleMethods * @since 3.0.0 * @type {Object} */ lifecycleMethods: { value: LIFECYCLE_METHODS }, /** * Set to `false` to force the Mapper to work with POJO objects only. * * @example * // Use POJOs only. * import { Mapper, Record } from 'js-data'; * const UserMapper = new Mapper({ recordClass: false }); * UserMapper.recordClass // false; * const user = UserMapper.createRecord(); * user instanceof Record; // false * * @example * // Set to a custom class to have records wrapped in your custom class. * import { Mapper, Record } from 'js-data'; * // Custom class * class User { * constructor (props = {}) { * for (var key in props) { * if (props.hasOwnProperty(key)) { * this[key] = props[key]; * } * } * } * } * const UserMapper = new Mapper({ recordClass: User }); * UserMapper.recordClass; // function User() {} * const user = UserMapper.createRecord(); * user instanceof Record; // false * user instanceof User; // true * * * @example * // Extend the {@link Record} class. * import { Mapper, Record } from 'js-data'; * // Custom class * class User extends Record { * constructor () { * super(props); * } * } * const UserMapper = new Mapper({ recordClass: User }); * UserMapper.recordClass; // function User() {} * const user = UserMapper.createRecord(); * user instanceof Record; // true * user instanceof User; // true * * @name Mapper#recordClass * @default {@link Record} * @see Record * @since 3.0.0 */ recordClass: { value: undefined, writable: true }, /** * This Mapper's {@link Schema}. * * @example <caption>Mapper#schema</caption> * const JSData = require('js-data'); * const { Mapper } = JSData; * console.log('Using JSData v' + JSData.version.full); * * const UserMapper = new Mapper({ * name: 'user', * schema: { * properties: { * id: { type: 'number' }, * first: { type: 'string', track: true }, * last: { type: 'string', track: true }, * role: { type: 'string', track: true, required: true }, * age: { type: 'integer', track: true }, * is_active: { type: 'number' } * } * } * }); * const user = UserMapper.createRecord({ * id: 1, * name: 'John', * role: 'admin' * }); * user.on('change', function (user, changes) { * console.log(changes); * }); * user.on('change:role', function (user, value) { * console.log('change:role - ' + value); * }); * user.role = 'owner'; * * @name Mapper#schema * @see Schema * @since 3.0.0 * @type {Schema} */ schema: { value: undefined, writable: true } }) // Apply user-provided configuration utils.fillIn(this, opts) // Fill in any missing options with the defaults utils.fillIn(this, utils.copy(MAPPER_DEFAULTS)) /** * The name for this Mapper. This is the minimum amount of meta information * required for a Mapper to be able to execute CRUD operations for a * Resource. * * @name Mapper#name * @since 3.0.0 * @type {string} */ if (!this.name) { throw utils.err(`new ${DOMAIN}`, 'opts.name')(400, 'string', this.name) } // Setup schema, with an empty default schema if necessary if (this.schema) { this.schema.type || (this.schema.type = 'object') if (!(this.schema instanceof Schema)) { this.schema = new Schema(this.schema || { type: 'object' }) } } // Create a subclass of Record that's tied to this Mapper if (this.recordClass === undefined) { const superClass = Record this.recordClass = superClass.extend({ constructor: (function Record () { var subClass = function Record (props, opts) { utils.classCallCheck(this, subClass) superClass.call(this, props, opts) } return subClass })() }) } if (this.recordClass) { this.recordClass.mapper = this /** * Functions that should be added to the prototype of {@link Mapper#recordClass}. * * @name Mapper#methods * @since 3.0.0 * @type {Object} */ if (utils.isObject(this.methods)) { utils.addHiddenPropsToTarget(this.recordClass.prototype, this.methods) } // We can only apply the schema to the prototype of this.recordClass if the // class extends Record if (Object.isPrototypeOf.call(Record, this.recordClass) && this.schema && this.schema.apply && this.applySchema) { this.schema.apply(this.recordClass.prototype) } } } export default Component.extend({ constructor: Mapper, /** * Mapper lifecycle hook called by {@link Mapper#count}. If this method * returns a promise then {@link Mapper#count} will wait for the promise * to resolve before continuing. * * @method Mapper#afterCount * @param {object} query The `query` argument passed to {@link Mapper#count}. * @param {object} opts The `opts` argument passed to {@link Mapper#count}. * @param {*} result The result, if any. * @since 3.0.0 */ afterCount: notify2, /** * Mapper lifecycle hook called by {@link Mapper#create}. If this method * returns a promise then {@link Mapper#create} will wait for the promise * to resolve before continuing. * * @method Mapper#afterCreate * @param {object} props The `props` argument passed to {@link Mapper#create}. * @param {object} opts The `opts` argument passed to {@link Mapper#create}. * @param {*} result The result, if any. * @since 3.0.0 */ afterCreate: notify2, /** * Mapper lifecycle hook called by {@link Mapper#createMany}. If this method * returns a promise then {@link Mapper#createMany} will wait for the promise * to resolve before continuing. * * @method Mapper#afterCreateMany * @param {array} records The `records` argument passed to {@link Mapper#createMany}. * @param {object} opts The `opts` argument passed to {@link Mapper#createMany}. * @param {*} result The result, if any. * @since 3.0.0 */ afterCreateMany: notify2, /** * Mapper lifecycle hook called by {@link Mapper#destroy}. If this method * returns a promise then {@link Mapper#destroy} will wait for the promise * to resolve before continuing. * * @method Mapper#afterDestroy * @param {(string|number)} id The `id` argument passed to {@link Mapper#destroy}. * @param {object} opts The `opts` argument passed to {@link Mapper#destroy}. * @param {*} result The result, if any. * @since 3.0.0 */ afterDestroy: notify2, /** * Mapper lifecycle hook called by {@link Mapper#destroyAll}. If this method * returns a promise then {@link Mapper#destroyAll} will wait for the promise * to resolve before continuing. * * @method Mapper#afterDestroyAll * @param {*} data The `data` returned by the adapter. * @param {query} query The `query` argument passed to {@link Mapper#destroyAll}. * @param {object} opts The `opts` argument passed to {@link Mapper#destroyAll}. * @param {*} result The result, if any. * @since 3.0.0 */ afterDestroyAll: notify2, /** * Mapper lifecycle hook called by {@link Mapper#find}. If this method * returns a promise then {@link Mapper#find} will wait for the promise * to resolve before continuing. * * @method Mapper#afterFind * @param {(string|number)} id The `id` argument passed to {@link Mapper#find}. * @param {object} opts The `opts` argument passed to {@link Mapper#find}. * @param {*} result The result, if any. * @since 3.0.0 */ afterFind: notify2, /** * Mapper lifecycle hook called by {@link Mapper#findAll}. If this method * returns a promise then {@link Mapper#findAll} will wait for the promise * to resolve before continuing. * * @method Mapper#afterFindAll * @param {object} query The `query` argument passed to {@link Mapper#findAll}. * @param {object} opts The `opts` argument passed to {@link Mapper#findAll}. * @param {*} result The result, if any. * @since 3.0.0 */ afterFindAll: notify2, /** * Mapper lifecycle hook called by {@link Mapper#sum}. If this method * returns a promise then {@link Mapper#sum} will wait for the promise * to resolve before continuing. * * @method Mapper#afterSum * @param {object} query The `query` argument passed to {@link Mapper#sum}. * @param {object} opts The `opts` argument passed to {@link Mapper#sum}. * @param {*} result The result, if any. * @since 3.0.0 */ afterSum: notify2, /** * Mapper lifecycle hook called by {@link Mapper#update}. If this method * returns a promise then {@link Mapper#update} will wait for the promise * to resolve before continuing. * * @method Mapper#afterUpdate * @param {(string|number)} id The `id` argument passed to {@link Mapper#update}. * @param {props} props The `props` argument passed to {@link Mapper#update}. * @param {object} opts The `opts` argument passed to {@link Mapper#update}. * @param {*} result The result, if any. * @since 3.0.0 */ afterUpdate: notify2, /** * Mapper lifecycle hook called by {@link Mapper#updateAll}. If this method * returns a promise then {@link Mapper#updateAll} will wait for the promise * to resolve before continuing. * * @method Mapper#afterUpdateAll * @param {object} props The `props` argument passed to {@link Mapper#updateAll}. * @param {object} query The `query` argument passed to {@link Mapper#updateAll}. * @param {object} opts The `opts` argument passed to {@link Mapper#updateAll}. * @param {*} result The result, if any. * @since 3.0.0 */ afterUpdateAll: notify2, /** * Mapper lifecycle hook called by {@link Mapper#updateMany}. If this method * returns a promise then {@link Mapper#updateMany} will wait for the promise * to resolve before continuing. * * @method Mapper#afterUpdateMany * @param {array} records The `records` argument passed to {@link Mapper#updateMany}. * @param {object} opts The `opts` argument passed to {@link Mapper#updateMany}. * @param {*} result The result, if any. * @since 3.0.0 */ afterUpdateMany: notify2, /** * Mapper lifecycle hook called by {@link Mapper#create}. If this method * returns a promise then {@link Mapper#create} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeCreate * @param {object} props The `props` argument passed to {@link Mapper#create}. * @param {object} opts The `opts` argument passed to {@link Mapper#create}. * @since 3.0.0 */ beforeCreate: notify, /** * Mapper lifecycle hook called by {@link Mapper#createMany}. If this method * returns a promise then {@link Mapper#createMany} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeCreateMany * @param {array} records The `records` argument passed to {@link Mapper#createMany}. * @param {object} opts The `opts` argument passed to {@link Mapper#createMany}. * @since 3.0.0 */ beforeCreateMany: notify, /** * Mapper lifecycle hook called by {@link Mapper#count}. If this method * returns a promise then {@link Mapper#count} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeCount * @param {object} query The `query` argument passed to {@link Mapper#count}. * @param {object} opts The `opts` argument passed to {@link Mapper#count}. * @since 3.0.0 */ beforeCount: notify, /** * Mapper lifecycle hook called by {@link Mapper#destroy}. If this method * returns a promise then {@link Mapper#destroy} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeDestroy * @param {(string|number)} id The `id` argument passed to {@link Mapper#destroy}. * @param {object} opts The `opts` argument passed to {@link Mapper#destroy}. * @since 3.0.0 */ beforeDestroy: notify, /** * Mapper lifecycle hook called by {@link Mapper#destroyAll}. If this method * returns a promise then {@link Mapper#destroyAll} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeDestroyAll * @param {query} query The `query` argument passed to {@link Mapper#destroyAll}. * @param {object} opts The `opts` argument passed to {@link Mapper#destroyAll}. * @since 3.0.0 */ beforeDestroyAll: notify, /** * Mappers lifecycle hook called by {@link Mapper#find}. If this method * returns a promise then {@link Mapper#find} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeFind * @param {(string|number)} id The `id` argument passed to {@link Mapper#find}. * @param {object} opts The `opts` argument passed to {@link Mapper#find}. * @since 3.0.0 */ beforeFind: notify, /** * Mapper lifecycle hook called by {@link Mapper#findAll}. If this method * returns a promise then {@link Mapper#findAll} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeFindAll * @param {object} query The `query` argument passed to {@link Mapper#findAll}. * @param {object} opts The `opts` argument passed to {@link Mapper#findAll}. * @since 3.0.0 */ beforeFindAll: notify, /** * Mapper lifecycle hook called by {@link Mapper#sum}. If this method * returns a promise then {@link Mapper#sum} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeSum * @param {string} field The `field` argument passed to {@link Mapper#sum}. * @param {object} query The `query` argument passed to {@link Mapper#sum}. * @param {object} opts The `opts` argument passed to {@link Mapper#sum}. * @since 3.0.0 */ beforeSum: notify, /** * Mapper lifecycle hook called by {@link Mapper#update}. If this method * returns a promise then {@link Mapper#update} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeUpdate * @param {(string|number)} id The `id` argument passed to {@link Mapper#update}. * @param {props} props The `props` argument passed to {@link Mapper#update}. * @param {object} opts The `opts` argument passed to {@link Mapper#update}. * @since 3.0.0 */ beforeUpdate: notify, /** * Mapper lifecycle hook called by {@link Mapper#updateAll}. If this method * returns a promise then {@link Mapper#updateAll} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeUpdateAll * @param {object} props The `props` argument passed to {@link Mapper#updateAll}. * @param {object} query The `query` argument passed to {@link Mapper#updateAll}. * @param {object} opts The `opts` argument passed to {@link Mapper#updateAll}. * @since 3.0.0 */ beforeUpdateAll: notify, /** * Mapper lifecycle hook called by {@link Mapper#updateMany}. If this method * returns a promise then {@link Mapper#updateMany} will wait for the promise * to resolve before continuing. * * @method Mapper#beforeUpdateMany * @param {array} records The `records` argument passed to {@link Mapper#updateMany}. * @param {object} opts The `opts` argument passed to {@link Mapper#updateMany}. * @since 3.0.0 */ beforeUpdateMany: notify, /** * This method is called at the end of most lifecycle methods. It does the * following: * * 1. If `opts.raw` is `true`, add this Mapper's configuration to the `opts` * argument as metadata for the operation. * 2. Wrap the result data appropriately using {@link Mapper#wrap}, which * calls {@link Mapper#createRecord}. * * @method Mapper#_end * @private * @since 3.0.0 */ _end (result, opts, skip) { if (opts.raw) { utils._(result, opts) } if (skip) { return result } let _data = opts.raw ? result.data : result if (_data && utils.isFunction(this.wrap)) { _data = this.wrap(_data, opts) if (opts.raw) { result.data = _data } else { result = _data } } return result }, /** * Define a belongsTo relationship. Only useful if you're managing your * Mappers manually and not using a Container or DataStore component. * * @example * PostMapper.belongsTo(UserMapper, { * // post.user_id points to user.id * foreignKey: 'user_id' * // user records will be attached to post records at "post.user" * localField: 'user' * }); * * CommentMapper.belongsTo(UserMapper, { * // comment.user_id points to user.id * foreignKey: 'user_id' * // user records will be attached to comment records at "comment.user" * localField: 'user' * }); * CommentMapper.belongsTo(PostMapper, { * // comment.post_id points to post.id * foreignKey: 'post_id' * // post records will be attached to comment records at "comment.post" * localField: 'post' * }); * * @method Mapper#belongsTo * @see http://www.js-data.io/v3.0/docs/relations * @since 3.0.0 */ belongsTo (relatedMapper, opts) { return belongsTo(relatedMapper, opts)(this) }, /** * Select records according to the `query` argument and return the count. * * {@link Mapper#beforeCount} will be called before calling the adapter. * {@link Mapper#afterCount} will be called after calling the adapter. * * @example * // Get the number of published blog posts * PostMapper.count({ status: 'published' }).then((numPublished) => { * console.log(numPublished); // e.g. 45 * }); * * @method Mapper#count * @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. Refer to the `count` method * of whatever adapter you're using for more configuration options. * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the * adapter to use. * @param {boolean} [opts.notify={@link Mapper#notify}] See {@link Mapper#notify}. * @param {boolean} [opts.raw={@link Mapper#raw}] See {@link Mapper#raw}. * @returns {Promise} Resolves with the count of the selected records. * @since 3.0.0 */ count (query, opts) { return this.crud('count', query, opts) }, /** * Fired during {@link Mapper#create}. See * {@link Mapper~beforeCreateListener} for how to listen for this event. * * @event Mapper#beforeCreate * @see Mapper~beforeCreateListener * @see Mapper#create */ /** * Callback signature for the {@link Mapper#event:beforeCreate} event. * * @example * function onBeforeCreate (props, opts) { * // do something * } * store.on('beforeCreate', onBeforeCreate); * * @callback Mapper~beforeCreateListener * @param {object} props The `props` argument passed to {@link Mapper#beforeCreate}. * @param {object} opts The `opts` argument passed to {@link Mapper#beforeCreate}. * @see Mapper#event:beforeCreate * @see Mapper#create * @since 3.0.0 */ /** * Fired during {@link Mapper#create}. See * {@link Mapper~afterCreateListener} for how to listen for this event. * * @event Mapper#afterCreate * @see Mapper~afterCreateListener * @see Mapper#create */ /** * Callback signature for the {@link Mapper#event:afterCreate} event. * * @example * function onAfterCreate (props, opts, result) { * // do something * } * store.on('afterCreate', onAfterCreate); * * @callback Mapper~afterCreateListener * @param {object} props The `props` argument passed to {@link Mapper#afterCreate}. * @param {object} opts The `opts` argument passed to {@link Mapper#afterCreate}. * @param {object} result The `result` argument passed to {@link Mapper#afterCreate}. * @see Mapper#event:afterCreate * @see Mapper#create * @since 3.0.0 */ /** * Create and save a new the record using the provided `props`. * * {@link Mapper#beforeCreate} will be called before calling the adapter. * {@link Mapper#afterCreate} will be called after calling the adapter. * * @example * // Create and save a new blog post * PostMapper.create({ * title: 'Modeling your data', * status: 'draft' * }).then((post) => { * console.log(post); // { id: 1234, status: 'draft', ... } * }); * * @fires Mapper#beforeCreate * @fires Mapper#afterCreate * @method Mapper#create * @param {object} props The properties for the new record. * @param {object} [opts] Configuration options. Refer to the `create` method * of whatever adapter you're using for more configuration options. * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the * adapter to use. * @param {boolean} [opts.noValidate={@link Mapper#noValidate}] See {@link Mapper#noValidate}. * @param {boolean} [opts.notify={@link Mapper#notify}] See {@link Mapper#notify}. * @param {boolean} [opts.raw={@link Mapper#raw}] See {@link Mapper#raw}. * @param {string[]} [opts.with=[]] Relations to create in a cascading * create if `props` contains nested relations. NOT performed in a * transaction. Each nested create will result in another {@link Mapper#create} * or {@link Mapper#createMany} call. * @param {string[]} [opts.pass=[]] Relations to send to the adapter as part * of the payload. Normally relations are not sent. * @returns {Promise} Resolves with the created record. * @since 3.0.0 */ create (props, opts) { // Default values for arguments props || (props = {}) opts || (opts = {}) const originalRecord = props let parentRelationMap = {} let adapterResponse = {} // Fill in "opts" with the Mapper's configuration utils._(opts, this) opts.adapter = this.getAdapterName(opts) opts.op = 'beforeCreate' return this._runHook(opts.op, props, opts).then((_value) => { // Allow for re-assignment from lifecycle hook props = _value !== undefined ? _value : props opts.with || (opts.with = []) return this._createParentRecordIfRequired(props, opts) }).then((relationMap) => { parentRelationMap = relationMap }).then(() => { opts.op = 'create' return this._invokeAdapterMethod(opts.op, props, opts) }).then((result) => { adapterResponse = result }).then(() => { const createdProps = opts.raw ? adapterResponse.data : adapterResponse return this._createOrAssignChildRecordIfRequired(createdProps, { opts, parentRelationMap, originalProps: props }) }).then((createdProps) => { return this._commitChanges(originalRecord, createdProps) }).then((record) => { if (opts.raw) { adapterResponse.data = record } else { adapterResponse = record } const result = this._end(adapterResponse, opts) opts.op = 'afterCreate' return this._runHook(opts.op, props, opts, result) }) }, _commitChanges (recordOrRecords, newValues) { if (utils.isArray(recordOrRecords)) { return recordOrRecords.map((record, i) => this._commitChanges(record, newValues[i])) } utils.set(recordOrRecords, newValues, { silent: true }) if (utils.isFunction(recordOrRecords.commit)) { recordOrRecords.commit() } return recordOrRecords }, /** * Use {@link Mapper#createRecord} instead. * @deprecated * @method Mapper#createInstance * @param {Object|Array} props See {@link Mapper#createRecord}. * @param {object} [opts] See {@link Mapper#createRecord}. * @returns {Object|Array} See {@link Mapper#createRecord}. * @see Mapper#createRecord * @since 3.0.0 */ createInstance (props, opts) { return this.createRecord(props, opts) }, /** * Creates parent record for relation types like BelongsTo or HasMany with localKeys * in order to satisfy foreignKey dependency (so called child records). * @param {object} props See {@link Mapper#create}. * @param {object} opts See {@link Mapper#create}. * @returns {Object} cached parent records map * @see Mapper#create * @since 3.0.0 */ _createParentRecordIfRequired (props, opts) { const tasks = [] const relations = [] utils.forEachRelation(this, opts, (def, optsCopy) => { if (!def.isRequiresParentId() || !def.getLocalField(props)) { return } optsCopy.raw = false relations.push(def) tasks.push(def.createParentRecord(props, optsCopy)) }) return utils.Promise.all(tasks).then(records => { return relations.reduce((map, relation, index) => { relation.setLocalField(map, records[index]) return map }, {}) }) }, /** * Creates child record for relation types like HasOne or HasMany with foreignKey * in order to satisfy foreignKey dependency (so called parent records). * @param {object} props See {@link Mapper#create}. * @param {object} context contains collected information. * @param {object} context.opts See {@link Mapper#create}. * @param {object} context.parentRelationMap contains parent records map * @param {object} context.originalProps contains data passed into {@link Mapper#create} method * @return {Promise} updated props * @see Mapper#create * @since 3.0.0 */ _createOrAssignChildRecordIfRequired (props, context) { const tasks = [] utils.forEachRelation(this, context.opts, (def, optsCopy) => { const relationData = def.getLocalField(context.originalProps) if (!relationData) { return } optsCopy.raw = false // Create hasMany and hasOne after the main create because we needed // a generated id to attach to these items if (def.isRequiresChildId()) { tasks.push(def.createChildRecord(props, relationData, optsCopy)) } else if (def.isRequiresParentId()) { const parent = def.getLocalField(context.parentRelationMap) if (parent) { def.setLocalField(props, parent) } } }) return utils.Promise.all(tasks) .then(() => props) }, /** * Fired during {@link Mapper#createMany}. See * {@link Mapper~beforeCreateManyListener} for how to listen for this event. * * @event Mapper#beforeCreateMany * @see Mapper~beforeCreateManyListener * @see Mapper#createMany */ /** * Callback signature for the {@link Mapper#event:beforeCreateMany} event. * * @example * function onBeforeCreateMany (records, opts) { * // do something * } * store.on('beforeCreateMany', onBeforeCreateMany); * * @callback Mapper~beforeCreateManyListener * @param {object} records The `records` argument received by {@link Mapper#beforeCreateMany}. * @param {object} opts The `opts` argument received by {@link Mapper#beforeCreateMany}. * @see Mapper#event:beforeCreateMany * @see Mapper#createMany * @since 3.0.0 */ /** * Fired during {@link Mapper#createMany}. See * {@link Mapper~afterCreateManyListener} for how to listen for this event. * * @event Mapper#afterCreateMany * @see Mapper~afterCreateManyListener * @see Mapper#createMany */ /** * Callback signature for the {@link Mapper#event:afterCreateMany} event. * * @example * function onAfterCreateMany (records, opts, result) { * // do something * } * store.on('afterCreateMany', onAfterCreateMany); * * @callback Mapper~afterCreateManyListener * @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 Mapper#event:afterCreateMany * @see Mapper#createMany * @since 3.0.0 */ /** * Given an array of records, batch create them via an adapter. * * {@link Mapper#beforeCreateMany} will be called before calling the adapter. * {@link Mapper#afterCreateMany} will be called after calling the adapter. * * @example * // Create and save several new blog posts * PostMapper.createMany([{ * title: 'Modeling your data', * status: 'draft' * }, { * title: 'Reading data', * status: 'draft' * }]).then((posts) => { * console.log(posts[0]); // { id: 1234, status: 'draft', ... } * console.log(posts[1]); // { id: 1235, status: 'draft', ... } * }); * * @fires Mapper#beforeCreate * @fires Mapper#afterCreate * @method Mapper#createMany * @param {Record[]} records Array of records to be created in one batch. * @param {object} [opts] Configuration options. Refer to the `createMany` * method of whatever adapter you're using for more configuration options. * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the * adapter to use. * @param {boolean} [opts.noValidate={@link Mapper#noValidate}] See {@link Mapper#noValidate}. * @param {boolean} [opts.notify={@link Mapper#notify}] See {@link Mapper#notify}. * @param {boolean} [opts.raw={@link Mapper#raw}] See {@link Mapper#raw}. * @param {string[]} [opts.with=[]] Relations to create in a cascading * create if `records` contains nested relations. NOT performed in a * transaction. Each nested create will result in another {@link Mapper#createMany} * call. * @param {string[]} [opts.pass=[]] Relations to send to the adapter as part * of the payload. Normally relations are not sent. * @returns {Promise} Resolves with the created records. * @since 3.0.0 * @tutorial ["http://www.js-data.io/v3.0/docs/saving-data","Saving data"] */ createMany (records, opts) { // Default values for arguments records || (records = []) opts || (opts = {}) const originalRecords = records let adapterResponse // Fill in "opts" with the Mapper's configuration utils._(opts, this) opts.adapter = this.getAdapterName(opts) // beforeCreateMany lifecycle hook opts.op = 'beforeCreateMany' return this._runHook(opts.op, records, opts).then((_recordValues) => { // Allow for re-assignment from lifecycle hook records = _recordValues !== undefined ? _recordValues : records // Deep pre-create belongsTo relations const belongsToRelationData = {} opts.with || (opts.with = []) let tasks = [] utils.forEachRelation(this, opts, (def, optsCopy) => { const relationData = records .map((record) => def.getLocalField(record)) .filter(Boolean) if (def.type === belongsToType && relationData.length === records.length) { // Create belongsTo relation first because we need a generated id to // attach to the child optsCopy.raw = false tasks.push(def.createLinked(relationData, optsCopy).then((relatedRecords) => { records.forEach((record, i) => def.setForeignKey(record, relatedRecords[i])) }).then((relatedRecords) => { def.setLocalField(belongsToRelationData, relatedRecords) })) } }) return utils.Promise.all(tasks).then(() => { opts.op = 'createMany' return this._invokeAdapterMethod(opts.op, records, opts) }).then((result) => { adapterResponse = result }).then(() => { const createdRecordsData = opts.raw ? adapterResponse.data : adapterResponse // Deep post-create hasOne relations tasks = [] utils.forEachRelation(this, opts, (def, optsCopy) => { const relationData = records .map((record) => def.getLocalField(record)) .filter(Boolean) if (relationData.length !== records.length) { return } optsCopy.raw = false const belongsToData = def.getLocalField(belongsToRelationData) let task // Create hasMany and hasOne after the main create because we needed // a generated id to attach to these items if (def.type === hasManyType) { // Not supported this.log('warn', 'deep createMany of hasMany type not supported!') } else if (def.type === hasOneType) { createdRecordsData.forEach((createdRecordData, i) => { def.setForeignKey(createdRecordData, relationData[i]) }) task = def.getRelation().createMany(relationData, optsCopy).then((relatedData) => { createdRecordsData.forEach((createdRecordData, i) => { def.setLocalField(createdRecordData, relatedData[i]) }) }) } else if (def.type === belongsToType && belongsToData && belongsToData.length === createdRecordsData.length) { createdRecordsData.forEach((createdRecordData, i) => { def.setLocalField(createdRecordData, belongsToData[i]) }) } if (task) { tasks.push(task) } }) return utils.Promise.all(tasks).then(() => { return this._commitChanges(originalRecords, createdRecordsData) }) }) }).then((records) => { if (opts.raw) { adapterResponse.data = records } else { adapterResponse = records } const result = this._end(adapterResponse, opts) opts.op = 'afterCreateMany' return this._runHook(opts.op, records, opts, result) }) }, /** * Create an unsaved, uncached instance of this Mapper's * {@link Mapper#recordClass}. * * Returns `props` if `props` is already an instance of * {@link Mapper#recordClass}. * * __Note:__ This method does __not__ interact with any adapter, and does * __not__ save any data. It only creates new objects in memory. * * @example * // Create empty unsaved record instance * const post = PostMapper.createRecord(); * * @example * // Create an unsaved record instance with inital properties * const post = PostMapper.createRecord({ * title: 'Modeling your data', * status: 'draft' * }); * * @example * // Create a record instance that corresponds to a saved record * const post = PostMapper.createRecord({ * // JSData thinks this record has been saved if it has a primary key * id: 1234, * title: 'Modeling your data', * status: 'draft' * }); * * @example * // Create record instances from an array * const posts = PostMapper.createRecord([{ * title: 'Modeling your data', * status: 'draft' * }, { * title: 'Reading data', * status: 'draft' * }]); * * @example * // Records are validated by default * import { Mapper } from 'js-data'; * const PostMapper = new Mapper({ * name: 'post', * schema: { properties: { title: { type: 'string' } } } * }); * try { * const post = PostMapper.createRecord({ * title: 1234, * }); * } catch (err) { * console.log(err.errors); // [{ expected: 'one of (string)', actual: 'number', path: 'title' }] * } * * @example * // Skip validation * import { Mapper } from 'js-data'; * const PostMapper = new Mapper({ * name: 'post', * schema: { properties: { title: { type: 'string' } } } * }); * const post = PostMapper.createRecord({ * title: 1234, * }, { noValidate: true }); * console.log(post.isValid()); // false * * @method Mapper#createRecord * @param {Object|Object[]} props The properties for the Record instance or an * array of property objects for the Record instances. * @param {object} [opts] Configuration options. * @param {boolean} [opts.noValidate={@link Mapper#noValidate}] See {@link Mapper#noValidate}. * @returns {Record|Record[]} The Record instance or Record instances. * @since 3.0.0 */ createRecord (props, opts) { props || (props = {}) if (utils.isArray(props)) { return props.map((_props) => this.createRecord(_props, opts)) } if (!utils.isObject(props)) { throw utils.err(`${DOMAIN}#createRecord`, 'props')(400, 'array or object', props) } if (this.relationList) { this.relationList.forEach(function (def) { def.ensureLinkedDataHasProperType(props, opts) }) } const RecordCtor = this.recordClass return (!RecordCtor || props instanceof RecordCtor) ? props : new RecordCtor(props, opts) }, /** * Lifecycle invocation method. You probably won't call this method directly. * * @method Mapper#crud * @param {string} method Name of the lifecycle method to invoke. * @param {...*} args Arguments to pass to the lifecycle method. * @returns {Promise} * @since 3.0.0 */ crud (method, ...args) { const config = this.lifecycleMethods[method] if (!config) { throw utils.err(`${DOMAIN}#crud`, method)(404, 'method') } const upper = `${method.charAt(0).toUpperCase()}${method.substr(1)}` const before = `before${upper}` const after = `after${upper}` let op // Default values for arguments config.defaults.forEach((value, i) => { if (args[i] === undefined) { args[i] = utils.copy(value) } }) const opts = args[args.length - 1] // Fill in "opts" with the Mapper's configuration utils._(opts, this) const adapter = opts.adapter = this.getAdapterName(opts) // before lifecycle hook op = opts.op = before return utils.resolve(this[op](...args)).then((_value) => { if (args[config.beforeAssign] !== undefined) { // Allow for re-assignment from lifecycle hook args[config.beforeAssign] = _value === undefined ? args[config.beforeAssign] : _value } // Now delegate to the adapter op = opts.op = method args = config.adapterArgs ? config.adapterArgs(this, ...args) : args this.dbg(op, ...args) return utils.resolve(this.getAdapter(adapter)[op](this, ...args)) }).then((result) => { // force noValidate on find/findAll const noValidate = /find/.test(op) || opts.noValidate const _opts = Object.assign({}, opts, { noValidate }) result = this._end(result, _opts, !!config.skip) args.push(result) // after lifecycle hook op = opts.op = after return utils.resolve(this[op](...args)).then((_result) => { // Allow for re-assignment from lifecycle hook return _result === undefined ? result : _result }) }) }, /** * Fired during {@link Mapper#destroy}. See * {@link Mapper~beforeDestroyListener} for how to listen for this event. * * @event Mapper#beforeDestroy * @see Mapper~beforeDestroyListener * @see Mapper#destroy */ /** * Callback signature for the {@link Mapper#event:beforeDestroy} event. * * @example * function onBeforeDestroy (id, opts) { * // do something * } * store.on('beforeDestroy', onBeforeDestroy); * * @callback Mapper~beforeDestroyListener * @param {string|number} id The `id` argument passed to {@link Mapper#beforeDestroy}. * @param {object} opts The `opts` argument passed to {@link Mapper#beforeDestroy}. * @see Mapper#event:beforeDestroy * @see Mapper#destroy * @since 3.0.0 */ /** * Fired during {@link Mapper#destroy}. See * {@link Mapper~afterDestroyListener} for how to listen for this event. * * @event Mapper#afterDestroy * @see Mapper~afterDestroyListener * @see Mapper#destroy */ /** * Callback signature for the {@link Mapper#event:afterDestroy} event. * * @example * function onAfterDestroy (id, opts, result) { * // do something * } * store.on('afterDestroy', onAfterDestroy); * * @callback Mapper~afterDestroyListener * @param {string|number} id The `id` argument passed to {@link Mapper#afterDestroy}. * @param {o