UNPKG

27.2 kBJavaScriptView Raw
1// Copyright IBM Corp. 2013,2019. 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 = ModelBaseClass;
14
15/*!
16 * Module dependencies
17 */
18
19const g = require('strong-globalize')();
20const util = require('util');
21const jutil = require('./jutil');
22const List = require('./list');
23const DataAccessUtils = require('./model-utils');
24const Observer = require('./observer');
25const Hookable = require('./hooks');
26const validations = require('./validations');
27const _extend = util._extend;
28const utils = require('./utils');
29const fieldsToArray = utils.fieldsToArray;
30const uuid = require('uuid');
31const shortid = require('shortid');
32
33// Set up an object for quick lookup
34const BASE_TYPES = {
35 'String': true,
36 'Boolean': true,
37 'Number': true,
38 'Date': true,
39 'Text': true,
40 'ObjectID': true,
41};
42
43/**
44 * Model class: base class for all persistent objects.
45 *
46 * `ModelBaseClass` mixes `Validatable` and `Hookable` classes methods
47 *
48 * @class
49 * @param {Object} data Initial object data
50 * @param {Object} options An object to control the instantiation
51 * @returns {ModelBaseClass} an instance of the ModelBaseClass
52 */
53function ModelBaseClass(data, options) {
54 options = options || {};
55 if (!('applySetters' in options)) {
56 // Default to true
57 options.applySetters = true;
58 }
59 if (!('applyDefaultValues' in options)) {
60 options.applyDefaultValues = true;
61 }
62 this._initProperties(data, options);
63}
64
65/**
66 * Initialize the model instance with a list of properties
67 * @param {Object} data The data object
68 * @param {Object} options An object to control the instantiation
69 * @property {Boolean} applySetters Controls if the setters will be applied
70 * @property {Boolean} applyDefaultValues Default attributes and values will be applied
71 * @property {Boolean} strict Set the instance level strict mode
72 * @property {Boolean} persisted Whether the instance has been persisted
73 * @private
74 */
75ModelBaseClass.prototype._initProperties = function(data, options) {
76 const self = this;
77 const ctor = this.constructor;
78
79 if (typeof data !== 'undefined' && data !== null && data.constructor &&
80 typeof (data.constructor) !== 'function') {
81 throw new Error(g.f('Property name "{{constructor}}" is not allowed in %s data', ctor.modelName));
82 }
83
84 if (data instanceof ctor) {
85 // Convert the data to be plain object to avoid pollutions
86 data = data.toObject(false);
87 }
88 const properties = _extend({}, ctor.definition.properties);
89 data = data || {};
90
91 if (typeof ctor.applyProperties === 'function') {
92 ctor.applyProperties(data);
93 }
94
95 options = options || {};
96 const applySetters = options.applySetters;
97 const applyDefaultValues = options.applyDefaultValues;
98 let strict = options.strict;
99
100 if (strict === undefined) {
101 strict = ctor.definition.settings.strict;
102 } else if (strict === 'throw') {
103 g.warn('Warning: Model %s, {{strict mode: `throw`}} has been removed, ' +
104 'please use {{`strict: true`}} instead, which returns' +
105 '{{`Validation Error`}} for unknown properties,', ctor.modelName);
106 }
107
108 const persistUndefinedAsNull = ctor.definition.settings.persistUndefinedAsNull;
109
110 if (ctor.hideInternalProperties) {
111 // Object.defineProperty() is expensive. We only try to make the internal
112 // properties hidden (non-enumerable) if the model class has the
113 // `hideInternalProperties` set to true
114 Object.defineProperties(this, {
115 __cachedRelations: {
116 writable: true,
117 enumerable: false,
118 configurable: true,
119 value: {},
120 },
121
122 __data: {
123 writable: true,
124 enumerable: false,
125 configurable: true,
126 value: {},
127 },
128
129 // Instance level data source
130 __dataSource: {
131 writable: true,
132 enumerable: false,
133 configurable: true,
134 value: options.dataSource,
135 },
136
137 // Instance level strict mode
138 __strict: {
139 writable: true,
140 enumerable: false,
141 configurable: true,
142 value: strict,
143 },
144
145 __persisted: {
146 writable: true,
147 enumerable: false,
148 configurable: true,
149 value: false,
150 },
151 });
152
153 if (strict) {
154 Object.defineProperty(this, '__unknownProperties', {
155 writable: true,
156 enumerable: false,
157 configrable: true,
158 value: [],
159 });
160 }
161 } else {
162 this.__cachedRelations = {};
163 this.__data = {};
164 this.__dataSource = options.dataSource;
165 this.__strict = strict;
166 this.__persisted = false;
167 if (strict) {
168 this.__unknownProperties = [];
169 }
170 }
171
172 if (options.persisted !== undefined) {
173 this.__persisted = options.persisted === true;
174 }
175
176 if (data.__cachedRelations) {
177 this.__cachedRelations = data.__cachedRelations;
178 }
179
180 let keys = Object.keys(data);
181
182 if (Array.isArray(options.fields)) {
183 keys = keys.filter(function(k) {
184 return (options.fields.indexOf(k) != -1);
185 });
186 }
187
188 let size = keys.length;
189 let p, propVal;
190 for (let k = 0; k < size; k++) {
191 p = keys[k];
192 propVal = data[p];
193 if (typeof propVal === 'function') {
194 continue;
195 }
196
197 if (propVal === undefined && persistUndefinedAsNull) {
198 propVal = null;
199 }
200
201 if (properties[p]) {
202 // Managed property
203 if (applySetters || properties[p].id) {
204 self[p] = propVal;
205 } else {
206 self.__data[p] = propVal;
207 }
208 } else if (ctor.relations[p]) {
209 const relationType = ctor.relations[p].type;
210
211 let modelTo;
212 if (!properties[p]) {
213 modelTo = ctor.relations[p].modelTo || ModelBaseClass;
214 const multiple = ctor.relations[p].multiple;
215 const typeName = multiple ? 'Array' : modelTo.modelName;
216 const propType = multiple ? [modelTo] : modelTo;
217 properties[p] = {name: typeName, type: propType};
218 /* Issue #1252
219 this.setStrict(false);
220 */
221 }
222
223 // Relation
224 if (relationType === 'belongsTo' && propVal != null) {
225 // If the related model is populated
226 self.__data[ctor.relations[p].keyFrom] = propVal[ctor.relations[p].keyTo];
227
228 if (ctor.relations[p].options.embedsProperties) {
229 const fields = fieldsToArray(ctor.relations[p].properties,
230 modelTo.definition.properties, modelTo.settings.strict);
231 if (!~fields.indexOf(ctor.relations[p].keyTo)) {
232 fields.push(ctor.relations[p].keyTo);
233 }
234 self.__data[p] = new modelTo(propVal, {
235 fields: fields,
236 applySetters: false,
237 persisted: options.persisted,
238 });
239 }
240 }
241
242 self.__cachedRelations[p] = propVal;
243 } else {
244 // Un-managed property
245 if (strict === false || self.__cachedRelations[p]) {
246 self[p] = self.__data[p] =
247 (propVal !== undefined) ? propVal : self.__cachedRelations[p];
248
249 // Throw error for properties with unsupported names
250 if (/\./.test(p)) {
251 throw new Error(g.f(
252 'Property names containing dot(s) are not supported. ' +
253 'Model: %s, dynamic property: %s',
254 this.constructor.modelName, p,
255 ));
256 }
257 } else {
258 if (strict !== 'filter') {
259 this.__unknownProperties.push(p);
260 }
261 }
262 }
263 }
264
265 keys = Object.keys(properties);
266
267 if (Array.isArray(options.fields)) {
268 keys = keys.filter(function(k) {
269 return (options.fields.indexOf(k) != -1);
270 });
271 }
272
273 size = keys.length;
274
275 for (let k = 0; k < size; k++) {
276 p = keys[k];
277 propVal = self.__data[p];
278 const type = properties[p].type;
279
280 // Set default values
281 if (applyDefaultValues && propVal === undefined && appliesDefaultsOnWrites(properties[p])) {
282 let def = properties[p]['default'];
283 if (def !== undefined) {
284 if (typeof def === 'function') {
285 if (def === Date) {
286 // FIXME: We should coerce the value in general
287 // This is a work around to {default: Date}
288 // Date() will return a string instead of Date
289 def = new Date();
290 } else {
291 def = def();
292 }
293 } else if (type.name === 'Date' && def === '$now') {
294 def = new Date();
295 }
296 // FIXME: We should coerce the value
297 // will implement it after we refactor the PropertyDefinition
298 self.__data[p] = propVal = def;
299 }
300 }
301
302 if (ignoresMatchedDefault(properties[p]) && properties[p].default === propVal) {
303 delete self.__data[p];
304 }
305
306 // Set default value using a named function
307 if (applyDefaultValues && propVal === undefined) {
308 const defn = properties[p].defaultFn;
309 switch (defn) {
310 case undefined:
311 break;
312 case 'guid':
313 case 'uuid':
314 // Generate a v1 (time-based) id
315 propVal = uuid.v1();
316 break;
317 case 'uuidv4':
318 // Generate a RFC4122 v4 UUID
319 propVal = uuid.v4();
320 break;
321 case 'now':
322 propVal = new Date();
323 break;
324 case 'shortid':
325 propVal = shortid.generate();
326 break;
327 default:
328 // TODO Support user-provided functions via a registry of functions
329 g.warn('Unknown default value provider %s', defn);
330 }
331 // FIXME: We should coerce the value
332 // will implement it after we refactor the PropertyDefinition
333 if (propVal !== undefined)
334 self.__data[p] = propVal;
335 }
336
337 if (propVal === undefined && persistUndefinedAsNull) {
338 self.__data[p] = propVal = null;
339 }
340
341 // Handle complex types (JSON/Object)
342 if (!BASE_TYPES[type.name]) {
343 if (typeof self.__data[p] !== 'object' && self.__data[p]) {
344 try {
345 self.__data[p] = JSON.parse(self.__data[p] + '');
346 } catch (e) {
347 self.__data[p] = String(self.__data[p]);
348 }
349 }
350
351 if (type.prototype instanceof ModelBaseClass) {
352 if (!(self.__data[p] instanceof type) &&
353 typeof self.__data[p] === 'object' &&
354 self.__data[p] !== null) {
355 self.__data[p] = new type(self.__data[p]);
356 utils.applyParentProperty(self.__data[p], this);
357 }
358 } else if (type.name === 'Array' || Array.isArray(type)) {
359 if (!(self.__data[p] instanceof List) &&
360 self.__data[p] !== undefined &&
361 self.__data[p] !== null) {
362 self.__data[p] = List(self.__data[p], type, self);
363 }
364 }
365 }
366 }
367 this.trigger('initialize');
368};
369
370// Implementation of persistDefaultValues property
371function ignoresMatchedDefault(property) {
372 if (property && property.persistDefaultValues === false) {
373 return true;
374 }
375}
376
377// Helper function for determing the applyDefaultOnWrites value of a property
378function appliesDefaultsOnWrites(property) {
379 if (property && ('applyDefaultOnWrites' in property)) {
380 return property.applyDefaultOnWrites;
381 }
382 return true;
383}
384
385/**
386 * Define a property on the model.
387 * @param {String} prop Property name
388 * @param {Object} params Various property configuration
389 */
390ModelBaseClass.defineProperty = function(prop, params) {
391 if (this.dataSource) {
392 this.dataSource.defineProperty(this.modelName, prop, params);
393 } else {
394 this.modelBuilder.defineProperty(this.modelName, prop, params);
395 }
396};
397
398/**
399 * Get model property type.
400 * @param {String} propName Property name
401 * @returns {String} Name of property type
402 */
403ModelBaseClass.getPropertyType = function(propName) {
404 const prop = this.definition.properties[propName];
405 if (!prop) {
406 // The property is not part of the definition
407 return null;
408 }
409 if (!prop.type) {
410 throw new Error(g.f('Type not defined for property %s.%s', this.modelName, propName));
411 // return null;
412 }
413 return prop.type.name;
414};
415
416/**
417 * Get model property type.
418 * @param {String} propName Property name
419 * @returns {String} Name of property type
420 */
421ModelBaseClass.prototype.getPropertyType = function(propName) {
422 return this.constructor.getPropertyType(propName);
423};
424
425/**
426 * Return string representation of class
427 * This overrides the default `toString()` method
428 */
429ModelBaseClass.toString = function() {
430 return '[Model ' + this.modelName + ']';
431};
432
433/**
434 * Convert model instance to a plain JSON object.
435 * Returns a canonical object representation (no getters and setters).
436 *
437 * @param {Boolean} onlySchema Restrict properties to dataSource only. Default is false. If true, the function returns only properties defined in the schema; Otherwise it returns all enumerable properties.
438 * @param {Boolean} removeHidden Boolean flag as part of the transformation. If true, then hidden properties should not be brought out.
439 * @param {Boolean} removeProtected Boolean flag as part of the transformation. If true, then protected properties should not be brought out.
440 * @returns {Object} returns Plain JSON object
441 */
442ModelBaseClass.prototype.toObject = function(onlySchema, removeHidden, removeProtected) {
443 if (typeof onlySchema === 'object' && onlySchema != null) {
444 const options = onlySchema;
445 onlySchema = options.onlySchema;
446 removeHidden = options.removeHidden;
447 removeProtected = options.removeProtected;
448 }
449 if (onlySchema === undefined) {
450 onlySchema = true;
451 }
452 const data = {};
453 const self = this;
454 const Model = this.constructor;
455
456 // if it is already an Object
457 if (Model === Object) {
458 return self;
459 }
460
461 const strict = this.__strict;
462 const schemaLess = (strict === false) || !onlySchema;
463 const persistUndefinedAsNull = Model.definition.settings.persistUndefinedAsNull;
464
465 const props = Model.definition.properties;
466 let keys = Object.keys(props);
467 let propertyName, val;
468
469 for (let i = 0; i < keys.length; i++) {
470 propertyName = keys[i];
471 val = self[propertyName];
472
473 // Exclude functions
474 if (typeof val === 'function') {
475 continue;
476 }
477 // Exclude hidden properties
478 if (removeHidden && Model.isHiddenProperty(propertyName)) {
479 continue;
480 }
481
482 if (removeProtected && Model.isProtectedProperty(propertyName)) {
483 continue;
484 }
485
486 if (val instanceof List) {
487 data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
488 } else {
489 if (val !== undefined && val !== null && val.toObject) {
490 data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
491 } else {
492 if (val === undefined && persistUndefinedAsNull) {
493 val = null;
494 }
495 data[propertyName] = val;
496 }
497 }
498 }
499
500 if (schemaLess) {
501 // Find its own properties which can be set via myModel.myProperty = 'myValue'.
502 // If the property is not declared in the model definition, no setter will be
503 // triggered to add it to __data
504 keys = Object.keys(self);
505 let size = keys.length;
506 for (let i = 0; i < size; i++) {
507 propertyName = keys[i];
508 if (props[propertyName]) {
509 continue;
510 }
511 if (propertyName.indexOf('__') === 0) {
512 continue;
513 }
514 if (removeHidden && Model.isHiddenProperty(propertyName)) {
515 continue;
516 }
517 if (removeProtected && Model.isProtectedProperty(propertyName)) {
518 continue;
519 }
520 if (data[propertyName] !== undefined) {
521 continue;
522 }
523 val = self[propertyName];
524 if (val !== undefined) {
525 if (typeof val === 'function') {
526 continue;
527 }
528 if (val !== null && val.toObject) {
529 data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
530 } else {
531 data[propertyName] = val;
532 }
533 } else if (persistUndefinedAsNull) {
534 data[propertyName] = null;
535 }
536 }
537 // Now continue to check __data
538 keys = Object.keys(self.__data);
539 size = keys.length;
540 for (let i = 0; i < size; i++) {
541 propertyName = keys[i];
542 if (propertyName.indexOf('__') === 0) {
543 continue;
544 }
545 if (data[propertyName] === undefined) {
546 if (removeHidden && Model.isHiddenProperty(propertyName)) {
547 continue;
548 }
549 if (removeProtected && Model.isProtectedProperty(propertyName)) {
550 continue;
551 }
552 const ownVal = self[propertyName];
553 // The ownVal can be a relation function
554 val = (ownVal !== undefined && (typeof ownVal !== 'function')) ? ownVal : self.__data[propertyName];
555 if (typeof val === 'function') {
556 continue;
557 }
558
559 if (val !== undefined && val !== null && val.toObject) {
560 data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
561 } else if (val === undefined && persistUndefinedAsNull) {
562 data[propertyName] = null;
563 } else {
564 data[propertyName] = val;
565 }
566 }
567 }
568 }
569
570 return data;
571};
572
573/**
574 * Convert an array of strings into an object as the map
575 * @param {string[]} arr An array of strings
576 */
577function asObjectMap(arr) {
578 const obj = {};
579 if (Array.isArray(arr)) {
580 for (let i = 0; i < arr.length; i++) {
581 obj[arr[i]] = true;
582 }
583 return obj;
584 }
585 return arr || obj;
586}
587/**
588 * Checks if property is protected.
589 * @param {String} propertyName Property name
590 * @returns {Boolean} true or false if protected or not.
591 */
592ModelBaseClass.isProtectedProperty = function(propertyName) {
593 const settings = (this.definition && this.definition.settings) || {};
594 const protectedProperties = settings.protectedProperties || settings.protected;
595 settings.protectedProperties = asObjectMap(protectedProperties);
596 return settings.protectedProperties[propertyName];
597};
598
599/**
600 * Checks if property is hidden.
601 * @param {String} propertyName Property name
602 * @returns {Boolean} true or false if hidden or not.
603 */
604ModelBaseClass.isHiddenProperty = function(propertyName) {
605 const settings = (this.definition && this.definition.settings) || {};
606 const hiddenProperties = settings.hiddenProperties || settings.hidden;
607 settings.hiddenProperties = asObjectMap(hiddenProperties);
608 return settings.hiddenProperties[propertyName];
609};
610
611ModelBaseClass.prototype.toJSON = function() {
612 return this.toObject(false, true, false);
613};
614
615ModelBaseClass.prototype.fromObject = function(obj) {
616 for (const key in obj) {
617 this[key] = obj[key];
618 }
619};
620
621/**
622 * Reset dirty attributes.
623 * This method does not perform any database operations; it just resets the object to its
624 * initial state.
625 */
626ModelBaseClass.prototype.reset = function() {
627 const obj = this;
628 for (const k in obj) {
629 if (k !== 'id' && !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) {
630 delete obj[k];
631 }
632 }
633};
634
635// Node v0.11+ allows custom inspect functions to return an object
636// instead of string. That way options like `showHidden` and `colors`
637// can be preserved.
638const versionParts = process.versions && process.versions.node ?
639 process.versions.node.split(/\./g).map(function(v) { return +v; }) :
640 [1, 0, 0]; // browserify ships 1.0-compatible version of util.inspect
641
642const INSPECT_SUPPORTS_OBJECT_RETVAL =
643 versionParts[0] > 0 ||
644 versionParts[1] > 11 ||
645 (versionParts[0] === 11 && versionParts[1] >= 14);
646
647ModelBaseClass.prototype.inspect = function(depth) {
648 if (INSPECT_SUPPORTS_OBJECT_RETVAL)
649 return this.__data;
650
651 // Workaround for older versions
652 // See also https://github.com/joyent/node/commit/66280de133
653 return util.inspect(this.__data, {
654 showHidden: false,
655 depth: depth,
656 colors: false,
657 });
658};
659
660if (util.inspect.custom) {
661 // Node.js 12+ no longer recognizes "inspect" method,
662 // it uses "inspect.custom" symbol as the key instead
663 // TODO(semver-major) always use the symbol key only (requires Node.js 8+).
664 ModelBaseClass.prototype[util.inspect.custom] = ModelBaseClass.prototype.inspect;
665}
666
667/**
668 *
669 * @param {String} anotherClass could be string or class. Name of the class or the class itself
670 * @param {Object} options An object to control the instantiation
671 * @returns {ModelClass}
672 */
673ModelBaseClass.mixin = function(anotherClass, options) {
674 if (typeof anotherClass === 'string') {
675 this.modelBuilder.mixins.applyMixin(this, anotherClass, options);
676 } else {
677 if (anotherClass.prototype instanceof ModelBaseClass) {
678 const props = anotherClass.definition.properties;
679 for (const i in props) {
680 if (this.definition.properties[i]) {
681 continue;
682 }
683 this.defineProperty(i, props[i]);
684 }
685 }
686 return jutil.mixin(this, anotherClass, options);
687 }
688};
689
690ModelBaseClass.prototype.getDataSource = function() {
691 return this.__dataSource || this.constructor.dataSource;
692};
693
694ModelBaseClass.getDataSource = function() {
695 return this.dataSource;
696};
697
698ModelBaseClass.prototype.setStrict = function(strict) {
699 this.__strict = strict;
700};
701
702/**
703 *
704 * `getMergePolicy()` provides model merge policies to apply when extending
705 * a child model from a base model. Such a policy drives the way parent/child model
706 * properties/settings are merged/mixed-in together.
707 *
708 * Below is presented the expected merge behaviour for each option.
709 * NOTE: This applies to top-level settings properties
710 *
711 *
712 * - Any
713 * - `{replace: true}` (default): child replaces the value from parent
714 * - assignin `null` on child setting deletes the inherited setting
715 *
716 * - Arrays:
717 * - `{replace: false}`: unique elements of parent and child cumulate
718 * - `{rank: true}` adds the model inheritance rank to array
719 * elements of type Object {} as internal property `__rank`
720 *
721 * - Object {}:
722 * - `{replace: false}`: deep merges parent and child objects
723 * - `{patch: true}`: child replaces inner properties from parent
724 *
725 *
726 * The recommended built-in merge policy is as follows. It is returned by getMergePolicy()
727 * when calling the method with option `{configureModelMerge: true}`.
728 *
729 * ```
730 * {
731 * description: {replace: true}, // string or array
732 * options: {patch: true}, // object
733 * hidden: {replace: false}, // array
734 * protected: {replace: false}, // array
735 * indexes: {patch: true}, // object
736 * methods: {patch: true}, // object
737 * mixins: {patch: true}, // object
738 * relations: {patch: true}, // object
739 * scope: {replace: true}, // object
740 * scopes: {patch: true}, // object
741 * acls: {rank: true}, // array
742 * // this setting controls which child model property's value allows deleting
743 * // a base model's property
744 * __delete: null,
745 * // this setting controls the default merge behaviour for settings not defined
746 * // in the mergePolicy specification
747 * __default: {replace: true},
748 * }
749 * ```
750 *
751 * The legacy built-in merge policy is as follows, it is retuned by `getMergePolicy()`
752 * when avoiding option `configureModelMerge`.
753 * NOTE: it also provides the ACLs ranking in addition to the legacy behaviour, as well
754 * as fixes for settings 'description' and 'relations': matching relations from child
755 * replace relations from parents.
756 *
757 * ```
758 * {
759 * description: {replace: true}, // string or array
760 * properties: {patch: true}, // object
761 * hidden: {replace: false}, // array
762 * protected: {replace: false}, // array
763 * relations: {acls: true}, // object
764 * acls: {rank: true}, // array
765 * }
766 * ```
767 *
768 *
769 * `getMergePolicy()` can be customized using model's setting `configureModelMerge` as follows:
770 *
771 * ``` json
772 * {
773 * // ..
774 * options: {
775 * configureModelMerge: {
776 * // merge options
777 * }
778 * }
779 * // ..
780 * }
781 * ```
782 *
783 * NOTE: mergePolicy parameter can also defined at JSON model definition root
784 *
785 * `getMergePolicy()` method can also be extended programmatically as follows:
786 *
787 * ``` js
788 * myModel.getMergePolicy = function(options) {
789 * const origin = myModel.base.getMergePolicy(options);
790 * return Object.assign({}, origin, {
791 * // new/overriding options
792 * });
793 * };
794 * ```
795 *
796 * @param {Object} options option `configureModelMerge` can be used to alter the
797 * returned merge policy:
798 * - `configureModelMerge: true` will have the method return the recommended merge policy.
799 * - `configureModelMerge: {..}` will actually have the method return the provided object.
800 * - not providing this options will have the method return a merge policy emulating the
801 * the model merge behaviour up to datasource-juggler v3.6.1, as well as the ACLs ranking.
802 * @returns {Object} mergePolicy The model merge policy to apply when using the
803 * current model as base class for a child model
804 */
805ModelBaseClass.getMergePolicy = function(options) {
806 // NOTE: merge policy equivalent to datasource-juggler behaviour up to v3.6.1
807 // + fix for description arrays that should not be merged
808 // + fix for relations that should patch matching relations
809 // + ranking of ACLs
810 let mergePolicy = {
811 description: {replace: true}, // string or array
812 properties: {patch: true}, // object
813 hidden: {replace: false}, // array
814 protected: {replace: false}, // array
815 relations: {patch: true}, // object
816 acls: {rank: true}, // array
817 };
818
819 const config = (options || {}).configureModelMerge;
820
821 if (config === true) {
822 // NOTE: recommended merge policy from datasource-juggler v3.6.2
823 mergePolicy = {
824 description: {replace: true}, // string or array
825 options: {patch: true}, // object
826 // properties: {patch: true}, // object // NOTE: not part of configurable merge
827 hidden: {replace: false}, // array
828 protected: {replace: false}, // array
829 indexes: {patch: true}, // object
830 methods: {patch: true}, // object
831 mixins: {patch: true}, // object
832 // validations: {patch: true}, // object // NOTE: not implemented
833 relations: {patch: true}, // object
834 scope: {replace: true}, // object
835 scopes: {patch: true}, // object
836 acls: {rank: true}, // array
837 // this option controls which value assigned on child model allows deleting
838 // a base model's setting
839 __delete: null,
840 // this option controls the default merge behaviour for settings not defined
841 // in the mergePolicy specification
842 __default: {replace: true},
843 };
844 }
845
846 // override mergePolicy with provided model setting if required
847 if (config && typeof config === 'object' && !Array.isArray(config)) {
848 // config is an object
849 mergePolicy = config;
850 }
851
852 return mergePolicy;
853};
854
855/**
856 * Gets properties defined with 'updateOnly' flag set to true from the model. This flag is also set to true
857 * internally for the id property, if this property is generated and IdInjection is true.
858 * @returns {updateOnlyProps} List of properties with updateOnly set to true.
859 */
860
861ModelBaseClass.getUpdateOnlyProperties = function() {
862 const props = this.definition.properties;
863 return Object.keys(props).filter(key => props[key].updateOnly);
864};
865
866// Mix in utils
867jutil.mixin(ModelBaseClass, DataAccessUtils);
868
869// Mixin observer
870jutil.mixin(ModelBaseClass, Observer);
871
872jutil.mixin(ModelBaseClass, Hookable);
873jutil.mixin(ModelBaseClass, validations.Validatable);