UNPKG

26.6 kBJavaScriptView Raw
1// Copyright IBM Corp. 2012,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'use strict';
7
8exports.safeRequire = safeRequire;
9exports.fieldsToArray = fieldsToArray;
10exports.selectFields = selectFields;
11exports.sanitizeQuery = sanitizeQuery;
12exports.parseSettings = parseSettings;
13exports.mergeSettings = exports.deepMerge = deepMerge;
14exports.deepMergeProperty = deepMergeProperty;
15exports.isPlainObject = isPlainObject;
16exports.defineCachedRelations = defineCachedRelations;
17exports.sortObjectsByIds = sortObjectsByIds;
18exports.setScopeValuesFromWhere = setScopeValuesFromWhere;
19exports.mergeQuery = mergeQuery;
20exports.mergeIncludes = mergeIncludes;
21exports.createPromiseCallback = createPromiseCallback;
22exports.uniq = uniq;
23exports.toRegExp = toRegExp;
24exports.hasRegExpFlags = hasRegExpFlags;
25exports.idEquals = idEquals;
26exports.findIndexOf = findIndexOf;
27exports.collectTargetIds = collectTargetIds;
28exports.idName = idName;
29exports.rankArrayElements = rankArrayElements;
30exports.idsHaveDuplicates = idsHaveDuplicates;
31exports.isClass = isClass;
32exports.escapeRegExp = escapeRegExp;
33exports.applyParentProperty = applyParentProperty;
34
35const g = require('strong-globalize')();
36const traverse = require('traverse');
37const assert = require('assert');
38const debug = require('debug')('loopback:juggler:utils');
39
40/**
41 * The name of the property in modelBuilder settings that will enable the child parent reference functionality
42 * @type {string}
43 */
44const BUILDER_PARENT_SETTING = 'parentRef';
45
46/**
47 * The property name that should be defined on each child instance if parent feature flag enabled
48 * @type {string}
49 */
50const PARENT_PROPERTY_NAME = '__parent';
51
52function safeRequire(module) {
53 try {
54 return require(module);
55 } catch (e) {
56 g.log('Run "{{npm install loopback-datasource-juggler}} %s" command ',
57 'to use {{loopback-datasource-juggler}} using %s database engine',
58 module, module);
59 process.exit(1);
60 }
61}
62
63/*
64 * Extracting fixed property values for the scope from the where clause into
65 * the data object
66 *
67 * @param {Object} The data object
68 * @param {Object} The where clause
69 */
70function setScopeValuesFromWhere(data, where, targetModel) {
71 for (const i in where) {
72 if (i === 'and') {
73 // Find fixed property values from each subclauses
74 for (let w = 0, n = where[i].length; w < n; w++) {
75 setScopeValuesFromWhere(data, where[i][w], targetModel);
76 }
77 continue;
78 }
79 const prop = targetModel.definition.properties[i];
80 if (prop) {
81 const val = where[i];
82 if (typeof val !== 'object' || val instanceof prop.type ||
83 prop.type.name === 'ObjectID' || // MongoDB key
84 prop.type.name === 'uuidFromString') { // C*
85 // Only pick the {propertyName: propertyValue}
86 data[i] = where[i];
87 }
88 }
89 }
90}
91
92/**
93 * Merge include options of default scope with runtime include option.
94 * exhibits the _.extend behaviour. Property value of source overrides
95 * property value of destination if property name collision occurs
96 * @param {String|Array|Object} destination The default value of `include` option
97 * @param {String|Array|Object} source The runtime value of `include` option
98 * @returns {Object}
99 */
100function mergeIncludes(destination, source) {
101 const destArray = convertToArray(destination);
102 const sourceArray = convertToArray(source);
103 if (destArray.length === 0) {
104 return sourceArray;
105 }
106 if (sourceArray.length === 0) {
107 return destArray;
108 }
109 const relationNames = [];
110 const resultArray = [];
111 for (const j in sourceArray) {
112 const sourceEntry = sourceArray[j];
113 const sourceEntryRelationName = (typeof (sourceEntry.rel || sourceEntry.relation) === 'string') ?
114 sourceEntry.relation : Object.keys(sourceEntry)[0];
115 relationNames.push(sourceEntryRelationName);
116 resultArray.push(sourceEntry);
117 }
118 for (const i in destArray) {
119 const destEntry = destArray[i];
120 const destEntryRelationName = (typeof (destEntry.rel || destEntry.relation) === 'string') ?
121 destEntry.relation : Object.keys(destEntry)[0];
122 if (relationNames.indexOf(destEntryRelationName) === -1) {
123 resultArray.push(destEntry);
124 }
125 }
126 return resultArray;
127}
128
129/**
130 * Converts input parameter into array of objects which wraps the value.
131 * "someValue" is converted to [{"someValue":true}]
132 * ["someValue"] is converted to [{"someValue":true}]
133 * {"someValue":true} is converted to [{"someValue":true}]
134 * @param {String|Array|Object} param - Input parameter to be converted
135 * @returns {Array}
136 */
137function convertToArray(include) {
138 if (typeof include === 'string') {
139 const obj = {};
140 obj[include] = true;
141 return [obj];
142 } else if (isPlainObject(include)) {
143 // if include is of the form - {relation:'',scope:''}
144 if (include.rel || include.relation) {
145 return [include];
146 }
147 // Build an array of key/value pairs
148 const newInclude = [];
149 for (const key in include) {
150 const obj = {};
151 obj[key] = include[key];
152 newInclude.push(obj);
153 }
154 return newInclude;
155 } else if (Array.isArray(include)) {
156 const normalized = [];
157 for (const i in include) {
158 const includeEntry = include[i];
159 if (typeof includeEntry === 'string') {
160 const obj = {};
161 obj[includeEntry] = true;
162 normalized.push(obj);
163 } else {
164 normalized.push(includeEntry);
165 }
166 }
167 return normalized;
168 }
169 return [];
170}
171
172/*!
173 * Merge query parameters
174 * @param {Object} base The base object to contain the merged results
175 * @param {Object} update The object containing updates to be merged
176 * @param {Object} spec Optionally specifies parameters to exclude (set to false)
177 * @returns {*|Object} The base object
178 * @private
179 */
180function mergeQuery(base, update, spec) {
181 if (!update) {
182 return;
183 }
184 spec = spec || {};
185 base = base || {};
186
187 if (update.where && Object.keys(update.where).length > 0) {
188 if (base.where && Object.keys(base.where).length > 0) {
189 base.where = {and: [base.where, update.where]};
190 } else {
191 base.where = update.where;
192 }
193 }
194
195 // Merge inclusion
196 if (spec.include !== false && update.include) {
197 if (!base.include) {
198 base.include = update.include;
199 } else {
200 if (spec.nestedInclude === true) {
201 // specify nestedInclude=true to force nesting of inclusions on scoped
202 // queries. e.g. In physician.patients.find({include: 'address'}),
203 // inclusion should be on patient model, not on physician model.
204 const saved = base.include;
205 base.include = {};
206 base.include[update.include] = saved;
207 } else {
208 // default behaviour of inclusion merge - merge inclusions at the same
209 // level. - https://github.com/strongloop/loopback-datasource-juggler/pull/569#issuecomment-95310874
210 base.include = mergeIncludes(base.include, update.include);
211 }
212 }
213 }
214
215 if (spec.collect !== false && update.collect) {
216 base.collect = update.collect;
217 }
218
219 // Overwrite fields
220 if (spec.fields !== false && update.fields !== undefined) {
221 base.fields = update.fields;
222 } else if (update.fields !== undefined) {
223 base.fields = [].concat(base.fields).concat(update.fields);
224 }
225
226 // set order
227 if ((!base.order || spec.order === false) && update.order) {
228 base.order = update.order;
229 }
230
231 // overwrite pagination
232 if (spec.limit !== false && update.limit !== undefined) {
233 base.limit = update.limit;
234 }
235
236 const skip = spec.skip !== false && spec.offset !== false;
237
238 if (skip && update.skip !== undefined) {
239 base.skip = update.skip;
240 }
241
242 if (skip && update.offset !== undefined) {
243 base.offset = update.offset;
244 }
245
246 return base;
247}
248
249/**
250 * Normalize fields to an array of included properties
251 * @param {String|String[]|Object} fields Fields filter
252 * @param {String[]} properties Property names
253 * @param {Boolean} excludeUnknown To exclude fields that are unknown properties
254 * @returns {String[]} An array of included property names
255 */
256function fieldsToArray(fields, properties, excludeUnknown) {
257 if (!fields) return;
258
259 // include all properties by default
260 let result = properties;
261 let i, n;
262
263 if (typeof fields === 'string') {
264 result = [fields];
265 } else if (Array.isArray(fields) && fields.length > 0) {
266 // No empty array, including all the fields
267 result = fields;
268 } else if ('object' === typeof fields) {
269 // { field1: boolean, field2: boolean ... }
270 const included = [];
271 const excluded = [];
272 const keys = Object.keys(fields);
273 if (!keys.length) return;
274
275 for (i = 0, n = keys.length; i < n; i++) {
276 const k = keys[i];
277 if (fields[k]) {
278 included.push(k);
279 } else if ((k in fields) && !fields[k]) {
280 excluded.push(k);
281 }
282 }
283 if (included.length > 0) {
284 result = included;
285 } else if (excluded.length > 0) {
286 for (i = 0, n = excluded.length; i < n; i++) {
287 const index = result.indexOf(excluded[i]);
288 if (index !== -1) result.splice(index, 1); // only when existing field excluded
289 }
290 }
291 }
292
293 let fieldArray = [];
294 if (excludeUnknown) {
295 for (i = 0, n = result.length; i < n; i++) {
296 if (properties.indexOf(result[i]) !== -1) {
297 fieldArray.push(result[i]);
298 }
299 }
300 } else {
301 fieldArray = result;
302 }
303 return fieldArray;
304}
305
306function selectFields(fields) {
307 // map function
308 return function(obj) {
309 const result = {};
310 let key;
311
312 for (let i = 0; i < fields.length; i++) {
313 key = fields[i];
314
315 result[key] = obj[key];
316 }
317 return result;
318 };
319}
320
321function isProhibited(key, prohibitedKeys) {
322 if (!prohibitedKeys || !prohibitedKeys.length) return false;
323 if (typeof key !== 'string') {
324 return false;
325 }
326 for (const k of prohibitedKeys) {
327 if (k === key) return true;
328 // x.secret, secret.y, or x.secret.y
329 if (key.split('.').indexOf(k) !== -1) return true;
330 }
331 return false;
332}
333
334/**
335 * Accept an operator key and return whether it is used for a regular expression query or not
336 * @param {string} operator
337 * @returns {boolean}
338 */
339function isRegExpOperator(operator) {
340 return ['like', 'nlike', 'ilike', 'nilike', 'regexp'].includes(operator);
341}
342
343/**
344 * Accept a RegExp string and make sure that any special characters for RegExp are escaped in case they
345 * create an invalid Regexp
346 * @param {string} str
347 * @returns {string}
348 */
349function escapeRegExp(str) {
350 assert.strictEqual(typeof str, 'string', 'String required for regexp escaping');
351 try {
352 new RegExp(str); // try to parse string as regexp
353 return str;
354 } catch (unused) {
355 console.warn(
356 'Auto-escaping invalid RegExp value %j supplied by the caller. ' +
357 'Please note this behavior may change in the future.',
358 str,
359 );
360 return str.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, '\\$&');
361 }
362}
363
364/**
365 * Sanitize the query object
366 * @param query {object} The query object
367 * @param options
368 * @property normalizeUndefinedInQuery {String} either "nullify", "throw" or "ignore" (default: "ignore")
369 * @property prohibitedKeys {String[]} An array of prohibited keys to be removed
370 * @returns {*}
371 */
372function sanitizeQuery(query, options) {
373 debug('Sanitizing query object: %j', query);
374 if (typeof query !== 'object' || query === null) {
375 return query;
376 }
377 options = options || {};
378 if (typeof options === 'string') {
379 // Keep it backward compatible
380 options = {normalizeUndefinedInQuery: options};
381 }
382 const prohibitedKeys = options.prohibitedKeys;
383 const offendingKeys = [];
384 const normalizeUndefinedInQuery = options.normalizeUndefinedInQuery;
385 const maxDepth = options.maxDepth || Number.MAX_SAFE_INTEGER;
386 // WARNING: [rfeng] Use map() will cause mongodb to produce invalid BSON
387 // as traverse doesn't transform the ObjectId correctly
388 const result = traverse(query).forEach(function(x) {
389 /**
390 * Security risk if the client passes in a very deep where object
391 */
392 if (this.circular) {
393 const msg = g.f('The query object is circular');
394 const err = new Error(msg);
395 err.statusCode = 400;
396 err.code = 'QUERY_OBJECT_IS_CIRCULAR';
397 throw err;
398 }
399 if (this.level > maxDepth) {
400 const msg = g.f('The query object exceeds maximum depth %d', maxDepth);
401 const err = new Error(msg);
402 err.statusCode = 400;
403 err.code = 'QUERY_OBJECT_TOO_DEEP';
404 throw err;
405 }
406 /**
407 * Make sure prohibited keys are removed from the query to prevent
408 * sensitive values from being guessed
409 */
410 if (isProhibited(this.key, prohibitedKeys)) {
411 offendingKeys.push(this.key);
412 this.remove();
413 return;
414 }
415
416 /**
417 * Handle undefined values
418 */
419 if (x === undefined) {
420 switch (normalizeUndefinedInQuery) {
421 case 'nullify':
422 this.update(null);
423 break;
424 case 'throw':
425 throw new Error(g.f('Unexpected `undefined` in query'));
426 case 'ignore':
427 default:
428 this.remove();
429 }
430 }
431
432 if (!Array.isArray(x) && (typeof x === 'object' && x !== null &&
433 x.constructor !== Object)) {
434 // This object is not a plain object
435 this.update(x, true); // Stop navigating into this object
436 return x;
437 }
438
439 if (isRegExpOperator(this.key) && typeof x === 'string') { // we have regexp supporting operator and string to escape
440 return escapeRegExp(x);
441 }
442
443 return x;
444 });
445
446 if (offendingKeys.length) {
447 console.error(
448 g.f(
449 'Potential security alert: hidden/protected properties %j are used in query.',
450 offendingKeys,
451 ),
452 );
453 }
454 return result;
455}
456
457const url = require('url');
458const qs = require('qs');
459
460/**
461 * Parse a URL into a settings object
462 * @param {String} urlStr The URL for connector settings
463 * @returns {Object} The settings object
464 */
465function parseSettings(urlStr) {
466 if (!urlStr) {
467 return {};
468 }
469 const uri = url.parse(urlStr, false);
470 const settings = {};
471 settings.connector = uri.protocol && uri.protocol.split(':')[0]; // Remove the trailing :
472 settings.host = settings.hostname = uri.hostname;
473 settings.port = uri.port && Number(uri.port); // port is a string
474 settings.user = settings.username = uri.auth && uri.auth.split(':')[0]; // <username>:<password>
475 settings.password = uri.auth && uri.auth.split(':')[1];
476 settings.database = uri.pathname && uri.pathname.split('/')[1]; // remove the leading /
477 settings.url = urlStr;
478 if (uri.query) {
479 const params = qs.parse(uri.query);
480 for (const p in params) {
481 settings[p] = params[p];
482 }
483 }
484 return settings;
485}
486
487/**
488 * Objects deep merge
489 *
490 * Forked from https://github.com/nrf110/deepmerge/blob/master/index.js
491 *
492 * The original function tries to merge array items if they are objects, this
493 * was changed to always push new items in arrays, independently of their type.
494 *
495 * NOTE: The function operates as a deep clone when called with a single object
496 * argument.
497 *
498 * @param {Object} base The base object
499 * @param {Object} extras The object to merge with base
500 * @returns {Object} The merged object
501 */
502function deepMerge(base, extras) {
503 // deepMerge allows undefined extras to allow deep cloning of arrays
504 const array = Array.isArray(base) && (Array.isArray(extras) || !extras);
505 let dst = array && [] || {};
506
507 if (array) {
508 // extras or base is an array
509 extras = extras || [];
510 // Add items from base into dst
511 dst = dst.concat(base);
512 // Add non-existent items from extras into dst
513 extras.forEach(function(e) {
514 if (dst.indexOf(e) === -1) {
515 dst.push(e);
516 }
517 });
518 } else {
519 if (base != null && typeof base === 'object') {
520 // Add properties from base to dst
521 Object.keys(base).forEach(function(key) {
522 if (base[key] && typeof base[key] === 'object') {
523 // call deepMerge on nested object to operate a deep clone
524 dst[key] = deepMerge(base[key]);
525 } else {
526 dst[key] = base[key];
527 }
528 });
529 }
530 if (extras != null && typeof extras === 'object') {
531 // extras is an object {}
532 Object.keys(extras).forEach(function(key) {
533 const extra = extras[key];
534 if (extra == null || typeof extra !== 'object') {
535 // extra item value is null, undefined or not an object
536 dst[key] = extra;
537 } else {
538 // The extra item value is an object
539 if (base == null || typeof base !== 'object' ||
540 base[key] == null) {
541 // base is not an object or base item value is undefined or null
542 dst[key] = extra;
543 } else {
544 // call deepMerge on nested object
545 dst[key] = deepMerge(base[key], extra);
546 }
547 }
548 });
549 }
550 }
551
552 return dst;
553}
554
555/**
556 * Properties deep merge
557 * Similar as deepMerge but also works on single properties of any type
558 *
559 * @param {Object} base The base property
560 * @param {Object} extras The property to merge with base
561 * @returns {Object} The merged property
562 */
563function deepMergeProperty(base, extras) {
564 const mergedObject = deepMerge({key: base}, {key: extras});
565 const mergedProperty = mergedObject.key;
566 return mergedProperty;
567}
568
569const numberIsFinite = Number.isFinite || function(value) {
570 return typeof value === 'number' && isFinite(value);
571};
572
573/**
574 * Adds a property __rank to array elements of type object {}
575 * If an inner element already has the __rank property it is not altered
576 * NOTE: the function mutates the provided array
577 *
578 * @param array The original array
579 * @param rank The rank to apply to array elements
580 * @return rankedArray The original array with newly ranked elements
581 */
582function rankArrayElements(array, rank) {
583 if (!Array.isArray(array) || !numberIsFinite(rank))
584 return array;
585
586 array.forEach(function(el) {
587 // only apply ranking on objects {} in array
588 if (!el || typeof el != 'object' || Array.isArray(el))
589 return;
590
591 // property rank is already defined for array element
592 if (el.__rank)
593 return;
594
595 // define rank property as non-enumerable and read-only
596 Object.defineProperty(el, '__rank', {
597 writable: false,
598 enumerable: false,
599 configurable: false,
600 value: rank,
601 });
602 });
603 return array;
604}
605
606/**
607 * Define an non-enumerable __cachedRelations property
608 * @param {Object} obj The obj to receive the __cachedRelations
609 */
610function defineCachedRelations(obj) {
611 if (!obj.__cachedRelations) {
612 Object.defineProperty(obj, '__cachedRelations', {
613 writable: true,
614 enumerable: false,
615 configurable: true,
616 value: {},
617 });
618 }
619}
620
621/**
622 * Check if the argument is plain object
623 * @param {*} obj The obj value
624 * @returns {boolean}
625 */
626function isPlainObject(obj) {
627 return (typeof obj === 'object') && (obj !== null) &&
628 (obj.constructor === Object);
629}
630
631function sortObjectsByIds(idName, ids, objects, strict) {
632 ids = ids.map(function(id) {
633 return (typeof id === 'object') ? String(id) : id;
634 });
635
636 const indexOf = function(x) {
637 const isObj = (typeof x[idName] === 'object'); // ObjectID
638 const id = isObj ? String(x[idName]) : x[idName];
639 return ids.indexOf(id);
640 };
641
642 const heading = [];
643 const tailing = [];
644
645 objects.forEach(function(x) {
646 if (typeof x === 'object') {
647 const idx = indexOf(x);
648 if (strict && idx === -1) return;
649 idx === -1 ? tailing.push(x) : heading.push(x);
650 }
651 });
652
653 heading.sort(function(x, y) {
654 const a = indexOf(x);
655 const b = indexOf(y);
656 if (a === -1 || b === -1) return 1; // last
657 if (a === b) return 0;
658 if (a > b) return 1;
659 if (a < b) return -1;
660 });
661
662 return heading.concat(tailing);
663}
664
665function createPromiseCallback() {
666 let cb;
667 const promise = new Promise(function(resolve, reject) {
668 cb = function(err, data) {
669 if (err) return reject(err);
670 return resolve(data);
671 };
672 });
673 cb.promise = promise;
674 return cb;
675}
676
677function isBsonType(value) {
678 // bson@1.x stores _bsontype on ObjectID instance, bson@4.x on prototype
679 return value.hasOwnProperty('_bsontype') ||
680 value.constructor.prototype.hasOwnProperty('_bsontype');
681}
682
683/**
684 * Dedupe an array
685 * @param {Array} an array
686 * @returns {Array} an array with unique items
687 */
688function uniq(a) {
689 const uniqArray = [];
690 if (!a) {
691 return uniqArray;
692 }
693 assert(Array.isArray(a), 'array argument is required');
694 const comparableA = a.map(
695 item => isBsonType(item) ? item.toString() : item,
696 );
697 for (let i = 0, n = comparableA.length; i < n; i++) {
698 if (comparableA.indexOf(comparableA[i]) === i) {
699 uniqArray.push(a[i]);
700 }
701 }
702 return uniqArray;
703}
704
705/**
706 * Converts a string, regex literal, or a RegExp object to a RegExp object.
707 * @param {String|Object} The string, regex literal, or RegExp object to convert
708 * @returns {Object} A RegExp object
709 */
710function toRegExp(regex) {
711 const isString = typeof regex === 'string';
712 const isRegExp = regex instanceof RegExp;
713
714 if (!(isString || isRegExp))
715 return new Error(g.f('Invalid argument, must be a string, {{regex}} literal, or ' +
716 '{{RegExp}} object'));
717
718 if (isRegExp)
719 return regex;
720
721 if (!hasRegExpFlags(regex))
722 return new RegExp(regex);
723
724 // only accept i, g, or m as valid regex flags
725 const flags = regex.split('/').pop().split('');
726 const validFlags = ['i', 'g', 'm'];
727 const invalidFlags = [];
728 flags.forEach(function(flag) {
729 if (validFlags.indexOf(flag) === -1)
730 invalidFlags.push(flag);
731 });
732
733 const hasInvalidFlags = invalidFlags.length > 0;
734 if (hasInvalidFlags)
735 return new Error(g.f('Invalid {{regex}} flags: %s', invalidFlags));
736
737 // strip regex delimiter forward slashes
738 const expression = regex.substr(1, regex.lastIndexOf('/') - 1);
739 return new RegExp(expression, flags.join(''));
740}
741
742function hasRegExpFlags(regex) {
743 return regex instanceof RegExp ?
744 regex.toString().split('/').pop() :
745 !!regex.match(/.*\/.+$/);
746}
747
748// Compare two id values to decide if updateAttributes is trying to change
749// the id value for a given instance
750function idEquals(id1, id2) {
751 if (id1 === id2) {
752 return true;
753 }
754 // Allows number/string conversions
755 if ((typeof id1 === 'number' && typeof id2 === 'string') ||
756 (typeof id1 === 'string' && typeof id2 === 'number')) {
757 return id1 == id2;
758 }
759 // For complex id types such as MongoDB ObjectID
760 id1 = JSON.stringify(id1);
761 id2 = JSON.stringify(id2);
762 if (id1 === id2) {
763 return true;
764 }
765
766 return false;
767}
768
769// Defaults to native Array.prototype.indexOf when no idEqual is present
770// Otherwise, returns the lowest index for which isEqual(arr[]index, target) is true
771function findIndexOf(arr, target, isEqual) {
772 if (!isEqual) {
773 return arr.indexOf(target);
774 }
775
776 for (let i = 0; i < arr.length; i++) {
777 if (isEqual(arr[i], target)) { return i; }
778 }
779
780 return -1;
781}
782
783/**
784 * Returns an object that queries targetIds.
785 * @param {Array} The array of targetData
786 * @param {String} The Id property name of target model
787 * @returns {Object} The object that queries targetIds
788 */
789function collectTargetIds(targetData, idPropertyName) {
790 const targetIds = [];
791 for (let i = 0; i < targetData.length; i++) {
792 const targetId = targetData[i][idPropertyName];
793 targetIds.push(targetId);
794 }
795 const IdQuery = {
796 inq: uniq(targetIds),
797 };
798 return IdQuery;
799}
800
801/**
802 * Find the idKey of a Model.
803 * @param {ModelConstructor} m - Model Constructor
804 * @returns {String}
805 */
806function idName(m) {
807 return m.definition.idName() || 'id';
808}
809
810/**
811 * Check a list of IDs to see if there are any duplicates.
812 *
813 * @param {Array} The array of IDs to check
814 * @returns {boolean} If any duplicates were found
815 */
816function idsHaveDuplicates(ids) {
817 // use Set if available and all ids are of string or number type
818 let hasDuplicates = undefined;
819 let i, j;
820 if (typeof Set === 'function') {
821 const uniqueIds = new Set();
822 for (i = 0; i < ids.length; ++i) {
823 const idType = typeof ids[i];
824 if (idType === 'string' || idType === 'number') {
825 if (uniqueIds.has(ids[i])) {
826 hasDuplicates = true;
827 break;
828 } else {
829 uniqueIds.add(ids[i]);
830 }
831 } else {
832 // ids are not all string/number that can be checked via Set, stop and do the slow test
833 break;
834 }
835 }
836 if (hasDuplicates === undefined && uniqueIds.size === ids.length) {
837 hasDuplicates = false;
838 }
839 }
840 if (hasDuplicates === undefined) {
841 // fast check was inconclusive or unavailable, do the slow check
842 // can still optimize this by doing 1/2 N^2 instead of the full N^2
843 for (i = 0; i < ids.length && hasDuplicates === undefined; ++i) {
844 for (j = 0; j < i; ++j) {
845 if (idEquals(ids[i], ids[j])) {
846 hasDuplicates = true;
847 break;
848 }
849 }
850 }
851 }
852 return hasDuplicates === true;
853}
854
855function isClass(fn) {
856 return fn && fn.toString().startsWith('class ');
857}
858
859/**
860 * Accept an element, and attach the __parent property to it, unless no object given, while also
861 * making sure to check for already created properties
862 *
863 * @param {object} element
864 * @param {Model} parent
865 */
866function applyParentProperty(element, parent) {
867 assert.strictEqual(typeof element, 'object', 'Non object element given to assign parent');
868 const {constructor: {modelBuilder: {settings: builderSettings} = {}} = {}} = element;
869 if (!builderSettings || !builderSettings[BUILDER_PARENT_SETTING]) {
870 // parentRef flag not enabled on ModelBuilder settings
871 return;
872 }
873
874 if (element.hasOwnProperty(PARENT_PROPERTY_NAME)) {
875 // property already created on model, just assign
876 const existingParent = element[PARENT_PROPERTY_NAME];
877 if (existingParent && existingParent !== parent) {
878 // parent re-assigned (child model assigned to other model instance)
879 g.warn('Re-assigning child model instance to another parent than the original!\n' +
880 'Although supported, this is not a recommended practice: ' +
881 `${element.constructor.name} -> ${parent.constructor.name}\n` +
882 'You should create an independent copy of the child model using `new Model(CHILD)` OR ' +
883 '`new Model(CHILD.toJSON())` and assign to new parent');
884 }
885 element[PARENT_PROPERTY_NAME] = parent;
886 } else {
887 // first time defining the property on the element
888 Object.defineProperty(element, PARENT_PROPERTY_NAME, {
889 value: parent,
890 writable: true,
891 enumerable: false,
892 configurable: false,
893 });
894 }
895}