js-data
Version:
Robust, framework-agnostic in-memory data store.
1,499 lines (1,405 loc) • 87.7 kB
JavaScript
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