@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 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
- Object.assign(annots, tempAnnot.by_type[jsonColumn.type.typename]);
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
- Object.assign(annots, tempAnnot.by_name[jsonColumn.name]);
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
- Object.assign(annots, tempAnnot.asset[assetCategory]);
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
- Object.assign(annots, jsonColumn.annotations);
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] + ")";
@@ -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
- keyValues.$fkeys = {};
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
- keyValues.$fkeys[cons[0]] = {};
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
- fixedEncodeURIComponent,
23
- hexToBase64,
24
- simpleDeepCopy,
25
- shallowCopyExtras,
26
- urlEncodeBase64,
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
- _constraintTypes,
30
- _contexts,
31
- _ERMrestFeatures,
32
- _ERMrestFilterPredicates,
33
- _ERMrestLogicalOperators,
34
- _facetingErrors,
35
- _facetFilterTypes,
36
- _facetHeuristicIgnoredTypes,
37
- _FacetsLogicalOperators,
38
- _facetUnsupportedTypes,
39
- _pseudoColAggregateFns,
40
- _shorterVersion,
41
- _sourceDefinitionAttributes,
42
- _sourceProperties,
43
- _specialSourceDefinitions,
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, _getFormattedKeyValues } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
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.1.0",
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.12.2",
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.43.1",
58
- "typescript": "~5.8.3",
59
- "vite": "^6.4.1",
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.36.0",
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": "^24.6.1",
69
- "eslint": "^9.36.0",
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.6.2",
81
+ "prettier": "^3.7.4",
80
82
  "require-reload": "^0.2.2",
81
83
  "rollup-plugin-visualizer": "^6.0.3",
82
- "typescript-eslint": "^8.45.0",
83
- "vite-plugin-dts": "^4.5.4"
84
+ "typescript-eslint": "^8.51.0"
84
85
  }
85
86
  }
@@ -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: any) => {
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: any) => {
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: any[] = [];
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
@@ -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
- // eslint-disable-next-line no-useless-escape
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';
@@ -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
- return str.replace(/^\/+|\/+$/g, '');
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
+ }