UNPKG

92.5 kBJavaScriptView Raw
1// Copyright IBM Corp. 2013,2020. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6// Turning on strict for this file breaks lots of test cases;
7// disabling strict for this file
8/* eslint-disable strict */
9
10/*!
11 * Module exports class Model
12 */
13module.exports = DataAccessObject;
14
15/*!
16 * Module dependencies
17 */
18const g = require('strong-globalize')();
19const async = require('async');
20const jutil = require('./jutil');
21const DataAccessObjectUtils = require('./model-utils');
22const ValidationError = require('./validations').ValidationError;
23const Relation = require('./relations.js');
24const Inclusion = require('./include.js');
25const List = require('./list.js');
26const geo = require('./geo');
27const Memory = require('./connectors/memory').Memory;
28const utils = require('./utils');
29const fieldsToArray = utils.fieldsToArray;
30const sanitizeQueryOrData = utils.sanitizeQuery;
31const setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
32const idEquals = utils.idEquals;
33const mergeQuery = utils.mergeQuery;
34const util = require('util');
35const assert = require('assert');
36const BaseModel = require('./model');
37const debug = require('debug')('loopback:dao');
38
39/**
40 * Base class for all persistent objects.
41 * Provides a common API to access any database connector.
42 * This class describes only abstract behavior. Refer to the specific connector for additional details.
43 *
44 * `DataAccessObject` mixes `Inclusion` classes methods.
45 * @class DataAccessObject
46 */
47function DataAccessObject() {
48 if (DataAccessObject._mixins) {
49 const self = this;
50 const args = arguments;
51 DataAccessObject._mixins.forEach(function(m) {
52 m.call(self, args);
53 });
54 }
55}
56
57function idName(m) {
58 return m.definition.idName() || 'id';
59}
60
61function getIdValue(m, data) {
62 return data && data[idName(m)];
63}
64
65function copyData(from, to) {
66 for (const key in from) {
67 to[key] = from[key];
68 }
69}
70
71function convertSubsetOfPropertiesByType(inst, data) {
72 const typedData = {};
73 for (const key in data) {
74 // Convert the properties by type
75 typedData[key] = inst[key];
76 if (typeof typedData[key] === 'object' &&
77 typedData[key] !== null &&
78 typeof typedData[key].toObject === 'function') {
79 typedData[key] = typedData[key].toObject();
80 }
81 }
82 return typedData;
83}
84
85/**
86 * Apply strict check for model's data.
87 * Notice: Please note this method modifies `inst` when `strict` is `validate`.
88 */
89function applyStrictCheck(model, strict, data, inst, cb) {
90 const props = model.definition.properties;
91 const keys = Object.keys(data);
92 const result = {};
93 for (let i = 0; i < keys.length; i++) {
94 const key = keys[i];
95 if (props[key]) {
96 result[key] = data[key];
97 } else if (strict && strict !== 'filter') {
98 inst.__unknownProperties.push(key);
99 }
100 }
101 cb(null, result);
102}
103
104function setIdValue(m, data, value) {
105 if (data) {
106 data[idName(m)] = value;
107 }
108}
109
110function byIdQuery(m, id) {
111 const pk = idName(m);
112 const query = {where: {}};
113 query.where[pk] = id;
114 return query;
115}
116
117function isWhereByGivenId(Model, where, idValue) {
118 const keys = Object.keys(where);
119 if (keys.length != 1) return false;
120
121 const pk = idName(Model);
122 if (keys[0] !== pk) return false;
123
124 return where[pk] === idValue;
125}
126
127function errorModelNotFound(idValue) {
128 const msg = g.f('Could not update attributes. {{Object}} with {{id}} %s does not exist!', idValue);
129 const error = new Error(msg);
130 error.statusCode = error.status = 404;
131 return error;
132}
133
134function invokeConnectorMethod(connector, method, Model, args, options, cb) {
135 const dataSource = Model.getDataSource();
136 // If the DataSource is a transaction and no transaction object is provide in
137 // the options yet, add it to the options, see: DataSource#transaction()
138 const opts = dataSource.isTransaction && !options.transaction ? Object.assign(
139 options, {transaction: dataSource.currentTransaction},
140 ) : options;
141 const optionsSupported = connector[method].length >= args.length + 3;
142 const transaction = opts.transaction;
143 if (transaction) {
144 if (!optionsSupported) {
145 return process.nextTick(function() {
146 cb(new Error(g.f(
147 'The connector does not support {{method}} within a transaction', method,
148 )));
149 });
150 }
151 // transaction isn't always a Transaction instance. Some tests provide a
152 // string to test if options get passed through, so check for ensureActive:
153 if (transaction.ensureActive && !transaction.ensureActive(cb)) {
154 return;
155 }
156 }
157 const modelName = Model.modelName;
158 let fullArgs;
159 if (!optionsSupported && method === 'count') {
160 // NOTE: The old count() signature is irregular, with `where` coming last:
161 // [modelName, cb, where]
162 const where = args[0];
163 fullArgs = [modelName, cb, where];
164 } else {
165 // Standard signature: [modelName, ...args, (opts, ) cb]
166 fullArgs = [modelName].concat(args);
167 if (optionsSupported) {
168 fullArgs.push(opts);
169 }
170 fullArgs.push(cb);
171 }
172 connector[method].apply(connector, fullArgs);
173}
174
175DataAccessObject._forDB = function(data) {
176 if (!(this.getDataSource().isRelational && this.getDataSource().isRelational())) {
177 return data;
178 }
179 const res = {};
180 for (const propName in data) {
181 const type = this.getPropertyType(propName);
182 if (type === 'JSON' || type === 'Any' || type === 'Object' || data[propName] instanceof Array) {
183 res[propName] = JSON.stringify(data[propName]);
184 } else {
185 res[propName] = data[propName];
186 }
187 }
188 return res;
189};
190
191DataAccessObject.defaultScope = function(target, inst) {
192 let scope = this.definition.settings.scope;
193 if (typeof scope === 'function') {
194 scope = this.definition.settings.scope.call(this, target, inst);
195 }
196 return scope;
197};
198
199DataAccessObject.applyScope = function(query, inst) {
200 const scope = this.defaultScope(query, inst) || {};
201 if (typeof scope === 'object') {
202 mergeQuery(query, scope || {}, this.definition.settings.scope);
203 }
204};
205
206DataAccessObject.applyProperties = function(data, inst) {
207 let properties = this.definition.settings.properties;
208 properties = properties || this.definition.settings.attributes;
209 if (typeof properties === 'object') {
210 util._extend(data, properties);
211 } else if (typeof properties === 'function') {
212 util._extend(data, properties.call(this, data, inst) || {});
213 } else if (properties !== false) {
214 const scope = this.defaultScope(data, inst) || {};
215 if (typeof scope.where === 'object') {
216 setScopeValuesFromWhere(data, scope.where, this);
217 }
218 }
219};
220
221DataAccessObject.lookupModel = function(data) {
222 return this;
223};
224
225/**
226 * Get the connector instance for the given model class
227 * @returns {Connector} The connector instance
228 */
229DataAccessObject.getConnector = function() {
230 return this.getDataSource().connector;
231};
232
233/**
234 * Create an instance of Model with given data and save to the attached data source. Callback is optional.
235 * Example:
236 *```js
237 * User.create({first: 'Joe', last: 'Bob'}, function(err, user) {
238 * console.log(user instanceof User); // true
239 * });
240 * ```
241 * Note: You must include a callback and use the created model provided in the callback if your code depends on your model being
242 * saved or having an ID.
243 *
244 * @param {Object} [data] Optional data object
245 * @param {Object} [options] Options for create
246 * @param {Function} [cb] Callback function called with these arguments:
247 * - err (null or Error)
248 * - instance (null or Model)
249 */
250DataAccessObject.create = function(data, options, cb) {
251 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
252 if (connectionPromise) {
253 return connectionPromise;
254 }
255
256 let Model = this;
257 const connector = Model.getConnector();
258 assert(typeof connector.create === 'function',
259 'create() must be implemented by the connector');
260
261 const self = this;
262
263 if (options === undefined && cb === undefined) {
264 if (typeof data === 'function') {
265 // create(cb)
266 cb = data;
267 data = {};
268 }
269 } else if (cb === undefined) {
270 if (typeof options === 'function') {
271 // create(data, cb);
272 cb = options;
273 options = {};
274 }
275 }
276
277 data = data || {};
278 options = options || {};
279 cb = cb || utils.createPromiseCallback();
280
281 assert(typeof data === 'object', 'The data argument must be an object or array');
282 assert(typeof options === 'object', 'The options argument must be an object');
283 assert(typeof cb === 'function', 'The cb argument must be a function');
284
285 const hookState = {};
286
287 if (Array.isArray(data)) {
288 // Undefined item will be skipped by async.map() which internally uses
289 // Array.prototype.map(). The following loop makes sure all items are
290 // iterated
291 for (let i = 0, n = data.length; i < n; i++) {
292 if (data[i] === undefined) {
293 data[i] = {};
294 }
295 }
296 async.map(data, function(item, done) {
297 self.create(item, options, function(err, result) {
298 // Collect all errors and results
299 done(null, {err: err, result: result || item});
300 });
301 }, function(err, results) {
302 if (err) {
303 return cb(err, results);
304 }
305 // Convert the results into two arrays
306 let errors = null;
307 const data = [];
308 for (let i = 0, n = results.length; i < n; i++) {
309 if (results[i].err) {
310 if (!errors) {
311 errors = [];
312 }
313 errors[i] = results[i].err;
314 }
315 data[i] = results[i].result;
316 }
317 cb(errors, data);
318 });
319 return cb.promise;
320 }
321
322 const enforced = {};
323 let obj;
324 const idValue = getIdValue(this, data);
325
326 try {
327 // if we come from save
328 if (data instanceof Model && !idValue) {
329 obj = data;
330 } else {
331 obj = new Model(data);
332 }
333
334 this.applyProperties(enforced, obj);
335 obj.setAttributes(enforced);
336 } catch (err) {
337 process.nextTick(function() {
338 cb(err);
339 });
340 return cb.promise;
341 }
342
343 Model = this.lookupModel(data); // data-specific
344 if (Model !== obj.constructor) obj = new Model(data);
345
346 let context = {
347 Model: Model,
348 instance: obj,
349 isNewInstance: true,
350 hookState: hookState,
351 options: options,
352 };
353 Model.notifyObserversOf('before save', context, function(err) {
354 if (err) return cb(err);
355
356 data = obj.toObject(true);
357
358 // options has precedence on model-setting
359 if (options.validate === false) {
360 return create();
361 }
362
363 // only when options.validate is not set, take model-setting into consideration
364 if (options.validate === undefined && Model.settings.automaticValidation === false) {
365 return create();
366 }
367
368 // validation required
369 obj.isValid(function(valid) {
370 if (valid) {
371 create();
372 } else {
373 cb(new ValidationError(obj), obj);
374 }
375 }, data, options);
376 });
377
378 function create() {
379 obj.trigger('create', function(createDone) {
380 obj.trigger('save', function(saveDone) {
381 const _idName = idName(Model);
382 let val = Model._sanitizeData(obj.toObject(true), options);
383 function createCallback(err, id, rev) {
384 if (id) {
385 obj.__data[_idName] = id;
386 defineReadonlyProp(obj, _idName, id);
387 }
388 if (rev) {
389 obj._rev = rev;
390 }
391 if (err) {
392 return cb(err, obj);
393 }
394 obj.__persisted = true;
395
396 const context = {
397 Model: Model,
398 data: val,
399 isNewInstance: true,
400 hookState: hookState,
401 options: options,
402 };
403 Model.notifyObserversOf('loaded', context, function(err) {
404 if (err) return cb(err);
405
406 // By default, the instance passed to create callback is NOT updated
407 // with the changes made through persist/loaded hooks. To preserve
408 // backwards compatibility, we introduced a new setting updateOnLoad,
409 // which if set, will apply these changes to the model instance too.
410 if (Model.settings.updateOnLoad) {
411 obj.setAttributes(context.data);
412 }
413 saveDone.call(obj, function() {
414 createDone.call(obj, function() {
415 if (err) {
416 return cb(err, obj);
417 }
418 const context = {
419 Model: Model,
420 instance: obj,
421 isNewInstance: true,
422 hookState: hookState,
423 options: options,
424 };
425 if (options.notify !== false) {
426 Model.notifyObserversOf('after save', context, function(err) {
427 cb(err, obj);
428 });
429 } else {
430 cb(null, obj);
431 }
432 });
433 });
434 });
435 }
436
437 val = applyDefaultsOnWrites(val, Model.definition);
438
439 context = {
440 Model: Model,
441 data: val,
442 isNewInstance: true,
443 currentInstance: obj,
444 hookState: hookState,
445 options: options,
446 };
447 Model.notifyObserversOf('persist', context, function(err, ctx) {
448 if (err) return cb(err);
449 val = ctx.data;
450 invokeConnectorMethod(connector, 'create', Model, [obj.constructor._forDB(ctx.data)],
451 options, createCallback);
452 });
453 }, obj, cb);
454 }, obj, cb);
455 }
456
457 return cb.promise;
458};
459
460// Implementation of applyDefaultOnWrites property
461function applyDefaultsOnWrites(obj, modelDefinition) {
462 for (const key in modelDefinition.properties) {
463 const prop = modelDefinition.properties[key];
464 if ('applyDefaultOnWrites' in prop && !prop.applyDefaultOnWrites &&
465 prop.default !== undefined && prop.default === obj[key]) {
466 delete obj[key];
467 }
468 }
469
470 return obj;
471}
472
473function stillConnecting(dataSource, obj, args) {
474 if (typeof args[args.length - 1] === 'function') {
475 return dataSource.ready(obj, args);
476 }
477
478 // promise variant
479 const promiseArgs = Array.prototype.slice.call(args);
480 promiseArgs.callee = args.callee;
481 const cb = utils.createPromiseCallback();
482 promiseArgs.push(cb);
483 if (dataSource.ready(obj, promiseArgs)) {
484 return cb.promise;
485 } else {
486 return false;
487 }
488}
489
490/**
491 * Update or insert a model instance: update exiting record if one is found, such that parameter `data.id` matches `id` of model instance;
492 * otherwise, insert a new record.
493 *
494 * NOTE: No setters, validations, or hooks are applied when using upsert.
495 * `updateOrCreate` and `patchOrCreate` are aliases
496 * @param {Object} data The model instance data
497 * @param {Object} [options] Options for upsert
498 * @param {Function} cb The callback function (optional).
499 */
500// [FIXME] rfeng: This is a hack to set up 'upsert' first so that
501// 'upsert' will be used as the name for strong-remoting to keep it backward
502// compatible for angular SDK
503DataAccessObject.updateOrCreate =
504DataAccessObject.patchOrCreate =
505DataAccessObject.upsert = function(data, options, cb) {
506 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
507 if (connectionPromise) {
508 return connectionPromise;
509 }
510
511 if (options === undefined && cb === undefined) {
512 if (typeof data === 'function') {
513 // upsert(cb)
514 cb = data;
515 data = {};
516 }
517 } else if (cb === undefined) {
518 if (typeof options === 'function') {
519 // upsert(data, cb)
520 cb = options;
521 options = {};
522 }
523 }
524
525 cb = cb || utils.createPromiseCallback();
526 data = data || {};
527 options = options || {};
528
529 assert(typeof data === 'object', 'The data argument must be an object');
530 assert(typeof options === 'object', 'The options argument must be an object');
531 assert(typeof cb === 'function', 'The cb argument must be a function');
532
533 if (Array.isArray(data)) {
534 cb(new Error('updateOrCreate does not support bulk mode or any array input'));
535 return cb.promise;
536 }
537
538 const hookState = {};
539
540 const self = this;
541 let Model = this;
542 const connector = Model.getConnector();
543
544 const id = getIdValue(this, data);
545 if (id === undefined || id === null) {
546 return this.create(data, options, cb);
547 }
548 let doValidate = undefined;
549 if (options.validate === undefined) {
550 if (Model.settings.validateUpsert === undefined) {
551 if (Model.settings.automaticValidation !== undefined) {
552 doValidate = Model.settings.automaticValidation;
553 }
554 } else {
555 doValidate = Model.settings.validateUpsert;
556 }
557 } else {
558 doValidate = options.validate;
559 }
560
561 const forceId = this.settings.forceId;
562 if (forceId) {
563 options = Object.create(options);
564 options.validate = !!doValidate;
565 Model.findById(id, options, function(err, model) {
566 if (err) return cb(err);
567 if (!model) return cb(errorModelNotFound(id));
568 model.updateAttributes(data, options, cb);
569 });
570 return cb.promise;
571 }
572
573 const context = {
574 Model: Model,
575 query: byIdQuery(Model, id),
576 hookState: hookState,
577 options: options,
578 };
579 Model.notifyObserversOf('access', context, doUpdateOrCreate);
580
581 function doUpdateOrCreate(err, ctx) {
582 if (err) return cb(err);
583
584 const isOriginalQuery = isWhereByGivenId(Model, ctx.query.where, id);
585 if (connector.updateOrCreate && isOriginalQuery) {
586 let context = {
587 Model: Model,
588 where: ctx.query.where,
589 data: data,
590 hookState: hookState,
591 options: options,
592 };
593 Model.notifyObserversOf('before save', context, function(err, ctx) {
594 if (err) return cb(err);
595
596 data = ctx.data;
597 let update = data;
598 let inst = data;
599 if (!(data instanceof Model)) {
600 inst = new Model(data, {applyDefaultValues: false});
601 }
602 update = inst.toObject(false);
603
604 Model.applyProperties(update, inst);
605 Model = Model.lookupModel(update);
606
607 if (doValidate === false) {
608 callConnector();
609 } else {
610 inst.isValid(function(valid) {
611 if (!valid) {
612 if (doValidate) { // backwards compatibility with validateUpsert:undefined
613 return cb(new ValidationError(inst), inst);
614 } else {
615 // TODO(bajtos) Remove validateUpsert:undefined in v3.0
616 g.warn('Ignoring validation errors in {{updateOrCreate()}}:');
617 g.warn(' %s', new ValidationError(inst).message);
618 // continue with updateOrCreate
619 }
620 }
621 callConnector();
622 }, update, options);
623 }
624
625 function callConnector() {
626 update = Model._sanitizeData(update, options);
627 context = {
628 Model: Model,
629 where: ctx.where,
630 data: update,
631 currentInstance: inst,
632 hookState: ctx.hookState,
633 options: options,
634 };
635 Model.notifyObserversOf('persist', context, function(err) {
636 if (err) return done(err);
637 invokeConnectorMethod(connector, 'updateOrCreate', Model, [update], options, done);
638 });
639 }
640 function done(err, data, info) {
641 if (err) return cb(err);
642 const context = {
643 Model: Model,
644 data: data,
645 isNewInstance: info && info.isNewInstance,
646 hookState: ctx.hookState,
647 options: options,
648 };
649 Model.notifyObserversOf('loaded', context, function(err) {
650 if (err) return cb(err);
651
652 let obj;
653 if (data && !(data instanceof Model)) {
654 inst._initProperties(data, {persisted: true});
655 obj = inst;
656 } else {
657 obj = data;
658 }
659 if (err) {
660 cb(err, obj);
661 } else {
662 const context = {
663 Model: Model,
664 instance: obj,
665 isNewInstance: info ? info.isNewInstance : undefined,
666 hookState: hookState,
667 options: options,
668 };
669
670 if (options.notify !== false) {
671 Model.notifyObserversOf('after save', context, function(err) {
672 cb(err, obj);
673 });
674 } else {
675 cb(null, obj);
676 }
677 }
678 });
679 }
680 });
681 } else {
682 const opts = {notify: false};
683 if (ctx.options && ctx.options.transaction) {
684 opts.transaction = ctx.options.transaction;
685 }
686 Model.findOne({where: ctx.query.where}, opts, function(err, inst) {
687 if (err) {
688 return cb(err);
689 }
690 if (!isOriginalQuery) {
691 // The custom query returned from a hook may hide the fact that
692 // there is already a model with `id` value `data[idName(Model)]`
693 delete data[idName(Model)];
694 }
695 if (inst) {
696 inst.updateAttributes(data, options, cb);
697 } else {
698 Model = self.lookupModel(data);
699 const obj = new Model(data);
700 obj.save(options, cb);
701 }
702 });
703 }
704 }
705 return cb.promise;
706};
707/**
708 * Update or insert a model instance based on the search criteria.
709 * If there is a single instance retrieved, update the retrieved model.
710 * Creates a new model if no model instances were found.
711 * Returns an error if multiple instances are found.
712 * @param {Object} [where] `where` filter, like
713 * ```
714 * { key: val, key2: {gt: 'val2'}, ...}
715 * ```
716 * <br/>see
717 * [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforothermethods).
718 * @param {Object} data The model instance data to insert.
719 * @callback {Function} callback Callback function called with `cb(err, obj)` signature.
720 * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
721 * @param {Object} model Updated model instance.
722 */
723DataAccessObject.patchOrCreateWithWhere =
724DataAccessObject.upsertWithWhere = function(where, data, options, cb) {
725 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
726 if (connectionPromise) { return connectionPromise; }
727 if (cb === undefined) {
728 if (typeof options === 'function') {
729 // upsertWithWhere(where, data, cb)
730 cb = options;
731 options = {};
732 }
733 }
734 cb = cb || utils.createPromiseCallback();
735 options = options || {};
736 assert(typeof where === 'object', 'The where argument must be an object');
737 assert(typeof data === 'object', 'The data argument must be an object');
738 assert(typeof options === 'object', 'The options argument must be an object');
739 assert(typeof cb === 'function', 'The cb argument must be a function');
740 if (Object.keys(data).length === 0) {
741 const err = new Error('data object cannot be empty!');
742 err.statusCode = 400;
743 process.nextTick(function() { cb(err); });
744 return cb.promise;
745 }
746 const hookState = {};
747 const self = this;
748 let Model = this;
749 const connector = Model.getConnector();
750 const query = {where: where};
751 const context = {
752 Model: Model,
753 query: query,
754 hookState: hookState,
755 options: options,
756 };
757 Model.notifyObserversOf('access', context, doUpsertWithWhere);
758 function doUpsertWithWhere(err, ctx) {
759 if (err) return cb(err);
760 ctx.data = data;
761 if (connector.upsertWithWhere) {
762 let context = {
763 Model: Model,
764 where: ctx.query.where,
765 data: ctx.data,
766 hookState: hookState,
767 options: options,
768 };
769 Model.notifyObserversOf('before save', context, function(err, ctx) {
770 if (err) return cb(err);
771 data = ctx.data;
772 let update = data;
773 let inst = data;
774 if (!(data instanceof Model)) {
775 inst = new Model(data, {applyDefaultValues: false});
776 }
777 update = inst.toObject(false);
778 Model.applyScope(query);
779 Model.applyProperties(update, inst);
780 Model = Model.lookupModel(update);
781 if (options.validate === false) {
782 return callConnector();
783 }
784 if (options.validate === undefined && Model.settings.automaticValidation === false) {
785 return callConnector();
786 }
787 inst.isValid(function(valid) {
788 if (!valid) return cb(new ValidationError(inst), inst);
789 callConnector();
790 }, update, options);
791
792 function callConnector() {
793 try {
794 ctx.where = Model._sanitizeQuery(ctx.where, options);
795 ctx.where = Model._coerce(ctx.where, options);
796 update = Model._sanitizeData(update, options);
797 update = Model._coerce(update, options);
798 } catch (err) {
799 return process.nextTick(function() {
800 cb(err);
801 });
802 }
803 context = {
804 Model: Model,
805 where: ctx.where,
806 data: update,
807 currentInstance: inst,
808 hookState: ctx.hookState,
809 options: options,
810 };
811 Model.notifyObserversOf('persist', context, function(err) {
812 if (err) return done(err);
813 invokeConnectorMethod(connector, 'upsertWithWhere', Model, [ctx.where, update], options, done);
814 });
815 }
816 function done(err, data, info) {
817 if (err) return cb(err);
818 const contxt = {
819 Model: Model,
820 data: data,
821 isNewInstance: info && info.isNewInstance,
822 hookState: ctx.hookState,
823 options: options,
824 };
825 Model.notifyObserversOf('loaded', contxt, function(err) {
826 if (err) return cb(err);
827 let obj;
828 if (contxt.data && !(contxt.data instanceof Model)) {
829 inst._initProperties(contxt.data, {persisted: true});
830 obj = inst;
831 } else {
832 obj = contxt.data;
833 }
834 const context = {
835 Model: Model,
836 instance: obj,
837 isNewInstance: info ? info.isNewInstance : undefined,
838 hookState: hookState,
839 options: options,
840 };
841 Model.notifyObserversOf('after save', context, function(err) {
842 cb(err, obj);
843 });
844 });
845 }
846 });
847 } else {
848 const opts = {notify: false};
849 if (ctx.options && ctx.options.transaction) {
850 opts.transaction = ctx.options.transaction;
851 }
852 self.find({where: ctx.query.where}, opts, function(err, instances) {
853 if (err) return cb(err);
854 const modelsLength = instances.length;
855 if (modelsLength === 0) {
856 self.create(data, options, cb);
857 } else if (modelsLength === 1) {
858 const modelInst = instances[0];
859 modelInst.updateAttributes(data, options, cb);
860 } else {
861 process.nextTick(function() {
862 const error = new Error('There are multiple instances found.' +
863 'Upsert Operation will not be performed!');
864 error.statusCode = 400;
865 cb(error);
866 });
867 }
868 });
869 }
870 }
871 return cb.promise;
872};
873/**
874 * Replace or insert a model instance: replace exiting record if one is found, such that parameter `data.id` matches `id` of model instance;
875 * otherwise, insert a new record.
876 *
877 * @param {Object} data The model instance data
878 * @param {Object} [options] Options for replaceOrCreate
879 * @param {Function} cb The callback function (optional).
880 */
881
882DataAccessObject.replaceOrCreate = function replaceOrCreate(data, options, cb) {
883 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
884 if (connectionPromise) {
885 return connectionPromise;
886 }
887
888 if (cb === undefined) {
889 if (typeof options === 'function') {
890 // replaceOrCreta(data,cb)
891 cb = options;
892 options = {};
893 }
894 }
895
896 cb = cb || utils.createPromiseCallback();
897 data = data || {};
898 options = options || {};
899
900 assert(typeof data === 'object', 'The data argument must be an object');
901 assert(typeof options === 'object', 'The options argument must be an object');
902 assert(typeof cb === 'function', 'The cb argument must be a function');
903
904 const hookState = {};
905
906 const self = this;
907 let Model = this;
908 const connector = Model.getConnector();
909
910 let id = getIdValue(this, data);
911 if (id === undefined || id === null) {
912 return this.create(data, options, cb);
913 }
914
915 const forceId = this.settings.forceId;
916 if (forceId) {
917 return Model.replaceById(id, data, options, cb);
918 }
919
920 let inst;
921 if (data instanceof Model) {
922 inst = data;
923 } else {
924 inst = new Model(data);
925 }
926
927 const strict = inst.__strict;
928 const context = {
929 Model: Model,
930 query: byIdQuery(Model, id),
931 hookState: hookState,
932 options: options,
933 };
934 Model.notifyObserversOf('access', context, doReplaceOrCreate);
935
936 function doReplaceOrCreate(err, ctx) {
937 if (err) return cb(err);
938
939 const isOriginalQuery = isWhereByGivenId(Model, ctx.query.where, id);
940 const where = ctx.query.where;
941 if (connector.replaceOrCreate && isOriginalQuery) {
942 let context = {
943 Model: Model,
944 instance: inst,
945 hookState: hookState,
946 options: options,
947 };
948 Model.notifyObserversOf('before save', context, function(err, ctx) {
949 if (err) return cb(err);
950 let update = inst.toObject(false);
951 if (strict) {
952 applyStrictCheck(Model, strict, update, inst, validateAndCallConnector);
953 } else {
954 validateAndCallConnector();
955 }
956
957 function validateAndCallConnector(err) {
958 if (err) return cb(err);
959 Model.applyProperties(update, inst);
960 Model = Model.lookupModel(update);
961
962 if (options.validate === false) {
963 return callConnector();
964 }
965
966 // only when options.validate is not set, take model-setting into consideration
967 if (options.validate === undefined && Model.settings.automaticValidation === false) {
968 return callConnector();
969 }
970
971 inst.isValid(function(valid) {
972 if (!valid) return cb(new ValidationError(inst), inst);
973 callConnector();
974 }, update, options);
975
976 function callConnector() {
977 update = Model._sanitizeData(update, options);
978 context = {
979 Model: Model,
980 where: where,
981 data: update,
982 currentInstance: inst,
983 hookState: ctx.hookState,
984 options: options,
985 };
986 Model.notifyObserversOf('persist', context, function(err) {
987 if (err) return done(err);
988 invokeConnectorMethod(connector, 'replaceOrCreate', Model, [context.data], options, done);
989 });
990 }
991 function done(err, data, info) {
992 if (err) return cb(err);
993 const context = {
994 Model: Model,
995 data: data,
996 isNewInstance: info ? info.isNewInstance : undefined,
997 hookState: ctx.hookState,
998 options: options,
999 };
1000 Model.notifyObserversOf('loaded', context, function(err) {
1001 if (err) return cb(err);
1002
1003 let obj;
1004 if (data && !(data instanceof Model)) {
1005 inst._initProperties(data, {persisted: true});
1006 obj = inst;
1007 } else {
1008 obj = data;
1009 }
1010 if (err) {
1011 cb(err, obj);
1012 } else {
1013 const context = {
1014 Model: Model,
1015 instance: obj,
1016 isNewInstance: info ? info.isNewInstance : undefined,
1017 hookState: hookState,
1018 options: options,
1019 };
1020
1021 Model.notifyObserversOf('after save', context, function(err) {
1022 cb(err, obj, info);
1023 });
1024 }
1025 });
1026 }
1027 }
1028 });
1029 } else {
1030 const opts = {notify: false};
1031 if (ctx.options && ctx.options.transaction) {
1032 opts.transaction = ctx.options.transaction;
1033 }
1034 Model.findOne({where: ctx.query.where}, opts, function(err, found) {
1035 if (err) return cb(err);
1036 if (!isOriginalQuery) {
1037 // The custom query returned from a hook may hide the fact that
1038 // there is already a model with `id` value `data[idName(Model)]`
1039 const pkName = idName(Model);
1040 delete data[pkName];
1041 if (found) id = found[pkName];
1042 }
1043 if (found) {
1044 self.replaceById(id, data, options, cb);
1045 } else {
1046 Model = self.lookupModel(data);
1047 const obj = new Model(data);
1048 obj.save(options, cb);
1049 }
1050 });
1051 }
1052 }
1053 return cb.promise;
1054};
1055
1056/**
1057 * Find one record that matches specified query criteria. Same as `find`, but limited to one record, and this function returns an
1058 * object, not a collection.
1059 * If the specified instance is not found, then create it using data provided as second argument.
1060 *
1061 * @param {Object} query Search conditions. See [find](#dataaccessobjectfindquery-callback) for query format.
1062 * For example: `{where: {test: 'me'}}`.
1063 * @param {Object} data Object to create.
1064 * @param {Object} [options] Option for findOrCreate
1065 * @param {Function} cb Callback called with (err, instance, created)
1066 */
1067DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb) {
1068 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
1069 if (connectionPromise) {
1070 return connectionPromise;
1071 }
1072
1073 assert(arguments.length >= 1, 'At least one argument is required');
1074 if (data === undefined && options === undefined && cb === undefined) {
1075 assert(typeof query === 'object', 'Single argument must be data object');
1076 // findOrCreate(data);
1077 // query will be built from data, and method will return Promise
1078 data = query;
1079 query = {where: data};
1080 } else if (options === undefined && cb === undefined) {
1081 if (typeof data === 'function') {
1082 // findOrCreate(data, cb);
1083 // query will be built from data
1084 cb = data;
1085 data = query;
1086 query = {where: data};
1087 }
1088 } else if (cb === undefined) {
1089 if (typeof options === 'function') {
1090 // findOrCreate(query, data, cb)
1091 cb = options;
1092 options = {};
1093 }
1094 }
1095
1096 cb = cb || utils.createPromiseCallback();
1097 query = query || {where: {}};
1098 data = data || {};
1099 options = options || {};
1100
1101 assert(typeof query === 'object', 'The query argument must be an object');
1102 assert(typeof data === 'object', 'The data argument must be an object');
1103 assert(typeof options === 'object', 'The options argument must be an object');
1104 assert(typeof cb === 'function', 'The cb argument must be a function');
1105
1106 const hookState = {};
1107
1108 const Model = this;
1109 const self = this;
1110 const connector = Model.getConnector();
1111
1112 function _findOrCreate(query, data, currentInstance) {
1113 function findOrCreateCallback(err, data, created) {
1114 if (err) return cb(err);
1115 const context = {
1116 Model: Model,
1117 data: data,
1118 isNewInstance: created,
1119 hookState: hookState,
1120 options: options,
1121 };
1122 Model.notifyObserversOf('loaded', context, function(err, ctx) {
1123 if (err) return cb(err);
1124
1125 data = ctx.data;
1126 const Model = self.lookupModel(data);
1127 let obj;
1128
1129 if (data) {
1130 const ctorOpts = {
1131 fields: query.fields,
1132 applySetters: false,
1133 persisted: true,
1134 // see https://github.com/strongloop/loopback-datasource-juggler/issues/1692
1135 applyDefaultValues: false,
1136 };
1137 obj = new Model(data, ctorOpts);
1138 }
1139
1140 if (created) {
1141 const context = {
1142 Model: Model,
1143 instance: obj,
1144 isNewInstance: true,
1145 hookState: hookState,
1146 options: options,
1147 };
1148 Model.notifyObserversOf('after save', context, function(err) {
1149 if (cb.promise) {
1150 cb(err, [obj, created]);
1151 } else {
1152 cb(err, obj, created);
1153 }
1154 });
1155 } else {
1156 if (cb.promise) {
1157 cb(err, [obj, created]);
1158 } else {
1159 cb(err, obj, created);
1160 }
1161 }
1162 });
1163 }
1164
1165 data = Model._sanitizeData(data, options);
1166 const context = {
1167 Model: Model,
1168 where: query.where,
1169 data: data,
1170 isNewInstance: true,
1171 currentInstance: currentInstance,
1172 hookState: hookState,
1173 options: options,
1174 };
1175
1176 Model.notifyObserversOf('persist', context, function(err) {
1177 if (err) return cb(err);
1178 invokeConnectorMethod(connector, 'findOrCreate', Model, [query, self._forDB(context.data)],
1179 options, findOrCreateCallback);
1180 });
1181 }
1182
1183 if (connector.findOrCreate) {
1184 query.limit = 1;
1185
1186 try {
1187 this._normalize(query, options);
1188 } catch (err) {
1189 process.nextTick(function() {
1190 cb(err);
1191 });
1192 return cb.promise;
1193 }
1194
1195 this.applyScope(query);
1196
1197 const context = {
1198 Model: Model,
1199 query: query,
1200 hookState: hookState,
1201 options: options,
1202 };
1203 Model.notifyObserversOf('access', context, function(err, ctx) {
1204 if (err) return cb(err);
1205
1206 const query = ctx.query;
1207
1208 const enforced = {};
1209 const Model = self.lookupModel(data);
1210 const obj = data instanceof Model ? data : new Model(data);
1211
1212 Model.applyProperties(enforced, obj);
1213 obj.setAttributes(enforced);
1214
1215 const context = {
1216 Model: Model,
1217 instance: obj,
1218 isNewInstance: true,
1219 hookState: hookState,
1220 options: options,
1221 };
1222 Model.notifyObserversOf('before save', context, function(err, ctx) {
1223 if (err) return cb(err);
1224
1225 const obj = ctx.instance;
1226 const data = obj.toObject(true);
1227
1228 // options has precedence on model-setting
1229 if (options.validate === false) {
1230 return _findOrCreate(query, data, obj);
1231 }
1232
1233 // only when options.validate is not set, take model-setting into consideration
1234 if (options.validate === undefined && Model.settings.automaticValidation === false) {
1235 return _findOrCreate(query, data, obj);
1236 }
1237
1238 // validation required
1239 obj.isValid(function(valid) {
1240 if (valid) {
1241 _findOrCreate(query, data, obj);
1242 } else {
1243 cb(new ValidationError(obj), obj);
1244 }
1245 }, data, options);
1246 });
1247 });
1248 } else {
1249 Model.findOne(query, options, function(err, record) {
1250 if (err) return cb(err);
1251 if (record) {
1252 if (cb.promise) {
1253 return cb(null, [record, false]);
1254 } else {
1255 return cb(null, record, false);
1256 }
1257 }
1258 Model.create(data, options, function(err, record) {
1259 if (cb.promise) {
1260 cb(err, [record, record != null]);
1261 } else {
1262 cb(err, record, record != null);
1263 }
1264 });
1265 });
1266 }
1267 return cb.promise;
1268};
1269
1270/**
1271 * Check whether a model instance exists in database
1272 *
1273 * @param {id} id Identifier of object (primary key value)
1274 * @param {Object} [options] Options
1275 * @param {Function} cb Callback function called with (err, exists: Bool)
1276 */
1277DataAccessObject.exists = function exists(id, options, cb) {
1278 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
1279 if (connectionPromise) {
1280 return connectionPromise;
1281 }
1282
1283 assert(arguments.length >= 1, 'The id argument is required');
1284 if (cb === undefined) {
1285 if (typeof options === 'function') {
1286 // exists(id, cb)
1287 cb = options;
1288 options = {};
1289 }
1290 }
1291
1292 cb = cb || utils.createPromiseCallback();
1293 options = options || {};
1294
1295 assert(typeof options === 'object', 'The options argument must be an object');
1296 assert(typeof cb === 'function', 'The cb argument must be a function');
1297
1298 if (id !== undefined && id !== null && id !== '') {
1299 this.count(byIdQuery(this, id).where, options, function(err, count) {
1300 cb(err, err ? false : count === 1);
1301 });
1302 } else {
1303 process.nextTick(function() {
1304 cb(new Error(g.f('{{Model::exists}} requires the {{id}} argument')));
1305 });
1306 }
1307 return cb.promise;
1308};
1309
1310/**
1311 * Find model instance by ID.
1312 *
1313 * Example:
1314 * ```js
1315 * User.findById(23, function(err, user) {
1316 * console.info(user.id); // 23
1317 * });
1318 * ```
1319 *
1320 * @param {*} id Primary key value
1321 * @param {Object} [filter] The filter that contains `include` or `fields`.
1322 * Other settings such as `where`, `order`, `limit`, or `offset` will be
1323 * ignored.
1324 * @param {Object} [options] Options
1325 * @param {Function} cb Callback called with (err, instance)
1326 */
1327DataAccessObject.findById = function findById(id, filter, options, cb) {
1328 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
1329 if (connectionPromise) {
1330 return connectionPromise;
1331 }
1332
1333 assert(arguments.length >= 1, 'The id argument is required');
1334
1335 if (options === undefined && cb === undefined) {
1336 if (typeof filter === 'function') {
1337 // findById(id, cb)
1338 cb = filter;
1339 filter = {};
1340 }
1341 } else if (cb === undefined) {
1342 if (typeof options === 'function') {
1343 // findById(id, query, cb)
1344 cb = options;
1345 options = {};
1346 if (typeof filter === 'object' && !(filter.include || filter.fields)) {
1347 // If filter doesn't have include or fields, assuming it's options
1348 options = filter;
1349 filter = {};
1350 }
1351 }
1352 }
1353
1354 cb = cb || utils.createPromiseCallback();
1355 options = options || {};
1356 filter = filter || {};
1357
1358 assert(typeof filter === 'object', 'The filter argument must be an object');
1359 assert(typeof options === 'object', 'The options argument must be an object');
1360 assert(typeof cb === 'function', 'The cb argument must be a function');
1361
1362 if (isPKMissing(this, cb)) {
1363 return cb.promise;
1364 } else if (id == null || id === '') {
1365 process.nextTick(function() {
1366 cb(new Error(g.f('{{Model::findById}} requires the {{id}} argument')));
1367 });
1368 } else {
1369 const query = byIdQuery(this, id);
1370 if (filter.include) {
1371 query.include = filter.include;
1372 }
1373 if (filter.fields) {
1374 query.fields = filter.fields;
1375 }
1376 this.findOne(query, options, cb);
1377 }
1378 return cb.promise;
1379};
1380
1381/**
1382 * Find model instances by ids
1383 * @param {Array} ids An array of ids
1384 * @param {Object} query Query filter
1385 * @param {Object} [options] Options
1386 * @param {Function} cb Callback called with (err, instance)
1387 */
1388DataAccessObject.findByIds = function(ids, query, options, cb) {
1389 if (options === undefined && cb === undefined) {
1390 if (typeof query === 'function') {
1391 // findByIds(ids, cb)
1392 cb = query;
1393 query = {};
1394 }
1395 } else if (cb === undefined) {
1396 if (typeof options === 'function') {
1397 // findByIds(ids, query, cb)
1398 cb = options;
1399 options = {};
1400 }
1401 }
1402
1403 cb = cb || utils.createPromiseCallback();
1404 options = options || {};
1405 query = query || {};
1406
1407 assert(Array.isArray(ids), 'The ids argument must be an array');
1408 assert(typeof query === 'object', 'The query argument must be an object');
1409 assert(typeof options === 'object', 'The options argument must be an object');
1410 assert(typeof cb === 'function', 'The cb argument must be a function');
1411
1412 if (isPKMissing(this, cb)) {
1413 return cb.promise;
1414 } else if (ids.length === 0) {
1415 process.nextTick(function() { cb(null, []); });
1416 return cb.promise;
1417 }
1418
1419 const filter = {where: {}};
1420 const pk = idName(this);
1421 filter.where[pk] = {inq: [].concat(ids)};
1422 mergeQuery(filter, query || {});
1423
1424 // to know if the result need to be sorted by ids or not
1425 // this variable need to be initialized before the call to find, because filter is updated during the call with an order
1426 const toSortObjectsByIds = filter.order ? false : true;
1427
1428 this.find(filter, options, function(err, results) {
1429 cb(err, toSortObjectsByIds ? utils.sortObjectsByIds(pk, ids, results) : results);
1430 });
1431 return cb.promise;
1432};
1433
1434function convertNullToNotFoundError(ctx, cb) {
1435 if (ctx.result !== null) return cb();
1436
1437 const modelName = ctx.method.sharedClass.name;
1438 const id = ctx.getArgByName('id');
1439 const msg = g.f('Unknown "%s" {{id}} "%s".', modelName, id);
1440 const error = new Error(msg);
1441 error.statusCode = error.status = 404;
1442 cb(error);
1443}
1444
1445// alias function for backwards compat.
1446DataAccessObject.all = function() {
1447 return DataAccessObject.find.apply(this, arguments);
1448};
1449
1450/**
1451 * Find all instances of Model that match the specified query.
1452 * Fields used for filter and sort should be declared with `{index: true}` in model definition.
1453 * See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information.
1454 *
1455 * For example, find the second page of ten users over age 21 in descending order exluding the password property.
1456 *
1457 * ```js
1458 * User.find({
1459 * where: {
1460 * age: {gt: 21}},
1461 * order: 'age DESC',
1462 * limit: 10,
1463 * skip: 10,
1464 * fields: {password: false}
1465 * },
1466 * console.log
1467 * );
1468 * ```
1469 *
1470 * @options {Object} [query] Optional JSON object that specifies query criteria and parameters.
1471 * @property {Object} where Search criteria in JSON format `{ key: val, key2: {gt: 'val2'}}`.
1472 * Operations:
1473 * - gt: >
1474 * - gte: >=
1475 * - lt: <
1476 * - lte: <=
1477 * - between
1478 * - inq: IN
1479 * - nin: NOT IN
1480 * - neq: !=
1481 * - like: LIKE
1482 * - nlike: NOT LIKE
1483 * - ilike: ILIKE
1484 * - nilike: NOT ILIKE
1485 * - regexp: REGEXP
1486 *
1487 * You can also use `and` and `or` operations. See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information.
1488 * @property {String|Object|Array} include Allows you to load relations of several objects and optimize numbers of requests.
1489 * Format examples;
1490 * - `'posts'`: Load posts
1491 * - `['posts', 'passports']`: Load posts and passports
1492 * - `{'owner': 'posts'}`: Load owner and owner's posts
1493 * - `{'owner': ['posts', 'passports']}`: Load owner, owner's posts, and owner's passports
1494 * - `{'owner': [{posts: 'images'}, 'passports']}`: Load owner, owner's posts, owner's posts' images, and owner's passports
1495 * See `DataAccessObject.include()`.
1496 * @property {String} order Sort order. Format: `'key1 ASC, key2 DESC'`
1497 * @property {Number} limit Maximum number of instances to return.
1498 * @property {Number} skip Number of instances to skip.
1499 * @property {Number} offset Alias for `skip`.
1500 * @property {Object|Array|String} fields Included/excluded fields.
1501 * - `['foo']` or `'foo'` - include only the foo property
1502 * - `['foo', 'bar']` - include the foo and bar properties. Format:
1503 * - `{foo: true}` - include only foo
1504 * - `{bat: false}` - include all properties, exclude bat
1505 *
1506 * @param {Function} cb Optional callback function. Call this function with two arguments: `err` (null or Error) and an array of instances.
1507 * @return {Promise} results If no callback function is provided, a promise (which resolves to an array of instances) is returned
1508 */
1509
1510DataAccessObject.find = function find(query, options, cb) {
1511 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
1512 if (connectionPromise) {
1513 return connectionPromise;
1514 }
1515
1516 if (options === undefined && cb === undefined) {
1517 if (typeof query === 'function') {
1518 // find(cb);
1519 cb = query;
1520 query = {};
1521 }
1522 } else if (cb === undefined) {
1523 if (typeof options === 'function') {
1524 // find(query, cb);
1525 cb = options;
1526 options = {};
1527 }
1528 }
1529
1530 cb = cb || utils.createPromiseCallback();
1531 query = query || {};
1532 options = options || {};
1533
1534 assert(typeof query === 'object', 'The query argument must be an object');
1535 assert(typeof options === 'object', 'The options argument must be an object');
1536 assert(typeof cb === 'function', 'The cb argument must be a function');
1537
1538 const hookState = {};
1539 const self = this;
1540 const connector = self.getConnector();
1541
1542 assert(typeof connector.all === 'function',
1543 'all() must be implemented by the connector');
1544
1545 try {
1546 this._normalize(query, options);
1547 } catch (err) {
1548 process.nextTick(function() {
1549 cb(err);
1550 });
1551 return cb.promise;
1552 }
1553
1554 this.applyScope(query);
1555
1556 let near = query && geo.nearFilter(query.where);
1557 const supportsGeo = !!connector.buildNearFilter;
1558 let geoQueryObject;
1559
1560 if (near) {
1561 if (supportsGeo) {
1562 // convert it
1563 connector.buildNearFilter(query, near);
1564 } else if (query.where) {
1565 // do in memory query
1566 // using all documents
1567 // TODO [fabien] use default scope here?
1568 if (options.notify === false) {
1569 queryGeo(query);
1570 } else {
1571 withNotifyGeo();
1572 }
1573
1574 function withNotifyGeo() {
1575 const context = {
1576 Model: self,
1577 query: query,
1578 hookState: hookState,
1579 options: options,
1580 };
1581 self.notifyObserversOf('access', context, function(err, ctx) {
1582 if (err) return cb(err);
1583 queryGeo(ctx.query);
1584 });
1585 }
1586 // already handled
1587 return cb.promise;
1588 }
1589 }
1590
1591 function geoCallback(err, data) {
1592 const memory = new Memory();
1593 const modelName = self.modelName;
1594
1595 if (err) {
1596 cb(err);
1597 } else if (Array.isArray(data)) {
1598 memory.define({
1599 properties: self.dataSource.definitions[modelName].properties,
1600 settings: self.dataSource.definitions[modelName].settings,
1601 model: self,
1602 });
1603
1604 data.forEach(function(obj) {
1605 memory.create(modelName, obj, options, function() {
1606 // noop
1607 });
1608 });
1609
1610 memory.all(modelName, geoQueryObject, options, allCb);
1611 } else {
1612 cb(null, []);
1613 }
1614 }
1615
1616 function queryGeo(query) {
1617 geoQueryObject = query;
1618 near = query && geo.nearFilter(query.where);
1619 invokeConnectorMethod(connector, 'all', self, [{}], options, geoCallback);
1620 }
1621
1622 function allCb(err, data) {
1623 if (!err && Array.isArray(data)) {
1624 async.map(data, function(item, next) {
1625 const Model = self.lookupModel(item);
1626 if (options.notify === false) {
1627 buildResult(item, next);
1628 } else {
1629 withNotify(item, next);
1630 }
1631
1632 function buildResult(data, callback) {
1633 const ctorOpts = {
1634 fields: query.fields,
1635 applySetters: false,
1636 persisted: true,
1637 // see https://github.com/strongloop/loopback-datasource-juggler/issues/1692
1638 applyDefaultValues: false,
1639 };
1640 let obj;
1641 try {
1642 obj = new Model(data, ctorOpts);
1643 } catch (err) {
1644 return callback(err);
1645 }
1646
1647 if (query && query.include) {
1648 if (query.collect) {
1649 // The collect property indicates that the query is to return the
1650 // standalone items for a related model, not as child of the parent object
1651 // For example, article.tags
1652 obj = obj.__cachedRelations[query.collect];
1653 if (obj === null) {
1654 obj = undefined;
1655 }
1656 } else {
1657 // This handles the case to return parent items including the related
1658 // models. For example, Article.find({include: 'tags'}, ...);
1659 // Try to normalize the include
1660 const includes = Inclusion.normalizeInclude(query.include || []);
1661 includes.forEach(function(inc) {
1662 let relationName = inc;
1663 if (utils.isPlainObject(inc)) {
1664 relationName = Object.keys(inc)[0];
1665 }
1666
1667 // Promote the included model as a direct property
1668 let included = obj.__cachedRelations[relationName];
1669 if (Array.isArray(included)) {
1670 included = new List(included, null, obj);
1671 }
1672 if (included) obj.__data[relationName] = included;
1673 });
1674 delete obj.__data.__cachedRelations;
1675 }
1676 }
1677
1678 callback(null, obj);
1679 }
1680
1681 function withNotify(data, callback) {
1682 const context = {
1683 Model: Model,
1684 data: data,
1685 isNewInstance: false,
1686 hookState: hookState,
1687 options: options,
1688 };
1689
1690 Model.notifyObserversOf('loaded', context, function(err) {
1691 if (err) return callback(err);
1692 buildResult(context.data, callback);
1693 });
1694 }
1695 },
1696 function(err, results) {
1697 if (err) return cb(err);
1698
1699 // When applying query.collect, some root items may not have
1700 // any related/linked item. We store `undefined` in the results
1701 // array in such case, which is not desirable from API consumer's
1702 // point of view.
1703 results = results.filter(isDefined);
1704
1705 if (data && data.countBeforeLimit) {
1706 results.countBeforeLimit = data.countBeforeLimit;
1707 }
1708 if (!supportsGeo && near) {
1709 results = geo.filter(results, near);
1710 }
1711
1712 cb(err, results);
1713 });
1714 } else {
1715 cb(err, data || []);
1716 }
1717 }
1718
1719 if (options.notify === false) {
1720 invokeConnectorMethod(connector, 'all', self, [query], options, allCb);
1721 } else {
1722 const context = {
1723 Model: this,
1724 query: query,
1725 hookState: hookState,
1726 options: options,
1727 };
1728 this.notifyObserversOf('access', context, function(err, ctx) {
1729 if (err) return cb(err);
1730 invokeConnectorMethod(connector, 'all', self, [ctx.query], options, allCb);
1731 });
1732 }
1733 return cb.promise;
1734};
1735
1736function isDefined(value) {
1737 return value !== undefined;
1738}
1739
1740/**
1741 * Find one record, same as `find`, but limited to one result. This function returns an object, not a collection.
1742 *
1743 * @param {Object} query Search conditions. See [find](#dataaccessobjectfindquery-callback) for query format.
1744 * For example: `{where: {test: 'me'}}`.
1745 * @param {Object} [options] Options
1746 * @param {Function} cb Callback function called with (err, instance)
1747 */
1748DataAccessObject.findOne = function findOne(query, options, cb) {
1749 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
1750 if (connectionPromise) {
1751 return connectionPromise;
1752 }
1753
1754 if (options === undefined && cb === undefined) {
1755 if (typeof query === 'function') {
1756 cb = query;
1757 query = {};
1758 }
1759 } else if (cb === undefined) {
1760 if (typeof options === 'function') {
1761 cb = options;
1762 options = {};
1763 }
1764 }
1765
1766 cb = cb || utils.createPromiseCallback();
1767 query = query || {};
1768 options = options || {};
1769
1770 assert(typeof query === 'object', 'The query argument must be an object');
1771 assert(typeof options === 'object', 'The options argument must be an object');
1772 assert(typeof cb === 'function', 'The cb argument must be a function');
1773
1774 query.limit = 1;
1775 this.find(query, options, function(err, collection) {
1776 if (err || !collection || !collection.length > 0) return cb(err, null);
1777 cb(err, collection[0]);
1778 });
1779 return cb.promise;
1780};
1781
1782/**
1783 * Destroy all matching records.
1784 * Delete all model instances from data source. Note: destroyAll method does not destroy hooks.
1785 * Example:
1786 *````js
1787 * Product.destroyAll({price: {gt: 99}}, function(err) {
1788 // removed matching products
1789 * });
1790 * ````
1791 *
1792 * @param {Object} [where] Optional object that defines the criteria. This is a "where" object. Do NOT pass a filter object.
1793 * @param {Object) [options] Options
1794 * @param {Function} [cb] Callback called with (err, info)
1795 */
1796DataAccessObject.remove =
1797DataAccessObject.deleteAll =
1798DataAccessObject.destroyAll = function destroyAll(where, options, cb) {
1799 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
1800 if (connectionPromise) {
1801 return connectionPromise;
1802 }
1803
1804 const Model = this;
1805 const connector = Model.getConnector();
1806
1807 assert(typeof connector.destroyAll === 'function',
1808 'destroyAll() must be implemented by the connector');
1809
1810 if (options === undefined && cb === undefined) {
1811 if (typeof where === 'function') {
1812 cb = where;
1813 where = {};
1814 }
1815 } else if (cb === undefined) {
1816 if (typeof options === 'function') {
1817 cb = options;
1818 options = {};
1819 }
1820 }
1821
1822 cb = cb || utils.createPromiseCallback();
1823 where = where || {};
1824 options = options || {};
1825
1826 assert(typeof where === 'object', 'The where argument must be an object');
1827 assert(typeof options === 'object', 'The options argument must be an object');
1828 assert(typeof cb === 'function', 'The cb argument must be a function');
1829
1830 const hookState = {};
1831
1832 let query = {where: where};
1833
1834 this.applyScope(query);
1835 where = query.where;
1836
1837 if (options.notify === false) {
1838 doDelete(where);
1839 } else {
1840 query = {where: whereIsEmpty(where) ? {} : where};
1841 const context = {
1842 Model: Model,
1843 query: query,
1844 hookState: hookState,
1845 options: options,
1846 };
1847 Model.notifyObserversOf('access', context, function(err, ctx) {
1848 if (err) return cb(err);
1849 const context = {
1850 Model: Model,
1851 where: ctx.query.where,
1852 hookState: hookState,
1853 options: options,
1854 };
1855 Model.notifyObserversOf('before delete', context, function(err, ctx) {
1856 if (err) return cb(err);
1857 doDelete(ctx.where);
1858 });
1859 });
1860 }
1861
1862 function doDelete(where) {
1863 const context = {
1864 Model: Model,
1865 where: whereIsEmpty(where) ? {} : where,
1866 hookState: hookState,
1867 options: options,
1868 };
1869
1870 if (whereIsEmpty(where)) {
1871 invokeConnectorMethod(connector, 'destroyAll', Model, [{}], options, done);
1872 } else {
1873 try {
1874 // Support an optional where object
1875 // alter configuration of how sanitizeQuery handles undefined values
1876 where = Model._sanitizeQuery(where, options);
1877 where = Model._coerce(where, options);
1878 } catch (err) {
1879 return process.nextTick(function() {
1880 cb(err);
1881 });
1882 }
1883
1884 invokeConnectorMethod(connector, 'destroyAll', Model, [where], options, done);
1885 }
1886
1887 function done(err, info) {
1888 if (err) return cb(err);
1889
1890 if (options.notify === false) {
1891 return cb(err, info);
1892 }
1893
1894 const context = {
1895 Model: Model,
1896 where: where,
1897 hookState: hookState,
1898 options: options,
1899 info: info,
1900 };
1901 Model.notifyObserversOf('after delete', context, function(err) {
1902 cb(err, info);
1903 });
1904 }
1905 }
1906 return cb.promise;
1907};
1908
1909function whereIsEmpty(where) {
1910 return !where ||
1911 (typeof where === 'object' && Object.keys(where).length === 0);
1912}
1913
1914/**
1915 * Delete the record with the specified ID.
1916 * Aliases are `destroyById` and `deleteById`.
1917 * @param {*} id The id value
1918 * @param {Function} cb Callback called with (err)
1919 */
1920
1921// [FIXME] rfeng: This is a hack to set up 'deleteById' first so that
1922// 'deleteById' will be used as the name for strong-remoting to keep it backward
1923// compatible for angular SDK
1924DataAccessObject.removeById =
1925DataAccessObject.destroyById =
1926DataAccessObject.deleteById = function deleteById(id, options, cb) {
1927 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
1928 if (connectionPromise) {
1929 return connectionPromise;
1930 }
1931
1932 assert(arguments.length >= 1, 'The id argument is required');
1933 if (cb === undefined) {
1934 if (typeof options === 'function') {
1935 // destroyById(id, cb)
1936 cb = options;
1937 options = {};
1938 }
1939 }
1940
1941 options = options || {};
1942 cb = cb || utils.createPromiseCallback();
1943
1944 assert(typeof options === 'object', 'The options argument must be an object');
1945 assert(typeof cb === 'function', 'The cb argument must be a function');
1946
1947 if (isPKMissing(this, cb)) {
1948 return cb.promise;
1949 } else if (id == null || id === '') {
1950 process.nextTick(function() {
1951 cb(new Error(g.f('{{Model::deleteById}} requires the {{id}} argument')));
1952 });
1953 return cb.promise;
1954 }
1955
1956 const Model = this;
1957
1958 this.remove(byIdQuery(this, id).where, options, function(err, info) {
1959 if (err) return cb(err);
1960 const deleted = info && info.count > 0;
1961 if (Model.settings.strictDelete && !deleted) {
1962 err = new Error(g.f('No instance with {{id}} %s found for %s', id, Model.modelName));
1963 err.code = 'NOT_FOUND';
1964 err.statusCode = 404;
1965 return cb(err);
1966 }
1967
1968 cb(null, info);
1969 });
1970 return cb.promise;
1971};
1972
1973/**
1974 * Return count of matched records. Optional query parameter allows you to count filtered set of model instances.
1975 * Example:
1976 *
1977 *```js
1978 * User.count({approved: true}, function(err, count) {
1979 * console.log(count); // 2081
1980 * });
1981 * ```
1982 *
1983 * @param {Object} [where] Search conditions (optional)
1984 * @param {Object} [options] Options
1985 * @param {Function} cb Callback, called with (err, count)
1986 */
1987DataAccessObject.count = function(where, options, cb) {
1988 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
1989 if (connectionPromise) {
1990 return connectionPromise;
1991 }
1992
1993 if (options === undefined && cb === undefined) {
1994 if (typeof where === 'function') {
1995 // count(cb)
1996 cb = where;
1997 where = {};
1998 }
1999 } else if (cb === undefined) {
2000 if (typeof options === 'function') {
2001 // count(where, cb)
2002 cb = options;
2003 options = {};
2004 }
2005 }
2006
2007 cb = cb || utils.createPromiseCallback();
2008 where = where || {};
2009 options = options || {};
2010
2011 assert(typeof where === 'object', 'The where argument must be an object');
2012 assert(typeof options === 'object', 'The options argument must be an object');
2013 assert(typeof cb === 'function', 'The cb argument must be a function');
2014
2015 const Model = this;
2016 const connector = Model.getConnector();
2017 assert(typeof connector.count === 'function',
2018 'count() must be implemented by the connector');
2019 assert(connector.count.length >= 3,
2020 'count() must take at least 3 arguments');
2021
2022 const hookState = {};
2023
2024 const query = {where: where};
2025 this.applyScope(query);
2026 where = query.where;
2027
2028 try {
2029 // alter configuration of how sanitizeQuery handles undefined values
2030 where = Model._sanitizeQuery(where, options);
2031 where = this._coerce(where, options);
2032 } catch (err) {
2033 process.nextTick(function() {
2034 cb(err);
2035 });
2036 return cb.promise;
2037 }
2038
2039 const context = {
2040 Model: Model,
2041 query: {where: where},
2042 hookState: hookState,
2043 options: options,
2044 };
2045 this.notifyObserversOf('access', context, function(err, ctx) {
2046 if (err) return cb(err);
2047 invokeConnectorMethod(connector, 'count', Model, [ctx.query.where], options, cb);
2048 });
2049 return cb.promise;
2050};
2051
2052/**
2053 * Save instance. If the instance does not have an ID, call `create` instead.
2054 * Triggers: validate, save, update or create.
2055 * @options {Object} options Optional options to use.
2056 * @property {Boolean} validate Default is true.
2057 * @property {Boolean} throws Default is false.
2058 * @param {Function} cb Callback function with err and object arguments
2059 */
2060DataAccessObject.prototype.save = function(options, cb) {
2061 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
2062 if (connectionPromise) {
2063 return connectionPromise;
2064 }
2065 const Model = this.constructor;
2066
2067 if (typeof options === 'function') {
2068 cb = options;
2069 options = {};
2070 }
2071
2072 cb = cb || utils.createPromiseCallback();
2073 options = options || {};
2074
2075 assert(typeof options === 'object', 'The options argument should be an object');
2076 assert(typeof cb === 'function', 'The cb argument should be a function');
2077
2078 if (isPKMissing(Model, cb)) {
2079 return cb.promise;
2080 } else if (this.isNewRecord()) {
2081 return Model.create(this, options, cb);
2082 }
2083
2084 const hookState = {};
2085
2086 if (options.validate === undefined) {
2087 if (Model.settings.automaticValidation === undefined) {
2088 options.validate = true;
2089 } else {
2090 options.validate = Model.settings.automaticValidation;
2091 }
2092 }
2093
2094 if (options.throws === undefined) {
2095 options.throws = false;
2096 }
2097
2098 const inst = this;
2099 const connector = inst.getConnector();
2100
2101 let context = {
2102 Model: Model,
2103 instance: inst,
2104 hookState: hookState,
2105 options: options,
2106 };
2107 Model.notifyObserversOf('before save', context, function(err) {
2108 if (err) return cb(err);
2109
2110 let data = inst.toObject(true);
2111 Model.applyProperties(data, inst);
2112 inst.setAttributes(data);
2113
2114 // validate first
2115 if (!options.validate) {
2116 return save();
2117 }
2118
2119 inst.isValid(function(valid) {
2120 if (valid) {
2121 save();
2122 } else {
2123 const err = new ValidationError(inst);
2124 // throws option is dangerous for async usage
2125 if (options.throws) {
2126 throw err;
2127 }
2128 cb(err, inst);
2129 }
2130 }, data, options);
2131
2132 // then save
2133 function save() {
2134 inst.trigger('save', function(saveDone) {
2135 inst.trigger('update', function(updateDone) {
2136 data = Model._sanitizeData(data, options);
2137 function saveCallback(err, unusedData, result) {
2138 if (err) {
2139 return cb(err, inst);
2140 }
2141
2142 const context = {
2143 Model: Model,
2144 data: data,
2145 isNewInstance: result && result.isNewInstance,
2146 hookState: hookState,
2147 options: options,
2148 };
2149 Model.notifyObserversOf('loaded', context, function(err, ctx) {
2150 if (err) return cb(err);
2151
2152 data = ctx.data;
2153 inst._initProperties(data, {persisted: true});
2154
2155 const context = {
2156 Model: Model,
2157 instance: inst,
2158 isNewInstance: result && result.isNewInstance,
2159 hookState: hookState,
2160 options: options,
2161 };
2162 Model.notifyObserversOf('after save', context, function(err) {
2163 if (err) return cb(err, inst);
2164 updateDone.call(inst, function() {
2165 saveDone.call(inst, function() {
2166 cb(err, inst);
2167 });
2168 });
2169 });
2170 });
2171 }
2172
2173 context = {
2174 Model: Model,
2175 data: data,
2176 where: byIdQuery(Model, getIdValue(Model, inst)).where,
2177 currentInstance: inst,
2178 hookState: hookState,
2179 options: options,
2180 };
2181
2182 Model.notifyObserversOf('persist', context, function(err, ctx) {
2183 if (err) return cb(err);
2184 data = ctx.data;
2185 invokeConnectorMethod(connector, 'save', Model, [Model._forDB(data)],
2186 options, saveCallback);
2187 });
2188 }, data, cb);
2189 }, data, cb);
2190 }
2191 });
2192 return cb.promise;
2193};
2194
2195/**
2196 * Update multiple instances that match the where clause
2197 *
2198 * Example:
2199 *
2200 *```js
2201 * Employee.update({managerId: 'x001'}, {managerId: 'x002'}, function(err) {
2202 * ...
2203 * });
2204 * ```
2205 *
2206 * @param {Object} [where] Search conditions (optional)
2207 * @param {Object} data Changes to be made
2208 * @param {Object} [options] Options for update
2209 * @param {Function} cb Callback, called with (err, info)
2210 */
2211DataAccessObject.update =
2212DataAccessObject.updateAll = function(where, data, options, cb) {
2213 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
2214 if (connectionPromise) {
2215 return connectionPromise;
2216 }
2217
2218 assert(arguments.length >= 1, 'At least one argument is required');
2219
2220 if (data === undefined && options === undefined && cb === undefined && arguments.length === 1) {
2221 data = where;
2222 where = {};
2223 } else if (options === undefined && cb === undefined) {
2224 // One of:
2225 // updateAll(data, cb)
2226 // updateAll(where, data) -> Promise
2227 if (typeof data === 'function') {
2228 cb = data;
2229 data = where;
2230 where = {};
2231 }
2232 } else if (cb === undefined) {
2233 // One of:
2234 // updateAll(where, data, options) -> Promise
2235 // updateAll(where, data, cb)
2236 if (typeof options === 'function') {
2237 cb = options;
2238 options = {};
2239 }
2240 }
2241
2242 data = data || {};
2243 options = options || {};
2244 cb = cb || utils.createPromiseCallback();
2245
2246 assert(typeof where === 'object', 'The where argument must be an object');
2247 assert(typeof data === 'object', 'The data argument must be an object');
2248 assert(typeof options === 'object', 'The options argument must be an object');
2249 assert(typeof cb === 'function', 'The cb argument must be a function');
2250
2251 const Model = this;
2252 const connector = Model.getDataSource().connector;
2253 assert(typeof connector.update === 'function',
2254 'update() must be implemented by the connector');
2255
2256 const hookState = {};
2257
2258 const query = {where: where};
2259 this.applyScope(query);
2260 this.applyProperties(data);
2261
2262 let doValidate = false;
2263 if (options.validate === undefined) {
2264 if (Model.settings.validateUpdate === undefined) {
2265 if (Model.settings.automaticValidation !== undefined) {
2266 doValidate = Model.settings.automaticValidation;
2267 }
2268 } else {
2269 doValidate = Model.settings.validateUpdate;
2270 }
2271 } else {
2272 doValidate = options.validate;
2273 }
2274
2275 where = query.where;
2276
2277 const context = {
2278 Model: Model,
2279 query: {where: where},
2280 hookState: hookState,
2281 options: options,
2282 };
2283 Model.notifyObserversOf('access', context, function(err, ctx) {
2284 if (err) return cb(err);
2285 const context = {
2286 Model: Model,
2287 where: ctx.query.where,
2288 data: data,
2289 hookState: hookState,
2290 options: options,
2291 };
2292 Model.notifyObserversOf('before save', context,
2293 function(err, ctx) {
2294 if (err) return cb(err);
2295
2296 data = ctx.data;
2297 let inst = data;
2298 if (!(data instanceof Model)) {
2299 try {
2300 inst = new Model(data, {applyDefaultValues: false});
2301 } catch (err) {
2302 return cb(err);
2303 }
2304 }
2305
2306 if (doValidate === false) {
2307 doUpdate(ctx.where, ctx.data);
2308 } else {
2309 // validation required
2310 inst.isValid(function(valid) {
2311 if (!valid) {
2312 return cb(new ValidationError(inst));
2313 }
2314 doUpdate(ctx.where, ctx.data);
2315 }, options);
2316 }
2317 });
2318 });
2319
2320 function doUpdate(where, data) {
2321 try {
2322 // alter configuration of how sanitizeQuery handles undefined values
2323 where = Model._sanitizeQuery(where, options);
2324 where = Model._coerce(where, options);
2325 data = Model._sanitizeData(data, options);
2326 data = Model._coerce(data, options);
2327 } catch (err) {
2328 return process.nextTick(function() {
2329 cb(err);
2330 });
2331 }
2332
2333 function updateCallback(err, info) {
2334 if (err) return cb(err);
2335
2336 const context = {
2337 Model: Model,
2338 where: where,
2339 data: data,
2340 hookState: hookState,
2341 options: options,
2342 info: info,
2343 };
2344 Model.notifyObserversOf('after save', context, function(err, ctx) {
2345 return cb(err, info);
2346 });
2347 }
2348
2349 const context = {
2350 Model: Model,
2351 where: where,
2352 data: data,
2353 hookState: hookState,
2354 options: options,
2355 };
2356 Model.notifyObserversOf('persist', context, function(err, ctx) {
2357 if (err) return cb(err);
2358 data = ctx.data;
2359 invokeConnectorMethod(connector, 'update', Model, [where, data], options, updateCallback);
2360 });
2361 }
2362 return cb.promise;
2363};
2364
2365DataAccessObject.prototype.isNewRecord = function() {
2366 return !this.__persisted;
2367};
2368
2369/**
2370 * Return connector of current record
2371 * @private
2372 */
2373DataAccessObject.prototype.getConnector = function() {
2374 return this.getDataSource().connector;
2375};
2376
2377/**
2378 * Delete object from persistence
2379 *
2380 * Triggers `destroy` hook (async) before and after destroying object
2381 *
2382 * @param {Object} [options] Options for delete
2383 * @param {Function} cb Callback
2384 */
2385DataAccessObject.prototype.remove =
2386 DataAccessObject.prototype.delete =
2387 DataAccessObject.prototype.destroy = function(options, cb) {
2388 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
2389 if (connectionPromise) {
2390 return connectionPromise;
2391 }
2392
2393 if (cb === undefined && typeof options === 'function') {
2394 cb = options;
2395 options = {};
2396 }
2397
2398 cb = cb || utils.createPromiseCallback();
2399 options = options || {};
2400
2401 assert(typeof options === 'object', 'The options argument should be an object');
2402 assert(typeof cb === 'function', 'The cb argument should be a function');
2403
2404 const inst = this;
2405 const connector = this.getConnector();
2406
2407 const Model = this.constructor;
2408 const id = getIdValue(this.constructor, this);
2409 const hookState = {};
2410
2411 if (isPKMissing(Model, cb))
2412 return cb.promise;
2413
2414 const context = {
2415 Model: Model,
2416 query: byIdQuery(Model, id),
2417 hookState: hookState,
2418 options: options,
2419 };
2420
2421 Model.notifyObserversOf('access', context, function(err, ctx) {
2422 if (err) return cb(err);
2423 const context = {
2424 Model: Model,
2425 where: ctx.query.where,
2426 instance: inst,
2427 hookState: hookState,
2428 options: options,
2429 };
2430 Model.notifyObserversOf('before delete', context, function(err, ctx) {
2431 if (err) return cb(err);
2432 doDeleteInstance(ctx.where);
2433 });
2434 });
2435
2436 function doDeleteInstance(where) {
2437 if (!isWhereByGivenId(Model, where, id)) {
2438 // A hook modified the query, it is no longer
2439 // a simple 'delete model with the given id'.
2440 // We must switch to full query-based delete.
2441 Model.deleteAll(where, {notify: false}, function(err, info) {
2442 if (err) return cb(err, false);
2443 const deleted = info && info.count > 0;
2444 if (Model.settings.strictDelete && !deleted) {
2445 err = new Error(g.f('No instance with {{id}} %s found for %s', id, Model.modelName));
2446 err.code = 'NOT_FOUND';
2447 err.statusCode = 404;
2448 return cb(err, false);
2449 }
2450 const context = {
2451 Model: Model,
2452 where: where,
2453 instance: inst,
2454 hookState: hookState,
2455 options: options,
2456 info: info,
2457 };
2458 Model.notifyObserversOf('after delete', context, function(err) {
2459 cb(err, info);
2460 });
2461 });
2462 return;
2463 }
2464
2465 inst.trigger('destroy', function(destroyed) {
2466 function destroyCallback(err, info) {
2467 if (err) return cb(err);
2468 const deleted = info && info.count > 0;
2469 if (Model.settings.strictDelete && !deleted) {
2470 err = new Error(g.f('No instance with {{id}} %s found for %s', id, Model.modelName));
2471 err.code = 'NOT_FOUND';
2472 err.statusCode = 404;
2473 return cb(err);
2474 }
2475
2476 destroyed(function() {
2477 const context = {
2478 Model: Model,
2479 where: where,
2480 instance: inst,
2481 hookState: hookState,
2482 options: options,
2483 info: info,
2484 };
2485 Model.notifyObserversOf('after delete', context, function(err) {
2486 cb(err, info);
2487 });
2488 });
2489 }
2490
2491 invokeConnectorMethod(connector, 'destroy', Model, [id], options, destroyCallback);
2492 }, null, cb);
2493 }
2494 return cb.promise;
2495 };
2496
2497/**
2498 * Set a single attribute.
2499 * Equivalent to `setAttributes({name: value})`
2500 *
2501 * @param {String} name Name of property
2502 * @param {Mixed} value Value of property
2503 */
2504DataAccessObject.prototype.setAttribute = function setAttribute(name, value) {
2505 this[name] = value; // TODO [fabien] - currently not protected by applyProperties
2506};
2507
2508/**
2509 * Update a single attribute.
2510 * Equivalent to `updateAttributes({name: value}, cb)`
2511 *
2512 * @param {String} name Name of property
2513 * @param {Mixed} value Value of property
2514 * @param {Function} cb Callback function called with (err, instance)
2515 */
2516DataAccessObject.prototype.updateAttribute = function updateAttribute(name, value, options, cb) {
2517 const data = {};
2518 data[name] = value;
2519 return this.updateAttributes(data, options, cb);
2520};
2521
2522/**
2523 * Update set of attributes.
2524 *
2525 * @trigger `change` hook
2526 * @param {Object} data Data to update
2527 */
2528DataAccessObject.prototype.setAttributes = function setAttributes(data) {
2529 if (typeof data !== 'object') return;
2530
2531 this.constructor.applyProperties(data, this);
2532
2533 const Model = this.constructor;
2534 const inst = this;
2535
2536 // update instance's properties
2537 for (const key in data) {
2538 inst.setAttribute(key, data[key]);
2539 }
2540
2541 Model.emit('set', inst);
2542};
2543
2544DataAccessObject.prototype.unsetAttribute = function unsetAttribute(name, nullify) {
2545 if (nullify || this.constructor.definition.settings.persistUndefinedAsNull) {
2546 this[name] = this.__data[name] = null;
2547 } else {
2548 delete this[name];
2549 delete this.__data[name];
2550 }
2551};
2552
2553/**
2554 * Replace set of attributes.
2555 * Performs validation before replacing.
2556 *
2557 * @trigger `validation`, `save` and `update` hooks
2558 * @param {Object} data Data to replace
2559 * @param {Object} [options] Options for replace
2560 * @param {Function} cb Callback function called with (err, instance)
2561 */
2562DataAccessObject.prototype.replaceAttributes = function(data, options, cb) {
2563 const Model = this.constructor;
2564 const id = getIdValue(this.constructor, this);
2565 return Model.replaceById(id, data, options, cb);
2566};
2567
2568DataAccessObject.replaceById = function(id, data, options, cb) {
2569 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
2570 if (connectionPromise) {
2571 return connectionPromise;
2572 }
2573
2574 if (cb === undefined) {
2575 if (typeof options === 'function') {
2576 cb = options;
2577 options = {};
2578 }
2579 }
2580
2581 cb = cb || utils.createPromiseCallback();
2582 options = options || {};
2583
2584 assert((typeof data === 'object') && (data !== null),
2585 'The data argument must be an object');
2586 assert(typeof options === 'object', 'The options argument must be an object');
2587 assert(typeof cb === 'function', 'The cb argument must be a function');
2588
2589 const connector = this.getConnector();
2590
2591 let err;
2592 if (typeof connector.replaceById !== 'function') {
2593 err = new Error(g.f(
2594 'The connector %s does not support {{replaceById}} operation. This is not a bug in LoopBack. ' +
2595 'Please contact the authors of the connector, preferably via GitHub issues.',
2596 connector.name,
2597 ));
2598 return cb(err);
2599 }
2600
2601 const pkName = idName(this);
2602 if (!data[pkName]) data[pkName] = id;
2603 let Model = this;
2604 let inst;
2605 try {
2606 inst = new Model(data, {persisted: true});
2607 const enforced = {};
2608
2609 this.applyProperties(enforced, inst);
2610 inst.setAttributes(enforced);
2611 } catch (err) {
2612 process.nextTick(function() {
2613 cb(err);
2614 });
2615 return cb.promise;
2616 }
2617
2618 Model = this.lookupModel(data); // data-specific
2619 if (Model !== inst.constructor) inst = new Model(data);
2620 const strict = inst.__strict;
2621
2622 if (isPKMissing(Model, cb))
2623 return cb.promise;
2624
2625 const hookState = {};
2626
2627 if (id !== data[pkName]) {
2628 err = new Error(g.f('{{id}} property (%s) ' +
2629 'cannot be updated from %s to %s', pkName, id, data[pkName]));
2630 err.statusCode = 400;
2631 process.nextTick(function() { cb(err); });
2632 return cb.promise;
2633 } else {
2634 // Ensure any type conversion applied by the instance constructor
2635 // on `data.id` is applied on the `id` value too.
2636 // Typically, MongoDB converts PK value from a string to an ObjectID.
2637 id = inst[pkName];
2638 }
2639
2640 let context = {
2641 Model: Model,
2642 instance: inst,
2643 isNewInstance: false,
2644 hookState: hookState,
2645 options: options,
2646 };
2647
2648 Model.notifyObserversOf('before save', context, function(err, ctx) {
2649 if (err) return cb(err);
2650
2651 if (ctx.instance[pkName] !== id && !Model._warned.cannotOverwritePKInBeforeSaveHook) {
2652 Model._warned.cannotOverwritePKInBeforeSaveHook = true;
2653 g.warn('WARNING: {{id}} property cannot be changed from %s to %s for model:%s ' +
2654 'in {{\'before save\'}} operation hook', id, inst[pkName], Model.modelName);
2655 }
2656
2657 data = inst.toObject(false);
2658
2659 if (strict) {
2660 applyStrictCheck(Model, strict, data, inst, validateAndCallConnector);
2661 } else {
2662 validateAndCallConnector(null, data);
2663 }
2664
2665 function validateAndCallConnector(err, data) {
2666 if (err) return cb(err);
2667 data = Model._sanitizeData(data, options);
2668 // update instance's properties
2669 inst.setAttributes(data);
2670
2671 let doValidate = true;
2672 if (options.validate === undefined) {
2673 if (Model.settings.automaticValidation !== undefined) {
2674 doValidate = Model.settings.automaticValidation;
2675 }
2676 } else {
2677 doValidate = options.validate;
2678 }
2679
2680 if (doValidate) {
2681 inst.isValid(function(valid) {
2682 if (!valid) return cb(new ValidationError(inst), inst);
2683
2684 callConnector();
2685 }, data, options);
2686 } else {
2687 callConnector();
2688 }
2689
2690 function callConnector() {
2691 copyData(data, inst);
2692 const typedData = convertSubsetOfPropertiesByType(inst, data);
2693 context.data = typedData;
2694
2695 // Depending on the connector, the database response can
2696 // contain information about the updated record(s). This object
2697 // has usually database-specific structure and does not match
2698 // model properties. For example, MySQL returns OkPacket:
2699 // {fieldCount, affectedRows, insertId, /*...*/, changedRows}
2700 function replaceCallback(err, dbResponse) {
2701 if (err) return cb(err);
2702 if (typeof connector.generateContextData === 'function') {
2703 context = connector.generateContextData(context, dbResponse);
2704 }
2705 const ctx = {
2706 Model: Model,
2707 hookState: hookState,
2708 data: context.data,
2709 isNewInstance: false,
2710 options: options,
2711 };
2712 Model.notifyObserversOf('loaded', ctx, function(err) {
2713 if (err) return cb(err);
2714
2715 if (ctx.data[pkName] !== id && !Model._warned.cannotOverwritePKInLoadedHook) {
2716 Model._warned.cannotOverwritePKInLoadedHook = true;
2717 g.warn('WARNING: {{id}} property cannot be changed from %s to %s for model:%s in ' +
2718 '{{\'loaded\'}} operation hook',
2719 id, ctx.data[pkName], Model.modelName);
2720 }
2721
2722 inst.__persisted = true;
2723 ctx.data[pkName] = id;
2724 inst.setAttributes(ctx.data);
2725
2726 const context = {
2727 Model: Model,
2728 instance: inst,
2729 isNewInstance: false,
2730 hookState: hookState,
2731 options: options,
2732 };
2733 Model.notifyObserversOf('after save', context, function(err) {
2734 cb(err, inst);
2735 });
2736 });
2737 }
2738
2739 const ctx = {
2740 Model: Model,
2741 where: byIdQuery(Model, id).where,
2742 data: context.data,
2743 isNewInstance: false,
2744 currentInstance: inst,
2745 hookState: hookState,
2746 options: options,
2747 };
2748 Model.notifyObserversOf('persist', ctx, function(err) {
2749 if (err) return cb(err);
2750 // apply updates made by "persist" observers
2751 context.data = ctx.data;
2752 invokeConnectorMethod(connector, 'replaceById', Model, [id, Model._forDB(ctx.data)],
2753 options, replaceCallback);
2754 });
2755 }
2756 }
2757 });
2758 return cb.promise;
2759};
2760
2761/**
2762 * Update set of attributes.
2763 * Performs validation before updating.
2764 * NOTE: `patchOrCreate` is an alias.
2765 *
2766 * @trigger `validation`, `save` and `update` hooks
2767 * @param {Object} data Data to update
2768 * @param {Object} [options] Options for updateAttributes
2769 * @param {Function} cb Callback function called with (err, instance)
2770 */
2771DataAccessObject.prototype.updateAttributes =
2772DataAccessObject.prototype.patchAttributes =
2773function(data, options, cb) {
2774 const self = this;
2775 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
2776 if (connectionPromise) {
2777 return connectionPromise;
2778 }
2779
2780 if (options === undefined && cb === undefined) {
2781 if (typeof data === 'function') {
2782 // updateAttributes(cb)
2783 cb = data;
2784 data = undefined;
2785 }
2786 } else if (cb === undefined) {
2787 if (typeof options === 'function') {
2788 // updateAttributes(data, cb)
2789 cb = options;
2790 options = {};
2791 }
2792 }
2793
2794 cb = cb || utils.createPromiseCallback();
2795 options = options || {};
2796
2797 assert((typeof data === 'object') && (data !== null),
2798 'The data argument must be an object');
2799 assert(typeof options === 'object', 'The options argument must be an object');
2800 assert(typeof cb === 'function', 'The cb argument must be a function');
2801
2802 const inst = this;
2803 const Model = this.constructor;
2804 const connector = inst.getConnector();
2805 assert(typeof connector.updateAttributes === 'function',
2806 'updateAttributes() must be implemented by the connector');
2807
2808 if (isPKMissing(Model, cb))
2809 return cb.promise;
2810
2811 const allowExtendedOperators = Model._allowExtendedOperators(options);
2812 const strict = this.__strict;
2813 const hookState = {};
2814
2815 // Convert the data to be plain object so that update won't be confused
2816 if (data instanceof Model) {
2817 data = data.toObject(false);
2818 }
2819 data = Model._sanitizeData(data, options);
2820
2821 // Make sure id(s) cannot be changed
2822 const idNames = Model.definition.idNames();
2823 for (let i = 0, n = idNames.length; i < n; i++) {
2824 const idName = idNames[i];
2825 if (data[idName] !== undefined && !idEquals(data[idName], inst[idName])) {
2826 const err = new Error(g.f('{{id}} cannot be updated from ' +
2827 '%s to %s when {{forceId}} is set to true',
2828 inst[idName], data[idName]));
2829 err.statusCode = 400;
2830 process.nextTick(function() {
2831 cb(err);
2832 });
2833 return cb.promise;
2834 }
2835 }
2836
2837 let context = {
2838 Model: Model,
2839 where: byIdQuery(Model, getIdValue(Model, inst)).where,
2840 data: data,
2841 currentInstance: inst,
2842 hookState: hookState,
2843 options: options,
2844 };
2845
2846 Model.notifyObserversOf('before save', context, function(err, ctx) {
2847 if (err) return cb(err);
2848 data = ctx.data;
2849
2850 if (strict && !allowExtendedOperators) {
2851 applyStrictCheck(self.constructor, strict, data, inst, validateAndSave);
2852 } else {
2853 validateAndSave(null, data);
2854 }
2855
2856 function validateAndSave(err, data) {
2857 if (err) return cb(err);
2858 let doValidate = true;
2859 if (options.validate === undefined) {
2860 if (Model.settings.automaticValidation !== undefined) {
2861 doValidate = Model.settings.automaticValidation;
2862 }
2863 } else {
2864 doValidate = options.validate;
2865 }
2866
2867 // update instance's properties
2868 try {
2869 inst.setAttributes(data);
2870 } catch (err) {
2871 return cb(err);
2872 }
2873
2874 if (doValidate) {
2875 inst.isValid(function(valid) {
2876 if (!valid) {
2877 cb(new ValidationError(inst), inst);
2878 return;
2879 }
2880
2881 triggerSave();
2882 }, data, options);
2883 } else {
2884 triggerSave();
2885 }
2886
2887 function triggerSave() {
2888 inst.trigger('save', function(saveDone) {
2889 inst.trigger('update', function(done) {
2890 copyData(data, inst);
2891 const typedData = convertSubsetOfPropertiesByType(inst, data);
2892 context.data = Model._sanitizeData(typedData, options);
2893
2894 // Depending on the connector, the database response can
2895 // contain information about the updated record(s), but also
2896 // just a number of updated rows (e.g. {count: 1} for MySQL).
2897 function updateAttributesCallback(err, dbResponse) {
2898 if (err) return cb(err);
2899 if (typeof connector.generateContextData === 'function') {
2900 context = connector.generateContextData(context, dbResponse);
2901 }
2902 const ctx = {
2903 Model: Model,
2904 data: context.data,
2905 hookState: hookState,
2906 options: options,
2907 isNewInstance: false,
2908 };
2909 Model.notifyObserversOf('loaded', ctx, function(err) {
2910 if (err) return cb(err);
2911
2912 inst.__persisted = true;
2913
2914 // By default, the instance passed to updateAttributes callback is NOT updated
2915 // with the changes made through persist/loaded hooks. To preserve
2916 // backwards compatibility, we introduced a new setting updateOnLoad,
2917 // which if set, will apply these changes to the model instance too.
2918 if (Model.settings.updateOnLoad) {
2919 inst.setAttributes(ctx.data);
2920 }
2921 done.call(inst, function() {
2922 saveDone.call(inst, function() {
2923 if (err) return cb(err, inst);
2924
2925 const context = {
2926 Model: Model,
2927 instance: inst,
2928 isNewInstance: false,
2929 hookState: hookState,
2930 options: options,
2931 };
2932 Model.notifyObserversOf('after save', context, function(err) {
2933 cb(err, inst);
2934 });
2935 });
2936 });
2937 });
2938 }
2939
2940 const ctx = {
2941 Model: Model,
2942 where: byIdQuery(Model, getIdValue(Model, inst)).where,
2943 data: context.data,
2944 currentInstance: inst,
2945 isNewInstance: false,
2946 hookState: hookState,
2947 options: options,
2948 };
2949 Model.notifyObserversOf('persist', ctx, function(err) {
2950 if (err) return cb(err);
2951 // apply updates made by "persist" observers
2952 context.data = ctx.data;
2953 invokeConnectorMethod(connector, 'updateAttributes', Model,
2954 [getIdValue(Model, inst), Model._forDB(ctx.data)],
2955 options, updateAttributesCallback);
2956 });
2957 }, data, cb);
2958 }, data, cb);
2959 }
2960 }
2961 });
2962 return cb.promise;
2963};
2964
2965/**
2966 * Reload object from persistence
2967 * Requires `id` member of `object` to be able to call `find`
2968 * @param {Function} cb Called with (err, instance) arguments
2969 * @private
2970 */
2971DataAccessObject.prototype.reload = function reload(cb) {
2972 const connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
2973 if (connectionPromise) {
2974 return connectionPromise;
2975 }
2976
2977 return this.constructor.findById(getIdValue(this.constructor, this), cb);
2978};
2979
2980/*
2981 * Define readonly property on object
2982 *
2983 * @param {Object} obj
2984 * @param {String} key
2985 * @param {Mixed} value
2986 * @private
2987 */
2988function defineReadonlyProp(obj, key, value) {
2989 Object.defineProperty(obj, key, {
2990 writable: false,
2991 enumerable: true,
2992 configurable: true,
2993 value: value,
2994 });
2995}
2996
2997const defineScope = require('./scope.js').defineScope;
2998
2999/**
3000 * Define a scope for the model class. Scopes enable you to specify commonly-used
3001 * queries that you can reference as method calls on a model.
3002 *
3003 * @param {String} name The scope name
3004 * @param {Object} query The query object for DataAccessObject.find()
3005 * @param {ModelClass} [targetClass] The model class for the query, default to
3006 * the declaring model
3007 */
3008DataAccessObject.scope = function(name, query, targetClass, methods, options) {
3009 let cls = this;
3010 if (options && options.isStatic === false) {
3011 cls = cls.prototype;
3012 }
3013 return defineScope(cls, targetClass || cls, name, query, methods, options);
3014};
3015
3016/*
3017 * Add 'include'
3018 */
3019jutil.mixin(DataAccessObject, Inclusion);
3020
3021/*
3022 * Add 'relation'
3023 */
3024jutil.mixin(DataAccessObject, Relation);
3025
3026/*
3027 * Add 'transaction'
3028 */
3029jutil.mixin(DataAccessObject, require('./transaction'));
3030
3031function PKMissingError(modelName) {
3032 this.name = 'PKMissingError';
3033 this.message = 'Primary key is missing for the ' + modelName + ' model';
3034}
3035PKMissingError.prototype = new Error();
3036
3037function isPKMissing(modelClass, cb) {
3038 const hasPK = modelClass.definition.hasPK();
3039 if (hasPK) return false;
3040 process.nextTick(function() {
3041 cb(new PKMissingError(modelClass.modelName));
3042 });
3043 return true;
3044}