@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,850 @@
1
+ // models
2
+ import SourceObjectWrapper from '@isrd-isi-edu/ermrestjs/src/models/source-object-wrapper';
3
+ import SourceObjectNode from '@isrd-isi-edu/ermrestjs/src/models/source-object-node';
4
+ import { ReferenceColumn, ReferenceColumnTypes } from '@isrd-isi-edu/ermrestjs/src/models/reference-column';
5
+ import { CommentType } from '@isrd-isi-edu/ermrestjs/src/models/comment';
6
+ import { DisplayName } from '@isrd-isi-edu/ermrestjs/src/models/display-name';
7
+ import { Reference, RelatedReference, type Page, type Tuple } from '@isrd-isi-edu/ermrestjs/src/models/reference';
8
+
9
+ // services
10
+ import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
11
+ import ErrorService from '@isrd-isi-edu/ermrestjs/src/services/error';
12
+
13
+ // utils
14
+ import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils';
15
+ import { isDefinedAndNotNull, isObject, isObjectAndNotNull, isStringAndNotEmpty, verify } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
16
+ import { fixedEncodeURIComponent } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
17
+ import { processAggregateValue } from '@isrd-isi-edu/ermrestjs/src/utils/column-utils';
18
+ import {
19
+ _pseudoColAggregateFns,
20
+ _pseudoColEntityAggregateFns,
21
+ _pseudoColAggregateExplicitName,
22
+ _pseudoColAggregateNames,
23
+ URL_PATH_LENGTH_LIMIT,
24
+ } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
25
+
26
+ // legacy
27
+ import { Column, Key } from '@isrd-isi-edu/ermrestjs/js/core';
28
+ import { parse } from '@isrd-isi-edu/ermrestjs/js/parser';
29
+ import {
30
+ _isEntryContext,
31
+ _getFormattedKeyValues,
32
+ _getRowTemplateVariables,
33
+ _generateRowPresentation,
34
+ generateKeyValueFilters,
35
+ processMarkdownPattern,
36
+ _processModelComment,
37
+ _processSourceObjectComment,
38
+ } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
39
+
40
+ /**
41
+ * A pseudo-column without any actual source definition behind it.
42
+ * This constructor assumes that the sourceObject has markdown_name and display.markdown_pattern.
43
+ *
44
+ * The name is currently generated by the visible columns logic. It will use the
45
+ * "$<markdown_name>" pattern and if a column with this name already exists in the table,
46
+ * it will append "-<integer>" to it.
47
+ */
48
+ export class VirtualColumn extends ReferenceColumn {
49
+ public isPseudo: boolean = true;
50
+
51
+ isVirtualColumn = true;
52
+
53
+ constructor(reference: Reference, sourceObjectWrapper: SourceObjectWrapper, name: string, mainTuple?: Tuple) {
54
+ super(reference, [], sourceObjectWrapper, name, mainTuple);
55
+
56
+ this.referenceColumnType = ReferenceColumnTypes.VIRTUAL;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * If you want to create an object of this type, use the `createPseudoColumn` method.
62
+ * This will only be used for general purpose pseudo-columns, using that method ensures That
63
+ * we're creating the more specific object instead. Therefore only these cases should
64
+ * be using this type of object:
65
+ * 1. When sourceObject has aggregate
66
+ * 2. When sourceObject has a path that is not just an outbound fk, or it doesn't define a related
67
+ * entity (inbound or p&b association)
68
+ *
69
+ * @memberof ERMrest
70
+ * @param {Reference} reference column's reference
71
+ * @param {Column} column the column that this pseudo-column is representing
72
+ * @param {SourceObjectWrapper} sourceObjectWrapper the sourceObjectWrapper object (might be undefined)
73
+ * @param {string=} name to avoid processing the name again, this might be undefined.
74
+ * @param {Tuple=} mainTuple if the reference is referring to just one tuple, this is defined.
75
+ * @constructor
76
+ * @class
77
+ */
78
+ export class PseudoColumn extends ReferenceColumn {
79
+ /**
80
+ * indicates that this object represents a PseudoColumn.
81
+ */
82
+ public isPseudo: boolean = true;
83
+
84
+ public isPathColumn: boolean = true;
85
+
86
+ /**
87
+ * If the pseudo-column is connected via a path to the table or not.
88
+ */
89
+ public hasPath: boolean;
90
+
91
+ /**
92
+ * If the pseudoColumn is in entity mode
93
+ */
94
+ public isEntityMode: boolean;
95
+
96
+ /**
97
+ * If the pseudoColumn is referring to a unique row (the path is one to one)
98
+ */
99
+ public isUnique: boolean;
100
+
101
+ /**
102
+ * If aggregate function is defined on the column.
103
+ */
104
+ public hasAggregate: boolean;
105
+
106
+ public baseColumn: Column;
107
+
108
+ constructor(reference: Reference, column: Column, sourceObjectWrapper: SourceObjectWrapper, name?: string, mainTuple?: Tuple) {
109
+ super(reference, [column], sourceObjectWrapper, name, mainTuple);
110
+
111
+ this.referenceColumnType = ReferenceColumnTypes.PSEUDO;
112
+
113
+ this.hasPath = this.sourceObjectWrapper!.hasPath;
114
+ this.isEntityMode = this.sourceObjectWrapper!.isEntityMode;
115
+ this.isUnique = this.sourceObjectWrapper!.isUnique;
116
+ this.hasAggregate = this.sourceObjectWrapper!.hasAggregate;
117
+
118
+ this.baseColumn = column;
119
+ this._currentTable = reference.table;
120
+ this.table = column.table;
121
+ }
122
+
123
+ /**
124
+ * Format the presentation value corresponding to this pseudo-column definition.
125
+ * 1. If source is not in entity mode: use the column's heuristic
126
+ * 2. Otherwise if it's not a path, apply the same logic as KeyPseudoColumn presentation based on the key.
127
+ * 2. Otherwise if path is one to one (all outbound), use the same logic as ForeignKeyPseudoColumn based on last fk.
128
+ * 3. Otherwise return null value.
129
+ *
130
+ * @param {Object} data the raw data of the table
131
+ * @param {String=} context the app context (optional)
132
+ * @param {Object=} templateVariables the template variables that should be used (optional)
133
+ * @param {Object=} options (optional)
134
+ * @returns {Object} A key value pair containing value and isHTML that detemrines the presentation.
135
+ */
136
+ formatPresentation(data: any = {}, context?: string, templateVariables?: any, options: any = {}): any {
137
+ if (!isStringAndNotEmpty(context)) {
138
+ context = this._context;
139
+ }
140
+
141
+ const nullValue = {
142
+ isHTML: false,
143
+ value: this._getNullValue(context!),
144
+ unformatted: this._getNullValue(context!),
145
+ };
146
+
147
+ if (_isEntryContext(context)) {
148
+ return nullValue;
149
+ }
150
+
151
+ if (this.hasWaitFor && !options.skipWaitFor) {
152
+ return nullValue;
153
+ }
154
+
155
+ // has aggregate, we should get the value by calling aggregate function
156
+ if (this.hasAggregate) {
157
+ return nullValue;
158
+ }
159
+
160
+ // not representing a row
161
+ if (!this.isUnique) {
162
+ return nullValue;
163
+ }
164
+
165
+ // make sure templateVariables is valid
166
+ if (!isObjectAndNotNull(templateVariables)) {
167
+ templateVariables = _getFormattedKeyValues(this.table, context!, data);
168
+ }
169
+
170
+ // not in entity mode, just return the column value.
171
+ if (!this.isEntityMode) {
172
+ // we should not pass the same templateVariables to the parent,
173
+ // since when it goes to the parent it will be based on the leaf table
174
+ // while the templateVariables is based on the parent table.
175
+ // only if we're going to use this with sourceMarkdownPattern we should pass this value
176
+ return super.formatPresentation(data, context, this.display.sourceMarkdownPattern ? templateVariables : null);
177
+ }
178
+
179
+ if (this.display.sourceMarkdownPattern) {
180
+ const keyValues: any = {};
181
+ const selfTemplateVariables = {
182
+ $self: _getRowTemplateVariables(this.table, context!, data),
183
+ };
184
+ Object.assign(keyValues, templateVariables, selfTemplateVariables);
185
+ return processMarkdownPattern(this.display.sourceMarkdownPattern, keyValues, this.table, context!, {
186
+ templateEngine: this.display.sourceTemplateEngine,
187
+ });
188
+ }
189
+
190
+ // in entity mode, return the foreignkey value
191
+ const pres = _generateRowPresentation(this.lastForeignKeyNode!.nodeObject.key, data, context!, this._getShowForeignKeyLink(context!));
192
+ return pres ? pres : nullValue;
193
+ }
194
+
195
+ sourceFormatPresentation(templateVariables: any, columnValue: any, mainTuple: Tuple) {
196
+ const baseCol = this.baseColumn;
197
+ const context = this._context;
198
+ let selfTemplateVariables: any = {};
199
+
200
+ if (this.display.sourceMarkdownPattern) {
201
+ // for aggregate, the columnValue has the value precomputed
202
+ if (this.hasAggregate && columnValue) {
203
+ selfTemplateVariables = columnValue.templateVariables;
204
+ }
205
+ // all-outbound paths
206
+ else if (this.hasPath && this.isUnique) {
207
+ // use the linked data if exists
208
+ if (!mainTuple.linkedData[this.name]) {
209
+ selfTemplateVariables = {};
210
+ }
211
+ // scalar default
212
+ else if (!this.isEntityMode) {
213
+ selfTemplateVariables = {
214
+ $self: baseCol.formatvalue(mainTuple.linkedData[this.name][baseCol.name], context),
215
+ $_self: mainTuple.linkedData[this.name][baseCol.name],
216
+ };
217
+ }
218
+ // entity default
219
+ else {
220
+ selfTemplateVariables = {
221
+ $self: _getRowTemplateVariables(this.table, context, mainTuple.linkedData[this.name]),
222
+ };
223
+ }
224
+ }
225
+ // any other paths
226
+ else if (baseCol) {
227
+ selfTemplateVariables = {
228
+ $self: baseCol.formatvalue(mainTuple.data[baseCol.name], context),
229
+ $_self: mainTuple.data[baseCol.name],
230
+ };
231
+ }
232
+
233
+ const keyValues = {};
234
+ Object.assign(keyValues, templateVariables, selfTemplateVariables);
235
+ return processMarkdownPattern(this.display.sourceMarkdownPattern, keyValues, this.table, context, {
236
+ templateEngine: this.display.sourceTemplateEngine,
237
+ });
238
+ }
239
+
240
+ // aggregate
241
+ if (this.hasAggregate) {
242
+ const nullValue = this._getNullValue(context);
243
+ return columnValue ? columnValue : { isHTML: false, value: nullValue, unformatted: nullValue };
244
+ }
245
+
246
+ // all outbound
247
+ if (this.hasPath && this.isUnique) {
248
+ return this.formatPresentation(mainTuple.linkedData[this.name], mainTuple.page.reference.context, null, { skipWaitFor: true });
249
+ }
250
+
251
+ // other cases
252
+ return super.sourceFormatPresentation(templateVariables, columnValue, mainTuple);
253
+ }
254
+
255
+ /**
256
+ * Returns a promise that gets resolved with list of aggregated values in the same
257
+ * order of tuples of the page that is passed.
258
+ * Each returned value has the following attributes:
259
+ * - value
260
+ * - isHTML
261
+ * - templateVariables: the template variables that the client uses to eventually pass to sourceFormatPresentation
262
+ *
263
+ * implementation Notes:
264
+ * 1. This function will take care of url limitation. It might generate multiple
265
+ * ermrest requests based on the url length, and will resolve the promise when
266
+ * all the requests have been succeeded. If we cannot fit all the requests, an
267
+ * error will be thrown.
268
+ * 2. Only in case of entity scalar aggregate we are going to get all the row data.
269
+ * In other cases, the returned data will only include the scalar value.
270
+ * 3. Regarding the returned value:
271
+ * 3.0. Null and empty string values are treated the same way as any array column.
272
+ * We are going to show the special value for them.
273
+ * 3.1. If it's an array aggregate:
274
+ * 3.1.1. array_display will dictate how we should join the values (csv, olist, ulist, raw).
275
+ * 3.1.2. array_options will dictate the sort and length criteria.
276
+ * 3.1.3. Based on entity/scalar mode:
277
+ * 3.1.3.1. In scalar mode, only pre_format will be applied to each value.
278
+ * 3.1.3.2. In entity mode, we are going to return list of row_names derived from `row_name/compact`.
279
+ * 3.2. Otherwise we will only apply the pre_format annotation for the column.
280
+ *
281
+ * @param {Page} page the page object of main (current) refernece
282
+ * @param {Object} contextHeaderParams the object that we want to log.
283
+ * @return {Promise}
284
+ */
285
+ getAggregatedValue(page: Page, contextHeaderParams?: any): Promise<{ value: any; isHTML: boolean; templateVariables: any }[]> {
286
+ return new Promise((resolve, reject) => {
287
+ const values: { value: any; isHTML: boolean; templateVariables: any }[] = [];
288
+ const mainTable = this._currentTable;
289
+ const location = this._baseReference.location;
290
+ const http = this._baseReference.server.http;
291
+ const column = this.baseColumns[0];
292
+ let pathToCol: string;
293
+
294
+ // this will dictates whether we should show rowname or not
295
+ const aggFn = this.sourceObject.aggregate;
296
+ const isRow = this.isEntityMode && _pseudoColEntityAggregateFns.indexOf(aggFn) !== -1;
297
+
298
+ // verify the input
299
+ try {
300
+ verify(this.hasAggregate, 'this function should only be used when `hasAggregate` is true.');
301
+ verify(page && page.reference.table === mainTable, 'given page object must be defined and from the base table.');
302
+ } catch (e) {
303
+ reject(e);
304
+ return;
305
+ }
306
+
307
+ // create the header
308
+ if (!contextHeaderParams || !isObject(contextHeaderParams)) {
309
+ contextHeaderParams = { action: 'read/aggregate' };
310
+ }
311
+ const config = {
312
+ headers: this.reference._generateContextHeader(contextHeaderParams),
313
+ };
314
+
315
+ // return empty list if page is empty
316
+ if (page.tuples.length === 0) {
317
+ resolve(values);
318
+ return;
319
+ }
320
+
321
+ // make sure table has shortestkey of length 1
322
+ if (mainTable.shortestKey.length > 1) {
323
+ $log.warn('This function only works with tables that have at least a simple key.');
324
+ resolve(values);
325
+ return;
326
+ }
327
+
328
+ const currTable = 'T';
329
+ const baseTable = this.hasPath ? 'M' : currTable;
330
+
331
+ const keyColName = mainTable.shortestKey[0].name;
332
+ const keyColNameEncoded = fixedEncodeURIComponent(mainTable.shortestKey[0].name);
333
+ const projection = `/c:=:${baseTable}:${keyColNameEncoded};v:=${aggFn}(${currTable}:${isRow ? '*' : fixedEncodeURIComponent(column.name)})`;
334
+
335
+ // generate the base path in the following format:
336
+ // <baseUri><basePath><filters><path-to-pseudo-col><projection>
337
+ // the following shows where `/` is stored for each part:
338
+ // <baseUri/><basePath/><filters></path-from-main-to-pseudo-col></projection>
339
+ const baseUri = [location.service, 'catalog', location.catalog, 'attributegroup'].join('/') + '/';
340
+ const basePath = baseTable + ':=' + fixedEncodeURIComponent(mainTable.schema.name) + ':' + fixedEncodeURIComponent(mainTable.name) + '/';
341
+ pathToCol = this.sourceObjectWrapper!.toString(false, false, currTable);
342
+ if (pathToCol.length > 0) {
343
+ pathToCol = '/' + pathToCol;
344
+ }
345
+
346
+ // make sure just projection and base uri doesn't go over limit.
347
+ if (basePath.length + pathToCol.length + projection.length >= URL_PATH_LENGTH_LIMIT) {
348
+ $log.warn("couldn't generate the requests because of url limitation");
349
+ resolve(values);
350
+ return;
351
+ }
352
+
353
+ // get the computed filters
354
+ const keyValueRes = generateKeyValueFilters(
355
+ mainTable.shortestKey,
356
+ page.tuples.map((t) => t.data),
357
+ mainTable.schema.catalog,
358
+ (basePath + pathToCol + projection).length,
359
+ mainTable.displayname.value,
360
+ );
361
+
362
+ if (!keyValueRes.successful || !keyValueRes.filters) {
363
+ $log.warn(keyValueRes.message);
364
+ resolve(values);
365
+ return;
366
+ }
367
+
368
+ // turn the paths into requests
369
+ const httpPromises = keyValueRes.filters.map((f) => {
370
+ return http.get(baseUri + basePath + f.path + pathToCol + projection, config);
371
+ });
372
+
373
+ // if adding any of the filters would go over url limit
374
+ if (httpPromises.length === 0) {
375
+ $log.warn("couldn't generate the requests because of url limitation");
376
+ resolve(values);
377
+ return;
378
+ }
379
+
380
+ Promise.all(httpPromises)
381
+ .then((response) => {
382
+ const result: any[] = [];
383
+ let responseValues: any[] = [];
384
+ let value: any;
385
+
386
+ response.forEach((r: any) => {
387
+ responseValues = responseValues.concat(r.data);
388
+ });
389
+
390
+ // make sure we're returning the result in the same order as input
391
+ page.tuples.forEach((t) => {
392
+ // find the corresponding value in result
393
+ value = responseValues.find((v) => {
394
+ return v.c === t.data[keyColName];
395
+ });
396
+
397
+ result.push(processAggregateValue(value && value.v ? value.v : null, this, aggFn, isRow));
398
+ });
399
+
400
+ resolve(result);
401
+ })
402
+ .catch((err) => {
403
+ reject(ErrorService.responseToError(err));
404
+ });
405
+ });
406
+ }
407
+
408
+ protected _determineSortable(): void {
409
+ this._sortColumns_cached = [];
410
+ this._sortable = false;
411
+
412
+ // disable sort if it has aggregate
413
+ if (this.hasAggregate) {
414
+ return;
415
+ }
416
+
417
+ if (this.isUnique) {
418
+ if (this.isEntityMode) {
419
+ const fk = this.lastForeignKeyNode!.nodeObject;
420
+ const display = fk.getDisplay(this._context);
421
+
422
+ // disable the sort
423
+ if (display !== undefined && display.columnOrder === false) return;
424
+
425
+ // use the column_order
426
+ if (display !== undefined && display.columnOrder !== undefined && display.columnOrder.length !== 0) {
427
+ this._sortColumns_cached = display.columnOrder;
428
+ this._sortable = true;
429
+ return;
430
+ }
431
+
432
+ if (this.reference.display._rowOrder !== undefined) {
433
+ this._sortColumns_cached = this.reference.display._rowOrder;
434
+ this._sortable = true;
435
+ return;
436
+ }
437
+ }
438
+
439
+ // use the column's
440
+ this._sortColumns_cached = this.baseColumns[0]._getSortColumns(this._context); //might return undefined
441
+
442
+ if (typeof this._sortColumns_cached === 'undefined') {
443
+ this._sortColumns_cached = [];
444
+ } else {
445
+ this._sortable = true;
446
+ }
447
+ }
448
+ }
449
+
450
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
451
+ protected _determineInputDisabled(context: string): boolean | { message: string } {
452
+ throw new Error('can not use this type of column in entry mode.');
453
+ }
454
+
455
+ /**
456
+ * If the first foreign key is outbound, this function will return the value that this pseudo-column represents.
457
+ *
458
+ * The first fk must be outbound because the path is generated from the table that the first fk refers to. This is
459
+ * useful for fetching the values of wait-fors in the entry contexts. Since the main record might not be available yet,
460
+ * we're starting from the fk table.
461
+ *
462
+ * Ideally this and getAggregatedValue should be merged, these are the differences:
463
+ * - for this function agg fn is not required.
464
+ * - this function requires the first fk to be outbound, while getAggregatedValue doesn't.
465
+ * - the generated path here ignores the first hop.
466
+ * - if the key value for any of the rows is null, this will only ignore that row (getAggregatedValue will just give up and return empty).
467
+ *
468
+ * @param {any} data the submission data
469
+ * @param {Record<string, any>} contextHeaderParams
470
+ */
471
+ getFirstOutboundValue(data: any[], contextHeaderParams?: Record<string, any>): Promise<any[]> {
472
+ return new Promise((resolve, reject) => {
473
+ const encode = fixedEncodeURIComponent;
474
+ const location = this._baseReference.location;
475
+ const http = this._baseReference.server.http;
476
+
477
+ // these are the same checks as the processWaitFor in the entry context. since that's the only usecase of this for now.
478
+ const sw = this.sourceObjectWrapper!;
479
+ const firstFk = this.firstForeignKeyNode ? this.firstForeignKeyNode.nodeObject : null;
480
+ if (firstFk === undefined || this.firstForeignKeyNode!.isInbound) {
481
+ $log.warn('This function should only be used when the first foreign key is outbound.');
482
+ resolve([]);
483
+ return;
484
+ }
485
+ if (sw.foreignKeyPathLength < 2 || sw.hasPrefix || (sw.isFiltered && sw.filterProps && sw.filterProps.hasRootFilter)) {
486
+ $log.warn('This function only support paths that start with outbound, have no filter or prefix, and at least 2 fk hops.');
487
+ resolve([]);
488
+ return;
489
+ }
490
+
491
+ // create the header
492
+ if (!contextHeaderParams || !isObject(contextHeaderParams)) {
493
+ contextHeaderParams = { action: 'read/outbound' };
494
+ }
495
+ const config = {
496
+ headers: this.reference._generateContextHeader(contextHeaderParams),
497
+ };
498
+ // if agg is missing, we're getting the rows
499
+ const aggFn = this.sourceObject.aggregate ? this.sourceObject.aggregate : 'array_d';
500
+ const isRow = this.isEntityMode && _pseudoColEntityAggregateFns.indexOf(aggFn) !== -1;
501
+ const isAllOutbound = this.isPathColumn && this.hasPath && this.isUnique && !this.hasAggregate;
502
+
503
+ // the table that the path starts with
504
+ const baseTable = firstFk.key.table;
505
+ const baseTableKeyColumns = firstFk.key.colset.columns;
506
+
507
+ const currTableAlias = 'T';
508
+ const baseTableAlias = 'M';
509
+ let aliasUsedForProjectedValue = 'v';
510
+ let num = 1;
511
+ while (baseTableKeyColumns.some((c: any) => c.name === aliasUsedForProjectedValue)) {
512
+ aliasUsedForProjectedValue = 'v' + num++;
513
+ }
514
+
515
+ const column = this.baseColumns[0];
516
+ let projection =
517
+ '/' +
518
+ baseTableKeyColumns
519
+ .map((c: any) => {
520
+ return `${baseTableAlias}:${encode(c.name)}`;
521
+ })
522
+ .join(',');
523
+ projection += `;${aliasUsedForProjectedValue}:=${aggFn}(${currTableAlias}:${isRow ? '*' : encode(column.name)})`;
524
+
525
+ const lastFk = this.lastForeignKeyNode;
526
+ const baseUri = `${location.service}/catalog/${location.catalog}/attributegroup/`;
527
+ const basePath = `${baseTableAlias}:=${encode(baseTable.schema.name)}:${encode(baseTable.name)}/`;
528
+
529
+ // NOTE we're not allowing first hop filter or path filter for this case, so the following is assuming that
530
+ const pathToCol = this.sourceObjectWrapper!.sourceObjectNodes!.reduce((acc: string, sn: SourceObjectNode, index: number) => {
531
+ // ignoring the first hop
532
+ if (index === 0) return '/';
533
+ // add alias to the last hop
534
+ const addAlias = sn === lastFk;
535
+ return acc + (index > 1 ? '/' : '') + (addAlias ? `${currTableAlias}:=` : '') + sn.toString();
536
+ }, '');
537
+
538
+ // make sure just projection and base uri doesn't go over limit.
539
+ if ((basePath + pathToCol + projection).length >= URL_PATH_LENGTH_LIMIT) {
540
+ $log.warn("couldn't generate the requests because of url limitation");
541
+ resolve([]);
542
+ return;
543
+ }
544
+
545
+ // get the computed filters
546
+ const keyData: any[] = [];
547
+ for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
548
+ const temp: any = {};
549
+ let hasNull = false;
550
+ for (let colIndex = 0; colIndex < firstFk.colset.columns.length; colIndex++) {
551
+ const c = firstFk.colset.columns[colIndex];
552
+ if (isDefinedAndNotNull(data[rowIndex][c.name])) {
553
+ temp[firstFk.mapping.get(c).name] = data[rowIndex][c.name];
554
+ } else {
555
+ hasNull = true;
556
+ break;
557
+ }
558
+ }
559
+ // ignore the row if any of the key values is null
560
+ if (!hasNull) {
561
+ keyData.push(temp);
562
+ }
563
+ }
564
+ const keyValueRes = generateKeyValueFilters(
565
+ baseTableKeyColumns,
566
+ keyData,
567
+ baseTable.schema.catalog,
568
+ (basePath + pathToCol + projection).length,
569
+ baseTable.displayname.value,
570
+ );
571
+
572
+ if (!keyValueRes.successful || !keyValueRes.filters) {
573
+ $log.warn(keyValueRes.message);
574
+ resolve([]);
575
+ return;
576
+ }
577
+
578
+ // turn the paths into request
579
+ const promises = keyValueRes.filters.map((f: any) => {
580
+ return http.get(baseUri + basePath + f.path + pathToCol + projection, config);
581
+ });
582
+
583
+ // if adding any of the filters would go over url limit
584
+ if (promises.length === 0) {
585
+ $log.warn("couldn't generate the requests because of url limitation");
586
+ resolve([]);
587
+ return;
588
+ }
589
+
590
+ Promise.all(promises)
591
+ .then((response) => {
592
+ const result: any[] = [];
593
+ let values: any[] = [];
594
+ response.forEach((r: any) => {
595
+ values = values.concat(r.data);
596
+ });
597
+
598
+ data.forEach((d) => {
599
+ // find the value corresponding to the current tuple
600
+ const value = values.find((v) => {
601
+ return baseTableKeyColumns.every((c: any) => {
602
+ return v[c.name] === d[firstFk.mapping.getFromColumn(c).name];
603
+ });
604
+ });
605
+
606
+ const presValue = processAggregateValue(value ? value[aliasUsedForProjectedValue] : null, this, aggFn, isRow);
607
+ // if it's alloutbound and agg fn was missing, we added array_d so we can send the request,
608
+ // so make sure the returned value is not actually array
609
+ if (isAllOutbound && !this.sourceObject.aggregate) {
610
+ if ('$self' in presValue.templateVariables && Array.isArray(presValue.templateVariables.$self)) {
611
+ presValue.templateVariables.$self = presValue.templateVariables.$self[0];
612
+ }
613
+ if ('$_self' in presValue.templateVariables && Array.isArray(presValue.templateVariables.$_self)) {
614
+ presValue.templateVariables.$_self = presValue.templateVariables.$_self[0];
615
+ }
616
+ }
617
+
618
+ result.push(presValue);
619
+ });
620
+
621
+ resolve(result);
622
+ })
623
+ .catch((err) => {
624
+ reject(ErrorService.responseToError(err));
625
+ });
626
+ });
627
+ }
628
+
629
+ // Private cached properties for getters
630
+ private _key?: any;
631
+ private _aggregateFn?: string | null;
632
+ private _reference?: Reference;
633
+ private _canUseScalarProjection?: boolean;
634
+
635
+ /**
636
+ * The tooltip that should be used for this column.
637
+ * It will return the first applicable rule:
638
+ * 1. comment that is defined on the sourceObject, use it.
639
+ * 2. if aggregate and scalar use the "<function> <col_displayname>"
640
+ * 3. if aggregate and entity use the "<function> <table_displayname>"
641
+ * 3. In entity mode, return the table's displayname.
642
+ * 4. In scalar return the column's displayname.
643
+ */
644
+ get comment(): CommentType {
645
+ if (this._comment === undefined) {
646
+ const getComment = (self: PseudoColumn): any => {
647
+ if (self.hasAggregate) {
648
+ // if defined on the sourceObject use it.
649
+ const com = _processSourceObjectComment(self.sourceObject);
650
+ if (com) {
651
+ return com;
652
+ }
653
+
654
+ // otherwise generate one
655
+ const agIndex = _pseudoColAggregateFns.indexOf(self.sourceObject.aggregate);
656
+ let dname = self.baseColumns[0].displayname.unformatted;
657
+ if (self.isEntityMode) {
658
+ dname = self.baseColumns[0].table.displayname.unformatted;
659
+ }
660
+
661
+ return _processModelComment([_pseudoColAggregateExplicitName[agIndex], dname].join(' '), false);
662
+ }
663
+
664
+ // if it's not aggregate, we can get it from the table or column depending on entity mode:
665
+ let disp: any, commentDisplayMode: any;
666
+ if (!self.isEntityMode) {
667
+ disp = self.baseColumns[0].getDisplay(self._context);
668
+ commentDisplayMode = disp.commentDisplayMode;
669
+ } else {
670
+ disp = self.table.getDisplay(self._context);
671
+ commentDisplayMode = disp.tableCommentDisplayMode;
672
+ }
673
+
674
+ return _processSourceObjectComment(
675
+ self.sourceObject,
676
+ disp.comment ? disp.comment.unformatted : null,
677
+ disp.commentRenderMarkdown,
678
+ commentDisplayMode,
679
+ );
680
+ };
681
+
682
+ this._comment = getComment(this);
683
+ }
684
+ return this._comment!;
685
+ }
686
+
687
+ /**
688
+ * The tooltip that should be used for this column.
689
+ * It will return the first applicable rule:
690
+ * 1. comment that is defined on the sourceObject, use it.
691
+ * 2. if aggregate and scalar use the "<function> <col_displayname>"
692
+ * 3. if aggregate and entity use the "<function> <table_displayname>"
693
+ * 3. In entity mode, return the table's displayname.
694
+ * 4. In scalar return the column's displayname.
695
+ */
696
+ get displayname(): DisplayName {
697
+ if (this._displayname === undefined) {
698
+ const attachDisplayname = (self: PseudoColumn): void => {
699
+ if (self.sourceObject.markdown_name) {
700
+ self._displayname = {
701
+ value: renderMarkdown(self.sourceObject.markdown_name, true),
702
+ unformatted: self.sourceObject.markdown_name,
703
+ isHTML: true,
704
+ };
705
+ return;
706
+ }
707
+
708
+ if (self.hasAggregate) {
709
+ // actual displayname
710
+ void super.displayname; // this will set the _displayname if it wasn't set before
711
+ const displayname = self.isEntityMode ? self.baseColumns[0].table.displayname : self._displayname!;
712
+
713
+ // prefix
714
+ const agIndex = _pseudoColAggregateFns.indexOf(self.sourceObject.aggregate);
715
+ const name = _pseudoColAggregateNames[agIndex];
716
+
717
+ self._displayname = {
718
+ value: name ? [name, displayname.value].join(' ') : displayname.value,
719
+ unformatted: name ? [name, displayname.unformatted].join(' ') : displayname.unformatted,
720
+ isHTML: displayname.isHTML,
721
+ };
722
+ return;
723
+ }
724
+
725
+ if (!self.isEntityMode) {
726
+ Object.getOwnPropertyDescriptor(ReferenceColumn.prototype, 'displayname')!.get!.call(self);
727
+ return;
728
+ }
729
+
730
+ // displayname of the table.
731
+ self._displayname = self.baseColumns[0].table.displayname;
732
+ return;
733
+ };
734
+
735
+ attachDisplayname(this);
736
+ }
737
+ return this._displayname!;
738
+ }
739
+
740
+ /**
741
+ * If the pseudoColumn is in entity mode will return the key that this column represents
742
+ */
743
+ get key(): Key | null {
744
+ if (this._key === undefined) {
745
+ this._key = this.isEntityMode ? this.baseColumn.uniqueNotNullKey : null;
746
+ }
747
+ return this._key;
748
+ }
749
+
750
+ get aggregateFn(): string | null {
751
+ if (this._aggregateFn === undefined) {
752
+ this._aggregateFn = this.hasAggregate ? this.sourceObject.aggregate : null;
753
+ }
754
+ return this._aggregateFn!;
755
+ }
756
+
757
+ /**
758
+ * Returns a reference to the current pseudo-column
759
+ * This is how it behaves:
760
+ * 1. If pseudo-column has no path, it will return the base reference.
761
+ * 3. if mainTuple is available, create the reference based on this path:
762
+ * <pseudoColumnSchema:PseudoColumnTable>/<path from pseudo-column to main table>/<facets based on value of shortestkey of main table>
763
+ * 4. Otherwise create the path by traversing the path
764
+ */
765
+ get reference(): Reference | RelatedReference {
766
+ if (this._reference === undefined) {
767
+ if (!this.hasPath) {
768
+ this._reference = this._baseReference.copy(undefined, undefined, this);
769
+ } else {
770
+ let facet: unknown;
771
+ if (this._mainTuple) {
772
+ facet = this.sourceObjectWrapper!.getReverseAsFacet(this._mainTuple, this._baseReference.table);
773
+ }
774
+
775
+ // if data didn't exist, we should traverse the path
776
+ let uri = this.table.uri;
777
+ if (!isObjectAndNotNull(facet)) {
778
+ uri = this._baseReference.location.compactUri + '/' + this.sourceObjectWrapper!.toString(false, false);
779
+ }
780
+
781
+ // this._reference = new Reference(parse(uri), this.table.schema.catalog, this.displayname, this.comment, this);
782
+ this._reference = new RelatedReference(
783
+ parse(uri),
784
+ this.table.schema.catalog,
785
+ this._baseReference.table,
786
+ this.firstForeignKeyNode!.nodeObject,
787
+ [],
788
+ [],
789
+ this.compressedDataSource,
790
+ undefined,
791
+ this.displayname,
792
+ this.comment,
793
+ this,
794
+ );
795
+
796
+ // make sure data exists
797
+ if (isObjectAndNotNull(facet)) {
798
+ this._reference.location.facets = facet;
799
+ }
800
+ }
801
+ }
802
+ return this._reference;
803
+ }
804
+
805
+ set reference(ref: Reference) {
806
+ //TODO this should be revisited, chaise is mutating the reference!
807
+ this._reference = ref;
808
+ }
809
+ get default(): unknown {
810
+ throw new Error('can not use this type of column in entry mode.');
811
+ }
812
+
813
+ get nullok(): boolean {
814
+ throw new Error('can not use this type of column in entry mode.');
815
+ }
816
+
817
+ /**
818
+ * Whether we can use the raw column in the projection list or not.
819
+ *
820
+ * If we only need the value of scalar column and none of the other columns of the
821
+ * all-outbound path then we can simply use the scalar projection.
822
+ * Therefore the pseudo-column must:
823
+ * - be all-outbound path in scalar mode
824
+ * - the leaf column cannot have any column_display annotation
825
+ * - the leaf column cannot be sorted or doesn't have a sort based on other columns of the table.
826
+ */
827
+ get canUseScalarProjection(): boolean {
828
+ if (this._canUseScalarProjection === undefined) {
829
+ const populate = (self: PseudoColumn): boolean => {
830
+ // only in scalar mode
831
+ if (self.isEntityMode || !self.isUnique) {
832
+ return false;
833
+ }
834
+ // if it has column_display we cannot use scalar
835
+ if (self.baseColumn.getDisplay(self._context).isMarkdownPattern) {
836
+ return false;
837
+ }
838
+ // if it's sortable and based on other columns, we cannot use scalar
839
+ const sortCols = (self as any)._sortColumns;
840
+ if (self.sortable && (sortCols.length !== 1 || sortCols[0].column.name !== self.baseColumn.name)) {
841
+ return false;
842
+ }
843
+
844
+ return true;
845
+ };
846
+ this._canUseScalarProjection = populate(this);
847
+ }
848
+ return this._canUseScalarProjection;
849
+ }
850
+ }