@isrd-isi-edu/ermrestjs 2.1.1 → 2.3.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/hatrac.js +2 -60
- package/js/parser.js +6 -0
- package/js/utils/helpers.js +5 -3
- package/js/utils/pseudocolumn_helpers.js +21 -28
- package/package.json +11 -9
- package/src/index.ts +2 -0
- package/src/models/errors.ts +3 -3
- package/src/models/reference-column/asset-pseudo-column.ts +241 -19
- package/src/models/reference-column/facet-column.ts +8 -3
- package/src/services/error.ts +1 -0
- package/src/services/file-preview.ts +229 -0
- package/src/services/handlebars.ts +3 -0
- package/src/services/logger.ts +6 -7
- package/src/utils/constants.ts +5 -0
- package/src/utils/file-utils.ts +198 -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/hatrac.js
CHANGED
|
@@ -15,6 +15,7 @@ import ConfigService from '@isrd-isi-edu/ermrestjs/src/services/config';
|
|
|
15
15
|
import { hexToBase64 } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
|
|
16
16
|
import { isObject, isObjectAndNotNull } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
17
17
|
import { contextHeaderName, ENV_IS_NODE } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
18
|
+
import { getFilenameExtension } from '@isrd-isi-edu/ermrestjs/src/utils/file-utils';
|
|
18
19
|
|
|
19
20
|
// legacy
|
|
20
21
|
import { _validateTemplate, _renderTemplate, _getFormattedKeyValues, _parseUrl } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
@@ -138,65 +139,6 @@ const _generateContextHeader = function (contextHeaderParams) {
|
|
|
138
139
|
return headers;
|
|
139
140
|
};
|
|
140
141
|
|
|
141
|
-
/**
|
|
142
|
-
* given a filename, will return the extension
|
|
143
|
-
* By default, it will extract the last of the filename after the last `.`.
|
|
144
|
-
* The second parameter can be used for passing a regular expression
|
|
145
|
-
* if we want a different method of extracting the extension.
|
|
146
|
-
* @param {string} filename
|
|
147
|
-
* @param {string[]} allowedExtensions
|
|
148
|
-
* @param {string[]} regexArr
|
|
149
|
-
* @returns the filename extension string. if we cannot find any matches, it will return null
|
|
150
|
-
* @private
|
|
151
|
-
* @ignore
|
|
152
|
-
*/
|
|
153
|
-
const _getFilenameExtension = function (filename, allowedExtensions, regexArr) {
|
|
154
|
-
if (typeof filename !== 'string' || filename.length === 0) {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// first find in the list of allowed extensions
|
|
159
|
-
var res = -1;
|
|
160
|
-
var isInAllowed =
|
|
161
|
-
Array.isArray(allowedExtensions) &&
|
|
162
|
-
allowedExtensions.some(function (ext) {
|
|
163
|
-
res = ext;
|
|
164
|
-
return typeof ext === 'string' && ext.length > 0 && filename.endsWith(ext);
|
|
165
|
-
});
|
|
166
|
-
if (isInAllowed) {
|
|
167
|
-
return res;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// we will return null if we cannot find anything
|
|
171
|
-
res = null;
|
|
172
|
-
// no matching allowed extension, try the regular expressions
|
|
173
|
-
if (Array.isArray(regexArr) && regexArr.length > 0) {
|
|
174
|
-
regexArr.some(function (regexp) {
|
|
175
|
-
// since regular expression comes from annotation, it might not be valid
|
|
176
|
-
try {
|
|
177
|
-
var matches = filename.match(new RegExp(regexp, 'g'));
|
|
178
|
-
if (matches && matches[0] && typeof matches[0] === 'string') {
|
|
179
|
-
res = matches[0];
|
|
180
|
-
} else {
|
|
181
|
-
res = null;
|
|
182
|
-
}
|
|
183
|
-
return res;
|
|
184
|
-
} catch {
|
|
185
|
-
res = null;
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
} else {
|
|
190
|
-
var dotIndex = filename.lastIndexOf('.');
|
|
191
|
-
// it's only a valid filename if there's some string after `.`
|
|
192
|
-
if (dotIndex !== -1 && dotIndex !== filename.length - 1) {
|
|
193
|
-
res = filename.slice(dotIndex);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return res;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
142
|
/**
|
|
201
143
|
* @desc upload Object
|
|
202
144
|
* Create a new instance with new upload(file, otherInfo)
|
|
@@ -801,7 +743,7 @@ Upload.prototype._generateURL = function (row, linkedData, templateVariables) {
|
|
|
801
743
|
row[this.column.name].md5_base64 = this.hash.md5_base64;
|
|
802
744
|
row[this.column.name].sha256 = this.hash.sha256;
|
|
803
745
|
row[this.column.name].filename = this.file.name;
|
|
804
|
-
var filename_ext =
|
|
746
|
+
var filename_ext = getFilenameExtension(this.file.name, this.column.filenameExtFilter, this.column.filenameExtRegexp);
|
|
805
747
|
row[this.column.name].filename_ext = filename_ext;
|
|
806
748
|
// filename_basename is everything from the file name except the last ext
|
|
807
749
|
// For example if we have a file named "file.tar.zip"
|
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
|
@@ -404,7 +404,7 @@ import AuthnService from '@isrd-isi-edu/ermrestjs/src/services/authn';
|
|
|
404
404
|
|
|
405
405
|
/**
|
|
406
406
|
* @param {string} context the context that we want the value of.
|
|
407
|
-
* @param {
|
|
407
|
+
* @param {any} annotation the annotation object.
|
|
408
408
|
* @param {Boolean=} dontUseDefaultContext Whether we should use the default (*) context
|
|
409
409
|
* @desc returns the annotation value based on the given context.
|
|
410
410
|
*/
|
|
@@ -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.3.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
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",
|
|
@@ -56,17 +58,18 @@
|
|
|
56
58
|
"spark-md5": "^3.0.0",
|
|
57
59
|
"terser": "^5.44.1",
|
|
58
60
|
"typescript": "~5.9.3",
|
|
59
|
-
"vite": "^
|
|
60
|
-
"vite-plugin-compression2": "^2.2.1"
|
|
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
75
|
"globals": "^16.4.0",
|
|
@@ -78,7 +81,6 @@
|
|
|
78
81
|
"prettier": "^3.7.4",
|
|
79
82
|
"require-reload": "^0.2.2",
|
|
80
83
|
"rollup-plugin-visualizer": "^6.0.3",
|
|
81
|
-
"typescript-eslint": "^8.
|
|
82
|
-
"vite-plugin-dts": "^4.5.4"
|
|
84
|
+
"typescript-eslint": "^8.51.0"
|
|
83
85
|
}
|
|
84
86
|
}
|
package/src/index.ts
CHANGED
|
@@ -77,6 +77,7 @@ import HandlebarsService from '@isrd-isi-edu/ermrestjs/src/services/handlebars';
|
|
|
77
77
|
import { Exporter } from '@isrd-isi-edu/ermrestjs/js/export';
|
|
78
78
|
import validateJSONLD from '@isrd-isi-edu/ermrestjs/js/json_ld_validator.js';
|
|
79
79
|
import HistoryService from '@isrd-isi-edu/ermrestjs/src/services/history';
|
|
80
|
+
import FilePreviewService from '@isrd-isi-edu/ermrestjs/src/services/file-preview';
|
|
80
81
|
|
|
81
82
|
const logError = ErrorService.logError;
|
|
82
83
|
const responseToError = ErrorService.responseToError;
|
|
@@ -111,6 +112,7 @@ export {
|
|
|
111
112
|
AuthnService,
|
|
112
113
|
Exporter,
|
|
113
114
|
HistoryService,
|
|
115
|
+
FilePreviewService,
|
|
114
116
|
|
|
115
117
|
// constants
|
|
116
118
|
contextHeaderName,
|
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
|
};
|
|
@@ -3,10 +3,14 @@ import { ReferenceColumn, ReferenceColumnTypes } from '@isrd-isi-edu/ermrestjs/s
|
|
|
3
3
|
import type SourceObjectWrapper from '@isrd-isi-edu/ermrestjs/src/models/source-object-wrapper';
|
|
4
4
|
import type { Reference, Tuple, VisibleColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference';
|
|
5
5
|
|
|
6
|
+
// services
|
|
7
|
+
import { FilePreviewTypes, isFilePreviewType, USE_EXT_MAPPING } from '@isrd-isi-edu/ermrestjs/src/services/file-preview';
|
|
8
|
+
|
|
6
9
|
// utils
|
|
7
10
|
import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils';
|
|
8
11
|
import { isDefinedAndNotNull, isObjectAndKeyExists, isObjectAndNotNull, isStringAndNotEmpty } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
9
12
|
import { _annotations, _contexts, _classNames } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
13
|
+
import { getFilename } from '@isrd-isi-edu/ermrestjs/src/utils/file-utils';
|
|
10
14
|
|
|
11
15
|
// legacy
|
|
12
16
|
import { _getAnnotationValueByContext, _isEntryContext, _renderTemplate, _isSameHost } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
@@ -70,7 +74,7 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
70
74
|
private _filenameExtFilter?: string[];
|
|
71
75
|
private _filenameExtRegexp?: string[];
|
|
72
76
|
private _displayImagePreview?: boolean;
|
|
73
|
-
private _filePreview?:
|
|
77
|
+
private _filePreview?: FilePreviewConfig | null;
|
|
74
78
|
|
|
75
79
|
constructor(reference: Reference, column: Column, sourceObjectWrapper?: SourceObjectWrapper, name?: string, mainTuple?: Tuple) {
|
|
76
80
|
// call the parent constructor
|
|
@@ -168,17 +172,9 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
168
172
|
|
|
169
173
|
// if we're using the url as caption
|
|
170
174
|
if (urlCaption) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (parts && parts.length === 4) {
|
|
175
|
-
caption = parts[2];
|
|
176
|
-
} else {
|
|
177
|
-
// otherwise return the last part of url
|
|
178
|
-
const newCaption = caption.split('/').pop();
|
|
179
|
-
if (newCaption && newCaption.length !== 0) {
|
|
180
|
-
caption = newCaption;
|
|
181
|
-
}
|
|
175
|
+
const newCaption = getFilename(caption);
|
|
176
|
+
if (newCaption && newCaption.length !== 0) {
|
|
177
|
+
caption = newCaption;
|
|
182
178
|
}
|
|
183
179
|
}
|
|
184
180
|
|
|
@@ -457,7 +453,7 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
457
453
|
/**
|
|
458
454
|
* whether we should show the file preview or not
|
|
459
455
|
*/
|
|
460
|
-
get filePreview():
|
|
456
|
+
get filePreview(): FilePreviewConfig | null {
|
|
461
457
|
if (this._filePreview === undefined) {
|
|
462
458
|
const disp = this._annotation.display;
|
|
463
459
|
const currDisplay = isObjectAndNotNull(disp) ? _getAnnotationValueByContext(this._context, disp) : null;
|
|
@@ -465,12 +461,7 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
465
461
|
if (settings === false) {
|
|
466
462
|
this._filePreview = null;
|
|
467
463
|
} else {
|
|
468
|
-
|
|
469
|
-
let showCsvHeader = false;
|
|
470
|
-
if (isObjectAndKeyExists(settings, 'show_csv_header') && typeof settings.show_csv_header === 'boolean') {
|
|
471
|
-
showCsvHeader = settings.show_csv_header;
|
|
472
|
-
}
|
|
473
|
-
this._filePreview = { showCsvHeader };
|
|
464
|
+
this._filePreview = new FilePreviewConfig(settings);
|
|
474
465
|
}
|
|
475
466
|
}
|
|
476
467
|
return this._filePreview;
|
|
@@ -496,3 +487,234 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
496
487
|
return this._waitFor;
|
|
497
488
|
}
|
|
498
489
|
}
|
|
490
|
+
|
|
491
|
+
class FilePreviewConfig {
|
|
492
|
+
private static previewTypes = Object.values(FilePreviewTypes);
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* whether we should show the CSV header or not
|
|
496
|
+
* (default: false)
|
|
497
|
+
*/
|
|
498
|
+
showCsvHeader: boolean = false;
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* the height of the preview container
|
|
502
|
+
*/
|
|
503
|
+
defaultHeight: number | null = null;
|
|
504
|
+
|
|
505
|
+
private _prefetchBytes: { [key: string]: number | null } = {
|
|
506
|
+
image: null,
|
|
507
|
+
markdown: null,
|
|
508
|
+
csv: null,
|
|
509
|
+
tsv: null,
|
|
510
|
+
json: null,
|
|
511
|
+
text: null,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
private _prefetchMaxFileSize: { [key: string]: number | null } = {
|
|
515
|
+
image: null,
|
|
516
|
+
markdown: null,
|
|
517
|
+
csv: null,
|
|
518
|
+
tsv: null,
|
|
519
|
+
json: null,
|
|
520
|
+
text: null,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
filenameExtMapping: { [key: string]: FilePreviewTypes | false } | null = null;
|
|
524
|
+
|
|
525
|
+
contentTypeMapping: {
|
|
526
|
+
exactMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } | null;
|
|
527
|
+
prefixMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } | null;
|
|
528
|
+
default: FilePreviewTypes | typeof USE_EXT_MAPPING | false | null;
|
|
529
|
+
} | null = null;
|
|
530
|
+
|
|
531
|
+
disabledTypes: FilePreviewTypes[] = [];
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* populate the props based on the given annotation object.
|
|
535
|
+
* The supported annotation properties are:
|
|
536
|
+
* - show_csv_header
|
|
537
|
+
* - default_height
|
|
538
|
+
* - prefetch_bytes
|
|
539
|
+
* - prefetch_max_file_size
|
|
540
|
+
* - filename_ext_mapping
|
|
541
|
+
* - content_type_mapping
|
|
542
|
+
* - disabled
|
|
543
|
+
*/
|
|
544
|
+
constructor(settings: any) {
|
|
545
|
+
if (isObjectAndKeyExists(settings, 'show_csv_header') && typeof settings.show_csv_header === 'boolean') {
|
|
546
|
+
this.showCsvHeader = settings.show_csv_header;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (isObjectAndKeyExists(settings, 'default_height') && typeof settings.default_height === 'number' && settings.default_height >= 0) {
|
|
550
|
+
this.defaultHeight = settings.default_height;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this._prefetchBytes = this._populateProps<number>(settings, 'prefetch_bytes', (value: unknown) => {
|
|
554
|
+
return typeof value === 'number' && value >= 0;
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
this._prefetchMaxFileSize = this._populateProps<number>(settings, 'prefetch_max_file_size', (value: unknown) => {
|
|
558
|
+
return typeof value === 'number' && value >= 0;
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
if (isObjectAndKeyExists(settings, 'filename_ext_mapping')) {
|
|
562
|
+
this.filenameExtMapping = {};
|
|
563
|
+
for (const [key, val] of Object.entries(settings.filename_ext_mapping)) {
|
|
564
|
+
if (val === false || (typeof val === 'string' && isFilePreviewType(val))) {
|
|
565
|
+
this.filenameExtMapping[key] = val;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (isObjectAndKeyExists(settings, 'content_type_mapping')) {
|
|
571
|
+
const exactMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } = {};
|
|
572
|
+
const prefixMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } = {};
|
|
573
|
+
let defaultMapping: FilePreviewTypes | typeof USE_EXT_MAPPING | false | null = null;
|
|
574
|
+
let hasExactMatch = false;
|
|
575
|
+
let hasPrefixMatch = false;
|
|
576
|
+
Object.keys(settings.content_type_mapping).forEach((key) => {
|
|
577
|
+
const val = settings.content_type_mapping[key];
|
|
578
|
+
const validValue = val === false || (typeof val === 'string' && (isFilePreviewType(val) || val === USE_EXT_MAPPING));
|
|
579
|
+
if (!validValue) return;
|
|
580
|
+
// * could be used for default mapping
|
|
581
|
+
if (key === '*') {
|
|
582
|
+
defaultMapping = val;
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// only type/ or type/subtype are valid
|
|
587
|
+
const parts = key.split('/');
|
|
588
|
+
if (parts.length !== 2 || parts[0].length === 0) return;
|
|
589
|
+
|
|
590
|
+
if (parts[1].length > 0) {
|
|
591
|
+
exactMatch[key] = val;
|
|
592
|
+
hasExactMatch = true;
|
|
593
|
+
} else {
|
|
594
|
+
prefixMatch[parts[0] + '/'] = val;
|
|
595
|
+
hasPrefixMatch = true;
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
if (hasPrefixMatch || hasExactMatch || defaultMapping !== null) {
|
|
600
|
+
this.contentTypeMapping = {
|
|
601
|
+
exactMatch: hasExactMatch ? exactMatch : null,
|
|
602
|
+
prefixMatch: hasPrefixMatch ? prefixMatch : null,
|
|
603
|
+
default: defaultMapping,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (isObjectAndKeyExists(settings, 'disabled') && Array.isArray(settings.disabled)) {
|
|
609
|
+
this.disabledTypes = settings.disabled.filter(
|
|
610
|
+
(t: unknown) => typeof t === 'string' && FilePreviewConfig.previewTypes.includes(t as FilePreviewTypes),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* return the number of bytes to prefetch for previewing the file
|
|
617
|
+
*/
|
|
618
|
+
getPrefetchBytes(filePreviewType: FilePreviewTypes | null): number | null {
|
|
619
|
+
switch (filePreviewType) {
|
|
620
|
+
case FilePreviewTypes.IMAGE:
|
|
621
|
+
return this._prefetchBytes.image;
|
|
622
|
+
case FilePreviewTypes.MARKDOWN:
|
|
623
|
+
return this._prefetchBytes.markdown;
|
|
624
|
+
case FilePreviewTypes.CSV:
|
|
625
|
+
return this._prefetchBytes.csv;
|
|
626
|
+
case FilePreviewTypes.TSV:
|
|
627
|
+
return this._prefetchBytes.tsv;
|
|
628
|
+
case FilePreviewTypes.JSON:
|
|
629
|
+
return this._prefetchBytes.json;
|
|
630
|
+
case FilePreviewTypes.TEXT:
|
|
631
|
+
return this._prefetchBytes.text;
|
|
632
|
+
default:
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* return the max file size for previewing the file
|
|
639
|
+
*/
|
|
640
|
+
getPrefetchMaxFileSize(filePreviewType: FilePreviewTypes | null): number | null {
|
|
641
|
+
switch (filePreviewType) {
|
|
642
|
+
case FilePreviewTypes.IMAGE:
|
|
643
|
+
return this._prefetchMaxFileSize.image;
|
|
644
|
+
case FilePreviewTypes.MARKDOWN:
|
|
645
|
+
return this._prefetchMaxFileSize.markdown;
|
|
646
|
+
case FilePreviewTypes.CSV:
|
|
647
|
+
return this._prefetchMaxFileSize.csv;
|
|
648
|
+
case FilePreviewTypes.TSV:
|
|
649
|
+
return this._prefetchMaxFileSize.tsv;
|
|
650
|
+
case FilePreviewTypes.JSON:
|
|
651
|
+
return this._prefetchMaxFileSize.json;
|
|
652
|
+
case FilePreviewTypes.TEXT:
|
|
653
|
+
return this._prefetchMaxFileSize.text;
|
|
654
|
+
default:
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* The settings could be either just one value or an object with different values for each type.
|
|
661
|
+
* This function will populate the result object with the appropriate values for each type.
|
|
662
|
+
*/
|
|
663
|
+
private _populateProps<T>(settings: any, propName: string, validate?: (value: unknown) => boolean): { [key: string]: T | null } {
|
|
664
|
+
const res: { [key: string]: T | null } = {
|
|
665
|
+
text: null,
|
|
666
|
+
markdown: null,
|
|
667
|
+
csv: null,
|
|
668
|
+
tsv: null,
|
|
669
|
+
json: null,
|
|
670
|
+
image: null,
|
|
671
|
+
};
|
|
672
|
+
if (!isObjectAndKeyExists(settings, propName)) return res;
|
|
673
|
+
|
|
674
|
+
if (isObjectAndNotNull(settings[propName])) {
|
|
675
|
+
// for each preview type, try to get its value (which will fall back to * if not defined)
|
|
676
|
+
for (const key of FilePreviewConfig.previewTypes) {
|
|
677
|
+
const definedRes = this._getPropForType(key, settings[propName]);
|
|
678
|
+
if (!validate || validate(definedRes)) {
|
|
679
|
+
res[key] = definedRes;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
} else {
|
|
683
|
+
const definedRes = settings[propName];
|
|
684
|
+
if (!validate || validate(definedRes)) {
|
|
685
|
+
for (const key of FilePreviewConfig.previewTypes) {
|
|
686
|
+
res[key] = definedRes;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return res;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Get the property for a specific file type. If not defined, will try to get the default value (*).
|
|
696
|
+
* Otherwise returns null.
|
|
697
|
+
*/
|
|
698
|
+
private _getPropForType(fileType: string, settings: any): any {
|
|
699
|
+
const DEFAULT_TYPE = '*';
|
|
700
|
+
|
|
701
|
+
let isDefined = false;
|
|
702
|
+
let res;
|
|
703
|
+
if (isObjectAndKeyExists(settings, fileType)) {
|
|
704
|
+
if (typeof settings[fileType] === 'string' && settings[fileType] in FilePreviewConfig.previewTypes) {
|
|
705
|
+
res = this._getPropForType(settings[fileType], settings);
|
|
706
|
+
} else {
|
|
707
|
+
res = settings[fileType];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (res !== null && res !== undefined) isDefined = true;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (!isDefined && settings[DEFAULT_TYPE]) {
|
|
714
|
+
res = this._getPropForType(DEFAULT_TYPE, settings);
|
|
715
|
+
if (res !== null && res !== undefined) isDefined = true;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return isDefined ? res : null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
@@ -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),
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { AssetPseudoColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference-column';
|
|
2
|
+
import { FILE_PREVIEW } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
3
|
+
|
|
4
|
+
import { getFilename, getFilenameExtension } from '@isrd-isi-edu/ermrestjs/src/utils/file-utils';
|
|
5
|
+
import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The supported file preview types
|
|
9
|
+
*/
|
|
10
|
+
export enum FilePreviewTypes {
|
|
11
|
+
IMAGE = 'image',
|
|
12
|
+
MARKDOWN = 'markdown',
|
|
13
|
+
CSV = 'csv',
|
|
14
|
+
TSV = 'tsv',
|
|
15
|
+
JSON = 'json',
|
|
16
|
+
TEXT = 'text',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Type guard to check if a value is a FilePreviewTypes
|
|
21
|
+
*/
|
|
22
|
+
export const isFilePreviewType = (value: unknown): value is FilePreviewTypes => {
|
|
23
|
+
if (typeof value !== 'string') return false;
|
|
24
|
+
return Object.values(FilePreviewTypes).includes(value as FilePreviewTypes);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const USE_EXT_MAPPING = 'use_ext_mapping';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_CONTENT_TYPE_MAPPING: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } = {
|
|
30
|
+
// image:
|
|
31
|
+
'image/png': FilePreviewTypes.IMAGE,
|
|
32
|
+
'image/jpeg': FilePreviewTypes.IMAGE,
|
|
33
|
+
'image/jpg': FilePreviewTypes.IMAGE,
|
|
34
|
+
'image/gif': FilePreviewTypes.IMAGE,
|
|
35
|
+
'image/bmp': FilePreviewTypes.IMAGE,
|
|
36
|
+
'image/webp': FilePreviewTypes.IMAGE,
|
|
37
|
+
'image/svg+xml': FilePreviewTypes.IMAGE,
|
|
38
|
+
'image/x-icon': FilePreviewTypes.IMAGE,
|
|
39
|
+
'image/avif': FilePreviewTypes.IMAGE,
|
|
40
|
+
'image/apng': FilePreviewTypes.IMAGE,
|
|
41
|
+
// markdown:
|
|
42
|
+
'text/markdown': FilePreviewTypes.MARKDOWN,
|
|
43
|
+
// csv:
|
|
44
|
+
'text/csv': FilePreviewTypes.CSV,
|
|
45
|
+
// tsv:
|
|
46
|
+
'text/tab-separated-values': FilePreviewTypes.TSV,
|
|
47
|
+
// json:
|
|
48
|
+
'application/json': FilePreviewTypes.JSON,
|
|
49
|
+
// text:
|
|
50
|
+
'chemical/x-mmcif': FilePreviewTypes.TEXT,
|
|
51
|
+
'chemical/x-cif': FilePreviewTypes.TEXT,
|
|
52
|
+
// generic:
|
|
53
|
+
'text/plain': USE_EXT_MAPPING,
|
|
54
|
+
'application/octet-stream': USE_EXT_MAPPING,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const DEFAULT_EXTENSION_MAPPING: { [key: string]: FilePreviewTypes | false } = {
|
|
58
|
+
// image:
|
|
59
|
+
'.png': FilePreviewTypes.IMAGE,
|
|
60
|
+
'.jpeg': FilePreviewTypes.IMAGE,
|
|
61
|
+
'.jpg': FilePreviewTypes.IMAGE,
|
|
62
|
+
'.gif': FilePreviewTypes.IMAGE,
|
|
63
|
+
'.bmp': FilePreviewTypes.IMAGE,
|
|
64
|
+
'.webp': FilePreviewTypes.IMAGE,
|
|
65
|
+
'.svg': FilePreviewTypes.IMAGE,
|
|
66
|
+
'.ico': FilePreviewTypes.IMAGE,
|
|
67
|
+
'.avif': FilePreviewTypes.IMAGE,
|
|
68
|
+
'.apng': FilePreviewTypes.IMAGE,
|
|
69
|
+
// markdown:
|
|
70
|
+
'.md': FilePreviewTypes.MARKDOWN,
|
|
71
|
+
'.markdown': FilePreviewTypes.MARKDOWN,
|
|
72
|
+
// csv:
|
|
73
|
+
'.csv': FilePreviewTypes.CSV,
|
|
74
|
+
// tsv:
|
|
75
|
+
'.tsv': FilePreviewTypes.TSV,
|
|
76
|
+
// json:
|
|
77
|
+
'.json': FilePreviewTypes.JSON,
|
|
78
|
+
'.mvsj': FilePreviewTypes.JSON, // MolViewSpec JSON (mol* viewer)
|
|
79
|
+
// text:
|
|
80
|
+
'.txt': FilePreviewTypes.TEXT,
|
|
81
|
+
'.log': FilePreviewTypes.TEXT,
|
|
82
|
+
'.cif': FilePreviewTypes.TEXT,
|
|
83
|
+
'.pdb': FilePreviewTypes.TEXT,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default class FilePreviewService {
|
|
87
|
+
/**
|
|
88
|
+
* Returns the preview info based on the given file properties and the column's file preview settings.
|
|
89
|
+
* @param url the file url
|
|
90
|
+
* @param column the asset column
|
|
91
|
+
* @param storedFilename the stored filename
|
|
92
|
+
* @param contentDisposition content-disposition header value
|
|
93
|
+
* @param contentType content-type header value
|
|
94
|
+
*/
|
|
95
|
+
static getFilePreviewInfo(
|
|
96
|
+
url: string,
|
|
97
|
+
column?: AssetPseudoColumn,
|
|
98
|
+
storedFilename?: string,
|
|
99
|
+
contentDisposition?: string,
|
|
100
|
+
contentType?: string,
|
|
101
|
+
): {
|
|
102
|
+
previewType: FilePreviewTypes | null;
|
|
103
|
+
prefetchBytes: number | null;
|
|
104
|
+
prefetchMaxFileSize: number | null;
|
|
105
|
+
} {
|
|
106
|
+
const disabledValue = { previewType: null, prefetchBytes: null, prefetchMaxFileSize: null };
|
|
107
|
+
const previewType = FilePreviewService.getFilePreviewType(url, column, storedFilename, contentDisposition, contentType);
|
|
108
|
+
let prefetchBytes: number | null = null;
|
|
109
|
+
let prefetchMaxFileSize: number | null = null;
|
|
110
|
+
|
|
111
|
+
if (previewType === null) {
|
|
112
|
+
return disabledValue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (column && column.filePreview) {
|
|
116
|
+
if (column.filePreview.disabledTypes.includes(previewType)) {
|
|
117
|
+
return disabledValue;
|
|
118
|
+
}
|
|
119
|
+
prefetchBytes = column.filePreview.getPrefetchBytes(previewType);
|
|
120
|
+
prefetchMaxFileSize = column.filePreview.getPrefetchMaxFileSize(previewType);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof prefetchBytes !== 'number' || prefetchBytes < 0) {
|
|
124
|
+
prefetchBytes = FILE_PREVIEW.PREFETCH_BYTES;
|
|
125
|
+
}
|
|
126
|
+
if (typeof prefetchMaxFileSize !== 'number' || prefetchMaxFileSize < 0) {
|
|
127
|
+
prefetchMaxFileSize = FILE_PREVIEW.MAX_FILE_SIZE;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// if prefetchMaxFileSize is 0, we should not show the preview
|
|
131
|
+
if (prefetchMaxFileSize === 0) {
|
|
132
|
+
return disabledValue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { previewType, prefetchBytes, prefetchMaxFileSize };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Returns the preview type based on the given file properties and the column's file preview settings.
|
|
140
|
+
* @param url the file url
|
|
141
|
+
* @param column the asset column
|
|
142
|
+
* @param storedFilename the stored filename
|
|
143
|
+
* @param contentDisposition content-disposition header value
|
|
144
|
+
* @param contentType content-type header value
|
|
145
|
+
*/
|
|
146
|
+
private static getFilePreviewType(
|
|
147
|
+
url: string,
|
|
148
|
+
column?: AssetPseudoColumn,
|
|
149
|
+
storedFilename?: string,
|
|
150
|
+
contentDisposition?: string,
|
|
151
|
+
contentType?: string,
|
|
152
|
+
): FilePreviewTypes | null {
|
|
153
|
+
const filename = storedFilename || getFilename(url, contentDisposition);
|
|
154
|
+
const extension = getFilenameExtension(filename, column?.filenameExtFilter, column?.filenameExtRegexp);
|
|
155
|
+
let mappedFilePreviewType: FilePreviewTypes | typeof USE_EXT_MAPPING | false = USE_EXT_MAPPING;
|
|
156
|
+
|
|
157
|
+
// extend the mappings based on the annotations
|
|
158
|
+
let annotExtensionMapping;
|
|
159
|
+
let annotContentTypeMapping;
|
|
160
|
+
if (column && column.isAsset) {
|
|
161
|
+
// if file_preview is false, then no preview is allowed
|
|
162
|
+
if (!column.filePreview) return null;
|
|
163
|
+
if (column.filePreview.contentTypeMapping) {
|
|
164
|
+
annotContentTypeMapping = column.filePreview.contentTypeMapping;
|
|
165
|
+
}
|
|
166
|
+
if (column.filePreview.filenameExtMapping) {
|
|
167
|
+
annotExtensionMapping = column.filePreview.filenameExtMapping;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// if content-type is available, we must get the type from it.
|
|
172
|
+
if (typeof contentType === 'string' && contentType.length > 0) {
|
|
173
|
+
// remove any extra info like charset
|
|
174
|
+
contentType = contentType.split(';')[0].trim().toLowerCase();
|
|
175
|
+
let matched = false;
|
|
176
|
+
|
|
177
|
+
// first match through annotation mapping
|
|
178
|
+
if (annotContentTypeMapping) {
|
|
179
|
+
// if the exact match is found, use it
|
|
180
|
+
if (annotContentTypeMapping.exactMatch && contentType in annotContentTypeMapping.exactMatch) {
|
|
181
|
+
mappedFilePreviewType = annotContentTypeMapping.exactMatch[contentType];
|
|
182
|
+
matched = true;
|
|
183
|
+
}
|
|
184
|
+
// if exact match not found, try prefix matching
|
|
185
|
+
else if (annotContentTypeMapping.prefixMatch) {
|
|
186
|
+
const match = Object.keys(annotContentTypeMapping.prefixMatch).find((prefix) => contentType!.startsWith(prefix));
|
|
187
|
+
if (match) {
|
|
188
|
+
mappedFilePreviewType = annotContentTypeMapping.prefixMatch[match];
|
|
189
|
+
matched = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// if still not matched, check for default mapping (`*` in annotation)
|
|
194
|
+
if (!matched && annotContentTypeMapping.default !== null) {
|
|
195
|
+
mappedFilePreviewType = annotContentTypeMapping.default;
|
|
196
|
+
matched = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// if no match found through annotation, try the default mapping
|
|
201
|
+
if (!matched && contentType in DEFAULT_CONTENT_TYPE_MAPPING) {
|
|
202
|
+
mappedFilePreviewType = DEFAULT_CONTENT_TYPE_MAPPING[contentType];
|
|
203
|
+
matched = true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// if no match found, disable the preview
|
|
207
|
+
if (!matched) {
|
|
208
|
+
mappedFilePreviewType = false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
$log.debug(`FilePreviewService: Mapped content-type '${contentType}' to preview type '${mappedFilePreviewType}'`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// use extenstion mapping, if the content-type matching dictates so
|
|
215
|
+
if (mappedFilePreviewType === USE_EXT_MAPPING && typeof extension === 'string' && extension.length > 0) {
|
|
216
|
+
if (annotExtensionMapping && extension in annotExtensionMapping) {
|
|
217
|
+
mappedFilePreviewType = annotExtensionMapping[extension];
|
|
218
|
+
} else if (extension in DEFAULT_EXTENSION_MAPPING) {
|
|
219
|
+
mappedFilePreviewType = DEFAULT_EXTENSION_MAPPING[extension];
|
|
220
|
+
} else {
|
|
221
|
+
mappedFilePreviewType = false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
$log.debug(`FilePreviewService: Mapped extension '${extension}' to preview type '${mappedFilePreviewType}'`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return isFilePreviewType(mappedFilePreviewType) ? mappedFilePreviewType : null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -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
|
package/src/services/logger.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-duplicate-enum-values */
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
2
|
|
|
4
3
|
import { ENV_IS_DEV_MODE } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
5
4
|
|
|
@@ -32,32 +31,32 @@ class Logger {
|
|
|
32
31
|
this._level = level;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
public trace(...args:
|
|
34
|
+
public trace(...args: unknown[]): void {
|
|
36
35
|
if (!this.isAllowed(LoggerLevels.TRACE)) return;
|
|
37
36
|
console.trace(...args);
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
public debug(...args:
|
|
39
|
+
public debug(...args: unknown[]): void {
|
|
41
40
|
if (!this.isAllowed(LoggerLevels.DEBUG)) return;
|
|
42
41
|
console.debug(...args);
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
public info(...args:
|
|
44
|
+
public info(...args: unknown[]): void {
|
|
46
45
|
if (!this.isAllowed(LoggerLevels.INFO)) return;
|
|
47
46
|
console.info(...args);
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
public log(...args:
|
|
49
|
+
public log(...args: unknown[]): void {
|
|
51
50
|
if (!this.isAllowed(LoggerLevels.LOG)) return;
|
|
52
51
|
console.log(...args);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
public warn(...args:
|
|
54
|
+
public warn(...args: unknown[]): void {
|
|
56
55
|
if (!this.isAllowed(LoggerLevels.WARN)) return;
|
|
57
56
|
console.warn(...args);
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
public error(...args:
|
|
59
|
+
public error(...args: unknown[]): void {
|
|
61
60
|
if (!this.isAllowed(LoggerLevels.ERROR)) return;
|
|
62
61
|
console.error(...args);
|
|
63
62
|
}
|
package/src/utils/constants.ts
CHANGED
|
@@ -27,6 +27,11 @@ export const URL_PATH_LENGTH_LIMIT = 4000;
|
|
|
27
27
|
*/
|
|
28
28
|
export const CONTEXT_HEADER_LENGTH_LIMIT = 6500;
|
|
29
29
|
|
|
30
|
+
export const FILE_PREVIEW = {
|
|
31
|
+
PREFETCH_BYTES: 0.5 * 1024 * 1024,
|
|
32
|
+
MAX_FILE_SIZE: 1 * 1024 * 1024,
|
|
33
|
+
};
|
|
34
|
+
|
|
30
35
|
export enum _constraintTypes {
|
|
31
36
|
KEY = 'k',
|
|
32
37
|
FOREIGN_KEY = 'fk',
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* given a url and optional content-disposition header, will return the filename.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: might return an empty string if no filename is found.
|
|
5
|
+
*/
|
|
6
|
+
export const getFilename = (url: string, contentDisposition?: string): string => {
|
|
7
|
+
if (contentDisposition) {
|
|
8
|
+
// try UTF-8 encoded filename first
|
|
9
|
+
const prefixUTF8 = "filename*=UTF-8''";
|
|
10
|
+
let filenameIndex = contentDisposition.indexOf(prefixUTF8);
|
|
11
|
+
if (filenameIndex !== -1) {
|
|
12
|
+
const filename = contentDisposition.substring(filenameIndex + prefixUTF8.length);
|
|
13
|
+
if (filename) return filename.replace(/"/g, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// try standard filename=
|
|
17
|
+
const prefixStandard = 'filename=';
|
|
18
|
+
filenameIndex = contentDisposition.indexOf(prefixStandard);
|
|
19
|
+
if (filenameIndex !== -1) {
|
|
20
|
+
let filename = contentDisposition.substring(filenameIndex + prefixStandard.length);
|
|
21
|
+
// remove quotes and any trailing content after semicolon
|
|
22
|
+
filename = filename.split(';')[0].replace(/"/g, '').trim();
|
|
23
|
+
if (filename) return filename;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// hatrac files have a different format
|
|
28
|
+
// eslint-disable-next-line no-useless-escape
|
|
29
|
+
const parts = url.match(/^\/hatrac\/([^\/]+\/)*([^\/:]+)(:[^:]+)?$/);
|
|
30
|
+
if (parts && parts.length === 4) {
|
|
31
|
+
return parts[2];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// strip query parameters and fragments from URL
|
|
35
|
+
const cleanUrl = url.split('?')[0].split('#')[0];
|
|
36
|
+
return cleanUrl.split('/').pop() || '';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* given a filename, will return the extension
|
|
41
|
+
* By default, it will extract the last of the filename after the last `.` (including the dot).
|
|
42
|
+
* The second parameter can be used for passing a regular expression
|
|
43
|
+
* if we want a different method of extracting the extension.
|
|
44
|
+
* @param {string} filename
|
|
45
|
+
* @param {string[]} allowedExtensions
|
|
46
|
+
* @param {string[]} regexArr
|
|
47
|
+
* @returns the filename extension string. if we cannot find any matches, it will return null
|
|
48
|
+
* @private
|
|
49
|
+
* @ignore
|
|
50
|
+
*/
|
|
51
|
+
export const getFilenameExtension = function (filename: string, allowedExtensions?: string[], regexArr?: string[]): string | null {
|
|
52
|
+
if (typeof filename !== 'string' || filename.length === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// first find in the list of allowed extensions (case-insensitive)
|
|
57
|
+
let res: string | null = null;
|
|
58
|
+
const filenameLower = filename.toLowerCase();
|
|
59
|
+
const isInAllowed =
|
|
60
|
+
Array.isArray(allowedExtensions) &&
|
|
61
|
+
allowedExtensions.some((ext) => {
|
|
62
|
+
res = ext;
|
|
63
|
+
return typeof ext === 'string' && ext.length > 0 && filenameLower.endsWith(ext.toLowerCase());
|
|
64
|
+
});
|
|
65
|
+
if (isInAllowed) {
|
|
66
|
+
return res;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// we will return null if we cannot find anything
|
|
70
|
+
res = null;
|
|
71
|
+
// no matching allowed extension, try the regular expressions
|
|
72
|
+
if (Array.isArray(regexArr) && regexArr.length > 0) {
|
|
73
|
+
regexArr.some((regexp) => {
|
|
74
|
+
// since regular expression comes from annotation, it might not be valid
|
|
75
|
+
try {
|
|
76
|
+
const matches = filename.match(new RegExp(regexp, 'g'));
|
|
77
|
+
if (matches && matches[0] && typeof matches[0] === 'string') {
|
|
78
|
+
res = matches[0];
|
|
79
|
+
} else {
|
|
80
|
+
res = null;
|
|
81
|
+
}
|
|
82
|
+
return res;
|
|
83
|
+
} catch {
|
|
84
|
+
res = null;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
const dotIndex = filename.lastIndexOf('.');
|
|
90
|
+
// it's only a valid filename if there's some string after `.`
|
|
91
|
+
if (dotIndex !== -1 && dotIndex !== filename.length - 1) {
|
|
92
|
+
res = filename.slice(dotIndex).toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return res;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if file is a text-like file based on content type or extension
|
|
101
|
+
*/
|
|
102
|
+
export const checkIsTextFile = (contentType?: string, extension?: string | null): boolean => {
|
|
103
|
+
// text-like files using content-type
|
|
104
|
+
if (
|
|
105
|
+
contentType &&
|
|
106
|
+
(contentType.startsWith('text/') ||
|
|
107
|
+
// cif files
|
|
108
|
+
contentType === 'chemical/x-mmcif' ||
|
|
109
|
+
contentType === 'chemical/x-cif')
|
|
110
|
+
) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// text-like files using extension
|
|
115
|
+
if (extension && ['.txt', '.js', '.log', '.cif', '.pdb'].includes(extension)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if file is markdown based on content type or extension
|
|
124
|
+
*/
|
|
125
|
+
export const checkIsMarkdownFile = (contentType?: string, extension?: string | null): boolean => {
|
|
126
|
+
if (contentType && (contentType.includes('markdown') || contentType.includes('md'))) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (extension && ['.md', '.markdown'].includes(extension)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if file is CSV based on content type or extension
|
|
139
|
+
*/
|
|
140
|
+
export const checkIsCsvFile = (contentType?: string, extension?: string | null): boolean => {
|
|
141
|
+
if (contentType && (contentType.includes('csv') || contentType.includes('comma-separated-values'))) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (extension && extension === '.csv') {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if file is TSV based on content type or extension
|
|
154
|
+
*/
|
|
155
|
+
export const checkIsTsvFile = (contentType?: string, extension?: string | null): boolean => {
|
|
156
|
+
if (contentType && contentType.includes('tab-separated-values')) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (extension && extension === '.tsv') {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return false;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if file is JSON based on content type or extension
|
|
169
|
+
*/
|
|
170
|
+
export const checkIsJSONFile = (contentType?: string, extension?: string | null): boolean => {
|
|
171
|
+
if (contentType && contentType.includes('application/json')) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// mvsj: MolViewSpec JSON (mol* viewer)
|
|
176
|
+
if (extension && ['.json', '.mvsj'].includes(extension)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return false;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if file is an image based on content type or extension
|
|
185
|
+
* Checks for common image formats supported by HTML img tag
|
|
186
|
+
*/
|
|
187
|
+
export const checkIsImageFile = (contentType?: string, extension?: string | null): boolean => {
|
|
188
|
+
// TODO this should be more specific based on supported image formats
|
|
189
|
+
if (contentType && contentType.startsWith('image/')) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (extension && ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico', '.avif', '.apng'].includes(extension)) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return false;
|
|
198
|
+
};
|
|
@@ -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
|
+
}
|