import utils from './utils' import Component from './Component' const DOMAIN = 'Record' const superMethod = function (mapper, name) { const store = mapper.datastore if (store && store[name]) { return function (...args) { return store[name](mapper.name, ...args) } } return mapper[name].bind(mapper) } /** * js-data's Record class. * * ```javascript * import {Record} from 'js-data' * ``` * * @class Record * @extends Component * @param {Object} [props] The initial properties of the new Record instance. * @param {Object} [opts] Configuration options. * @param {boolean} [opts.noValidate=false] Whether to skip validation on the * initial properties. */ const Record = Component.extend({ constructor: function Record (props, opts) { const self = this utils.classCallCheck(self, Record) props || (props = {}) opts || (opts = {}) const _props = {} Object.defineProperties(self, { _get: { value (key) { return utils.get(_props, key) } }, _set: { value (key, value) { return utils.set(_props, key, value) } }, _unset: { value (key) { return utils.unset(_props, key) } } }) const _set = self._set // TODO: Optimize these strings _set('creating', true) if (opts.noValidate) { _set('noValidate', true) } utils.fillIn(self, props) _set('creating', false) _set('noValidate', false) _set('previous', utils.copy(props)) }, /** * TODO * * @name Record#_mapper * @method * @ignore */ _mapper () { const self = this const mapper = self.constructor.mapper if (!mapper) { throw utils.err(`${DOMAIN}#_mapper`, '')(404, 'mapper') } return mapper }, /** * TODO * * @name Record#afterLoadRelations * @method * @param {string[]} relations TODO * @param {Object} opts TODO */ afterLoadRelations () {}, /** * TODO * * @name Record#beforeLoadRelations * @method * @param {string[]} relations TODO * @param {Object} opts TODO */ beforeLoadRelations () {}, /** * Return changes to this record since it was instantiated or * {@link Record#commit} was called. * * @name Record#changes * @method * @param [opts] Configuration options. * @param {Function} [opts.equalsFn] Equality function. Default uses `===`. * @param {Array} [opts.ignore] Array of strings or RegExp of fields to ignore. */ changes (opts) { const self = this opts || (opts = {}) return utils.diffObjects(self, self._get('previous'), opts) }, /** * TODO * * @name Record#commit * @method */ commit () { const self = this self._set('changed') // unset self._set('previous', utils.copy(self)) return self }, /** * Call {@link Mapper#destroy} using this record's primary key. * * @name Record#destroy * @method * @param {Object} [opts] Configuration options passed to {@link Mapper#destroy}. * @return {Promise} The result of calling {@link Mapper#destroy}. */ destroy (opts) { const self = this opts || (opts = {}) const mapper = self._mapper() return superMethod(mapper, 'destroy')(utils.get(self, mapper.idAttribute), opts) }, /** * Return the value at the given path for this instance. * * @name Record#get * @method * @param {string} key - Path of value to retrieve. * @return {*} Value at path. */ 'get' (key) { return utils.get(this, key) }, /** * Return whether this record has changed since it was instantiated or * {@link Record#commit} was called. * * @name Record#hasChanges * @method * @param [opts] Configuration options. * @param {Function} [opts.equalsFn] Equality function. Default uses `===`. * @param {Array} [opts.ignore] Array of strings or RegExp of fields to ignore. */ hasChanges (opts) { const self = this const quickHasChanges = !!(self._get('changed') || []).length return quickHasChanges || utils.areDifferent(self, self._get('previous'), opts) }, /** * TODO * * @name Record#hashCode * @method */ hashCode () { const self = this return utils.get(self, self._mapper().idAttribute) }, isValid (opts) { const self = this return !self._mapper().validate(self, opts) }, /** * TODO * * @name Record#loadRelations * @method * @param {string[]} [relations] TODO * @param {Object} [opts] TODO */ loadRelations (relations, opts) { let op const self = this const mapper = self._mapper() // Default values for arguments relations || (relations = []) if (utils.isString(relations)) { relations = [relations] } opts || (opts = {}) opts.with = relations // Fill in "opts" with the Model's configuration utils._(opts, mapper) opts.adapter = mapper.getAdapterName(opts) // beforeLoadRelations lifecycle hook op = opts.op = 'beforeLoadRelations' return utils.resolve(self[op](relations, opts)).then(function () { // Now delegate to the adapter op = opts.op = 'loadRelations' mapper.dbg(op, self, relations, opts) let tasks = [] let task utils.forEachRelation(mapper, opts, function (def, optsCopy) { const relatedMapper = def.getRelation() optsCopy.raw = false if (utils.isFunction(def.load)) { task = def.load(mapper, def, self, opts) } else if (def.type === 'hasMany' || def.type === 'hasOne') { if (def.foreignKey) { task = superMethod(relatedMapper, 'findAll')({ [def.foreignKey]: utils.get(self, mapper.idAttribute) }, optsCopy).then(function (relatedData) { if (def.type === 'hasOne') { return relatedData.length ? relatedData[0] : undefined } return relatedData }) } else if (def.localKeys) { task = superMethod(relatedMapper, 'findAll')({ where: { [relatedMapper.idAttribute]: { 'in': utils.get(self, def.localKeys) } } }) } else if (def.foreignKeys) { task = superMethod(relatedMapper, 'findAll')({ where: { [def.foreignKeys]: { 'contains': utils.get(self, mapper.idAttribute) } } }, opts) } } else if (def.type === 'belongsTo') { const key = utils.get(self, def.foreignKey) if (utils.isSorN(key)) { task = superMethod(relatedMapper, 'find')(key, optsCopy) } } if (task) { task = task.then(function (relatedData) { def.setLocalField(self, relatedData) }) tasks.push(task) } }) return Promise.all(tasks) }).then(function () { // afterLoadRelations lifecycle hook op = opts.op = 'afterLoadRelations' return utils.resolve(self[op](relations, opts)).then(function () { return self }) }) }, /** * TODO * * @name Record#previous * @method * @param {string} [key] TODO */ previous (key) { const self = this if (key) { return self._get(`previous.${key}`) } return self._get('previous') }, /** * TODO * * @name Record#revert * @method * @param {Object} [opts] Configuration options. */ revert (opts) { const self = this const previous = self._get('previous') opts || (opts = {}) opts.preserve || (opts.preserve = []) utils.forOwn(self, (value, key) => { if (key !== self._mapper().idAttribute && !previous.hasOwnProperty(key) && self.hasOwnProperty(key) && opts.preserve.indexOf(key) === -1) { delete self[key] } }) utils.forOwn(previous, (value, key) => { if (opts.preserve.indexOf(key) === -1) { self[key] = value } }) self.commit() return self }, /** * Delegates to {@link Mapper#create} or {@link Mapper#update}. * * @name Record#save * @method * @param {Object} [opts] Configuration options. See {@link Mapper#create}. * @param [opts] Configuration options. * @param {boolean} [opts.changesOnly] Equality function. Default uses `===`. * @param {Function} [opts.equalsFn] Passed to {@link Record#changes} when * `changesOnly` is `true`. * @param {Array} [opts.ignore] Passed to {@link Record#changes} when * `changesOnly` is `true`. * @return {Promise} The result of calling {@link Mapper#create} or * {@link Mapper#update}. */ save (opts) { const self = this opts || (opts = {}) const mapper = self._mapper() const id = utils.get(self, mapper.idAttribute) let props = self if (utils.isUndefined(id)) { return superMethod(mapper, 'create')(props, opts) } if (opts.changesOnly) { const changes = self.changes(opts) props = {} utils.fillIn(props, changes.added) utils.fillIn(props, changes.changed) } return superMethod(mapper, 'update')(id, props, opts) }, /** * Set the value for a given key, or the values for the given keys if "key" is * an object. * * @name Record#set * @method * @param {(string|Object)} key - Key to set or hash of key-value pairs to set. * @param {*} [value] - Value to set for the given key. * @param {Object} [opts] - Optional configuration. * @param {boolean} [opts.silent=false] - Whether to trigger change events. */ 'set' (key, value, opts) { const self = this if (utils.isObject(key)) { opts = value } opts || (opts = {}) if (opts.silent) { self._set('silent', true) } utils.set(self, key, value) if (!self._get('eventId')) { self._set('silent') // unset } }, // TODO: move logic for single-item async operations onto the instance. /** * Return a plain object representation of this record. If the class from * which this record was created has a mapper, then {@link Mapper#toJSON} will * be called instead. * * @name Record#toJSON * @method * @param {Object} [opts] Configuration options. * @param {string[]} [opts.with] Array of relation names or relation fields * to include in the representation. Only available as an option if the class * from which this record was created has a mapper. * @return {Object} Plain object representation of this record. */ toJSON (opts) { const mapper = this.constructor.mapper if (mapper) { return mapper.toJSON(this, opts) } else { const json = {} utils.forOwn(this, function (prop, key) { json[key] = utils.copy(prop) }) return json } }, /** * Unset the value for a given key. * * @name Record#unset * @method * @param {string} key - Key to unset. * @param {Object} [opts] - Optional configuration. * @param {boolean} [opts.silent=false] - Whether to trigger change events. */ unset (key, opts) { this.set(key, undefined, opts) }, validate (opts) { return this._mapper().validate(this, opts) } }) /** * Allow records to emit events. * * An record's registered listeners are stored in the record's private data. */ utils.eventify( Record.prototype, function () { return this._get('events') }, function (value) { this._set('events', value) } ) export default Record