@isrd-isi-edu/ermrestjs 2.0.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.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +55 -0
  3. package/dist/ermrest.d.ts +3481 -0
  4. package/dist/ermrest.js +45 -0
  5. package/dist/ermrest.js.gz +0 -0
  6. package/dist/ermrest.js.map +1 -0
  7. package/dist/ermrest.min.js +45 -0
  8. package/dist/ermrest.min.js.gz +0 -0
  9. package/dist/ermrest.min.js.map +1 -0
  10. package/dist/ermrest.ver.txt +1 -0
  11. package/dist/stats.html +4949 -0
  12. package/js/ag_reference.js +1483 -0
  13. package/js/core.js +4931 -0
  14. package/js/datapath.js +336 -0
  15. package/js/export.js +956 -0
  16. package/js/filters.js +192 -0
  17. package/js/format.js +344 -0
  18. package/js/hatrac.js +1130 -0
  19. package/js/json_ld_validator.js +285 -0
  20. package/js/parser.js +2320 -0
  21. package/js/setup/node.js +27 -0
  22. package/js/utils/helpers.js +2300 -0
  23. package/js/utils/json_ld_schema.js +680 -0
  24. package/js/utils/pseudocolumn_helpers.js +2196 -0
  25. package/package.json +79 -0
  26. package/src/index.ts +204 -0
  27. package/src/models/comment.ts +14 -0
  28. package/src/models/deferred-promise.ts +16 -0
  29. package/src/models/display-name.ts +5 -0
  30. package/src/models/errors.ts +408 -0
  31. package/src/models/path-prefix-alias-mapping.ts +130 -0
  32. package/src/models/reference/bulk-create-foreign-key-object.ts +133 -0
  33. package/src/models/reference/citation.ts +98 -0
  34. package/src/models/reference/contextualize.ts +535 -0
  35. package/src/models/reference/google-dataset-metadata.ts +72 -0
  36. package/src/models/reference/index.ts +14 -0
  37. package/src/models/reference/page.ts +520 -0
  38. package/src/models/reference/reference-aggregate-fn.ts +37 -0
  39. package/src/models/reference/reference.ts +2813 -0
  40. package/src/models/reference/related-reference.ts +467 -0
  41. package/src/models/reference/tuple.ts +652 -0
  42. package/src/models/reference-column/asset-pseudo-column.ts +498 -0
  43. package/src/models/reference-column/column-aggregate.ts +313 -0
  44. package/src/models/reference-column/facet-column.ts +1380 -0
  45. package/src/models/reference-column/foreign-key-pseudo-column.ts +626 -0
  46. package/src/models/reference-column/inbound-foreign-key-pseudo-column.ts +131 -0
  47. package/src/models/reference-column/index.ts +13 -0
  48. package/src/models/reference-column/key-pseudo-column.ts +236 -0
  49. package/src/models/reference-column/pseudo-column.ts +850 -0
  50. package/src/models/reference-column/reference-column.ts +740 -0
  51. package/src/models/source-object-node.ts +156 -0
  52. package/src/models/source-object-wrapper.ts +694 -0
  53. package/src/models/table-source-definitions.ts +98 -0
  54. package/src/services/authn.ts +43 -0
  55. package/src/services/catalog.ts +37 -0
  56. package/src/services/config.ts +202 -0
  57. package/src/services/error.ts +247 -0
  58. package/src/services/handlebars.ts +607 -0
  59. package/src/services/history.ts +136 -0
  60. package/src/services/http.ts +536 -0
  61. package/src/services/logger.ts +70 -0
  62. package/src/services/mustache.ts +0 -0
  63. package/src/utils/column-utils.ts +308 -0
  64. package/src/utils/constants.ts +526 -0
  65. package/src/utils/markdown-utils.ts +855 -0
  66. package/src/utils/reference-utils.ts +1658 -0
  67. package/src/utils/template-utils.ts +0 -0
  68. package/src/utils/type-utils.ts +89 -0
  69. package/src/utils/value-utils.ts +127 -0
  70. package/tsconfig.json +30 -0
  71. package/vite.config.mts +104 -0
@@ -0,0 +1,2300 @@
1
+ /* eslint-disable no-control-regex */
2
+ /* eslint-disable prettier/prettier */
3
+ /* eslint-disable @typescript-eslint/no-unused-vars */
4
+ /* eslint-disable no-useless-escape */
5
+ import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string';
6
+ import moment from 'moment-timezone';
7
+ import { default as mustache } from 'mustache';
8
+
9
+ import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
10
+ import ConfigService from '@isrd-isi-edu/ermrestjs/src/services/config';
11
+ import { InvalidFacetOperatorError } from '@isrd-isi-edu/ermrestjs/src/models/errors';
12
+ import { Reference } from '@isrd-isi-edu/ermrestjs/src/models/reference';
13
+
14
+ // legacy
15
+ import { isObject, isObjectAndNotNull, isValidColorRGBHex, isStringAndNotEmpty } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
16
+ import { fixedEncodeURIComponent } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
17
+ import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils';
18
+ import {
19
+ _systemColumns,
20
+ _dataFormats,
21
+ _contextArray,
22
+ _contexts,
23
+ _annotations,
24
+ _nonSortableTypes,
25
+ _commentDisplayModes,
26
+ _facetingErrors,
27
+ URL_PATH_LENGTH_LIMIT,
28
+ _ERMrestFeatures,
29
+ _systemColumnNames,
30
+ _specialPresentation,
31
+ _classNames,
32
+ TEMPLATE_ENGINES,
33
+ _entryContexts,
34
+ _compactContexts,
35
+ ENV_IS_NODE,
36
+ } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
37
+ import { parse } from '@isrd-isi-edu/ermrestjs/js/parser';
38
+ import { Column, Key } from '@isrd-isi-edu/ermrestjs/js/core';
39
+ import HandlebarsService from '@isrd-isi-edu/ermrestjs/src/services/handlebars';
40
+ import AuthnService from '@isrd-isi-edu/ermrestjs/src/services/authn';
41
+
42
+ /**
43
+ * Given a string represting a JSON document returns the compressed version of it.
44
+ * It will return null if the given string is not a valid JSON.
45
+ * @param {String} str
46
+ * @return {String}
47
+ */
48
+ export function encodeFacetString(str) {
49
+ try {
50
+ JSON.parse(str);
51
+ } catch (e) {
52
+ return "";
53
+ }
54
+ return compressToEncodedURIComponent(str);
55
+ };
56
+
57
+ /**
58
+ * Given an object, returns the string comrpessed version of it
59
+ * @param {Object} obj
60
+ * @return {String}
61
+ * @memberof ERMrest
62
+ * @function encodeFacet
63
+ */
64
+ export function encodeFacet(obj) {
65
+ return compressToEncodedURIComponent(JSON.stringify(obj,null,0));
66
+ };
67
+
68
+ /**
69
+ * Turn a given encoded facet blob into a facet object
70
+ * @param {string} blob the encoded facet blob
71
+ * @param {string?} path (optional) used for better error message
72
+ * @returns {Object}
73
+ * @memberof ERMrest
74
+ * @function decodeFacet
75
+ */
76
+ export function decodeFacet(blob, path) {
77
+ var err = new InvalidFacetOperatorError(
78
+ typeof path === "string" ? path : "",
79
+ _facetingErrors.invalidString
80
+ );
81
+
82
+ try {
83
+ var str = decompressFromEncodedURIComponent(blob);
84
+ if (str === null) {
85
+ throw err;
86
+ }
87
+ return JSON.parse(str);
88
+ } catch (exception) {
89
+ $log.error(exception);
90
+ throw err;
91
+ }
92
+ };
93
+
94
+ /**
95
+ * can be used to compare the "position columns" of colsets.
96
+ *
97
+ * @private
98
+ * @param {Array} a an array of sorted integer values
99
+ * @param {Array} b an array of sorted integer values
100
+ * @param {boolean=} greater - whether we should do greater check instead of greater equal
101
+ *
102
+ * return,
103
+ * - 1 if the position in the first argument are before the second one.
104
+ * - -1 if the other way around.
105
+ * - 0 if identical
106
+ * Notes:
107
+ * - both arguments are array and sorted ascendingly
108
+ * - if greater argument is true, we're doing a greater check so
109
+ * in the identical case this function will return -1.
110
+ */
111
+ export function compareColumnPositions(a, b, greater) {
112
+ for (var i = 0; i < a.length && i < b.length ; i++) {
113
+ if (a[i] !== b[i]) {
114
+ return a[i] > b[i] ? 1 : -1;
115
+ }
116
+ }
117
+ // all the columns were identical and only one has extra
118
+ if (a.length !== b.length) {
119
+ return a.length > b.length ? 1 : -1;
120
+ }
121
+ return greater ? -1 : 0;
122
+ };
123
+
124
+ /**
125
+ * Given an object and two string (k1, k2), if object has k1 key, will
126
+ * rename that key to k2 instead (values that were accessible through k1
127
+ * key name will be moved to k2 instead)
128
+ * @param {Object} obj
129
+ * @param {String} oldKey
130
+ * @param {String} newKey
131
+ */
132
+ export function renameKey(obj, oldKey, newKey) {
133
+ if (!isObjectAndNotNull(obj)) return;
134
+ if (oldKey === newKey) return;
135
+ if (!Object.prototype.hasOwnProperty.call(obj, oldKey)) return;
136
+
137
+ Object.defineProperty(obj, newKey, Object.getOwnPropertyDescriptor(obj, oldKey));
138
+ delete obj[oldKey];
139
+ };
140
+
141
+ /**
142
+ * Replaces characters in strings that are illegal/unsafe for filenames.
143
+ * Unsafe characters are either removed or replaced by a substitute set
144
+ * in the optional `options` object.
145
+ *
146
+ * Illegal Characters on Various Operating Systems
147
+ * / ? < > \ : * | "
148
+ * https://kb.acronis.com/content/39790
149
+ *
150
+ * Unicode Control codes
151
+ * C0 0x00-0x1f & C1 (0x80-0x9f)
152
+ * http://en.wikipedia.org/wiki/C0_and_C1_control_codes
153
+ *
154
+ * Reserved filenames on Unix-based systems (".", "..")
155
+ * Reserved filenames in Windows ("CON", "PRN", "AUX", "NUL", "COM1",
156
+ * "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
157
+ * "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", and
158
+ * "LPT9") case-insesitively and with or without filename extensions.
159
+ *
160
+ * source: https://github.com/parshap/node-sanitize-filename/blob/master/index.js
161
+ *
162
+ * @param {String} str original filename
163
+ * @param {String=} replacement the string that the invalid characters should be replaced with
164
+ * @return {String} sanitized filename
165
+ */
166
+ export function _sanitizeFilename(str, replacement) {
167
+ replacement = (typeof replacement == "string") ? replacement : '_';
168
+
169
+ var illegalRe = /[\/\?<>\\:\*\|":]/g;
170
+ var controlRe = /[\x00-\x1f\x80-\x9f]/g;
171
+ var reservedRe = /^\.+$/;
172
+ var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
173
+ var windowsTrailingRe = /[\. ]+$/;
174
+ return str.replace(illegalRe, replacement)
175
+ .replace(controlRe, replacement)
176
+ .replace(reservedRe, replacement)
177
+ .replace(windowsReservedRe, replacement)
178
+ .replace(windowsTrailingRe, replacement);
179
+ };
180
+
181
+ /**
182
+ * @private
183
+ * @param {Object} child child class
184
+ * @param {Object} parent parent class
185
+ * @desc
186
+ * This function should be called to extend a prototype with another one.
187
+ * Make sure to attach the right constructor to the prototypes after,
188
+ * and also call `child.superClass.call(this, arguments*)` in frist line of
189
+ * the child constructor with appropriate arguments.
190
+ * You can define the extra or overriden functions of child before calling _extends.
191
+ * This function will take care of copying those functions.
192
+ * *Must be called after defining parent prototype and child constructor*
193
+ */
194
+ export function _extends(child, parent) {
195
+ var childFns = child.prototype;
196
+ child.prototype = Object.create(parent.prototype);
197
+ child.prototype.constructor = child;
198
+ child.superClass = parent;
199
+ child.super = parent.prototype;
200
+ };
201
+
202
+ /**
203
+ * Given a string, will return the existing value in the object.
204
+ * It will return undefined if the key doesn't exist or invalid input.
205
+ * @param {Object} obj The object that we want the value from
206
+ * @param {String} path the string path (`a.b.c`)
207
+ * @return {Object} value
208
+ */
209
+ export function _getPath(obj, path) {
210
+ var pathNodes;
211
+
212
+ if (typeof path === "string") {
213
+ if (path.length === 0) {
214
+ return this[""];
215
+ }
216
+ pathNodes = path.split(".");
217
+ } else if (Array.isArray(path)) {
218
+ pathNodes = path;
219
+ } else {
220
+ return undefined;
221
+ }
222
+
223
+ for (var i = 0; i < pathNodes.length; i++) {
224
+ if (!Object.prototype.hasOwnProperty.call(obj, pathNodes[i])) {
225
+ return undefined;
226
+ }
227
+ obj = obj[pathNodes[i]];
228
+ }
229
+ return obj;
230
+ };
231
+
232
+ /**
233
+ * @function
234
+ * @param {String} str string to be converted.
235
+ * @desc
236
+ * Converts a string to title case (separators are space, hyphen, and underscore)
237
+ */
238
+ export function _toTitleCase(str) {
239
+ return str.replace(/([^\x00-\x7F]|([^\W_]))[^\-\s_]*/g, function(txt){
240
+ return txt.charAt(0).toLocaleUpperCase() + txt.substr(1).toLocaleLowerCase();
241
+ });
242
+ };
243
+
244
+ /**
245
+ * @function
246
+ * @param {String} str string to be manipulated.
247
+ * @private
248
+ * @desc
249
+ * Replaces underline with space.
250
+ */
251
+ export function _underlineToSpace(str) {
252
+ return str.replace(/_/g, ' ');
253
+ };
254
+
255
+ /**
256
+ * Given an object recursively replace all the dots in the keys with underscore.
257
+ * This will also remove any custom JavaScript objects.
258
+ * NOTE: This function will ignore any objects that has been created from a custom constructor.
259
+ * NOTE: This function does not detect loop, make sure that your object does not have circular references.
260
+ *
261
+ * @param {Object} obj A simple javascript object. It should not include anything that is not in JSON syntax (functions, etc.).
262
+ * @return {Object} A new object created by:
263
+ * 1. Replacing the dots in keys to underscore.
264
+ * 2. Ignoring any custom-type objects. The given object should be JSON not JavaScript object.
265
+ */
266
+ export function _replaceDotWithUnderscore(obj) {
267
+ var res = {}, val, k, newK;
268
+ for (k in obj) {
269
+ if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
270
+ val = obj[k];
271
+
272
+ // we don't accept custom type objects (we're not detecting circular reference)
273
+ if (isObject(val) && (val.constructor && val.constructor != Object)) continue;
274
+
275
+ newK = k;
276
+ if (k.includes(".")) {
277
+ // replace dot with underscore
278
+ newK = k.replace(/\./g,"_");
279
+ }
280
+
281
+ if (isObject(val)) {
282
+ res[newK] = _replaceDotWithUnderscore(val);
283
+ } else {
284
+ res[newK] = val;
285
+ }
286
+ }
287
+ return res;
288
+ };
289
+
290
+ /**
291
+ * @function
292
+ * @param {String} regExp string to be regular expression encoded
293
+ * @desc converts the string into a regular expression with properly encoded characters
294
+ */
295
+ export function _encodeRegexp(str) {
296
+ var stringReplaceExp = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$]/g;
297
+ // the first '\' escapes the second '\' which is used to escape the matched character in the returned string
298
+ // $& represents the matched character
299
+ var escapedRegexString = str.replace(stringReplaceExp, '\\$&');
300
+
301
+ return escapedRegexString;
302
+ };
303
+
304
+ export function _nextChar(c) {
305
+ return String.fromCharCode(c.charCodeAt(0) + 1);
306
+ };
307
+
308
+ /**
309
+ * @function
310
+ * @param {Object} element a model element (schema, table, or column)
311
+ * @param {boolean} useName determines whether we can use name and name_style or not
312
+ * @param {Object=} parentElement the upper element (schema->null, table->schema, column->table)
313
+ * @desc This function determines the display name for the schema, table, or
314
+ * column elements of a model.
315
+ */
316
+ export function _determineDisplayName(element, useName, parentElement) {
317
+ var value = useName ? element.name : undefined,
318
+ unformatted = useName ? element.name : undefined,
319
+ hasDisplayName = false,
320
+ isHTML = false;
321
+ try {
322
+ var display_annotation = element.annotations.get(_annotations.DISPLAY);
323
+ if (display_annotation && display_annotation.content) {
324
+
325
+ //get the markdown display name
326
+ if(display_annotation.content.markdown_name) {
327
+ value = renderMarkdown(display_annotation.content.markdown_name, true);
328
+ unformatted = display_annotation.content.name ? display_annotation.content.name : display_annotation.content.markdown_name;
329
+ hasDisplayName = true;
330
+ isHTML = true;
331
+ }
332
+ //get the specified display name
333
+ else if (display_annotation.content.name){
334
+ value = display_annotation.content.name;
335
+ unformatted = display_annotation.content.name;
336
+ hasDisplayName = true;
337
+ isHTML = false;
338
+ }
339
+
340
+ //get the name styles
341
+ if(useName && display_annotation.content.name_style){
342
+ element._nameStyle = display_annotation.content.name_style;
343
+ }
344
+ }
345
+ } catch (exception) {
346
+ // no display annotation, don't do anything
347
+ }
348
+
349
+ // if name styles are undefined, get them from the parent element
350
+ // if it's a system column, don't use the name_styles that are defined on the parent.
351
+ // NOTE: underline_space, title_case, markdown might be null.
352
+ if(parentElement && !(element instanceof Column && _systemColumns.indexOf(element.name) !== -1)){
353
+ if(!("underline_space" in element._nameStyle)){
354
+ element._nameStyle.underline_space = parentElement._nameStyle.underline_space;
355
+ }
356
+ if(!("title_case" in element._nameStyle)){
357
+ element._nameStyle.title_case = parentElement._nameStyle.title_case;
358
+ }
359
+ if(!("markdown" in element._nameStyle)){
360
+ element._nameStyle.markdown = parentElement._nameStyle.markdown;
361
+ }
362
+ }
363
+
364
+ // if name was not specified and name styles are defined, apply the heuristic functions (name styles)
365
+ if(useName && !hasDisplayName && element._nameStyle){
366
+ if(element._nameStyle.markdown){
367
+ value = renderMarkdown(element.name, true);
368
+ isHTML = true;
369
+ } else {
370
+ if(element._nameStyle.underline_space){
371
+ value = _underlineToSpace(value);
372
+ unformatted = _underlineToSpace(unformatted);
373
+ }
374
+ if(element._nameStyle.title_case){
375
+ value = _toTitleCase(value);
376
+ unformatted = _toTitleCase(unformatted);
377
+ }
378
+ }
379
+ }
380
+
381
+ return {"isHTML": isHTML, "value": value, "unformatted": unformatted};
382
+ };
383
+
384
+ /**
385
+ * @function
386
+ * @param {string} context the context that we want the value of.
387
+ * @param {Annotation} annotation the annotation object.
388
+ * @param {Boolean=} dontUseDefaultContext Whether we should use the default (*) context
389
+ * @desc This function returns the list that should be used for the given context.
390
+ * Used for visible columns and visible foreign keys.
391
+ */
392
+ export function _getRecursiveAnnotationValue(context, annotation, dontUseDefaultContext) {
393
+ var contextedAnnot = _getAnnotationValueByContext(context, annotation, dontUseDefaultContext);
394
+ if (contextedAnnot !== -1) { // found the context
395
+ if (typeof contextedAnnot == "object" || (_contextArray.indexOf(contextedAnnot) === -1) ) {
396
+ return contextedAnnot;
397
+ } else {
398
+ return _getRecursiveAnnotationValue(contextedAnnot, annotation, dontUseDefaultContext); // go to next level
399
+ }
400
+ }
401
+
402
+ return -1; // there was no annotation
403
+ };
404
+
405
+ /**
406
+ * @param {string} context the context that we want the value of.
407
+ * @param {Object} annotation the annotation object.
408
+ * @param {Boolean=} dontUseDefaultContext Whether we should use the default (*) context
409
+ * @desc returns the annotation value based on the given context.
410
+ */
411
+ export function _getAnnotationValueByContext(context, annotation, dontUseDefaultContext) {
412
+
413
+ // check annotation is an object
414
+ if (typeof annotation !== "object" || annotation == null) {
415
+ return -1;
416
+ }
417
+
418
+ if (typeof context === "string") {
419
+ // NOTE: We assume that context names are seperated with `/`
420
+ var partial = context,
421
+ parts = context.split("/");
422
+ while (partial !== "") {
423
+ if (partial in annotation) { // found the context
424
+ return annotation[partial];
425
+ }
426
+ parts.splice(-1,1); // remove the last part
427
+ partial = parts.join("/");
428
+ }
429
+ }
430
+
431
+ // if context wasn't in the annotations but there is a default context
432
+ if (dontUseDefaultContext !== true && _contexts.DEFAULT in annotation) {
433
+ return annotation[_contexts.DEFAULT];
434
+ }
435
+
436
+ return -1; // there was no annotation
437
+ };
438
+
439
+ /**
440
+ * retun the value that should be used for the display setting. If missing, it will return "-1".
441
+ *
442
+ * @param {Table|ERMrest.Column|ERMrest.ForeignKeyRef} obj either table object, or an object that has `.table`
443
+ * @param {String} context the context string
444
+ * @param {String} annotKey the annotation key that you want the annotation value for
445
+ * @param {Boolean} isTable if the first parameter is table, you should pass `true` for this parameter
446
+ */
447
+ export function _getHierarchicalDisplayAnnotationValue(obj, context, annotKey, isTable) {
448
+ var hierarichy = [obj], table, annot, value = -1;
449
+ var displayAnnot = _annotations.DISPLAY;
450
+
451
+ if (!isTable) {
452
+ table = obj.table;
453
+ hierarichy.push(obj.table);
454
+ } else {
455
+ table = obj;
456
+ }
457
+ hierarichy.push(table.schema, table.schema.catalog);
458
+
459
+ for (var i = 0; i < hierarichy.length; i++) {
460
+ if (!hierarichy[i].annotations.contains(displayAnnot)) continue;
461
+
462
+ annot = hierarichy[i].annotations.get(displayAnnot);
463
+ if (annot && annot.content && annot.content[annotKey]) {
464
+ value = _getAnnotationValueByContext(context, annot.content[annotKey]);
465
+ if (value !== -1) break;
466
+ }
467
+ }
468
+
469
+ return value;
470
+ };
471
+
472
+ /**
473
+ * @param {object} ref The object that we want the null value for.
474
+ * @param {string} context The context that we want the value of.
475
+ * @param {Array} elements All the possible levels of heirarchy (column, table, schema).
476
+ * @desc returns the null value for the column based on context and annotation and sets in the ref object too.
477
+ */
478
+ export function _getNullValue(ref, context, isTable) {
479
+ if (context in ref._nullValue) { // use the cached value
480
+ return ref._nullValue[context];
481
+ }
482
+
483
+ var value = _getHierarchicalDisplayAnnotationValue(ref, context, "show_null", isTable);
484
+
485
+ // backward compatibility: try show_nulls too
486
+ // TODO eventually should be removed
487
+ if (value === -1) {
488
+ value = _getHierarchicalDisplayAnnotationValue(ref, context, "show_nulls", isTable);
489
+ }
490
+
491
+ if (value === false) { //eliminate the field
492
+ value = null;
493
+ } else if (value === true) { //empty field
494
+ value = "";
495
+ } else if (typeof value !== "string") { // default
496
+ if (context === _contexts.DETAILED) {
497
+ value = null; // default null value for DETAILED context
498
+ } else {
499
+ value = ""; //default null value
500
+ }
501
+ }
502
+
503
+ ref._nullValue[context] = value; // cache the value
504
+ return value;
505
+ };
506
+
507
+ /**
508
+ * @param {Annotations} annotations - the defined annotation on the model
509
+ * @param {String} key - the annotation key
510
+ * @param {Boolean|null} defaultValue - the value that should be used if annotation is missing.
511
+ * (parent value or null)
512
+ * Returns:
513
+ * - true: if annotation is defined and it's not `false`.
514
+ * - false: if annotation is defined and it's `false`
515
+ * - defaultValue: if annotation is not defined
516
+ * @private
517
+ */
518
+ export function _processACLAnnotation(annotations, key, defaultValue) {
519
+ if (annotations.contains(key)) {
520
+ var ndAnnot = annotations.get(key).content;
521
+ if (ndAnnot !== false) {
522
+ return true;
523
+ }
524
+ return false;
525
+ }
526
+ return defaultValue;
527
+ };
528
+
529
+ // given a reference and associated data to it, will return a list of Values
530
+ // corresponding to its sort object
531
+ export function _getPagingValues(ref, rowData, rowLinkedData) {
532
+ if (!rowData) {
533
+ return null;
534
+ }
535
+ var loc = ref.location,
536
+ values = [], addedCols = {}, sortObjectNames = {},
537
+ col, i, j, sortCol, colName, data, fkData;
538
+
539
+ for (i = 0; i < loc.sortObject.length; i++) {
540
+ colName = loc.sortObject[i].column;
541
+
542
+ try {
543
+ col = ref.getColumnByName(colName);
544
+ } catch (e) {
545
+ return null; // column doesn't exist return null.
546
+ }
547
+
548
+ // avoid duplicate sort columns
549
+ if (col.name in sortObjectNames) continue;
550
+ sortObjectNames[col.name] = true;
551
+
552
+ if (!col.sortable) {
553
+ return null;
554
+ }
555
+
556
+ for (j = 0; j < col._sortColumns.length; j++) {
557
+ sortCol = col._sortColumns[j].column;
558
+
559
+ // avoid duplciate columns
560
+ if (sortCol in addedCols) continue;
561
+ addedCols[sortCol] = true;
562
+
563
+ if (col.isForeignKey || (col.isPathColumn && col.isUnique && col.hasPath)) {
564
+ fkData = rowLinkedData[col.name];
565
+ data = null;
566
+ if (isObjectAndNotNull(fkData)) {
567
+ data = fkData[sortCol.name];
568
+ }
569
+ } else {
570
+ data = rowData[sortCol.name];
571
+ }
572
+ values.push(data);
573
+ }
574
+ }
575
+ return values;
576
+ };
577
+
578
+ /**
579
+ * Process the given list of column order, and return the appropriate list
580
+ * of objects that have:
581
+ * - `column`: The {@link ERMrest.Column} object.
582
+ * - `descending`: The boolean that Indicates whether we should reverse sort order or not.
583
+ *
584
+ * @param {string} columnOrder The object that defines the column/row order
585
+ * @param {Table} table
586
+ * @param {Object=} options the extra options:
587
+ * - allowNumOccurrences: to allow the specific frequency column_order
588
+ * @return {Array=} If it's undefined, the column_order that is defined is not valid
589
+ * @private
590
+ */
591
+ export function _processColumnOrderList(columnOrder, table, options) {
592
+ options = options || {};
593
+
594
+ if (columnOrder === false) {
595
+ return false;
596
+ }
597
+
598
+ var res, colName, descending, colNames = {}, numOccurr = false;
599
+ if (Array.isArray(columnOrder)) {
600
+ res = [];
601
+ for (var i = 0 ; i < columnOrder.length; i++) {
602
+ try {
603
+ if (typeof columnOrder[i] === "string") {
604
+ colName = columnOrder[i];
605
+ } else if (columnOrder[i] && columnOrder[i].column) {
606
+ colName = columnOrder[i].column;
607
+ } else if (options.allowNumOccurrences && !numOccurr && columnOrder[i] && columnOrder[i].num_occurrences) {
608
+ numOccurr = true;
609
+ // add the frequency sort
610
+ res.push({num_occurrences: true, descending: (columnOrder[i] && columnOrder[i].descending === true)});
611
+
612
+ continue;
613
+ } else {
614
+ continue; // invalid syntax
615
+ }
616
+
617
+ const col = table.columns.get(colName);
618
+
619
+ // make sure it's sortable
620
+ if (_nonSortableTypes.indexOf(col.type.name) !== -1) {
621
+ continue;
622
+ }
623
+
624
+ // avoid duplicates
625
+ if (colName in colNames) {
626
+ continue;
627
+ }
628
+ colNames[colName] = true;
629
+
630
+ descending = (columnOrder[i] && columnOrder[i].descending === true);
631
+ res.push({
632
+ column: col,
633
+ descending: descending,
634
+ });
635
+ } catch(exception) {
636
+ // ignore
637
+ }
638
+ }
639
+ }
640
+ return res; // it might be undefined
641
+ };
642
+
643
+ /**
644
+ * Given the source object and default comment props, will return the comment that should be used.
645
+ * @returns {CommentType}
646
+ * @private
647
+ */
648
+ export function _processSourceObjectComment(sourceObject, defaultComment, defaultCommentRenderMd, defaultDisplayMode) {
649
+ if (sourceObject && _isValidModelComment(sourceObject.comment)) {
650
+ defaultComment = sourceObject.comment;
651
+ }
652
+ if (sourceObject && _isValidModelCommentDisplay(sourceObject.comment_display)) {
653
+ defaultDisplayMode = sourceObject.comment_display;
654
+ }
655
+ if (sourceObject && typeof sourceObject.comment_render_markdown === 'boolean') {
656
+ defaultCommentRenderMd = sourceObject.comment_render_markdown;
657
+ }
658
+ return _processModelComment(defaultComment, defaultCommentRenderMd, defaultDisplayMode);
659
+ };
660
+
661
+ /**
662
+ * Turn a comment annotaiton/string value into a proper comment object.
663
+ * @param {string|null|false} comment
664
+ * @param {boolean=} isMarkdown whether the given comment should be rendered as markdown (default: true).
665
+ * @param {string=} displayMode the display mode of the comment (inline, tooltip)
666
+ * @private
667
+ */
668
+ export function _processModelComment(comment, isMarkdown, displayMode) {
669
+ if (comment !== false && typeof comment !== 'string') {
670
+ return null;
671
+ }
672
+
673
+ var usedDisplayMode = _isValidModelCommentDisplay(displayMode) ? displayMode : _commentDisplayModes.tooltip;
674
+ if (comment === false) {
675
+ return { isHTML: false, unformatted: '', value: '', displayMode: usedDisplayMode };
676
+ }
677
+
678
+ return {
679
+ isHTML: isMarkdown !== false,
680
+ unformatted: comment,
681
+ value: (isMarkdown !== false && comment.length > 0) ? renderMarkdown(comment) : comment,
682
+ displayMode: usedDisplayMode
683
+ };
684
+ };
685
+
686
+ /**
687
+ * Given an input string for the comment, will return true or false depending if the comment is of a valid type and value
688
+ * - if =string : returns true.
689
+ * - if =false: returns true.
690
+ * - otherwise returns false
691
+ * @private
692
+ */
693
+ export function _isValidModelComment(comment) {
694
+ return typeof comment === "string" || comment === false;
695
+ };
696
+
697
+ /**
698
+ * Given an input string for the comment display, will return true or false depending if the display value is of a valid type and value
699
+ * - if =string && (="tooltip" || ="inline") : returns true.
700
+ * - otherwise returns false
701
+ * @private
702
+ */
703
+ export function _isValidModelCommentDisplay(display) {
704
+ return typeof display === "string" && _commentDisplayModes[display] !== -1;
705
+ };
706
+
707
+ /**
708
+ * Given a foreign key name, will return true or false depending if the name value is of a valid type and value
709
+ * - if =['', ''] : returns true
710
+ * - otherwise returns false
711
+ *
712
+ * @private
713
+ */
714
+ export function _isValidForeignKeyName(fkName) {
715
+ return Array.isArray(fkName) && fkName.length === 2 && typeof fkName[0] === 'string' && typeof fkName[1] === 'string';
716
+ };
717
+
718
+ /**
719
+ * Given input value for bulk_create_foreign_key, will return true or false depending if the value is of a valid type and value for the bulk_create_foreign_key
720
+ * - if =false | =null | =['', ''] : returns true
721
+ * - otherwise returns false
722
+ *
723
+ * @private
724
+ */
725
+ export function _isValidBulkCreateForeignKey(bulkCreateProp) {
726
+ return bulkCreateProp === false || bulkCreateProp === null || _isValidForeignKeyName(bulkCreateProp);
727
+ };
728
+
729
+ /**
730
+ * @function
731
+ * @param {Table} table The object that we want the formatted values for.
732
+ * @param {String} context the context that we want the formatted values for.
733
+ * @param {object} data The object which contains key value pairs of data to be transformed
734
+ * @param {object=} linkedData The object which contains key value paris of foreign key data.
735
+ * @return {any} A formatted keyvalue pair of object
736
+ * @desc Returns a formatted keyvalue pairs of object as a result of using `col.formatvalue`.
737
+ * If you want the formatted value of a single column, you should call formatvalue,
738
+ * this function is written for the purpose of being used in markdown.
739
+ * @private
740
+ */
741
+ export function _getFormattedKeyValues(table, context, data, linkedData) {
742
+ var keyValues, k, fkData, col, cons, rowname, v;
743
+
744
+ var getTableValues = function (d, currTable) {
745
+ var res = {};
746
+ currTable.sourceDefinitions.columns.forEach(function (col) {
747
+ if (!(col.name in d)) return;
748
+
749
+ try {
750
+ k = col.name;
751
+ v = col.formatvalue(d[k], context);
752
+ if (col.type.isArray) {
753
+ v = _formatUtils.printArray(v, {isMarkdown: true});
754
+ }
755
+
756
+ res[k] = v;
757
+ res["_" + k] = d[k];
758
+
759
+ // alternative names
760
+ // TODO this should change to allow usage of table column names.
761
+ if (Array.isArray(currTable.sourceDefinitions.sourceMapping[k]) ){
762
+ currTable.sourceDefinitions.sourceMapping[k].forEach(function (altKey) {
763
+ res[altKey] = v;
764
+ res["_" + altKey] = d[k];
765
+ });
766
+ }
767
+ } catch (e) {
768
+ // if the value is invalid (for example hatrac TODO can be imporved)
769
+ res[k] = d[k];
770
+ res["_" + k] = d[k];
771
+ }
772
+ });
773
+ return res;
774
+ };
775
+
776
+ // get the data from current table
777
+ keyValues = getTableValues(data, table);
778
+
779
+ //get foreignkey data if available
780
+ if (linkedData && typeof linkedData === "object" && table.sourceDefinitions.fkeys.length > 0) {
781
+ keyValues.$fkeys = {};
782
+ table.sourceDefinitions.fkeys.forEach(function (fk) {
783
+ var p = _generateRowLinkProperties(fk.key, linkedData[fk.name], context);
784
+ if (!p) return;
785
+
786
+ cons = fk.constraint_names[0];
787
+ if (!keyValues.$fkeys[cons[0]]) {
788
+ keyValues.$fkeys[cons[0]] = {};
789
+ }
790
+
791
+ var fkTempVal = {
792
+ "values": getTableValues(linkedData[fk.name], fk.key.table),
793
+ "rowName": p.unformatted,
794
+ "uri": {
795
+ "detailed": p.reference.contextualize.detailed.appLink
796
+ }
797
+ };
798
+
799
+ // the new format
800
+ keyValues["$fkey_" + cons[0] + "_" + cons[1]] = fkTempVal;
801
+
802
+ // the old format
803
+ keyValues.$fkeys[cons[0]][cons[1]] = fkTempVal;
804
+ });
805
+ }
806
+
807
+ return keyValues;
808
+ };
809
+
810
+ /**
811
+ * @param {string[]} columnNames Array of column names
812
+ * @return {string|false} the column name. if couldn't find any columns will return false.
813
+ * @private
814
+ */
815
+ export function _getCandidateRowNameColumn(columnNames) {
816
+ var candidates = [
817
+ 'title', 'name', 'term', 'label', 'accessionid', 'accessionnumber'
818
+ ];
819
+
820
+ var removeExtra = function (str) { // remove `.`, `-`, `_`, and space
821
+ return str.replace(/[\.\s\_-]+/g, "").toLocaleLowerCase();
822
+ };
823
+
824
+ for (var i = 0; i < candidates.length; i++) {
825
+ for (var j = 0; j < columnNames.length; j++) {
826
+ if (candidates[i] === removeExtra(columnNames[j])) {
827
+ return columnNames[j];
828
+ }
829
+ }
830
+ }
831
+
832
+ // no candidate columns found
833
+ return false;
834
+ };
835
+
836
+ /**
837
+ * returns an object with the following attributes:
838
+ * - values: the formatted and unformatted values
839
+ * - rowName: a rowname object.
840
+ * - uri.detailed: applink to detailed for the row
841
+ * @private
842
+ * @param {Table} table the table object
843
+ * @param {string} context current context
844
+ * @param {Object} data the raw data
845
+ * @param {any=} linkedData the raw data of foreignkeys
846
+ * @param {Key=} key the alternate key to use
847
+ * @return {{values: Record<string, any>, rowName: {value: string, isHTML: boolean, unformatted: string}, uri: {detailed: string}}}
848
+ */
849
+ export function _getRowTemplateVariables(table, context, data, linkedData, key) {
850
+ var uri = _generateRowURI(table, data, key);
851
+ if (uri == null) return {};
852
+ var ref = new Reference(parse(uri), table.schema.catalog);
853
+ return {
854
+ values: _getFormattedKeyValues(table, context, data, linkedData),
855
+ rowName: _generateRowName(table, context, data, linkedData).unformatted,
856
+ uri: {
857
+ detailed: ref.contextualize.detailed.appLink
858
+ }
859
+ };
860
+ };
861
+
862
+ /**
863
+ * Given the available linked data, generate the uniqueId for the row this data represents given the shortest key of the table
864
+ *
865
+ * @param {Column[]} tableShortestKey shortest key from the table the linkedData is for
866
+ * @param {Object} data data to use to generate the unique id
867
+ * @returns string | null - unique id for the row the linkedData represents
868
+ */
869
+ export function _generateTupleUniqueId(tableShortestKey, data) {
870
+ let hasNull = false, _uniqueId = "";
871
+
872
+ for (var i = 0; i < tableShortestKey.length; i++) {
873
+ const col = tableShortestKey[i];
874
+ const keyName = col.name;
875
+ if (data[keyName] == null) {
876
+ hasNull = true;
877
+ break;
878
+ }
879
+ if (i !== 0) _uniqueId += "_";
880
+ const isJSON = col.type.name === 'json' || col.type.name === 'jsonb';
881
+ // if the column is JSON, we need to stringify it otherwise it will print [object Object]
882
+ _uniqueId += isJSON ? JSON.stringify(data[keyName], undefined, 0) : data[keyName];
883
+ }
884
+
885
+ if (hasNull) {
886
+ _uniqueId = null;
887
+ }
888
+
889
+ return _uniqueId;
890
+ };
891
+
892
+ /**
893
+ * @function
894
+ * @param {Table} table The table that we want the row name for.
895
+ * @param {String} context Current context.
896
+ * @param {object} data The object which contains key value pairs of data.
897
+ * @param {Object} linkedData The object which contains key value pairs of foreign key data.
898
+ * @param {boolean} isTitle determines Whether we want rowname for title or not
899
+ * @returns {{value: string, isHTML: boolean, unformatted: string}} The displayname object for the row. It includes has value, isHTML, and unformatted.
900
+ * @desc Returns the row name (html) using annotation or heuristics.
901
+ * @private
902
+ */
903
+ export function _generateRowName(table, context, data, linkedData, isTitle) {
904
+ var annotation, col, template, keyValues, pattern, actualContext;
905
+
906
+ var templateVariables = _getFormattedKeyValues(table, context, data, linkedData);
907
+
908
+ // If table has table-display annotation then set it in annotation variable
909
+ if (table.annotations && table.annotations.contains(_annotations.TABLE_DISPLAY)) {
910
+ actualContext = isTitle ? "title" : (typeof context === "string" && context !== "*" ? context : "");
911
+ annotation = _getRecursiveAnnotationValue(
912
+ [_contexts.ROWNAME, actualContext].join("/"),
913
+ table.annotations.get(_annotations.TABLE_DISPLAY).content
914
+ );
915
+ }
916
+
917
+ // if annotation is populated and annotation has display.rowName property
918
+ if (annotation && typeof annotation.row_markdown_pattern === 'string') {
919
+ template = annotation.row_markdown_pattern;
920
+
921
+ pattern = _renderTemplate(template, templateVariables, table.schema.catalog, {templateEngine: annotation.template_engine});
922
+
923
+ }
924
+
925
+ // annotation was not defined, or it's producing empty string.
926
+ if (pattern == null || pattern.trim() === '') {
927
+
928
+ // no row_name annotation, use column with title, name, term
929
+ var candidate = _getCandidateRowNameColumn(Object.keys(data)), result;
930
+ if (candidate !== false) {
931
+ result = templateVariables[candidate];
932
+ }
933
+
934
+ if (!result) {
935
+
936
+ // no title, name, term, label column: use id:text type
937
+ // Check for id column whose type should not be integer or serial
938
+ var idCol = table.columns.all().filter(function (c) {
939
+ return ((c.name.toLowerCase() === "id") && (c.type.name.indexOf('serial') === -1) && (c.type.name.indexOf('int') === -1));
940
+ });
941
+
942
+
943
+ // no id:text, use the unique key
944
+ // If id column exists
945
+ if (idCol.length && typeof data[idCol[0].name] === 'string') {
946
+
947
+ result = templateVariables[idCol[0].name];
948
+
949
+ } else {
950
+
951
+ // Get the columns for displaykey
952
+ var keyColumns = table.displayKey;
953
+
954
+ // TODO this check needs to change. it is supposed to check if the table has a key or not
955
+ // if (keyColumns.length >= table.columns.length) {
956
+ // return null;
957
+ // }
958
+
959
+ var values = [];
960
+
961
+ // Iterate over the keycolumns to get their formatted values for `row_name` context
962
+ keyColumns.forEach(function (c) {
963
+ var value = templateVariables[c.name];
964
+ values.push(value);
965
+ });
966
+
967
+ /*
968
+ * join all values by ':' to get the display_name
969
+ * Eg: displayName for values=["12", "DNA results for human specimen"] would be
970
+ * "12:DNA results for human specimen"
971
+ */
972
+ result = values.join(':');
973
+ }
974
+ }
975
+
976
+ template = "{{{name}}}";
977
+ keyValues = {"name": result};
978
+
979
+ // get templated patten after replacing the values using Mustache
980
+ pattern = _renderTemplate(template, keyValues, table.schema.catalog);
981
+ }
982
+
983
+ // Render markdown content for the pattern
984
+ if (pattern == null || pattern.trim() === '') {
985
+ return {"value": "", "unformatted": ""};
986
+ }
987
+
988
+ return {
989
+ "value": renderMarkdown(pattern, true),
990
+ "unformatted": pattern,
991
+ "isHTML": true
992
+ };
993
+
994
+ };
995
+
996
+ /**
997
+ * @function
998
+ * @desc Given a key object, will return the presentation object that can bse used for it
999
+ * @param {Key} key the key object
1000
+ * @param {object} data the data for the table that key is from
1001
+ * @param {string} context the context string
1002
+ * @param {object=} templateVariables
1003
+ * @return {object} the presentation object that can be used for the key
1004
+ * (it has `isHTML`, `value`, and `unformatted`).
1005
+ * NOTE the function might return `null`.
1006
+ * @private
1007
+ */
1008
+ export function _generateKeyPresentation(key, data, context, templateVariables, addLink) {
1009
+ // if data is empty
1010
+ if (typeof data === "undefined" || data === null || Object.keys(data).length === 0) {
1011
+ return null;
1012
+ }
1013
+
1014
+ var value, caption, unformatted, i;
1015
+ var cols = key.colset.columns,
1016
+ rowURI = _generateRowURI(key.table, data, key);
1017
+
1018
+ // if any of key columns don't have data, this link is not valid.
1019
+ if (rowURI == null) {
1020
+ return null;
1021
+ }
1022
+
1023
+ // make sure that templateVariables is defined
1024
+ if (!isObjectAndNotNull(templateVariables)) {
1025
+ templateVariables = _getFormattedKeyValues(key.table, context, data);
1026
+ }
1027
+
1028
+ // use the markdown_pattern that is defiend in key-display annotation
1029
+ var display = key.getDisplay(context);
1030
+ if (display.isMarkdownPattern) {
1031
+ unformatted = _renderTemplate(
1032
+ display.markdownPattern,
1033
+ templateVariables,
1034
+ key.table.schema.catalog,
1035
+ {templateEngine: display.templateEngine}
1036
+ );
1037
+ unformatted = (unformatted === null || unformatted.trim() === '') ? "" : unformatted;
1038
+ caption = renderMarkdown(unformatted, true);
1039
+ } else {
1040
+ var values = [], unformattedValues = [];
1041
+
1042
+ // create the caption
1043
+ var presentation;
1044
+ for (i = 0; i < cols.length; i++) {
1045
+ try {
1046
+ presentation = cols[i].formatPresentation(data, context, templateVariables);
1047
+ values.push(presentation.value);
1048
+ unformattedValues.push(presentation.unformatted);
1049
+ } catch (exception) {
1050
+ // the value doesn't exist
1051
+ return null;
1052
+ }
1053
+ }
1054
+ caption = values.join(":");
1055
+ unformatted = unformattedValues.join(":");
1056
+
1057
+ // if the caption is empty we cannot add any link to that.
1058
+ if (caption.trim() === '') {
1059
+ return null;
1060
+ }
1061
+ }
1062
+
1063
+ if (!addLink || caption.match(/<a\b.+href=/)) {
1064
+ value = caption;
1065
+ } else {
1066
+ var keyRef = new Reference(parse(rowURI), key.table.schema.catalog);
1067
+ var appLink = keyRef.contextualize.detailed.appLink;
1068
+
1069
+ value = '<a href="' + appLink +'">' + caption + '</a>';
1070
+ unformatted = "[" + unformatted + "](" + appLink + ")";
1071
+ }
1072
+
1073
+ return {isHTML: true, value: value, unformatted: unformatted};
1074
+ };
1075
+
1076
+ /**
1077
+ * @function
1078
+ * @private
1079
+ * @desc Given the key of a table, and data for one row will return the
1080
+ * presentation object for the row.
1081
+ * @param {Key} key the key of the table
1082
+ * @param {String} context Current context
1083
+ * @param {object} data Data for the table that this key is referring to.
1084
+ * @param {boolean} addLink whether the function should attach link or just the rowname.
1085
+ * @return an object with `caption`, and `reference` object which can be used for getting uri.
1086
+ */
1087
+ export function _generateRowPresentation(key, data, context, addLink) {
1088
+ var presentation = _generateRowLinkProperties(key, data, context);
1089
+
1090
+ if (!presentation) {
1091
+ return null;
1092
+ }
1093
+
1094
+ var value, unformatted, appLink;
1095
+
1096
+ // if we don't want link, or caption has a link, or or context is EDIT: don't add the link.
1097
+ // create the link using reference.
1098
+ if (!addLink || presentation.caption.match(/<a\b.+href=/) || _isEntryContext(context)) {
1099
+ value = presentation.caption;
1100
+ unformatted = presentation.unformatted;
1101
+ } else {
1102
+ appLink = presentation.reference.contextualize.detailed.appLink;
1103
+ value = '<a href="' + appLink + '">' + presentation.caption + '</a>';
1104
+ unformatted = "[" + presentation.unformatted + "](" + appLink + ")";
1105
+ }
1106
+
1107
+ return {isHTML: true, value: value, unformatted: unformatted};
1108
+ };
1109
+
1110
+ /**
1111
+ * Given a table object and raw data for a row, return a uri to that row with fitlers.
1112
+ * @param {Table} table the table object
1113
+ * @param {Object} raw data for the row
1114
+ * @param {Key=} key if we want the link based on a specific key
1115
+ * @return {String|null} filter that represents the current row. If row data
1116
+ * is missing, it will return null.
1117
+ */
1118
+ export function _generateRowURI(table, data, key) {
1119
+ if (data == null) return null;
1120
+
1121
+ var cols = (isObjectAndNotNull(key) && key.colset) ? key.colset.columns : table.shortestKey;
1122
+ var keyPair = "", col, i;
1123
+ for (i = 0; i < cols.length; i++) {
1124
+ col = cols[i].name;
1125
+ if (data[col] == null) return null;
1126
+ keyPair += fixedEncodeURIComponent(col) + "=" + fixedEncodeURIComponent(data[col]);
1127
+ if (i != cols.length - 1) {
1128
+ keyPair += "&";
1129
+ }
1130
+ }
1131
+ return table.uri + "/" + keyPair;
1132
+ };
1133
+
1134
+ /**
1135
+ * @function
1136
+ * @private
1137
+ * @param {Key} key key of the table
1138
+ * @param {string} context current context
1139
+ * @param {object} data data for the table that this key is referring to
1140
+ * @return {object} an object with the following attributes:
1141
+ * - `caption`: The caption that can be used to refer to this row in a link
1142
+ * - `unformatted`: The unformatted version of caption.
1143
+ * - `refernece`: The reference object that can be used for generating link to the row
1144
+ * @desc
1145
+ * Creates the properies for generating a link to the given row of data.
1146
+ * It might return `null`.
1147
+ */
1148
+ export function _generateRowLinkProperties(key, data, context) {
1149
+
1150
+ // if data is empty
1151
+ if (typeof data === "undefined" || data === null || Object.keys(data).length === 0) {
1152
+ return null;
1153
+ }
1154
+
1155
+ var value, rowname, i, caption, unformatted;
1156
+ var table = key.table;
1157
+ var rowURI = _generateRowURI(table, data, key);
1158
+
1159
+ // if any of key columns don't have data, this link is not valid.
1160
+ if (rowURI == null) {
1161
+ return null;
1162
+ }
1163
+
1164
+ // use row name as the caption
1165
+ rowname = _generateRowName(table, context, data);
1166
+ caption = rowname.value;
1167
+ unformatted = rowname.unformatted;
1168
+
1169
+ // use key for displayname: "col_1:col_2:col_3"
1170
+ if (caption.trim() === '') {
1171
+ var templateVariables = _getFormattedKeyValues(table, context, data),
1172
+ formattedKeyCols = [],
1173
+ unformattedKeyCols = [],
1174
+ pres, col;
1175
+
1176
+ for (i = 0; i < key.colset.columns.length; i++) {
1177
+ col = key.colset.columns[i];
1178
+ pres = col.formatPresentation(data, context, {templateVariables: templateVariables});
1179
+ formattedKeyCols.push(pres.value);
1180
+ unformattedKeyCols.push(pres.unformatted);
1181
+ }
1182
+ caption = formattedKeyCols.join(":");
1183
+ unformatted = unformattedKeyCols.join(":");
1184
+
1185
+ if (caption.trim() === '') {
1186
+ return null;
1187
+ }
1188
+ }
1189
+
1190
+ // use the shortest key if it has data (for shorter url).
1191
+ var shortestKeyURI = _generateRowURI(table, data);
1192
+ if (shortestKeyURI != null) {
1193
+ rowURI = shortestKeyURI;
1194
+ }
1195
+
1196
+ return {
1197
+ unformatted: unformatted,
1198
+ caption: caption,
1199
+ reference: new Reference(parse(rowURI), table.schema.catalog)
1200
+ };
1201
+ };
1202
+
1203
+ /**
1204
+ * Generate the filter based on the given key and data.
1205
+ * The return object has the following properties:
1206
+ * - successful: whether we encounter any issues or not
1207
+ * - filters: If successful, it will be an array of {path, keyData}
1208
+ * - hasNull: If failed, it will signal that the issue was related to null value
1209
+ * for a column. `column` property will return the column name that had null value.
1210
+ * @param {Column[]} keyColumns
1211
+ * @param {Object} data
1212
+ * @param {Catalog} catalogObject
1213
+ * @param {number} pathOffsetLength the length of offset that should be considered for length limitation logic.
1214
+ * if the given value is negative, we will not check the url length limitation.
1215
+ * @param {string} displayname the displayname of reference, used for error message
1216
+ */
1217
+ export function generateKeyValueFilters(keyColumns, data, catalogObject, pathOffsetLength, displayname) {
1218
+ var encode = fixedEncodeURIComponent, pathLimit = URL_PATH_LENGTH_LIMIT;
1219
+
1220
+ // see if the quantified syntax can be used
1221
+ var canUseQuantified = false;
1222
+ if (keyColumns.length > 1 || data.length === 1) {
1223
+ canUseQuantified = false;
1224
+ }
1225
+ else if (catalogObject && keyColumns.length === 1 && keyColumns[0].name === _systemColumnNames.RID) {
1226
+ canUseQuantified = catalogObject.features[_ERMrestFeatures.QUANTIFIED_RID_LISTS];
1227
+ } else if (catalogObject) {
1228
+ canUseQuantified = catalogObject.features[_ERMrestFeatures.QUANTIFIED_VALUE_LISTS];
1229
+ }
1230
+
1231
+ var result = []; // computed, keyData
1232
+
1233
+ var keyData = [], filter = '', currentPath = '', keyColName, keyColVal, keyColumnsData;
1234
+ for (var rowIndex = 0; rowIndex < data.length; rowIndex++) {
1235
+ var rowData = data[rowIndex];
1236
+ if (canUseQuantified) {
1237
+ keyColName = keyColumns[0].name;
1238
+ keyColVal = rowData[keyColName];
1239
+ keyColumnsData = {};
1240
+
1241
+ if (keyColVal === undefined || keyColVal === null) {
1242
+ return {
1243
+ successful: false,
1244
+ message: "One or more " + displayname + " records have a null value for " + keyColName + ".",
1245
+ hasNull: true,
1246
+ column: keyColName
1247
+ };
1248
+ }
1249
+
1250
+ filter = encode(keyColVal);
1251
+ keyColumnsData[keyColName] = keyColVal;
1252
+
1253
+ // 6: for `=any()`
1254
+ // +1 is for the `,` that we're going to add
1255
+ // <pathOffset/><col>=any(<filter>,)
1256
+ if (rowIndex !== 0 && pathOffsetLength >= 0 &&
1257
+ (pathOffsetLength + encode(keyColName).length + 6 + currentPath.length + (rowIndex != 0 ? 1 : 0) + filter.length) > pathLimit) {
1258
+ result.push({
1259
+ path: encode(keyColName) + '=any(' + currentPath + ')',
1260
+ keyData: keyData
1261
+ });
1262
+ currentPath = '';
1263
+ keyData = [];
1264
+ } else if (rowIndex != 0) {
1265
+ filter = ',' + filter;
1266
+ }
1267
+
1268
+ currentPath += filter;
1269
+ keyData.push(keyColumnsData);
1270
+ } else {
1271
+ keyColumnsData = {};
1272
+ filter = keyColumns.length > 1 ? '(' : '';
1273
+ // add the values for the current row
1274
+ for (var keyIndex = 0; keyIndex < keyColumns.length; keyIndex++) {
1275
+ keyColName = keyColumns[keyIndex].name;
1276
+ keyColVal = rowData[keyColName];
1277
+
1278
+ if (keyColVal === undefined || keyColVal === null) {
1279
+ return {successful: false, message: "One or more records have a null value for " + keyColName, hasNull: true, column: keyColName};
1280
+ }
1281
+ if (keyIndex != 0) filter += '&';
1282
+ filter += encode(keyColName) + '=' + encode(keyColVal);
1283
+ keyColumnsData[keyColName] = keyColVal;
1284
+ }
1285
+ filter += keyColumns.length > 1 ? ')' : '';
1286
+
1287
+ // check url length limit if not first one;
1288
+ if (rowIndex != 0 && pathOffsetLength >= 0 &&
1289
+ (pathOffsetLength + currentPath.length + (rowIndex != 0 ? ';' : '') + filter).length > pathLimit) {
1290
+ // any more filters will go over the url length limit so save the current path and count
1291
+ // then clear both to start creating a new path
1292
+ result.push({
1293
+ path: currentPath,
1294
+ keyData: keyData
1295
+ });
1296
+ currentPath = '';
1297
+ keyData = [];
1298
+ } else if (rowIndex != 0) {
1299
+ // prepend the conjunction operator when it isn't the first filter to create and we aren't dealing with a url length limit
1300
+ filter = ";" + filter;
1301
+ }
1302
+
1303
+ // append the filter either on the previous path after adding ";", or on the new path started from compactPath
1304
+ currentPath += filter;
1305
+ keyData.push(keyColumnsData);
1306
+ }
1307
+ }
1308
+
1309
+ // After last iteration of loop, push the current path
1310
+ if (canUseQuantified) {
1311
+ result.push({
1312
+ path: encode(keyColName) + '=any(' + currentPath + ')',
1313
+ keyData: keyData
1314
+ });
1315
+ } else {
1316
+ result.push({
1317
+ path: currentPath,
1318
+ keyData: keyData
1319
+ });
1320
+ }
1321
+
1322
+ return {successful: true, filters: result};
1323
+ };
1324
+
1325
+ export function _stringToDate(_date, _format, _delimiter) {
1326
+ var formatLowerCase=_format.toLowerCase();
1327
+ var formatItems=formatLowerCase.split(_delimiter);
1328
+ var dateItems=_date.split(_delimiter);
1329
+ var monthIndex=formatItems.indexOf("mm");
1330
+ var dayIndex=formatItems.indexOf("dd");
1331
+ var yearIndex=formatItems.indexOf("yyyy");
1332
+ var month=parseInt(dateItems[monthIndex]);
1333
+ month-=1;
1334
+ var formatedDate = new Date(dateItems[yearIndex],month,dateItems[dayIndex].split(" ")[0]);
1335
+ return formatedDate;
1336
+ };
1337
+
1338
+ /**
1339
+ * Given a number and precision, it will truncate it to show the given number
1340
+ * of digits.
1341
+ *
1342
+ *
1343
+ * @param {number} num
1344
+ * @param {number} precision
1345
+ * @param {number} minAllowedPrecision
1346
+ */
1347
+ export function _toPrecision(num, precision, minAllowedPrecision) {
1348
+ precision = parseInt(precision);
1349
+ precision = isNaN(precision) || precision < minAllowedPrecision ? minAllowedPrecision : precision;
1350
+
1351
+ var isNegative = num < 0;
1352
+ if (isNegative) num = num * -1;
1353
+
1354
+ // this truncation logic only works because of the minimum precision that
1355
+ // we're allowing. if we want to allow less than that, then we should change this.
1356
+ var displayedNum = num.toString();
1357
+ var f = displayedNum.indexOf('.');
1358
+ if (f !== -1) {
1359
+ // find the number of digits after decimal point
1360
+ var decimalPlaces = Math.pow(10, precision - f);
1361
+
1362
+ // truncate the value
1363
+ displayedNum = Math.floor(num * decimalPlaces) / decimalPlaces;
1364
+ }
1365
+
1366
+ // if precision is too large, the calculation might return NaN.
1367
+ if (isNaN(displayedNum)) {
1368
+ return (isNegative ? '-' : '') + num;
1369
+ }
1370
+
1371
+ return (isNegative ? '-' : '') + displayedNum;
1372
+ };
1373
+
1374
+ /**
1375
+ * @desc An object of pretty print utility functions
1376
+ * @private
1377
+ */
1378
+ export const _formatUtils = {
1379
+ /**
1380
+ * @function
1381
+ * @param {Object} value A boolean value to transform
1382
+ * @param {Object} [options] Configuration options
1383
+ * @return {string} A string representation of a boolean value
1384
+ * @desc Formats a given boolean value into a string for display
1385
+ */
1386
+ printBoolean: function printBoolean(value, options) {
1387
+ options = (typeof options === 'undefined') ? {} : options;
1388
+ if (value === null) {
1389
+ return '';
1390
+ }
1391
+ return Boolean(value).toString();
1392
+ },
1393
+
1394
+ /**
1395
+ * @function
1396
+ * @param {Object} value An integer value to transform
1397
+ * @param {Object} [options] Configuration options
1398
+ * @return {string} A string representation of value
1399
+ * @desc Formats a given integer value into a whole number (with a thousands
1400
+ * separator if necessary), which is transformed into a string for display.
1401
+ */
1402
+ printInteger: function printInteger(value, options) {
1403
+ options = (typeof options === 'undefined') ? {} : options;
1404
+ if (value === null) {
1405
+ return '';
1406
+ }
1407
+
1408
+ // Remove fractional digits
1409
+ value = Math.round(value);
1410
+
1411
+ // Add comma separators
1412
+ return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1413
+ },
1414
+
1415
+ /**
1416
+ * @function
1417
+ * @param {Object} value An timestamp value to transform
1418
+ * @param {Object} [options] Configuration options. No options implemented so far.
1419
+ * @return {string} A string representation of value. Default is ISO 8601-ish like 2017-01-08 15:06:02.
1420
+ * @desc Formats a given timestamp value into a string for display.
1421
+ */
1422
+ printTimestamp: function printTimestamp(value, options) {
1423
+ options = (typeof options === 'undefined') ? {} : options;
1424
+ if (value === null) {
1425
+ return '';
1426
+ }
1427
+
1428
+ try {
1429
+ value = value.toString();
1430
+ } catch (exception) {
1431
+ $log.error("Couldn't extract timestamp from input: " + value);
1432
+ $log.error(exception);
1433
+ return '';
1434
+ }
1435
+
1436
+ if (!moment(value).isValid()) {
1437
+ $log.error("Couldn't transform input to a valid timestamp: " + value);
1438
+ return '';
1439
+ }
1440
+
1441
+ return moment(value).format(_dataFormats.DATETIME.display);
1442
+ },
1443
+
1444
+ /**
1445
+ * @function
1446
+ * @param {Object} value A date value to transform
1447
+ * @param {Object} [options] Configuration options. No options implemented so far.
1448
+ * @return {string} A string representation of value
1449
+ * @desc Formats a given date[time] value into a date string for display.
1450
+ * If any time information is provided, it will be left off.
1451
+ */
1452
+ printDate: function printDate(value, options) {
1453
+ options = (typeof options === 'undefined') ? {} : options;
1454
+ if (value === null) {
1455
+ return '';
1456
+ }
1457
+ // var year, month, date;
1458
+ try {
1459
+ value = value.toString();
1460
+ } catch (exception) {
1461
+ $log.error("Couldn't extract date info from input: " + value);
1462
+ $log.error(exception);
1463
+ return '';
1464
+ }
1465
+
1466
+ if (!moment(value).isValid()) {
1467
+ $log.error("Couldn't transform input to a valid date: " + value);
1468
+ return '';
1469
+ }
1470
+
1471
+ return moment(value).format(_dataFormats.DATE);
1472
+ },
1473
+
1474
+ /**
1475
+ * @function
1476
+ * @param {Object} value A float value to transform
1477
+ * @param {Object} [options] Configuration options.
1478
+ * - "numFracDigits" is the number of fractional digits to appear after the decimal point
1479
+ * @return {string} A string representation of value
1480
+ * @desc Formats a given float value into a string for display. Removes leading 0s; adds thousands separator.
1481
+ */
1482
+ printFloat: function printFloat(value, options) {
1483
+ options = (typeof options === 'undefined') ? {} : options;
1484
+
1485
+ if (value === null) {
1486
+ return '';
1487
+ }
1488
+
1489
+ value = parseFloat(value);
1490
+ if (options.numFracDigits) {
1491
+ value = value.toFixed(options.numFracDigits); // toFixed() rounds the value, is ok?
1492
+ } else {
1493
+ // if the float has 13 digits or more (1 trillion or greater)
1494
+ // or the float has 7 decimals or more, use scientific notation
1495
+ // NOTE: javascript in browser uses 22 as the threshold for large numbers
1496
+ // If there are 22 digits or more, then scientific notation is used
1497
+ // ecmascript language spec: https://262.ecma-international.org/5.1/#sec-9.8.1
1498
+ if (Math.abs(value) >= 1000000000000 || Math.abs(value) < 0.000001) {
1499
+ // this also ensures there are more digits than the precision used
1500
+ // so the number will be converted to scientific notation instead of
1501
+ // being padded with zeroes with no conversion
1502
+ // for example: 0.000001.toPrecision(4) ==> '0.000001000'
1503
+ value = value.toPrecision(5);
1504
+ } else {
1505
+ value = value.toFixed(4);
1506
+ }
1507
+
1508
+ }
1509
+
1510
+ // Remove leading zeroes
1511
+ value = value.toString().replace(/^0+(?!\.|$)/, '');
1512
+
1513
+ // Add comma separators
1514
+ var parts = value.split(".");
1515
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1516
+ return parts.join(".");
1517
+ },
1518
+
1519
+ /**
1520
+ * @function
1521
+ * @param {Object} value A text value to transform
1522
+ * @param {Object} [options] Configuration options.
1523
+ * @return {string} A string representation of value
1524
+ * @desc Formats a given text value into a string for display.
1525
+ */
1526
+ printText: function printText(value, options) {
1527
+ options = (typeof options === 'undefined') ? {} : options;
1528
+ if (value === null) {
1529
+ return '';
1530
+ }
1531
+ if (typeof value === 'object') {
1532
+ return JSON.stringify(value);
1533
+ }
1534
+ return value.toString();
1535
+ },
1536
+
1537
+ /**
1538
+ * @function
1539
+ * @param {Object} value The Markdown to transform
1540
+ * @param {Object} [options] Configuration options.
1541
+ * @return {string} A string representation of value
1542
+ * @desc Formats Markdown syntax into an HTML string for display.
1543
+ */
1544
+ printMarkdown: function printMarkdown(value, options) {
1545
+ options = (typeof options === 'undefined') ? {} : options;
1546
+ if (value === null) {
1547
+ return '';
1548
+ }
1549
+
1550
+ return renderMarkdown(value, options.inline);
1551
+ },
1552
+
1553
+ /**
1554
+ * @function
1555
+ * @param {Object} value A json value to transform
1556
+ * @return {string} A string representation of value based on different context
1557
+ * The beautified version of JSON in other cases
1558
+ * A special case to show null if the value is blank string
1559
+ * @desc Formats a given json value into a string for display.
1560
+ */
1561
+ printJSON: function printJSON(value, options) {
1562
+ return value === "" ? JSON.stringify(null) : JSON.stringify(value, undefined, 2);
1563
+ },
1564
+
1565
+ /**
1566
+ * @function
1567
+ * @param {string} value The gene sequence to transform
1568
+ * @param {Object} [options] Configuration options. Accepted parameters
1569
+ * are "increment" (desired number of characters in each segment) and
1570
+ * "separator" (desired separator between segments).
1571
+ * @return {string} A string representation of value
1572
+ * @desc Formats a gene sequence into a string for display. By default,
1573
+ * it will split gene sequence into an increment of 10 characters and
1574
+ * insert an empty space in between each increment.
1575
+ */
1576
+
1577
+ printGeneSeq: function printGeneSeq(value, options) {
1578
+ options = (typeof options === 'undefined') ? {} : options;
1579
+
1580
+ if (value === null) {
1581
+ return '';
1582
+ }
1583
+
1584
+ try {
1585
+ // Default separator is a space.
1586
+ if (!options.separator) {
1587
+ options.separator = ' ';
1588
+ }
1589
+ // Default increment is 10
1590
+ if (!options.increment) {
1591
+ options.increment = 10;
1592
+ }
1593
+ var inc = parseInt(options.increment, 10);
1594
+
1595
+ if (inc === 0) {
1596
+ return value.toString();
1597
+ }
1598
+
1599
+ // Reset the increment if it's negative
1600
+ if (inc <= -1) {
1601
+ inc = 1;
1602
+ }
1603
+
1604
+ var formattedSeq = '`';
1605
+ var separator = options.separator;
1606
+ while (value.length >= inc) {
1607
+ // Get the first inc number of chars
1608
+ var chunk = value.slice(0, inc);
1609
+ // Append the chunk and separator
1610
+ formattedSeq += chunk + separator;
1611
+ // Remove this chunk from value
1612
+ value = value.slice(inc);
1613
+ }
1614
+
1615
+ // Append any remaining chars from value that was too small to form an increment
1616
+ formattedSeq += value;
1617
+
1618
+ // Slice off separator at the end
1619
+ if (formattedSeq.slice(-1) == separator) {
1620
+ formattedSeq = formattedSeq.slice(0, -1);
1621
+ }
1622
+
1623
+ // Add the ending backtick at the end
1624
+ formattedSeq += '`';
1625
+
1626
+ // Run it through renderMarkdown to get the sequence in a fixed-width font
1627
+ return renderMarkdown(formattedSeq, true);
1628
+ } catch (e) {
1629
+ $log.error("Couldn't parse the given markdown value: " + value);
1630
+ $log.error(e);
1631
+ return value;
1632
+ }
1633
+
1634
+ },
1635
+
1636
+ /**
1637
+ * @function
1638
+ * @param {Array} value the array of values
1639
+ * @param {Object} options Configuration options. Accepted parameters:
1640
+ * - `isMarkdown`: if this is true, we will not esacpe markdown characters
1641
+ * - `returnArray`: if this is true, it will return an array of strings.
1642
+ * @return {string|string[]} A string represntation of array.
1643
+ * @desc
1644
+ * Will generate a comma seperated value for an array. It will also change `null` and `""`
1645
+ * to their special presentation.
1646
+ * The returned value might return markdown, which then should call printMarkdown on it.
1647
+ */
1648
+ printArray: function (value, options) {
1649
+ options = (typeof options === 'undefined') ? {} : options;
1650
+
1651
+ if (!value || !Array.isArray(value) || value.length === 0) {
1652
+ return '';
1653
+ }
1654
+
1655
+ var arr = value.map(function (v) {
1656
+ var isMarkdown = (options.isMarkdown === true);
1657
+ var pv = v;
1658
+ if (v === "") {
1659
+ pv = _specialPresentation.EMPTY_STR;
1660
+ isMarkdown = true;
1661
+ }
1662
+ else if (v == null) {
1663
+ pv = _specialPresentation.NULL;
1664
+ isMarkdown = true;
1665
+ }
1666
+
1667
+ if (!isMarkdown) pv = _escapeMarkdownCharacters(pv);
1668
+ return pv;
1669
+ });
1670
+
1671
+ if (options.returnArray) return arr;
1672
+ return arr.join(", ");
1673
+ },
1674
+
1675
+ printColor: function (value, options) {
1676
+ options = (typeof options === 'undefined') ? {} : options;
1677
+
1678
+ if (!isValidColorRGBHex(value)) {
1679
+ return '';
1680
+ }
1681
+
1682
+ value = value.toUpperCase();
1683
+ return ':span: :/span:{.' + _classNames.colorPreview + ' style=background-color:' + value +'} ' + value;
1684
+ },
1685
+
1686
+ /**
1687
+ * Return the humanize value of byte count
1688
+ *
1689
+ * This function will not round up and will only truncate the number
1690
+ * to honor the given precision. In 'si', precision below 3 is not allowed.
1691
+ * Similarly, precision below 4 is not allowed in 'binary'.
1692
+ * 'raw' will return the "formatted" value.
1693
+ *
1694
+ * @param {*} value
1695
+ * @param {?string} mode either `raw`, `si`, or `binary` (if invalid or missing, 'si' will be used)
1696
+ * @param {?number} precision An integer specifying the number of digits to be displayed
1697
+ * (if invalid or missing, `3` will be used by default.)
1698
+ * @param {?boolean} withTooltip whether we should return it with tooltip or just the value.
1699
+ */
1700
+ humanizeBytes: function (value, mode, precision, withTooltip) {
1701
+ // we cannot use parseInt here since it won't allow larger numbers.
1702
+ var v = parseFloat(value);
1703
+ mode = ['raw', 'si', 'binary'].indexOf(mode) === -1 ? 'si' : mode;
1704
+
1705
+ if (isNaN(v)) return '';
1706
+ if (v === 0 || mode === 'raw') {
1707
+ return _formatUtils.printInteger(value);
1708
+ }
1709
+
1710
+ var divisor = 1000, units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
1711
+ if (mode === 'binary') {
1712
+ divisor = 1024;
1713
+ units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
1714
+ }
1715
+
1716
+ // find the closest power of the divisor to the given number ('u').
1717
+ // in the end, 'v' will be the number that we should display.
1718
+ var u = 0;
1719
+ while (v >= divisor || -v >= divisor) {
1720
+ v /= divisor;
1721
+ u++;
1722
+ }
1723
+
1724
+ // our units don't support this, so just return the "raw" mode value.
1725
+ if (u >= units.length) {
1726
+ return _formatUtils.printInteger(value);
1727
+ }
1728
+
1729
+ // we don't want to truncate the value, so we should set a minimum
1730
+ var minP = mode === "si" ? 3 : 4;
1731
+
1732
+ var res = (u ? _toPrecision(v, precision, minP) : v) + ' ' + units[u];
1733
+ if (typeof withTooltip === 'boolean' && withTooltip && u > 0) {
1734
+ var numBytes = _formatUtils.printInteger(Math.pow(divisor, u));
1735
+ var tooltip = _formatUtils.printInteger(value);
1736
+ tooltip += ' bytes (1 ' + units[u] + ' = ' + numBytes + ' bytes)';
1737
+ res = ':span:' + res + ':/span:{data-chaise-tooltip="' + tooltip + '"}';
1738
+ }
1739
+ return res;
1740
+ }
1741
+ };
1742
+
1743
+ /**
1744
+ * format the raw value based on the column definition type, heuristics, annotations, etc.
1745
+ * @param {Type} type - the type object of the column
1746
+ * @param {Object} data - the 'raw' data value.
1747
+ * @returns {string} The formatted value.
1748
+ */
1749
+ export function _formatValueByType(type, data, options) {
1750
+ var utils = _formatUtils;
1751
+ switch(type.name) {
1752
+ case 'timestamp':
1753
+ case 'timestamptz':
1754
+ data = utils.printTimestamp(data, options);
1755
+ break;
1756
+ case 'date':
1757
+ data = utils.printDate(data, options);
1758
+ break;
1759
+ case 'numeric':
1760
+ case 'float4':
1761
+ case 'float8':
1762
+ data = utils.printFloat(data, options);
1763
+ break;
1764
+ case 'int2':
1765
+ case 'int4':
1766
+ case 'int8':
1767
+ data = utils.printInteger(data, options);
1768
+ break;
1769
+ case 'boolean':
1770
+ data = utils.printBoolean(data, options);
1771
+ break;
1772
+ case 'markdown':
1773
+ // Do nothing as we will format markdown at the end of format
1774
+ data = data.toString();
1775
+ break;
1776
+ case 'gene_sequence':
1777
+ data = utils.printGeneSeq(data, options);
1778
+ break;
1779
+ //Cases to support json and jsonb columns
1780
+ case 'json':
1781
+ case 'jsonb':
1782
+ data = utils.printJSON(data, options);
1783
+ break;
1784
+ case 'color_rgb_hex':
1785
+ data = utils.printColor(data, options);
1786
+ break;
1787
+ default: // includes 'text' and 'longtext' cases
1788
+ data = type.baseType ? _formatValueByType(type.baseType, data, options) : utils.printText(data, options);
1789
+ break;
1790
+ }
1791
+ return data;
1792
+ };
1793
+
1794
+ export function _isValidSortElement(element, index, array) {
1795
+ return (typeof element == 'object' &&
1796
+ typeof element.column == 'string' &&
1797
+ typeof element.descending == 'boolean');
1798
+ };
1799
+
1800
+ /**
1801
+ * @desc
1802
+ * Given a url and origin, test whether the url has the host.
1803
+ * Returns `null` if we cannot determine the origin.
1804
+ *
1805
+ * @function
1806
+ * @private
1807
+ * @param {string} url url string
1808
+ * @return {boolean|null}
1809
+ */
1810
+ export function _isSameHost(url) {
1811
+ // chaise-config internalHosts are not defined, so we cannot determine
1812
+ const _clientConfig = ConfigService.clientConfig;
1813
+ if (!isObjectAndNotNull(_clientConfig) || _clientConfig.internalHosts.length == 0) return null;
1814
+
1815
+ var hasProtocol = new RegExp('^(?:[a-z]+:)?//', 'i').test(url);
1816
+
1817
+ // if the url doesn't have origin (relative)
1818
+ if (!hasProtocol) return true;
1819
+
1820
+ var urlParts = url.split("/");
1821
+
1822
+ // invalid url format: cannot determine the origin
1823
+ if (urlParts.length < 3) return null;
1824
+
1825
+ // actual comparission of the origin
1826
+ return _clientConfig.internalHosts.some(function (host) {
1827
+ return typeof host === "string" && host.length > 0 && urlParts[2].indexOf(host) === 0;
1828
+ });
1829
+ };
1830
+
1831
+ // Characters to replace Markdown special characters
1832
+ const _escapeReplacementsForMarkdown = [
1833
+ [ /\*/g, '\\*' ],
1834
+ [ /#/g, '\\#' ],
1835
+ [ /\//g, '\\/' ],
1836
+ [ /\(/g, '\\(' ],
1837
+ [ /\)/g, '\\)' ],
1838
+ [ /\[/g, '\\[' ],
1839
+ [ /\]/g, '\\]' ],
1840
+ [ /\{/g, '\\{' ],
1841
+ [ /\}/g, '\\}' ],
1842
+ [ new RegExp("\<","g"), '&lt;' ],
1843
+ [ new RegExp("\>","g"), '&gt;' ],
1844
+ [ /_/g, '\\_' ],
1845
+ [ /\!/g, '\\!' ],
1846
+ [ /\./g, '\\.' ],
1847
+ [ /\+/g, '\\+' ],
1848
+ [ /\-/g, '\\-' ],
1849
+ [ /\`/g, '\\`' ]];
1850
+
1851
+ /**
1852
+ * @function
1853
+ * @param {String} text The text in which escaping needs to happen.
1854
+ * @desc
1855
+ * This private utility function escapes markdown special characters
1856
+ * It is used with Mustache to escape value of variables that have markdown characters in them
1857
+ * @returns {String} String after escaping
1858
+ */
1859
+ export function _escapeMarkdownCharacters(text) {
1860
+ return _escapeReplacementsForMarkdown.reduce(
1861
+ function(text, replacement) {
1862
+ return text.replace(replacement[0], replacement[1]);
1863
+ }, text);
1864
+ };
1865
+
1866
+ /**
1867
+ * @function
1868
+ * @desc
1869
+ * A function used by Mustache to encode strings in a template
1870
+ * @return {Function} A function that is called by Mustache when it stumbles across
1871
+ * {{#encode}} string while parsing the template.
1872
+ */
1873
+ export function _encodeForMustacheTemplate() {
1874
+ return function(text, render) {
1875
+ return fixedEncodeURIComponent(render(text));
1876
+ };
1877
+ };
1878
+
1879
+ /**
1880
+ * @function
1881
+ * @desc
1882
+ * A function used by Mustache to escape Markdown characters in a string
1883
+ * @return {Function} A function that is called by Mustache when it stumbles across
1884
+ * {{#escape}} string while parsing the template.
1885
+ */
1886
+ export function _escapeForMustacheTemplate() {
1887
+ return function(text, render) {
1888
+ return _escapeMarkdownCharacters(render(text));
1889
+ };
1890
+ };
1891
+
1892
+ /**
1893
+ * @function
1894
+ * @desc
1895
+ * Gets currDate object once the page loads for future access in templates
1896
+ * @return {Object} A date object that contains all properties
1897
+ */
1898
+ const getCurrDate = function() {
1899
+ var date = new Date();
1900
+
1901
+ var dateObj = {};
1902
+
1903
+ // Set date properties
1904
+ dateObj.day = date.getDay();
1905
+ dateObj.date = date.getDate();
1906
+ dateObj.month = date.getMonth() + 1;
1907
+ dateObj.year = date.getFullYear();
1908
+ dateObj.dateString = date.toDateString();
1909
+
1910
+ // Set Time porperties
1911
+ dateObj.hours = date.getHours();
1912
+ dateObj.minutes = date.getMinutes();
1913
+ dateObj.seconds = date.getSeconds();
1914
+ dateObj.milliseconds = date.getMilliseconds();
1915
+ dateObj.timestamp = date.getTime();
1916
+ dateObj.timeString = date.toTimeString();
1917
+
1918
+ dateObj.ISOString = date.toISOString();
1919
+ dateObj.GMTString = date.toGMTString();
1920
+ dateObj.UTCString = date.toUTCString();
1921
+
1922
+ dateObj.localeDateString = date.toLocaleDateString();
1923
+ dateObj.localeTimeString = date.toLocaleTimeString();
1924
+ dateObj.localeString = date.toLocaleString();
1925
+
1926
+ return dateObj;
1927
+ };
1928
+ export const _currDate = getCurrDate();
1929
+
1930
+ /**
1931
+ * @function
1932
+ * @desc
1933
+ * Add utility objects such as date (Computed value) to mustache data obj
1934
+ * so that they can be accessed in the template
1935
+ */
1936
+ export function _addErmrestVarsToTemplate(obj, catalog) {
1937
+
1938
+ // date object
1939
+ obj.$moment = _currDate;
1940
+
1941
+ // if there is a window object, we are in the browser
1942
+ if (!ENV_IS_NODE && typeof window === 'object' && window.location) {
1943
+ var chaiseBasePath = '/chaise/';
1944
+ if (isObjectAndNotNull(window.chaiseBuildVariables)) {
1945
+ // new version
1946
+ if (isStringAndNotEmpty(window.chaiseBuildVariables.CHAISE_BASE_PATH)) {
1947
+ chaiseBasePath = window.chaiseBuildVariables.CHAISE_BASE_PATH;
1948
+ }
1949
+ // angularj version
1950
+ else if (isStringAndNotEmpty(window.chaiseBuildVariables.chaiseBasePath)) {
1951
+ chaiseBasePath = window.chaiseBuildVariables.chaiseBasePath;
1952
+ }
1953
+ }
1954
+
1955
+ obj.$location = {
1956
+ origin: window.location.origin,
1957
+ host: window.location.host,
1958
+ hostname: window.location.hostname,
1959
+ chaise_path: chaiseBasePath
1960
+ };
1961
+ }
1962
+
1963
+ if (catalog) {
1964
+
1965
+ if (catalog.server) {
1966
+ // deriva-client-context
1967
+ obj.$dcctx = {
1968
+ cid: catalog.server.cid,
1969
+ pid: catalog.server.pid
1970
+ };
1971
+ }
1972
+
1973
+ var catalogSnapshot = catalog.id.split('@');
1974
+ obj.$catalog = {
1975
+ snapshot: catalog.id,
1976
+ id: catalogSnapshot[0]
1977
+ };
1978
+
1979
+ if (catalogSnapshot.length === 2) obj.$catalog.version = catalogSnapshot[1];
1980
+ }
1981
+
1982
+ if (AuthnService.session) {
1983
+ var session = AuthnService.session;
1984
+
1985
+ obj.$session = {};
1986
+ Object.keys(session).forEach(function (key) {
1987
+ obj.$session[key] = session[key];
1988
+ });
1989
+
1990
+ // If extensions is present, put all dbgap permissions into a map
1991
+ // NOTE: not sure if we want to check for `has_ras_permissions` too or not since if that is true, it means ras_dbgap_permissions is also defined
1992
+ // if it's false, the array won't be defined
1993
+ if (session.client.extensions && session.client.extensions.ras_dbgap_permissions && Array.isArray(session.client.extensions.ras_dbgap_permissions)) {
1994
+ var map = {};
1995
+ session.client.extensions.ras_dbgap_permissions.forEach(function (perm) {
1996
+ if (typeof perm === "object" && perm.phs_id) map[perm.phs_id] = true;
1997
+ });
1998
+
1999
+ obj.$session.client.extensions.ras_dbgap_phs_ids = map;
2000
+ }
2001
+ }
2002
+
2003
+ /**
2004
+ * TODO if we want ot add dynamic variables to the template,
2005
+ * we could add "site_var_queries" or "site_var_sources" (we have to decide which one to use).
2006
+ * We didn't completely fleshed out what this property looks like, but it should be similar to "source object"
2007
+ * where you can define a path and project list.
2008
+ */
2009
+ const cc = ConfigService.clientConfig;
2010
+ if (isObjectAndNotNull(cc) && isObjectAndNotNull(cc.templating) && isObjectAndNotNull(cc.templating.site_var)) {
2011
+ obj.$site_var = cc.templating.site_var;
2012
+ }
2013
+ };
2014
+
2015
+ /**
2016
+ * @function
2017
+ * @desc
2018
+ * Replace variables having dot with underscore so that they can be accessed in the template
2019
+ * @param {Object} keyValues The key-value pair of object.
2020
+ * @param {Object} options An object of options which might contain additional functions to be injected
2021
+ *
2022
+ * @return {Object} obj
2023
+ */
2024
+ export function _addTemplateVars(keyValues, catalog, options) {
2025
+
2026
+ var obj = {};
2027
+ if (keyValues && isObject(keyValues)) {
2028
+ try {
2029
+ // recursively replace dot with underscore in column names.
2030
+ obj = _replaceDotWithUnderscore(keyValues);
2031
+ } catch (err) {
2032
+ // This should not happen since we're guarding against custom type objects.
2033
+ obj = keyValues;
2034
+ $log.error("Could not process the given keyValues in _renderTemplate. Ignoring the _replaceDotWithUnderscore logic.");
2035
+ $log.error(err);
2036
+ }
2037
+ }
2038
+
2039
+ // Inject ermrest internal utility objects such as date
2040
+ _addErmrestVarsToTemplate(obj, catalog);
2041
+
2042
+ // Inject other functions provided in the options.functions array if needed
2043
+ if (options.functions && options.functions.length) {
2044
+ options.functions.forEach(function(f) {
2045
+ obj[f.name] = function() {
2046
+ return f.fn;
2047
+ };
2048
+ });
2049
+ }
2050
+
2051
+ return obj;
2052
+ };
2053
+
2054
+ /*
2055
+ * @function
2056
+ * @private
2057
+ * @param {String} template The template string to transform
2058
+ * @param {Object} obj The key-value pair of object to be used for template tags replacement.
2059
+ * @param {Object} [options] Configuration options.
2060
+ * @return {string} A string produced after templating
2061
+ * @desc Returns a string produced as a result of templating using `Mustache`.
2062
+ */
2063
+ export function renderMustacheTemplate(template, keyValues, catalog, options) {
2064
+
2065
+ options = options || {};
2066
+
2067
+ var obj = _addTemplateVars(keyValues, catalog, options), content;
2068
+
2069
+ // Inject the encode function in the obj object
2070
+ obj.encode = _encodeForMustacheTemplate;
2071
+
2072
+ // Inject the escape function in the obj object
2073
+ obj.escape = _escapeForMustacheTemplate;
2074
+
2075
+ // If we should validate, validate the template and if returns false, return null.
2076
+ if (!options.avoidValidation && !_validateMustacheTemplate(template, obj, catalog)) {
2077
+ return null;
2078
+ }
2079
+
2080
+ try {
2081
+ content = mustache.render(template, obj);
2082
+ } catch(e) {
2083
+ $log.error(e);
2084
+ content = null;
2085
+ }
2086
+
2087
+ return content;
2088
+ };
2089
+
2090
+ /**
2091
+ * Returns true if all the used keys have values.
2092
+ *
2093
+ * NOTE:
2094
+ * This implementation is very limited and if conditional Mustache statements
2095
+ * of the form {{#var}}{{/var}} or {{^var}}{{/var}} found then it won't check
2096
+ * for null values and will return true.
2097
+ *
2098
+ * @param {string} template mustache template
2099
+ * @param {object} keyValues key-value pairs
2100
+ * @param {Array.<string>=} ignoredColumns the columns that should be ignored (optional)
2101
+ * @return {boolean} true if all the used keys have values
2102
+ */
2103
+ export function _validateMustacheTemplate(template, keyValues, catalog, ignoredColumns) {
2104
+
2105
+ // Inject ermrest internal utility objects such as date
2106
+ // needs to be done in the case _validateTemplate is called without first calling _renderTemplate
2107
+ _addErmrestVarsToTemplate(keyValues, catalog);
2108
+
2109
+ var conditionalRegex = /\{\{(#|\^)([^\{\}]+)\}\}/, i, key, value;
2110
+
2111
+ // If no conditional Mustache statements of the form {{#var}}{{/var}} or {{^var}}{{/var}} not found then do direct null check
2112
+ if (!conditionalRegex.exec(template)) {
2113
+
2114
+ // Grab all placeholders ({{PROP_NAME}}) in the template
2115
+ var placeholders = template.match(/\{\{([^\{\}]+)\}\}/ig);
2116
+
2117
+ // If there are any placeholders
2118
+ if (placeholders && placeholders.length) {
2119
+
2120
+ // Get unique placeholders
2121
+ placeholders = placeholders.filter(function(item, i, ar) { return ar.indexOf(item) === i; });
2122
+
2123
+ /*
2124
+ * Iterate over all placeholders to set pattern as null if any of the
2125
+ * values turn out to be null or undefined
2126
+ */
2127
+ for (i=0; i<placeholders.length;i++) {
2128
+
2129
+ // Grab actual key from the placeholder {{name}} = name, remove "{{" and "}}" from the string for key
2130
+ key = placeholders[i].substring(2, placeholders[i].length - 2);
2131
+
2132
+ if (key[0] == "{") key = key.substring(1, key.length -1);
2133
+
2134
+ // find the value.
2135
+ value = _getPath(keyValues, key.trim());
2136
+
2137
+ // TODO since we're not going inside the object this logic of ignoredColumns is not needed anymore,
2138
+ // it was a hack that was added for asset columns.
2139
+ // If key is not in ingored columns value for the key is null or undefined then return null
2140
+ if ((!Array.isArray(ignoredColumns) || ignoredColumns.indexOf(key) == -1) && (value === null || value === undefined)) {
2141
+ return false;
2142
+ }
2143
+ }
2144
+ }
2145
+ }
2146
+ return true;
2147
+ };
2148
+
2149
+ /**
2150
+ * given a string, if it's a valid template_engine use it,
2151
+ * otherwise get it from the client config.
2152
+ * @param {string} engine
2153
+ */
2154
+ export function _getTemplateEngine(engine) {
2155
+ var isValid = function (val) {
2156
+ return isStringAndNotEmpty(val) && Object.values(TEMPLATE_ENGINES).indexOf(val) !== -1;
2157
+ };
2158
+ if (isValid(engine)) {
2159
+ return engine;
2160
+ }
2161
+ const _clientConfig = ConfigService.clientConfig;
2162
+ if (isObjectAndNotNull(_clientConfig) && isObjectAndNotNull(_clientConfig.templating) &&
2163
+ isValid(_clientConfig.templating.engine)) {
2164
+ return _clientConfig.templating.engine;
2165
+ }
2166
+ return TEMPLATE_ENGINES.MUSTACHE;
2167
+ };
2168
+
2169
+ /**
2170
+ * A wrapper for {renderMustacheTemplate}
2171
+ * acceptable options:
2172
+ * - templateEngine: "mustache" or "handlbars"
2173
+ * - avoidValidation: to avoid validation of the template
2174
+ * - allowObject: if the returned string is a parsable object, return it as object
2175
+ * instead of string.
2176
+ *
2177
+ * @param {string} template - template to be rendered
2178
+ * @param {object} keyValues - formatted key value pairs needed for the template
2179
+ * @param {Catalog} catalog - the catalog that this value is for
2180
+ * @param {any=} options optioanl parameters
2181
+ * @return {string} Returns a string produced as a result of templating using options.templateEngine or `Mustache` by default.
2182
+ */
2183
+ export function _renderTemplate(template, keyValues, catalog, options) {
2184
+
2185
+ if (typeof template !== 'string') return null;
2186
+
2187
+ options = options || {};
2188
+
2189
+ var res, objRes;
2190
+ if (_getTemplateEngine(options.templateEngine) === TEMPLATE_ENGINES.HANDLEBARS) {
2191
+ // render the template using Handlebars
2192
+ res = HandlebarsService.render(template, keyValues, catalog, options);
2193
+ } else {
2194
+ // render the template using Mustache
2195
+ res = renderMustacheTemplate(template, keyValues, catalog, options);
2196
+ }
2197
+
2198
+ if (options.allowObject) {
2199
+ try {
2200
+ // if it can be parsed and is an object, return the object
2201
+ objRes = JSON.parse(res);
2202
+ if (typeof objRes === "object") {
2203
+ return objRes;
2204
+ }
2205
+ } catch {
2206
+ // ignore
2207
+ }
2208
+ }
2209
+
2210
+ return res;
2211
+ };
2212
+
2213
+ /**
2214
+ * A wrapper for {_validateMustacheTemplate}
2215
+ * it will take care of adding formmatted and unformatted values.
2216
+ * options.ignoredColumns: list of columns that you want validator to ignore
2217
+ * options.templateEngine: "mustache" or "handlbars"
2218
+ *
2219
+ * @param {Table} table
2220
+ * @param {object} data
2221
+ * @param {string} template
2222
+ * @param {Catalog} catalog
2223
+ * @param {Array.<string>=} ignoredColumns the columns that should be ignored (optional)
2224
+ * @return {boolean} True if the template is valid.
2225
+ */
2226
+ export function _validateTemplate(template, data, catalog, options) {
2227
+
2228
+ var ignoredColumns;
2229
+ if (options !== undefined && Array.isArray(options.ignoredColumns)) {
2230
+ ignoredColumns = options.ignoredColumns;
2231
+ }
2232
+
2233
+ if (_getTemplateEngine(options ? options.templateEngine : '') === TEMPLATE_ENGINES.HANDLEBARS) {
2234
+ // call the actual Handlebar validator
2235
+ return HandlebarsService.validate(template, data, catalog, ignoredColumns)
2236
+ }
2237
+
2238
+ // call the actual mustache validator
2239
+ return _validateMustacheTemplate(template, data, catalog, ignoredColumns);
2240
+ };
2241
+
2242
+ /**
2243
+ * Given a markdown_pattern template and data, will return the appropriate
2244
+ * presentation value.
2245
+ *
2246
+ * @param {String} template the handlebars/mustache template
2247
+ * @param {Object} data the key-value pair of data
2248
+ * @param {Table} table the table object
2249
+ * @param {String} context context string
2250
+ * @param {Object} options
2251
+ * @return {{isHTML: boolean, value: string, unformatted: string}} An object with `isHTML` and `value` attributes.
2252
+ * @memberof ERMrest
2253
+ * @function processMarkdownPattern
2254
+ */
2255
+ export function processMarkdownPattern(template, data, table, context, options) {
2256
+ var res = _renderTemplate(template, data, table ? table.schema.catalog : null, options);
2257
+
2258
+ if (res === null || res.trim() === '') {
2259
+ res = table ? table._getNullValue(context) : "";
2260
+ return {isHTML: false, value: res, unformatted: res};
2261
+ }
2262
+ var isInline = options && options.isInline ? true : false;
2263
+ return {isHTML: true, value: renderMarkdown(res, isInline), unformatted: res};
2264
+ };
2265
+
2266
+ /**
2267
+ * Return an object containing window.location properties ('host', 'hostname', 'hash', 'href', 'port', 'protocol', 'search').
2268
+ *
2269
+ * @private
2270
+ * @param {string} url URL to be parsed
2271
+ * @return {object} The location object
2272
+ */
2273
+ export function _parseUrl(url) {
2274
+ var m = url.match(/^(([^:\/?#]+:)?(?:\/\/(([^\/?#:]*)(?::([^\/?#:]*))?)))?([^?#]*)(\?[^#]*)?(#.*)?$/),
2275
+ r = {
2276
+ hash: m[8] || "", // #asd
2277
+ host: m[3] || "", // localhost:257
2278
+ hostname: m[4] || "", // localhost
2279
+ href: m[0] || "", // http://localhost:257/deploy/?asd=asd#asd
2280
+ origin: m[1] || "", // http://localhost:257
2281
+ pathname: m[6] || (m[1] ? "/" : ""), // /deploy/
2282
+ port: m[5] || "", // 257
2283
+ protocol: m[2] || "", // http:
2284
+ search: m[7] || "" // ?asd=asd
2285
+ };
2286
+ if (r.protocol.length == 2) {
2287
+ r.protocol = "file:///" + r.protocol.toUpperCase();
2288
+ r.origin = r.protocol + "//" + r.host;
2289
+ }
2290
+ r.href = r.origin + r.pathname + r.search + r.hash;
2291
+ return m && r;
2292
+ };
2293
+
2294
+ export function _isEntryContext(context) {
2295
+ return _entryContexts.indexOf(context) !== -1;
2296
+ };
2297
+
2298
+ export function _isCompactContext(context) {
2299
+ return _compactContexts.indexOf(context) !== -1;
2300
+ };