@isrd-isi-edu/ermrestjs 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/core.js +6 -5
- package/js/parser.js +6 -0
- package/js/utils/helpers.js +4 -2
- package/js/utils/pseudocolumn_helpers.js +21 -28
- package/package.json +15 -14
- package/src/models/errors.ts +3 -3
- package/src/models/reference-column/facet-column.ts +8 -3
- package/src/services/error.ts +1 -0
- package/src/services/handlebars.ts +3 -0
- package/src/utils/markdown-utils.ts +1 -2
- package/src/utils/value-utils.ts +28 -1
package/js/core.js
CHANGED
|
@@ -22,7 +22,7 @@ import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
|
|
|
22
22
|
|
|
23
23
|
// utils
|
|
24
24
|
import { isObjectAndNotNull, isEmptyArray, isStringAndNotEmpty, isValidColorRGBHex } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
25
|
-
import { fixedEncodeURIComponent, escapeHTML } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
|
|
25
|
+
import { fixedEncodeURIComponent, escapeHTML, updateObject } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
|
|
26
26
|
import {
|
|
27
27
|
_annotations,
|
|
28
28
|
_commentDisplayModes,
|
|
@@ -3238,11 +3238,12 @@ import {
|
|
|
3238
3238
|
var defaultAnnotKey = _annotations.COLUMN_DEFAULTS;
|
|
3239
3239
|
var ancestors = [this.table.schema.catalog, this.table.schema, this.table];
|
|
3240
3240
|
// frist copy the by_type annots
|
|
3241
|
+
// frist copy the by_type annots
|
|
3241
3242
|
ancestors.forEach(function (el) {
|
|
3242
3243
|
if (el.annotations.contains(defaultAnnotKey)) {
|
|
3243
3244
|
var tempAnnot = el.annotations.get(defaultAnnotKey).content;
|
|
3244
3245
|
if (isObjectAndNotNull(tempAnnot) && isObjectAndNotNull(tempAnnot.by_type) && isObjectAndNotNull(tempAnnot.by_type[jsonColumn.type.typename])) {
|
|
3245
|
-
|
|
3246
|
+
updateObject(annots, tempAnnot.by_type[jsonColumn.type.typename]);
|
|
3246
3247
|
}
|
|
3247
3248
|
}
|
|
3248
3249
|
});
|
|
@@ -3251,7 +3252,7 @@ import {
|
|
|
3251
3252
|
if (el.annotations.contains(defaultAnnotKey)) {
|
|
3252
3253
|
var tempAnnot = el.annotations.get(defaultAnnotKey).content;
|
|
3253
3254
|
if (isObjectAndNotNull(tempAnnot) && isObjectAndNotNull(tempAnnot.by_name) && isObjectAndNotNull(tempAnnot.by_name[jsonColumn.name])) {
|
|
3254
|
-
|
|
3255
|
+
updateObject(annots, tempAnnot.by_name[jsonColumn.name]);
|
|
3255
3256
|
}
|
|
3256
3257
|
}
|
|
3257
3258
|
});
|
|
@@ -3262,14 +3263,14 @@ import {
|
|
|
3262
3263
|
if (el.annotations.contains(defaultAnnotKey)) {
|
|
3263
3264
|
var tempAnnot = el.annotations.get(defaultAnnotKey).content;
|
|
3264
3265
|
if (isObjectAndNotNull(tempAnnot) && isObjectAndNotNull(tempAnnot.asset) && isObjectAndNotNull(tempAnnot.asset[assetCategory])) {
|
|
3265
|
-
|
|
3266
|
+
updateObject(annots, tempAnnot.asset[assetCategory]);
|
|
3266
3267
|
}
|
|
3267
3268
|
}
|
|
3268
3269
|
});
|
|
3269
3270
|
}
|
|
3270
3271
|
|
|
3271
3272
|
// then copy the existing annots on the column
|
|
3272
|
-
|
|
3273
|
+
updateObject(annots, jsonColumn.annotations);
|
|
3273
3274
|
for (var uri in annots) {
|
|
3274
3275
|
var jsonAnnotation = annots[uri];
|
|
3275
3276
|
this.annotations._push(new Annotation("column", uri, jsonAnnotation));
|
package/js/parser.js
CHANGED
|
@@ -227,6 +227,7 @@ import HistoryService from '@isrd-isi-edu/ermrestjs/src/services/history';
|
|
|
227
227
|
this._compactUri = stripTrailingSlash(this._compactUri);
|
|
228
228
|
|
|
229
229
|
// service
|
|
230
|
+
// codeql[js/polynomial-redos]: URL input limited by browser length
|
|
230
231
|
parts = uri.match(/(.*)\/catalog\/([^\/]*)\/(entity|attribute|aggregate|attributegroup)\/(.*)/);
|
|
231
232
|
this._service = parts[1];
|
|
232
233
|
|
|
@@ -263,11 +264,13 @@ import HistoryService from '@isrd-isi-edu/ermrestjs/src/services/history';
|
|
|
263
264
|
// sort and paging
|
|
264
265
|
if (modifiers) {
|
|
265
266
|
if (modifiers.indexOf("@sort(") !== -1) {
|
|
267
|
+
// codeql[js/polynomial-redos]: URL input limited by browser length
|
|
266
268
|
this._sort = modifiers.match(/(@sort\([^\)]*\))/)[1];
|
|
267
269
|
}
|
|
268
270
|
// sort must specified to use @before and @after
|
|
269
271
|
if (modifiers.indexOf("@before(") !== -1) {
|
|
270
272
|
if (this._sort) {
|
|
273
|
+
// codeql[js/polynomial-redos]: URL input limited by browser length
|
|
271
274
|
this._before = modifiers.match(/(@before\([^\)]*\))/)[1];
|
|
272
275
|
} else {
|
|
273
276
|
throw new InvalidPageCriteria("Sort modifier is required with paging.", this._path);
|
|
@@ -276,6 +279,7 @@ import HistoryService from '@isrd-isi-edu/ermrestjs/src/services/history';
|
|
|
276
279
|
|
|
277
280
|
if (modifiers.indexOf("@after(") !== -1) {
|
|
278
281
|
if (this._sort) {
|
|
282
|
+
// codeql[js/polynomial-redos]: URL input limited by browser length
|
|
279
283
|
this._after = modifiers.match(/(@after\([^\)]*\))/)[1];
|
|
280
284
|
} else {
|
|
281
285
|
throw new InvalidPageCriteria("Sort modifier is required with paging.", this._path);
|
|
@@ -300,6 +304,7 @@ import HistoryService from '@isrd-isi-edu/ermrestjs/src/services/history';
|
|
|
300
304
|
}
|
|
301
305
|
|
|
302
306
|
// pathParts: <joins/facet/cfacet/filter/>
|
|
307
|
+
// codeql[js/polynomial-redos]: URL input limited by browser length
|
|
303
308
|
var joinRegExp = /(?:left|right|full|^)\((.*)\)=\((.*:.*:.*)\)/,
|
|
304
309
|
facetsRegExp = /\*::facets::(.+)/,
|
|
305
310
|
customFacetsRegExp = /\*::cfacets::(.+)/;
|
|
@@ -1770,6 +1775,7 @@ import HistoryService from '@isrd-isi-edu/ermrestjs/src/services/history';
|
|
|
1770
1775
|
function _createParsedJoinFromStr (linking, table, schema) {
|
|
1771
1776
|
var fromSchemaTable = schema ? [schema,table].join(":") : table;
|
|
1772
1777
|
var fromCols = linking[1].split(",");
|
|
1778
|
+
// codeql[js/polynomial-redos]: URL input limited by browser length
|
|
1773
1779
|
var toParts = linking[2].match(/([^:]*):([^:]*):([^\)]*)/);
|
|
1774
1780
|
var toCols = toParts[3].split(",");
|
|
1775
1781
|
var strReverse = "(" + toParts[3] + ")=(" + fromSchemaTable + ":" + linking[1] + ")";
|
package/js/utils/helpers.js
CHANGED
|
@@ -782,14 +782,16 @@ import AuthnService from '@isrd-isi-edu/ermrestjs/src/services/authn';
|
|
|
782
782
|
|
|
783
783
|
//get foreignkey data if available
|
|
784
784
|
if (linkedData && typeof linkedData === "object" && table.sourceDefinitions.fkeys.length > 0) {
|
|
785
|
-
|
|
785
|
+
// use a prototype-less object to avoid prototype pollution via constraint names
|
|
786
|
+
keyValues.$fkeys = Object.create(null);
|
|
786
787
|
table.sourceDefinitions.fkeys.forEach(function (fk) {
|
|
787
788
|
var p = _generateRowLinkProperties(fk.key, linkedData[fk.name], context);
|
|
788
789
|
if (!p) return;
|
|
789
790
|
|
|
790
791
|
cons = fk.constraint_names[0];
|
|
791
792
|
if (!keyValues.$fkeys[cons[0]]) {
|
|
792
|
-
|
|
793
|
+
// per-schema map should also be prototype-less
|
|
794
|
+
keyValues.$fkeys[cons[0]] = Object.create(null);
|
|
793
795
|
}
|
|
794
796
|
|
|
795
797
|
var fkTempVal = {
|
|
@@ -19,34 +19,32 @@ import ConfigService from '@isrd-isi-edu/ermrestjs/src/services/config';
|
|
|
19
19
|
import { createPseudoColumn } from '@isrd-isi-edu/ermrestjs/src/utils/column-utils';
|
|
20
20
|
import { isObjectAndNotNull, isObject, isDefinedAndNotNull, isStringAndNotEmpty } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
fixedEncodeURIComponent,
|
|
23
|
+
hexToBase64,
|
|
24
|
+
simpleDeepCopy,
|
|
25
|
+
shallowCopyExtras,
|
|
26
|
+
urlEncodeBase64,
|
|
27
27
|
} from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
|
|
28
28
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
_systemColumnNames,
|
|
45
|
-
_warningMessages,
|
|
29
|
+
_constraintTypes,
|
|
30
|
+
_contexts,
|
|
31
|
+
_ERMrestFeatures,
|
|
32
|
+
_ERMrestFilterPredicates,
|
|
33
|
+
_ERMrestLogicalOperators,
|
|
34
|
+
_facetingErrors,
|
|
35
|
+
_facetFilterTypes,
|
|
36
|
+
_facetHeuristicIgnoredTypes,
|
|
37
|
+
_FacetsLogicalOperators,
|
|
38
|
+
_facetUnsupportedTypes, _shorterVersion,
|
|
39
|
+
_sourceDefinitionAttributes,
|
|
40
|
+
_sourceProperties,
|
|
41
|
+
_specialSourceDefinitions,
|
|
42
|
+
_systemColumnNames,
|
|
43
|
+
_warningMessages
|
|
46
44
|
} from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
47
45
|
|
|
48
46
|
// legacy
|
|
49
|
-
import { generateKeyValueFilters, renameKey, _renderTemplate, _isEntryContext
|
|
47
|
+
import { generateKeyValueFilters, renameKey, _renderTemplate, _isEntryContext } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
50
48
|
import { Table, Catalog } from '@isrd-isi-edu/ermrestjs/js/core';
|
|
51
49
|
import { parse, _convertSearchTermToFilter } from '@isrd-isi-edu/ermrestjs/js/parser';
|
|
52
50
|
|
|
@@ -931,11 +929,6 @@ import { parse, _convertSearchTermToFilter } from '@isrd-isi-edu/ermrestjs/js/pa
|
|
|
931
929
|
throw new Error(_facetingErrors.aggregateFnNowtAllowed);
|
|
932
930
|
}
|
|
933
931
|
|
|
934
|
-
// column type array is not supported
|
|
935
|
-
if (col.type.isArray) {
|
|
936
|
-
throw new Error(_facetingErrors.arrayColumnTypeNotSupported);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
932
|
// check the column type
|
|
940
933
|
if (_facetUnsupportedTypes.indexOf(col.type.name) !== -1) {
|
|
941
934
|
throw new Error(`Facet of column type '${col.type.name}' is not supported.`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isrd-isi-edu/ermrestjs",
|
|
3
3
|
"description": "ERMrest client library in JavaScript",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">= 20.0.0",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"lint": "eslint src js --quiet",
|
|
27
27
|
"lint-w-warn": "eslint src js",
|
|
28
28
|
"format": "prettier --write src",
|
|
29
|
-
"prepare": "husky"
|
|
29
|
+
"prepare": "node .husky/install.mjs"
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
@@ -43,10 +43,12 @@
|
|
|
43
43
|
"library"
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
|
+
"@types/lodash-es": "^4.17.12",
|
|
46
47
|
"@types/markdown-it": "^14.1.2",
|
|
47
48
|
"@types/q": "^1.5.8",
|
|
48
|
-
"axios": "1.
|
|
49
|
+
"axios": "1.13.2",
|
|
49
50
|
"handlebars": "4.7.8",
|
|
51
|
+
"lodash-es": "^4.17.23",
|
|
50
52
|
"lz-string": "^1.5.0",
|
|
51
53
|
"markdown-it": "12.3.2",
|
|
52
54
|
"moment": "2.29.4",
|
|
@@ -54,32 +56,31 @@
|
|
|
54
56
|
"mustache": "x",
|
|
55
57
|
"q": "1.5.1",
|
|
56
58
|
"spark-md5": "^3.0.0",
|
|
57
|
-
"terser": "^5.
|
|
58
|
-
"typescript": "~5.
|
|
59
|
-
"vite": "^
|
|
60
|
-
"vite-plugin-compression2": "^2.2.1"
|
|
59
|
+
"terser": "^5.44.1",
|
|
60
|
+
"typescript": "~5.9.3",
|
|
61
|
+
"vite": "^7.3.1",
|
|
62
|
+
"vite-plugin-compression2": "^2.2.1",
|
|
63
|
+
"vite-plugin-dts": "^4.5.4"
|
|
61
64
|
},
|
|
62
65
|
"devDependencies": {
|
|
63
66
|
"@commitlint/cli": "^20.2.0",
|
|
64
67
|
"@commitlint/config-conventional": "^20.2.0",
|
|
65
|
-
"@eslint/js": "^9.
|
|
68
|
+
"@eslint/js": "^9.39.2",
|
|
66
69
|
"@isrd-isi-edu/ermrest-data-utils": "0.0.5",
|
|
67
70
|
"@semantic-release/git": "^10.0.1",
|
|
68
|
-
"@types/node": "^
|
|
69
|
-
"eslint": "^9.
|
|
71
|
+
"@types/node": "^25.0.3",
|
|
72
|
+
"eslint": "^9.39.2",
|
|
70
73
|
"eslint-config-prettier": "^10.1.8",
|
|
71
74
|
"eslint-plugin-prettier": "^5.5.4",
|
|
72
|
-
"file-api": "^0.10.4",
|
|
73
75
|
"globals": "^16.4.0",
|
|
74
76
|
"husky": "^9.1.7",
|
|
75
77
|
"jasmine": "2.5.3",
|
|
76
78
|
"jasmine-expect": "3.7.1",
|
|
77
79
|
"jasmine-spec-reporter": "2.5.0",
|
|
78
80
|
"nock": "13.5.4",
|
|
79
|
-
"prettier": "^3.
|
|
81
|
+
"prettier": "^3.7.4",
|
|
80
82
|
"require-reload": "^0.2.2",
|
|
81
83
|
"rollup-plugin-visualizer": "^6.0.3",
|
|
82
|
-
"typescript-eslint": "^8.
|
|
83
|
-
"vite-plugin-dts": "^4.5.4"
|
|
84
|
+
"typescript-eslint": "^8.51.0"
|
|
84
85
|
}
|
|
85
86
|
}
|
package/src/models/errors.ts
CHANGED
|
@@ -317,15 +317,15 @@ export class UnsupportedFilters extends ERMrestError {
|
|
|
317
317
|
|
|
318
318
|
const removePageCondition = (path?: string) => {
|
|
319
319
|
if (path !== undefined) {
|
|
320
|
-
path = path.replace(/(@before\([^)]*\))/, '');
|
|
321
|
-
path = path.replace(/(@after\([^)]*\))/, '');
|
|
320
|
+
path = path.replace(/(@before\([^)@]*\))/, '');
|
|
321
|
+
path = path.replace(/(@after\([^)@]*\))/, '');
|
|
322
322
|
}
|
|
323
323
|
return path;
|
|
324
324
|
};
|
|
325
325
|
|
|
326
326
|
const removeSortCondition = (path?: string) => {
|
|
327
327
|
if (path !== undefined) {
|
|
328
|
-
return path.replace(/(@sort\([^)]*\))/, '');
|
|
328
|
+
return path.replace(/(@sort\([^)@]*\))/, '');
|
|
329
329
|
}
|
|
330
330
|
return path;
|
|
331
331
|
};
|
|
@@ -350,6 +350,11 @@ export class FacetColumn {
|
|
|
350
350
|
return onlyChoice ? modes.CHOICE : modes.RANGE;
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
// only check_presence can be supported for arrays.
|
|
354
|
+
if (self._column.type.isArray) {
|
|
355
|
+
return modes.PRESENCE;
|
|
356
|
+
}
|
|
357
|
+
|
|
353
358
|
// use the defined ux_mode
|
|
354
359
|
if (_facetUXModeNames.indexOf(self._facetObject.ux_mode) !== -1) {
|
|
355
360
|
return self._facetObject.ux_mode;
|
|
@@ -1196,7 +1201,7 @@ export class FacetColumn {
|
|
|
1196
1201
|
verify(Array.isArray(values), 'given argument must be an array');
|
|
1197
1202
|
|
|
1198
1203
|
const filters = this.filters.slice();
|
|
1199
|
-
values.forEach((v
|
|
1204
|
+
values.forEach((v) => {
|
|
1200
1205
|
filters.push(new ChoiceFacetFilter(v, this._column));
|
|
1201
1206
|
});
|
|
1202
1207
|
|
|
@@ -1213,7 +1218,7 @@ export class FacetColumn {
|
|
|
1213
1218
|
const filters = this.filters.slice().filter((f: FacetFilter | NotNullFacetFilter) => {
|
|
1214
1219
|
return !(f instanceof ChoiceFacetFilter) && !(f instanceof NotNullFacetFilter);
|
|
1215
1220
|
});
|
|
1216
|
-
values.forEach((v
|
|
1221
|
+
values.forEach((v) => {
|
|
1217
1222
|
filters.push(new ChoiceFacetFilter(v, this._column));
|
|
1218
1223
|
});
|
|
1219
1224
|
|
|
@@ -1342,7 +1347,7 @@ export class FacetColumn {
|
|
|
1342
1347
|
|
|
1343
1348
|
// create a new FacetColumn so it doesn't reference to the current FacetColum
|
|
1344
1349
|
// TODO can be refactored
|
|
1345
|
-
const jsonFilters
|
|
1350
|
+
const jsonFilters = [];
|
|
1346
1351
|
|
|
1347
1352
|
// TODO might be able to imporve this. Instead of recreating the whole json file.
|
|
1348
1353
|
// gather all the filters from the facetColumns
|
package/src/services/error.ts
CHANGED
|
@@ -188,6 +188,7 @@ export default class ErrorService {
|
|
|
188
188
|
let msgTail;
|
|
189
189
|
const keyValueMap: Record<string, string> = {};
|
|
190
190
|
let duplicateReference = null;
|
|
191
|
+
// codeql[js/polynomial-redos]: Parsing structured database error messages
|
|
191
192
|
const columnRegExp = /\(([^)]+)\)/,
|
|
192
193
|
valueRegExp = /=\(([^)]+)\)/,
|
|
193
194
|
matches = columnRegExp.exec(generatedErrMessage),
|
|
@@ -114,11 +114,14 @@ export default class HandlebarsService {
|
|
|
114
114
|
_addErmrestVarsToTemplate(keyValues, catalog);
|
|
115
115
|
|
|
116
116
|
// If no conditional handlebars statements of the form {{#if VARNAME}}{{/if}} or {{^if VARNAME}}{{/if}} or {{#unless VARNAME}}{{/unless}} or {{^unless VARNAME}}{{/unless}} not found then do direct null check
|
|
117
|
+
// codeql[js/polynomial-redos]
|
|
117
118
|
if (!conditionalRegex.exec(template)) {
|
|
118
119
|
// Grab all placeholders ({{PROP_NAME}}) in the template
|
|
120
|
+
// codeql[js/polynomial-redos]: template is not user input
|
|
119
121
|
const placeholders = template.match(/\{\{([^\{\}\(\)\s]+)\}\}/gi);
|
|
120
122
|
|
|
121
123
|
// These will match the placeholders that are encapsulated in square brackets {{[string with space]}} or {{{[string with space]}}}
|
|
124
|
+
// codeql[js/polynomial-redos]: template is not user input
|
|
122
125
|
const specialPlaceholders = template.match(/\{\{((\[[^\{\}]+\])|(\{\[[^\{\}]+\]\}))\}\}/gi);
|
|
123
126
|
|
|
124
127
|
// If there are any placeholders
|
|
@@ -121,8 +121,7 @@ function _bindCustomMarkdownTags(md: typeof MarkdownIt) {
|
|
|
121
121
|
openingLink!.attrs!.forEach(function (attr) {
|
|
122
122
|
switch (attr[0]) {
|
|
123
123
|
case 'href':
|
|
124
|
-
|
|
125
|
-
isYTlink = attr[1].match('^(http(s)?:\/\/)?((w){3}.)?youtu(be|.be)?(\.com)?\/.+') != null;
|
|
124
|
+
isYTlink = attr[1].match('^(http(s)?://)?((w){3}.)?youtu(be|.be)?(.com)?/.+') != null;
|
|
126
125
|
iframeSrc = attr[1];
|
|
127
126
|
iframeHTML += ' src="' + attr[1] + '"';
|
|
128
127
|
videoText = 'Note: YouTube video ( ' + attr[1] + ' ) is hidden in print';
|
package/src/utils/value-utils.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isArray, mergeWith } from 'lodash-es';
|
|
2
|
+
|
|
1
3
|
import { ENV_IS_NODE } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -70,7 +72,17 @@ export function stripTrailingSlash(str: string): string {
|
|
|
70
72
|
* trim the slashes that might exist at the begining or end of the string
|
|
71
73
|
*/
|
|
72
74
|
export function trimSlashes(str: string) {
|
|
73
|
-
|
|
75
|
+
let start = 0;
|
|
76
|
+
let end = str.length - 1;
|
|
77
|
+
|
|
78
|
+
while (start <= end && str[start] === '/') {
|
|
79
|
+
start++;
|
|
80
|
+
}
|
|
81
|
+
while (end >= start && str[end] === '/') {
|
|
82
|
+
end--;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return start <= end ? str.slice(start, end + 1) : '';
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
/**
|
|
@@ -125,3 +137,18 @@ export function shallowCopyExtras(copyTo: any, copyFrom: any, enforcedList: stri
|
|
|
125
137
|
export function simpleDeepCopy(source: object): any {
|
|
126
138
|
return JSON.parse(JSON.stringify(source));
|
|
127
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* update the target object with the updates provided in the updates object.
|
|
143
|
+
* NOTE: this will mutate the target object.
|
|
144
|
+
*/
|
|
145
|
+
export function updateObject(target: any, updates: any) {
|
|
146
|
+
mergeWith(target, updates, (objValue: unknown, srcValue: unknown) => {
|
|
147
|
+
// by default lodash's mergeWith will concat arrays, we want to replace them instead.
|
|
148
|
+
if (isArray(objValue)) {
|
|
149
|
+
return srcValue;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return target;
|
|
154
|
+
}
|