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 {nanoid} = require('nanoid');
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 case 'nanoid':
326 propVal = nanoid(9);
327 break;
328 default:
329 // TODO Support user-provided functions via a registry of functions
330 g.warn('Unknown default value provider %s', defn);
331 }
332 // FIXME: We should coerce the value
333 // will implement it after we refactor the PropertyDefinition
334 if (propVal !== undefined)
335 self.__data[p] = propVal;
336 }
337
338 if (propVal === undefined && persistUndefinedAsNull) {
339 self.__data[p] = propVal = null;
340 }
341
342 // Handle complex types (JSON/Object)
343 if (!BASE_TYPES[type.name]) {
344 if (typeof self.__data[p] !== 'object' && self.__data[p]) {
345 try {
346 self.__data[p] = JSON.parse(self.__data[p] + '');
347 } catch (e) {
348 self.__data[p] = String(self.__data[p]);
349 }
350 }
351
352 if (type.prototype instanceof ModelBaseClass) {
353 if (!(self.__data[p] instanceof type) &&
354 typeof self.__data[p] === 'object' &&
355 self.__data[p] !== null) {
356 self.__data[p] = new type(self.__data[p]);
357 utils.applyParentProperty(self.__data[p], this);
358 }
359 } else if (type.name === 'Array' || Array.isArray(type)) {
360 if (!(self.__data[p] instanceof List) &&
361 self.__data[p] !== undefined &&
362 self.__data[p] !== null) {
363 self.__data[p] = List(self.__data[p], type, self);
364 }
365 }
366 }
367 }
368 this.trigger('initialize');
369};
370
371// Implementation of persistDefaultValues property
372function ignoresMatchedDefault(property) {
373 if (property && property.persistDefaultValues === false) {
374 return true;
375 }
376}
377
378// Helper function for determing the applyDefaultOnWrites value of a property
379function appliesDefaultsOnWrites(property) {
380 if (property && ('applyDefaultOnWrites' in property)) {
381 return property.applyDefaultOnWrites;
382 }
383 return true;
384}
385
386/**
387 * Define a property on the model.
388 * @param {String} prop Property name
389 * @param {Object} params Various property configuration
390 */
391ModelBaseClass.defineProperty = function(prop, params) {
392 if (this.dataSource) {
393 this.dataSource.defineProperty(this.modelName, prop, params);
394 } else {
395 this.modelBuilder.defineProperty(this.modelName, prop, params);
396 }
397};
398
399/**
400 * Get model property type.
401 * @param {String} propName Property name
402 * @returns {String} Name of property type
403 */
404ModelBaseClass.getPropertyType = function(propName) {
405 const prop = this.definition.properties[propName];
406 if (!prop) {
407 // The property is not part of the definition
408 return null;
409 }
410 if (!prop.type) {
411 throw new Error(g.f('Type not defined for property %s.%s', this.modelName, propName));
412 // return null;
413 }
414 return prop.type.name;
415};
416
417/**
418 * Get model property type.
419 * @param {String} propName Property name
420 * @returns {String} Name of property type
421 */
422ModelBaseClass.prototype.getPropertyType = function(propName) {
423 return this.constructor.getPropertyType(propName);
424};
425
426/**
427 * Return string representation of class
428 * This overrides the default `toString()` method
429 */
430ModelBaseClass.toString = function() {
431 return '[Model ' + this.modelName + ']';
432};
433
434/**
435 * Convert model instance to a plain JSON object.
436 * Returns a canonical object representation (no getters and setters).
437 *
438 * @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.
439 * @param {Boolean} removeHidden Boolean flag as part of the transformation. If true, then hidden properties should not be brought out.
440 * @param {Boolean} removeProtected Boolean flag as part of the transformation. If true, then protected properties should not be brought out.
441 * @returns {Object} returns Plain JSON object
442 */
443ModelBaseClass.prototype.toObject = function(onlySchema, removeHidden, removeProtected) {
444 if (typeof onlySchema === 'object' && onlySchema != null) {
445 const options = onlySchema;
446 onlySchema = options.onlySchema;
447 removeHidden = options.removeHidden;
448 removeProtected = options.removeProtected;
449 }
450 if (onlySchema === undefined) {
451 onlySchema = true;
452 }
453 const data = {};
454 const self = this;
455 const Model = this.constructor;
456
457 // if it is already an Object
458 if (Model === Object) {
459 return self;
460 }
461
462 const strict = this.__strict;
463 const schemaLess = (strict === false) || !onlySchema;
464 const persistUndefinedAsNull = Model.definition.settings.persistUndefinedAsNull;
465
466 const props = Model.definition.properties;
467 let keys = Object.keys(props);
468 let propertyName, val;
469
470 for (let i = 0; i < keys.length; i++) {
471 propertyName = keys[i];
472 val = self[propertyName];
473
474 // Exclude functions
475 if (typeof val === 'function') {
476 continue;
477 }
478 // Exclude hidden properties
479 if (removeHidden && Model.isHiddenProperty(propertyName)) {
480 continue;
481 }
482
483 if (removeProtected && Model.isProtectedProperty(propertyName)) {
484 continue;
485 }
486
487 if (val instanceof List) {
488 data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
489 } else {
490 if (val !== undefined && val !== null && val.toObject) {
491 data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
492 } else {
493 if (val === undefined && persistUndefinedAsNull) {
494 val = null;
495 }
496 data[propertyName] = val;
497 }
498 }
499 }
500
501 if (schemaLess) {
502 // Find its own properties which can be set via myModel.myProperty = 'myValue'.
503 // If the property is not declared in the model definition, no setter will be
504 // triggered to add it to __data
505 keys = Object.keys(self);
506 let size = keys.length;
507 for (let i = 0; i < size; i++) {
508 propertyName = keys[i];
509 if (props[propertyName]) {
510 continue;
511 }
512 if (propertyName.indexOf('__') === 0) {
513 continue;
514 }
515 if (removeHidden && Model.isHiddenProperty(propertyName)) {
516 continue;
517 }
518 if (removeProtected && Model.isProtectedProperty(propertyName)) {
519 continue;
520 }
521 if (data[propertyName] !== undefined) {
522 continue;
523 }
524 val = self[propertyName];
525 if (val !== undefined) {
526 if (typeof val === 'function') {
527 continue;
528 }
529 if (val !== null && val.toObject) {
530 data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
531 } else {
532 data[propertyName] = val;
533 }
534 } else if (persistUndefinedAsNull) {
535 data[propertyName] = null;
536 }
537 }
538 // Now continue to check __data
539 keys = Object.keys(self.__data);
540 size = keys.length;
541 for (let i = 0; i < size; i++) {
542 propertyName = keys[i];
543 if (propertyName.indexOf('__') === 0) {
544 continue;
545 }
546 if (data[propertyName] === undefined) {
547 if (removeHidden && Model.isHiddenProperty(propertyName)) {
548 continue;
549 }
550 if (removeProtected && Model.isProtectedProperty(propertyName)) {
551 continue;
552 }
553 const ownVal = self[propertyName];
554 // The ownVal can be a relation function
555 val = (ownVal !== undefined && (typeof ownVal !== 'function')) ? ownVal : self.__data[propertyName];
556 if (typeof val === 'function') {
557 continue;
558 }
559
560 if (val !== undefined && val !== null && val.toObject) {
561 data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
562 } else if (val === undefined && persistUndefinedAsNull) {
563 data[propertyName] = null;
564 } else {
565 data[propertyName] = val;
566 }
567 }
568 }
569 }
570
571 return data;
572};
573
574/**
575 * Convert an array of strings into an object as the map
576 * @param {string[]} arr An array of strings
577 */
578function asObjectMap(arr) {
579 const obj = {};
580 if (Array.isArray(arr)) {
581 for (let i = 0; i < arr.length; i++) {
582 obj[arr[i]] = true;
583 }
584 return obj;
585 }
586 return arr || obj;
587}
588/**
589 * Checks if property is protected.
590 * @param {String} propertyName Property name
591 * @returns {Boolean} true or false if protected or not.
592 */
593ModelBaseClass.isProtectedProperty = function(propertyName) {
594 const settings = (this.definition && this.definition.settings) || {};
595 const protectedProperties = settings.protectedProperties || settings.protected;
596 settings.protectedProperties = asObjectMap(protectedProperties);
597 return settings.protectedProperties[propertyName];
598};
599
600/**
601 * Checks if property is hidden.
602 * @param {String} propertyName Property name
603 * @returns {Boolean} true or false if hidden or not.
604 */
605ModelBaseClass.isHiddenProperty = function(propertyName) {
606 const settings = (this.definition && this.definition.settings) || {};
607 const hiddenProperties = settings.hiddenProperties || settings.hidden;
608 settings.hiddenProperties = asObjectMap(hiddenProperties);
609 return settings.hiddenProperties[propertyName];
610};
611
612ModelBaseClass.prototype.toJSON = function() {
613 return this.toObject(false, true, false);
614};
615
616ModelBaseClass.prototype.fromObject = function(obj) {
617 for (const key in obj) {
618 this[key] = obj[key];
619 }
620};
621
622/**
623 * Reset dirty attributes.
624 * This method does not perform any database operations; it just resets the object to its
625 * initial state.
626 */
627ModelBaseClass.prototype.reset = function() {
628 const obj = this;
629 for (const k in obj) {
630 if (k !== 'id' && !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) {
631 delete obj[k];
632 }
633 }
634};
635
636// Node v0.11+ allows custom inspect functions to return an object
637// instead of string. That way options like `showHidden` and `colors`
638// can be preserved.
639const versionParts = process.versions && process.versions.node ?
640 process.versions.node.split(/\./g).map(function(v) { return +v; }) :
641 [1, 0, 0]; // browserify ships 1.0-compatible version of util.inspect
642
643const INSPECT_SUPPORTS_OBJECT_RETVAL =
644 versionParts[0] > 0 ||
645 versionParts[1] > 11 ||
646 (versionParts[0] === 11 && versionParts[1] >= 14);
647
648ModelBaseClass.prototype.inspect = function(depth) {
649 if (INSPECT_SUPPORTS_OBJECT_RETVAL)
650 return this.__data;
651
652 // Workaround for older versions
653 // See also https://github.com/joyent/node/commit/66280de133
654 return util.inspect(this.__data, {
655 showHidden: false,
656 depth: depth,
657 colors: false,
658 });
659};
660
661if (util.inspect.custom) {
662 // Node.js 12+ no longer recognizes "inspect" method,
663 // it uses "inspect.custom" symbol as the key instead
664 // TODO(semver-major) always use the symbol key only (requires Node.js 8+).
665 ModelBaseClass.prototype[util.inspect.custom] = ModelBaseClass.prototype.inspect;
666}
667
668/**
669 *
670 * @param {String} anotherClass could be string or class. Name of the class or the class itself
671 * @param {Object} options An object to control the instantiation
672 * @returns {ModelClass}
673 */
674ModelBaseClass.mixin = function(anotherClass, options) {
675 if (typeof anotherClass === 'string') {
676 this.modelBuilder.mixins.applyMixin(this, anotherClass, options);
677 } else {
678 if (anotherClass.prototype instanceof ModelBaseClass) {
679 const props = anotherClass.definition.properties;
680 for (const i in props) {
681 if (this.definition.properties[i]) {
682 continue;
683 }
684 this.defineProperty(i, props[i]);
685 }
686 }
687 return jutil.mixin(this, anotherClass, options);
688 }
689};
690
691ModelBaseClass.prototype.getDataSource = function() {
692 return this.__dataSource || this.constructor.dataSource;
693};
694
695ModelBaseClass.getDataSource = function() {
696 return this.dataSource;
697};
698
699ModelBaseClass.prototype.setStrict = function(strict) {
700 this.__strict = strict;
701};
702
703/**
704 *
705 * `getMergePolicy()` provides model merge policies to apply when extending
706 * a child model from a base model. Such a policy drives the way parent/child model
707 * properties/settings are merged/mixed-in together.
708 *
709 * Below is presented the expected merge behaviour for each option.
710 * NOTE: This applies to top-level settings properties
711 *
712 *
713 * - Any
714 * - `{replace: true}` (default): child replaces the value from parent
715 * - assignin `null` on child setting deletes the inherited setting
716 *
717 * - Arrays:
718 * - `{replace: false}`: unique elements of parent and child cumulate
719 * - `{rank: true}` adds the model inheritance rank to array
720 * elements of type Object {} as internal property `__rank`
721 *
722 * - Object {}:
723 * - `{replace: false}`: deep merges parent and child objects
724 * - `{patch: true}`: child replaces inner properties from parent
725 *
726 *
727 * The recommended built-in merge policy is as follows. It is returned by getMergePolicy()
728 * when calling the method with option `{configureModelMerge: true}`.
729 *
730 * ```
731 * {
732 * description: {replace: true}, // string or array
733 * options: {patch: true}, // object
734 * hidden: {replace: false}, // array
735 * protected: {replace: false}, // array
736 * indexes: {patch: true}, // object
737 * methods: {patch: true}, // object
738 * mixins: {patch: true}, // object
739 * relations: {patch: true}, // object
740 * scope: {replace: true}, // object
741 * scopes: {patch: true}, // object
742 * acls: {rank: true}, // array
743 * // this setting controls which child model property's value allows deleting
744 * // a base model's property
745 * __delete: null,
746 * // this setting controls the default merge behaviour for settings not defined
747 * // in the mergePolicy specification
748 * __default: {replace: true},
749 * }
750 * ```
751 *
752 * The legacy built-in merge policy is as follows, it is retuned by `getMergePolicy()`
753 * when avoiding option `configureModelMerge`.
754 * NOTE: it also provides the ACLs ranking in addition to the legacy behaviour, as well
755 * as fixes for settings 'description' and 'relations': matching relations from child
756 * replace relations from parents.
757 *
758 * ```
759 * {
760 * description: {replace: true}, // string or array
761 * properties: {patch: true}, // object
762 * hidden: {replace: false}, // array
763 * protected: {replace: false}, // array
764 * relations: {acls: true}, // object
765 * acls: {rank: true}, // array
766 * }
767 * ```
768 *
769 *
770 * `getMergePolicy()` can be customized using model's setting `configureModelMerge` as follows:
771 *
772 * ``` json
773 * {
774 * // ..
775 * options: {
776 * configureModelMerge: {
777 * // merge options
778 * }
779 * }
780 * // ..
781 * }
782 * ```
783 *
784 * NOTE: mergePolicy parameter can also defined at JSON model definition root
785 *
786 * `getMergePolicy()` method can also be extended programmatically as follows:
787 *
788 * ``` js
789 * myModel.getMergePolicy = function(options) {
790 * const origin = myModel.base.getMergePolicy(options);
791 * return Object.assign({}, origin, {
792 * // new/overriding options
793 * });
794 * };
795 * ```
796 *
797 * @param {Object} options option `configureModelMerge` can be used to alter the
798 * returned merge policy:
799 * - `configureModelMerge: true` will have the method return the recommended merge policy.
800 * - `configureModelMerge: {..}` will actually have the method return the provided object.
801 * - not providing this options will have the method return a merge policy emulating the
802 * the model merge behaviour up to datasource-juggler v3.6.1, as well as the ACLs ranking.
803 * @returns {Object} mergePolicy The model merge policy to apply when using the
804 * current model as base class for a child model
805 */
806ModelBaseClass.getMergePolicy = function(options) {
807 // NOTE: merge policy equivalent to datasource-juggler behaviour up to v3.6.1
808 // + fix for description arrays that should not be merged
809 // + fix for relations that should patch matching relations
810 // + ranking of ACLs
811 let mergePolicy = {
812 description: {replace: true}, // string or array
813 properties: {patch: true}, // object
814 hidden: {replace: false}, // array
815 protected: {replace: false}, // array
816 relations: {patch: true}, // object
817 acls: {rank: true}, // array
818 };
819
820 const config = (options || {}).configureModelMerge;
821
822 if (config === true) {
823 // NOTE: recommended merge policy from datasource-juggler v3.6.2
824 mergePolicy = {
825 description: {replace: true}, // string or array
826 options: {patch: true}, // object
827 // properties: {patch: true}, // object // NOTE: not part of configurable merge
828 hidden: {replace: false}, // array
829 protected: {replace: false}, // array
830 indexes: {patch: true}, // object
831 methods: {patch: true}, // object
832 mixins: {patch: true}, // object
833 // validations: {patch: true}, // object // NOTE: not implemented
834 relations: {patch: true}, // object
835 scope: {replace: true}, // object
836 scopes: {patch: true}, // object
837 acls: {rank: true}, // array
838 // this option controls which value assigned on child model allows deleting
839 // a base model's setting
840 __delete: null,
841 // this option controls the default merge behaviour for settings not defined
842 // in the mergePolicy specification
843 __default: {replace: true},
844 };
845 }
846
847 // override mergePolicy with provided model setting if required
848 if (config && typeof config === 'object' && !Array.isArray(config)) {
849 // config is an object
850 mergePolicy = config;
851 }
852
853 return mergePolicy;
854};
855
856/**
857 * Gets properties defined with 'updateOnly' flag set to true from the model. This flag is also set to true
858 * internally for the id property, if this property is generated and IdInjection is true.
859 * @returns {updateOnlyProps} List of properties with updateOnly set to true.
860 */
861
862ModelBaseClass.getUpdateOnlyProperties = function() {
863 const props = this.definition.properties;
864 return Object.keys(props).filter(key => props[key].updateOnly);
865};
866
867// Mix in utils
868jutil.mixin(ModelBaseClass, DataAccessUtils);
869
870// Mixin observer
871jutil.mixin(ModelBaseClass, Observer);
872
873jutil.mixin(ModelBaseClass, Hookable);
874jutil.mixin(ModelBaseClass, validations.Validatable);