@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,2813 @@
1
+ import moment from 'moment-timezone';
2
+
3
+ // models
4
+ import {
5
+ BatchDeleteResponse,
6
+ ForbiddenError,
7
+ InvalidInputError,
8
+ InvalidServerResponse,
9
+ NoDataChangedError,
10
+ NotFoundError,
11
+ UnsupportedFilters,
12
+ } from '@isrd-isi-edu/ermrestjs/src/models/errors';
13
+ import {
14
+ ReferenceColumn,
15
+ FacetColumn,
16
+ VirtualColumn,
17
+ ForeignKeyPseudoColumn,
18
+ KeyPseudoColumn,
19
+ AssetPseudoColumn,
20
+ InboundForeignKeyPseudoColumn,
21
+ type PseudoColumn,
22
+ type ColumnAggregateFn,
23
+ } from '@isrd-isi-edu/ermrestjs/src/models/reference-column';
24
+ import {
25
+ Citation,
26
+ Page,
27
+ Tuple,
28
+ Contextualize,
29
+ ReferenceAggregateFn,
30
+ GoogleDatasetMetadata,
31
+ generateRelatedReference,
32
+ RelatedReference,
33
+ BulkCreateForeignKeyObject,
34
+ } from '@isrd-isi-edu/ermrestjs/src/models/reference';
35
+
36
+ // services
37
+ import ConfigService from '@isrd-isi-edu/ermrestjs/src/services/config';
38
+ import HTTPService from '@isrd-isi-edu/ermrestjs/src/services/http';
39
+ import ErrorService from '@isrd-isi-edu/ermrestjs/src/services/error';
40
+ import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
41
+
42
+ // utils
43
+ import { createPseudoColumn, isAllOutboundColumn, isRelatedColumn } from '@isrd-isi-edu/ermrestjs/src/utils/column-utils';
44
+ import {
45
+ computeReadPath,
46
+ generateFacetColumns,
47
+ computeReferenceDisplay,
48
+ type ReferenceDisplay,
49
+ generateColumnsList,
50
+ } from '@isrd-isi-edu/ermrestjs/src/utils/reference-utils';
51
+ import { isObject, isObjectAndNotNull, isStringAndNotEmpty, verify } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
52
+ import { fixedEncodeURIComponent, simpleDeepCopy } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
53
+ import {
54
+ _annotations,
55
+ _contexts,
56
+ _ERMrestACLs,
57
+ _ERMrestFeatures,
58
+ _operationsFlag,
59
+ _permissionMessages,
60
+ _systemColumns,
61
+ _tableKinds,
62
+ contextHeaderName,
63
+ URL_PATH_LENGTH_LIMIT,
64
+ } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
65
+
66
+ // legacy imports (these will need to be properly typed later)
67
+ import { parse, Location } from '@isrd-isi-edu/ermrestjs/js/parser';
68
+ import { type Server, ermrestFactory, type Catalog, type Table, type Column, type ForeignKeyRef, Type } from '@isrd-isi-edu/ermrestjs/js/core';
69
+ import { onload } from '@isrd-isi-edu/ermrestjs/js/setup/node';
70
+ import {
71
+ _getPagingValues,
72
+ _getRecursiveAnnotationValue,
73
+ _isEntryContext,
74
+ _isValidForeignKeyName,
75
+ _isValidSortElement,
76
+ compareColumnPositions,
77
+ generateKeyValueFilters,
78
+ } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
79
+ import { _compressFacetObject, _sourceColumnHelpers } from '@isrd-isi-edu/ermrestjs/js/utils/pseudocolumn_helpers';
80
+
81
+ import type { CommentType } from '@isrd-isi-edu/ermrestjs/src/models/comment';
82
+ import type { DisplayName } from '@isrd-isi-edu/ermrestjs/src/models/display-name';
83
+ import { _exportHelpers, _getDefaultExportTemplate, _referenceExportOutput, validateExportTemplate } from '@isrd-isi-edu/ermrestjs/js/export';
84
+
85
+ /**
86
+ * This function resolves a URI reference to a {@link Reference}
87
+ * object. It validates the syntax of the URI and validates that the
88
+ * references to model elements in it are correct. This function makes a
89
+ * call to the ERMrest server in order to get the `schema` which it uses to
90
+ * validate the URI path.
91
+ *
92
+ * For a consistent behavior, always contextualize the resolved `Reference` object.
93
+ * See {@link Reference#contextualize} for more information.
94
+ *
95
+ * Usage:
96
+ * ```
97
+ * // This example assumes that the client has access to the module
98
+ * resolve('https://example.org/catalog/42/entity/s:t/k=123').then(
99
+ * function(reference) {
100
+ * // the uri was successfully resolved to a `Reference` object
101
+ * console.log("The reference has URI", reference.uri);
102
+ * console.log("It has", reference.columns.length, "columns");
103
+ * console.log("Is it unique?", (reference.isUnique ? 'yes' : 'no'));
104
+ * ...
105
+ * },
106
+ * function(error) {
107
+ * // there was an error returned here
108
+ * ...
109
+ * });
110
+ * ```
111
+ * @param uri - An ERMrest resource URI, such as
112
+ * `https://example.org/ermrest/catalog/1/entity/s:t/k=123`.
113
+ * @param contextHeaderParams - An optional context header parameters object. The (key, value)
114
+ * pairs from the object are converted to URL `key=value` query parameters
115
+ * and appended to every request to the ERMrest service.
116
+ * @return Promise when resolved passes the
117
+ * {@link Reference} object. If rejected, passes one of the ermrest errors
118
+ */
119
+ export const resolve = async (uri: string, contextHeaderParams?: any): Promise<Reference> => {
120
+ verify(uri, "'uri' must be specified");
121
+ // make sure all the dependencies are loaded
122
+ await onload();
123
+ //added try block to make sure it rejects all parse() related error
124
+ // It should have been taken care by outer try but did not work
125
+ const location = parse(uri);
126
+
127
+ const server = ermrestFactory.getServer(location.service, contextHeaderParams);
128
+
129
+ const catalog = await server.catalogs.get(location.catalog);
130
+
131
+ return new Reference(location, catalog);
132
+ };
133
+
134
+ /**
135
+ * @param location - The location object generated from parsing the URI
136
+ * @param catalog - The catalog object. Since location.catalog is just an id, we need the actual catalog object too.
137
+ * @desc
138
+ * Creates a new Reference based on the given parameters. Other parts of API can access this function and it should only be used internally.
139
+ * @private
140
+ */
141
+ export const _createReference = (location: Location, catalog: Catalog): Reference => {
142
+ return new Reference(location, catalog);
143
+ };
144
+
145
+ // NOTE: This function is only being used in unit tests.
146
+ export const _createPage = (reference: Reference, etag: string, data: any, hasPrevious: boolean, hasNext: boolean): Page => {
147
+ return new Page(reference, etag, data, hasPrevious, hasNext);
148
+ };
149
+
150
+ type ActiveList = {
151
+ // TODO
152
+ // requests: {
153
+ // column: ReferenceColumn;
154
+ // objects: Array<{ index: number; column?: boolean; related?: boolean; inline?: boolean; citation?: boolean }>;
155
+ // type: 'column' | 'related' | 'inline' | 'citation';
156
+ // // TODO backwards compatibility:
157
+ // inline?: boolean;
158
+ // related?: boolean;
159
+ // citation?: boolean;
160
+ // };
161
+ requests: any[];
162
+ allOutBounds: Array<ForeignKeyPseudoColumn | PseudoColumn>;
163
+ selfLinks: Array<KeyPseudoColumn>;
164
+ };
165
+
166
+ export type VisibleColumn =
167
+ | ReferenceColumn
168
+ | PseudoColumn
169
+ | VirtualColumn
170
+ | ForeignKeyPseudoColumn
171
+ | InboundForeignKeyPseudoColumn
172
+ | ForeignKeyPseudoColumn
173
+ | AssetPseudoColumn;
174
+
175
+ /**
176
+ * Constructs a Reference object.
177
+ *
178
+ * For most uses, maybe all, of the library, the Reference
179
+ * will be the main object that the client will interact with. References
180
+ * are immutable objects and therefore can be safely passed around and
181
+ * used between multiple client components without risk that the underlying
182
+ * reference to server-side resources could change.
183
+ *
184
+ * Usage:
185
+ * Clients _do not_ directly access this constructor.
186
+ * See {@link resolve}.
187
+ * @param location - The location object generated from parsing the URI
188
+ * @param catalog - The catalog object. Since location.catalog is just an id, we need the actual catalog object too.
189
+ */
190
+ export class Reference {
191
+ /**
192
+ * The members of this object are _contextualized references_.
193
+ *
194
+ * These references will behave and reflect state according to the mode.
195
+ * For instance, in a `record` mode on a table some columns may be
196
+ * hidden.
197
+ *
198
+ * Usage:
199
+ * ```
200
+ * // assumes we have an uncontextualized `Reference` object
201
+ * var recordref = reference.contextualize.detailed;
202
+ * ```
203
+ * The `reference` is unchanged, while `recordref` now represents a
204
+ * reconfigured reference. For instance, `recordref.columns` may be
205
+ * different compared to `reference.columns`.
206
+ */
207
+ public contextualize: Contextualize;
208
+
209
+ /**
210
+ * The function that can be used to perform aggregation operations on columns in this reference.
211
+ */
212
+ public aggregate: ReferenceAggregateFn;
213
+
214
+ // private members that the public API can access through getters/setters
215
+ private _context: string;
216
+ private _location: Location;
217
+ private _server: Server;
218
+ private _table: Table;
219
+ private _facetBaseTable: Table;
220
+ private _shortestKey: Column[];
221
+ private _bulkCreateForeignKeyObject?: BulkCreateForeignKeyObject | null;
222
+ private _canCreate?: boolean;
223
+ private _canCreateReason?: string;
224
+ private _canRead?: boolean;
225
+ private _canUpdate?: boolean;
226
+ private _canUpdateReason?: string;
227
+ private _canDelete?: boolean;
228
+ private _canUseTRS?: boolean;
229
+ private _canUseTCRS?: boolean;
230
+ private _readPath?: string;
231
+ private _readAttributeGroupPathProps_cached?: any;
232
+ private _display?: ReferenceDisplay;
233
+ private _defaultExportTemplate?: any;
234
+ private _csvDownloadLink?: string | null;
235
+ private _searchColumns?: Array<VisibleColumn> | false;
236
+ private _cascadingDeletedItems?: Array<Table | RelatedReference | Reference>;
237
+ private _referenceColumns?: Array<VisibleColumn>;
238
+ private _related?: Array<RelatedReference>;
239
+ private _facetColumns?: Array<FacetColumn>;
240
+ private _activeList?: any;
241
+ private _citation?: Citation | null;
242
+ private _googleDatasetMetadata?: GoogleDatasetMetadata | null;
243
+
244
+ // props that the children can access
245
+ protected _displayname?: DisplayName;
246
+ protected _comment?: CommentType | null;
247
+ protected _pseudoColumn?: VisibleColumn;
248
+
249
+ constructor(location: Location, catalog: Catalog, displayname?: DisplayName, comment?: CommentType, pseudoColumn?: VisibleColumn) {
250
+ /**
251
+ * The members of this object are _contextualized references_.
252
+ *
253
+ * These references will behave and reflect state according to the mode.
254
+ * For instance, in a `record` mode on a table some columns may be
255
+ * hidden.
256
+ *
257
+ * Usage:
258
+ * ```
259
+ * // assumes we have an uncontextualized `Reference` object
260
+ * var recordref = reference.contextualize.detailed;
261
+ * ```
262
+ * The `reference` is unchanged, while `recordref` now represents a
263
+ * reconfigured reference. For instance, `recordref.columns` may be
264
+ * different compared to `reference.columns`.
265
+ */
266
+ this.contextualize = new Contextualize(this);
267
+
268
+ // make sure context is string to avoid breaking some code paths
269
+ this._context = '';
270
+
271
+ // make sure location object has the catalog
272
+ // TODO could be improved
273
+ location.catalogObject = catalog;
274
+
275
+ // make sure location object has the current reference
276
+ location.referenceObject = this;
277
+
278
+ this._location = location;
279
+
280
+ this._server = catalog.server;
281
+
282
+ // if schema was not provided in the URI, find the schema
283
+ this._table = catalog.schemas.findTable(location.tableName, location.schemaName);
284
+
285
+ this._facetBaseTable = catalog.schemas.findTable(location.facetBaseTableName, location.facetBaseSchemaName);
286
+
287
+ this._shortestKey = this._table.shortestKey;
288
+
289
+ this.aggregate = new ReferenceAggregateFn(this);
290
+
291
+ this._pseudoColumn = pseudoColumn;
292
+
293
+ this._displayname = displayname;
294
+
295
+ this._comment = comment;
296
+ }
297
+
298
+ get server() {
299
+ return this._server;
300
+ }
301
+
302
+ /**
303
+ * The table object for this reference
304
+ */
305
+ get table(): Table {
306
+ return this._table;
307
+ }
308
+
309
+ /**
310
+ * The base table object that is used for faceting,
311
+ * if there's a join in path, this will return a different object from .table
312
+ */
313
+ get facetBaseTable(): Table {
314
+ return this._facetBaseTable;
315
+ }
316
+
317
+ get shortestKey(): Column[] {
318
+ return this._shortestKey;
319
+ }
320
+
321
+ /**
322
+ * the pseudo column that this reference is based on
323
+ *
324
+ */
325
+ get pseudoColumn(): VisibleColumn | undefined {
326
+ return this._pseudoColumn;
327
+ }
328
+
329
+ setPseudoColumn(value: VisibleColumn) {
330
+ this._pseudoColumn = value;
331
+ }
332
+
333
+ /**
334
+ * the reference's context
335
+ */
336
+ get context(): string {
337
+ return this._context;
338
+ }
339
+
340
+ /**
341
+ * used by Contextualize to set the context of the reference
342
+ */
343
+ setContext(value: string) {
344
+ this._context = value;
345
+ }
346
+
347
+ /**
348
+ * The display name for this reference.
349
+ * displayname.isHTML will return true/false
350
+ * displayname.value has the value
351
+ */
352
+ get displayname(): DisplayName {
353
+ /* Note that displayname is context dependent. For instance,
354
+ * a reference to an entity set will use the table displayname
355
+ * as the reference displayname. However, a 'related' reference
356
+ * will use the FKR's displayname (actually its "to name" or
357
+ * "from name"). Like a Person table might have a FKR to its parent.
358
+ * In one direction, the FKR is named "parent" in the other
359
+ * direction it is named "child".
360
+ */
361
+ if (this._displayname === undefined) {
362
+ this._displayname = this._table.displayname;
363
+ }
364
+ return this._displayname;
365
+ }
366
+
367
+ /**
368
+ * The comment for this reference.
369
+ */
370
+ get comment(): CommentType | null {
371
+ /* Note that comment is context dependent. For instance,
372
+ * a reference to an entity set will use the table comment
373
+ * as the reference comment. However, a 'related' reference
374
+ * will use the FKR's comment (actually its "to comment" or
375
+ * "from comment"). Like a Person table might have a FKR to its parent.
376
+ * In one direction, the FKR is named "parent" in the other
377
+ * direction it is named "child".
378
+ */
379
+ if (this._comment === undefined) {
380
+ this._comment = this._table.getDisplay(this._context).comment as CommentType | null;
381
+ }
382
+ return this._comment;
383
+ }
384
+
385
+ /**
386
+ * The display mode configuration for this reference.
387
+ * This returns an object with properties for configuring how the reference should be displayed:
388
+ *
389
+ * - `type`: The display type ('table', 'markdown', or 'module')
390
+ * - `hideRowCount`: Whether to hide the row count
391
+ * - `showFaceting`: Whether faceting is enabled
392
+ * - `maxFacetDepth`: Maximum facet depth allowed
393
+ * - `facetPanelOpen`: Whether the facet panel should be open by default
394
+ * - `showSavedQuery`: Whether saved query UI should be shown
395
+ *
396
+ * For markdown type displays, it also includes:
397
+ * - `_rowMarkdownPattern`: The markdown pattern for rows
398
+ * - `_pageMarkdownPattern`: The markdown pattern for pages
399
+ * - `_separator`, `_prefix`, `_suffix`: Markdown formatting options
400
+ *
401
+ * @returns The display configuration object
402
+ */
403
+ get display(): ReferenceDisplay {
404
+ if (this._display === undefined) {
405
+ this._display = computeReferenceDisplay(this);
406
+ }
407
+ return this._display;
408
+ }
409
+
410
+ /**
411
+ * The string form of the `URI` for this reference.
412
+ * NOTE: It is not understandable by ermrest, and it also doesn't have the modifiers (sort, page).
413
+ * Should not be used for sending requests to ermrest, use this.location.ermrestCompactUri instead.
414
+ */
415
+ get uri(): string {
416
+ return this._location.compactUri;
417
+ }
418
+
419
+ /**
420
+ * Location object that has uri of current reference
421
+ */
422
+ get location(): Location {
423
+ return this._location;
424
+ }
425
+
426
+ /**
427
+ * this is used when we want to change the location of a reference without creating a new one.
428
+ * it will also attach the current reference to the location object.
429
+ */
430
+ setLocation(loc: Location) {
431
+ this._location = loc;
432
+ this._location.referenceObject = this;
433
+ }
434
+
435
+ get readPath(): string {
436
+ if (this._readPath === undefined) {
437
+ this._readPath = computeReadPath(this, false).value;
438
+ }
439
+ return this._readPath;
440
+ }
441
+
442
+ /**
443
+ * This will generate a new unfiltered reference each time.
444
+ * Returns a reference that points to all entities of current table
445
+ *
446
+ * NOTE: This will not have any context, and always returns a Reference object (not RelatedReference or other special reference types).
447
+ */
448
+ get unfilteredReference(): Reference {
449
+ const table = this.table;
450
+ const cat = table.schema.catalog;
451
+ const uri = `${cat.server.uri}/catalog/${cat.id}/${this.location.api}/${fixedEncodeURIComponent(table.schema.name)}:${fixedEncodeURIComponent(table.name)}`;
452
+ return new Reference(parse(uri), table.schema.catalog);
453
+ }
454
+
455
+ /**
456
+ * App-specific URL
457
+ *
458
+ * @throws {Error} if `_appLinkFn` is not defined.
459
+ */
460
+ get appLink() {
461
+ if (typeof ConfigService.appLinkFn !== 'function') {
462
+ throw new Error('`appLinkFn` function is not defined.');
463
+ }
464
+ const tag = this._context ? this._table._getAppLink(this._context) : this.table._getAppLink();
465
+ return ConfigService.appLinkFn(tag, this._location, this._context);
466
+ }
467
+
468
+ /**
469
+ * Returns a uri that will properly generate the download link for a csv document
470
+ * NOTE It will honor the visible columns in `export` context
471
+ *
472
+ **/
473
+ get csvDownloadLink(): string | null {
474
+ if (this._csvDownloadLink === undefined) {
475
+ const cid = this.table.schema.catalog.server.cid;
476
+ let qParam = '?limit=none&accept=csv&uinit=1';
477
+ qParam += cid ? '&cid=' + cid : '';
478
+ if (this.displayname && this.displayname.unformatted) {
479
+ qParam += '&download=' + fixedEncodeURIComponent(this.displayname.unformatted);
480
+ }
481
+
482
+ const isCompact = this._context === _contexts.COMPACT;
483
+ const defaultExportOutput = _referenceExportOutput(this, this.location.mainTableAlias, undefined, false, null, isCompact);
484
+
485
+ if (defaultExportOutput === null || defaultExportOutput === undefined) {
486
+ this._csvDownloadLink = null;
487
+ } else {
488
+ let uri;
489
+ if (['attributegroup', 'entity'].indexOf(defaultExportOutput.source.api) !== -1) {
490
+ // example.com/ermrest/catalog/<id>/<api>/<current-path>/<vis-col-projection-and-needed-joins>
491
+ uri = [
492
+ this.location.service,
493
+ 'catalog',
494
+ this._location.catalog,
495
+ defaultExportOutput.source.api,
496
+ this.location.ermrestCompactPath,
497
+ defaultExportOutput.source.path,
498
+ ].join('/');
499
+ } else {
500
+ // won't happen with the current code, but to make this future proof
501
+ uri = this.location.ermrestCompactUri;
502
+ }
503
+
504
+ this._csvDownloadLink = uri + qParam;
505
+ }
506
+ }
507
+ return this._csvDownloadLink;
508
+ }
509
+
510
+ /**
511
+ * The default information that we want to be logged. This includes:
512
+ * - catalog, schema_table
513
+ * TODO Evaluate whether we even need this function
514
+ */
515
+ get defaultLogInfo() {
516
+ return {
517
+ catalog: this.table.schema.catalog.id,
518
+ schema_table: this.table.schema.name + ':' + this.table.name,
519
+ };
520
+ }
521
+
522
+ /**
523
+ * The object that can be logged to capture the filter state of the reference.
524
+ * The return object can have:
525
+ * - filters: the facet object.
526
+ * - custom_filters:
527
+ * - the filter strings that parser couldn't turn to facet.
528
+ * - if we could turn the custom filter to facet, this will return `true`
529
+ * - cfacet: if there's a cfacet it will be 1
530
+ * - cfacet_str: if cfacet=1, it will be displayname of cfacet.
531
+ * - cfacet_path: if cfacet=1, it will be ermrest path of cfacet.
532
+ * This function creates a new object everytime that it's called, so it
533
+ * can be manipulated further.
534
+ */
535
+ get filterLogInfo() {
536
+ const obj: { cfacet?: 1; cfacet_str?: unknown; cfacet_path?: string; filters?: unknown; custom_filters?: string | boolean } = {};
537
+
538
+ // custom facet
539
+ if (this.location.customFacets) {
540
+ const cf = this.location.customFacets;
541
+ obj.cfacet = 1;
542
+ if (cf.displayname) {
543
+ obj.cfacet_str = cf.displayname;
544
+ } else if (cf.ermrestPath) {
545
+ obj.cfacet_path = cf.ermrestPath;
546
+ }
547
+ }
548
+
549
+ if (this.location.facets) {
550
+ obj.filters = _compressFacetObject(this.location.facets.decoded);
551
+ } else if (this.location.filter) {
552
+ if (this.location.filter.facet) {
553
+ obj.filters = _compressFacetObject(this.location.filter.facet);
554
+ obj.custom_filters = true;
555
+ } else {
556
+ obj.custom_filters = this.location.filtersString;
557
+ }
558
+ }
559
+ return obj;
560
+ }
561
+
562
+ /**
563
+ * This will ensure the ermrestCompactPath is also
564
+ * using the same aliases that we are going to use for allOutbounds
565
+ * I'm attaching this to Reference so it's cached and we don't have to
566
+ * compute it multiple times
567
+ * @private
568
+ */
569
+ get _readAttributeGroupPathProps(): any {
570
+ if (this._readAttributeGroupPathProps_cached === undefined) {
571
+ const allOutBounds = this.activeList.allOutBounds;
572
+ this._readAttributeGroupPathProps_cached = this._location.computeERMrestCompactPath(allOutBounds.map((ao) => ao.sourceObject));
573
+ }
574
+ return this._readAttributeGroupPathProps_cached;
575
+ }
576
+
577
+ /**
578
+ * The array of column definitions which represent the model of
579
+ * the resources accessible via this reference.
580
+ *
581
+ * _Note_: in database jargon, technically everything returned from
582
+ * ERMrest is a 'tuple' or a 'relation'. A tuple consists of attributes
583
+ * and the definitions of those attributes are represented here as the
584
+ * array of {@link Column}s. The column definitions may be
585
+ * contextualized (see {@link Reference#contextualize}).
586
+ *
587
+ * Usage:
588
+ * ```
589
+ * for (var i=0, len=reference.columns.length; i<len; i++) {
590
+ * var col = reference.columns[i];
591
+ * console.log("Column name:", col.name, "has display name:", col.displayname);
592
+ * }
593
+ * ```
594
+ */
595
+ get columns(): VisibleColumn[] {
596
+ if (this._referenceColumns === undefined) {
597
+ this.generateColumnsList();
598
+ }
599
+ return this._referenceColumns!;
600
+ }
601
+
602
+ /**
603
+ * Generate the list of columns for this reference based on context and annotations
604
+ */
605
+ generateColumnsList(tuple?: Tuple, columnsList?: any[], dontChangeReference?: boolean, skipLog?: boolean): ReferenceColumn[] {
606
+ const resultColumns = generateColumnsList(this, tuple, columnsList, skipLog);
607
+
608
+ if (!dontChangeReference) {
609
+ this._referenceColumns = resultColumns;
610
+ }
611
+ return resultColumns;
612
+ }
613
+
614
+ /**
615
+ * NOTE this will not map the entity choice pickers, use "generateFacetColumns" instead.
616
+ * so directly using this is not recommended.
617
+ */
618
+ get facetColumns(): FacetColumn[] {
619
+ if (this._facetColumns === undefined) {
620
+ const res = generateFacetColumns(this, true);
621
+ if (!(res instanceof Promise)) {
622
+ this._facetColumns = res.facetColumns;
623
+ }
624
+ }
625
+ return this._facetColumns!;
626
+ }
627
+
628
+ /**
629
+ * Returns the facets that should be represented to the user.
630
+ * It will return a promise resolved with the following object:
631
+ * {
632
+ * facetColumns: <an array of FacetColumn objects>
633
+ * issues: <if null it means that there wasn't any issues, otherwise will be a UnsupportedFilters object>
634
+ * }
635
+ *
636
+ * - If `filter` context is not defined for the table, following heuristics will be used:
637
+ * - All the visible columns in compact context.
638
+ * - All the related entities in detailed context.
639
+ * - This function will modify the Reference.location to reflect the preselected filters
640
+ * per annotation as well as validation.
641
+ * - This function will validate the facets in the url, by doing the following (any invalid filter will be ignored):
642
+ * - Making sure given `source` or `sourcekey` are valid
643
+ * - If `source_domain` is passed,
644
+ * - Making sure `source_domain.table` and `source_domain.schema` are valid
645
+ * - Using `source_domain.column` instead of end column in case of scalars
646
+ * - Sending request to fetch the rows associated with the entity choices,
647
+ * and ignoring the ones that don't return any result.
648
+ * - The valid filters in the url will either be matched with an existing facet,
649
+ * or result in a new facet column.
650
+ * Usage:
651
+ * ```
652
+ * reference.generateFacetColumns().then((result) => {
653
+ * var newRef = result.facetColumns[0].addChoiceFilters(['value']);
654
+ * var newRef2 = newRef.facetColumns[1].addSearchFilter('text 1');
655
+ * var newRef3 = newRef2.facetColumns[2].addRangeFilter(1, 2);
656
+ * var newRef4 = newRef3.facetColumns[3].removeAllFilters();
657
+ * for (var i=0, len=newRef4.facetColumns.length; i<len; i++) {
658
+ * var fc = reference.facetColumns[i];
659
+ * console.log("Column name:", fc.column.name, "has following facets:", fc.filters);
660
+ * }
661
+ * });
662
+ * ```
663
+ */
664
+ generateFacetColumns(): Promise<{ facetColumns: FacetColumn[]; issues: UnsupportedFilters | null }> {
665
+ return new Promise((resolve, reject) => {
666
+ const p = generateFacetColumns(this, false) as Promise<{ facetColumns: FacetColumn[]; issues: UnsupportedFilters | null }>;
667
+ p.then((res) => {
668
+ this._facetColumns = res.facetColumns;
669
+ resolve(res);
670
+ }).catch((err) => {
671
+ this._facetColumns = [];
672
+ reject(err);
673
+ });
674
+ });
675
+ }
676
+
677
+ /**
678
+ * This is only added so _applyFilters in facet-column can use it.
679
+ * SHOULD NOT be used outside of this library.
680
+ */
681
+ manuallySetFacetColumns(facetCols: FacetColumn[]) {
682
+ this._facetColumns = facetCols;
683
+ }
684
+
685
+ /**
686
+ * The "related" references. Relationships are defined by foreign key
687
+ * references between {@link Table}s. Those references can be
688
+ * considered "outbound" where the table has FKRs to other entities or
689
+ * "inbound" where other entities have FKRs to this entity. Finally,
690
+ * entities can be "associated" by means of associative entities. Those
691
+ * are entities in another table that establish _many-to-many_
692
+ * relationships between entities. If this help `A <- B -> C` where
693
+ * entities in `B` establish relationships between entities in `A` and
694
+ * `C`. Thus entities in `A` and `C` may be associated and we may
695
+ * ignore `B` and think of this relationship as `A <-> C`, unless `B`
696
+ * has other moderating attributes, for instance that indicate the
697
+ * `type` of relationship, but this is a model-depenent detail.
698
+ *
699
+ * NOTE: This API should not be used for generating related references
700
+ * since we need the main tuple data for generating related references.
701
+ * Please use `generateRelatedList` or `generateActiveList` before
702
+ * calling this API.
703
+ */
704
+ get related(): Array<RelatedReference> {
705
+ if (this._related === undefined) {
706
+ this.generateRelatedList();
707
+ }
708
+ return this._related!;
709
+ }
710
+
711
+ /**
712
+ * The function that can be used to generate .related API.
713
+ * The logic is as follows:
714
+ *
715
+ * 1. Get the list of visible inbound foreign keys (if annotation is not defined,
716
+ * it will consider all the inbound foreign keys).
717
+ *
718
+ * 2. Go through the list of visible inbound foreign keys
719
+ * 2.1 if it's not part of InboundForeignKeyPseudoColumn apply the generateRelatedRef logic.
720
+ * The logic for are sorted based on following attributes:
721
+ * 1. displayname
722
+ * 2. position of key columns that are involved in the foreignkey
723
+ * 3. position of columns that are involved in the foreignkey
724
+ *
725
+ * NOTE: Passing "tuple" to this function is highly recommended.
726
+ * Without tuple related references will be generated by appending the compactPath with
727
+ * join statements. Because of this we cannot optimize the URL and other
728
+ * parts of the code cannot behave properly (e.g. getUnlinkTRS in read cannot be used).
729
+ * By passing "tuple", we can create the related references by creaing a facet blob
730
+ * which can be integrated with other parts of the code.
731
+ *
732
+ */
733
+ generateRelatedList(tuple?: Tuple): Array<RelatedReference> {
734
+ this._related = [];
735
+
736
+ let visibleFKs = this.table.referredBy._contextualize(this._context, tuple);
737
+ let notSorted;
738
+
739
+ if (visibleFKs === -1) {
740
+ notSorted = true;
741
+ visibleFKs = this.table.referredBy.all().map((fkr) => ({ foreignKey: fkr }));
742
+ }
743
+
744
+ // if visible columns list is empty, make it.
745
+ if (this._referenceColumns === undefined) {
746
+ // will generate the this._inboundFKColumns
747
+ this.generateColumnsList(tuple);
748
+ }
749
+
750
+ const currentColumns: Record<string, boolean> = {};
751
+ if (Array.isArray(this._referenceColumns)) {
752
+ this._referenceColumns.forEach((col) => {
753
+ if (isRelatedColumn(col)) {
754
+ currentColumns[col.name] = true;
755
+ }
756
+ });
757
+ }
758
+
759
+ for (let i = 0; i < visibleFKs.length; i++) {
760
+ let fkr = visibleFKs[i];
761
+ let relatedRef: RelatedReference, fkName: string;
762
+ if (fkr.isPath) {
763
+ // since we're sure that the pseudoColumn either going to be
764
+ // general pseudoColumn or InboundForeignKeyPseudoColumn then it will have reference
765
+ const pseudoCol = createPseudoColumn(this, fkr.sourceObjectWrapper, tuple) as PseudoColumn | InboundForeignKeyPseudoColumn;
766
+ relatedRef = pseudoCol.reference as RelatedReference;
767
+ fkName = relatedRef.pseudoColumn!.name;
768
+ } else {
769
+ fkr = fkr.foreignKey;
770
+
771
+ // make sure that this fkr is not from an alternative table to self
772
+ if (
773
+ fkr._table._isAlternativeTable() &&
774
+ fkr._table._altForeignKey !== undefined &&
775
+ fkr._table._baseTable === this._table &&
776
+ fkr._table._altForeignKey === fkr
777
+ ) {
778
+ continue;
779
+ }
780
+ relatedRef = generateRelatedReference(this, fkr, tuple, true);
781
+ fkName = _sourceColumnHelpers.generateForeignKeyName(fkr, true) as string;
782
+ }
783
+
784
+ // if it's already added to the visible-columns (inline related) don't add it again.
785
+ if (currentColumns[fkName]) {
786
+ continue;
787
+ }
788
+ this._related.push(relatedRef);
789
+ }
790
+
791
+ if (notSorted && this._related.length !== 0) {
792
+ return this._related.sort((a, b) => {
793
+ // displayname
794
+ if (a.displayname.value !== b.displayname.value) {
795
+ return (a.displayname.value as string).localeCompare(b.displayname.value as string);
796
+ }
797
+
798
+ // columns
799
+ const keyColPositions = compareColumnPositions(a._relatedKeyColumnPositions, b._relatedKeyColumnPositions);
800
+ if (keyColPositions !== 0) {
801
+ return keyColPositions;
802
+ }
803
+
804
+ // foreignkey columns
805
+ const fkeyColPositions = compareColumnPositions(a._relatedFkColumnPositions, b._relatedFkColumnPositions);
806
+ return fkeyColPositions === -1 ? -1 : 1;
807
+ });
808
+ }
809
+
810
+ return this._related;
811
+ }
812
+
813
+ /**
814
+ * Refer to Reference.generateActiveList
815
+ */
816
+ get activeList(): ActiveList {
817
+ if (this._activeList === undefined) {
818
+ this.generateActiveList();
819
+ }
820
+ return this._activeList;
821
+ }
822
+
823
+ /**
824
+ * Generates the list of extra elements that the page might need,
825
+ * this should include
826
+ * - requests: An array of the secondary request objects which includes aggregates, entitysets, inline tables, and related tables.
827
+ * Depending on the type of request it can have different attributes.
828
+ * - for aggregate, entitysets, uniquefiltered, and outboundFirst (in entry):
829
+ * {column: ERMrest.ReferenceColumn, <type>: true, objects: [{index: integer, column: boolean, related: boolean, inline: boolean, citation: boolean}]
830
+ * where the type is aggregate`, `entity`, `entityset`, or `firstOutbound`. Each object is capturing where in the page needs this pseudo-column.
831
+ * - for related and inline tables:
832
+ * {<type>: true, index: integer}
833
+ * where the type is `inline` or `related`.
834
+ * - allOutBounds: the all-outbound foreign keys (added so we can later append to the url).
835
+ * ERMrest.ReferenceColumn[]
836
+ * - selfLinks: the self-links (so it can be added to the template variables)
837
+ * ERMrest.KeyPseudoColumn[]
838
+ *
839
+ * TODO we might want to detect duplicates in allOutBounds better?
840
+ * currently it's done based on name, but based on the path should be enough..
841
+ * as long as it's entity the last column is useless...
842
+ * the old code was kinda handling this by just adding the multi ones,
843
+ * so if the fk definition is based on fkcolumn and not the RID, it would handle it.
844
+ *
845
+ * @param tuple - optional tuple parameter
846
+ * @private
847
+ */
848
+ generateActiveList(tuple?: Tuple): ActiveList {
849
+ // VARIABLES:
850
+ const allOutBounds: Array<PseudoColumn | ForeignKeyPseudoColumn> = [];
851
+ const requests: any[] = [];
852
+ const selfLinks: Array<KeyPseudoColumn> = [];
853
+ const consideredUniqueFiltered: { [key: string]: number } = {};
854
+ const consideredSets: { [key: string]: number } = {};
855
+ const consideredOutbounds: { [key: string]: boolean } = {};
856
+ const consideredAggregates: { [key: string]: number } = {};
857
+ const consideredSelfLinks: { [key: string]: boolean } = {};
858
+ const consideredEntryWaitFors: { [key: string]: number } = {};
859
+
860
+ const sds = this.table.sourceDefinitions;
861
+
862
+ // in detailed, we want related and citation
863
+ const isDetailed = this._context === _contexts.DETAILED;
864
+ // in entry, don't include waitfors in the activelist used for loading the page.
865
+ // the waitfors will be used in chaise instead prior to submission.
866
+ const isEntry = _isEntryContext(this._context);
867
+
868
+ const COLUMN_TYPE = 'column';
869
+ const RELATED_TYPE = 'related';
870
+ const CITATION_TYPE = 'citation';
871
+ const INLINE_TYPE = 'inline';
872
+
873
+ // FUNCTIONS:
874
+ const hasAggregate = (col: VisibleColumn) => {
875
+ return col.hasWaitForAggregate || ((col as PseudoColumn).isPathColumn && (col as PseudoColumn).hasAggregate);
876
+ };
877
+
878
+ // col: the column that we need its data
879
+ // isWaitFor: whether it was part of waitFor or just visible
880
+ // type: where in the page it belongs to
881
+ // index: the container index (column index, or related index) (optional)
882
+ const addColToActiveList = (col: VisibleColumn, isWaitFor: boolean, type: string, index?: number) => {
883
+ const obj: any = { isWaitFor: isWaitFor };
884
+
885
+ // add the type
886
+ obj[type] = true;
887
+
888
+ // add index if available (not available in citation)
889
+ if (Number.isInteger(index)) {
890
+ obj.index = index;
891
+ }
892
+
893
+ if (isWaitFor && isEntry) {
894
+ if (col.name in consideredEntryWaitFors) {
895
+ requests[consideredEntryWaitFors[col.name]].objects.push(obj);
896
+ return;
897
+ }
898
+ consideredEntryWaitFors[col.name] = requests.length;
899
+ requests.push({ firstOutbound: true, column: col, objects: [obj] });
900
+ return;
901
+ }
902
+
903
+ // unique filtered
904
+ // TODO FILTER_IN_SOURCE chaise should use this type of column as well?
905
+ // TODO FILTER_IN_SOURCE should be added to documentation as well
906
+ if ((col as PseudoColumn).sourceObjectWrapper?.isUniqueFiltered) {
907
+ // duplicate
908
+ if (col.name in consideredUniqueFiltered) {
909
+ requests[consideredUniqueFiltered[col.name]].objects.push(obj);
910
+ return;
911
+ }
912
+
913
+ // new
914
+ consideredUniqueFiltered[col.name] = requests.length;
915
+ requests.push({ entity: true, column: col, objects: [obj] });
916
+ return;
917
+ }
918
+
919
+ // aggregates
920
+ if ((col as PseudoColumn).isPathColumn && (col as PseudoColumn).hasAggregate) {
921
+ // duplicate
922
+ if (col.name in consideredAggregates) {
923
+ requests[consideredAggregates[col.name]].objects.push(obj);
924
+ return;
925
+ }
926
+
927
+ // new
928
+ consideredAggregates[col.name] = requests.length;
929
+ requests.push({ aggregate: true, column: col, objects: [obj] });
930
+ return;
931
+ }
932
+
933
+ //entitysets
934
+ if (isRelatedColumn(col)) {
935
+ if (!isDetailed) {
936
+ return; // only acceptable in detailed
937
+ }
938
+
939
+ if (col.name in consideredSets) {
940
+ requests[consideredSets[col.name]].objects.push(obj);
941
+ return;
942
+ }
943
+
944
+ consideredSets[col.name] = requests.length;
945
+ requests.push({ entityset: true, column: col, objects: [obj] });
946
+ }
947
+
948
+ // all outbounds
949
+ if (isAllOutboundColumn(col)) {
950
+ if (col.name in consideredOutbounds) return;
951
+ consideredOutbounds[col.name] = true;
952
+ allOutBounds.push(col as PseudoColumn | ForeignKeyPseudoColumn);
953
+ }
954
+
955
+ // self-links
956
+ if ((col as KeyPseudoColumn).isKey) {
957
+ if (col.name in consideredSelfLinks) return;
958
+ consideredSelfLinks[col.name] = true;
959
+ selfLinks.push(col as KeyPseudoColumn);
960
+ }
961
+ };
962
+
963
+ const addInlineColumn = (col: VisibleColumn, i: number) => {
964
+ if (isRelatedColumn(col)) {
965
+ requests.push({ inline: true, index: i });
966
+ } else {
967
+ addColToActiveList(col, false, COLUMN_TYPE, i);
968
+ }
969
+
970
+ col.waitFor.forEach((wf: any) => {
971
+ addColToActiveList(wf, true, isRelatedColumn(col) ? INLINE_TYPE : COLUMN_TYPE, i);
972
+ });
973
+ };
974
+
975
+ // THE CODE STARTS HERE:
976
+ const columns = this.generateColumnsList(tuple);
977
+
978
+ // citation
979
+ if (isDetailed && this.citation) {
980
+ this.citation.waitFor.forEach((col) => {
981
+ addColToActiveList(col, true, CITATION_TYPE);
982
+ });
983
+ }
984
+
985
+ // columns without aggregate
986
+ columns.forEach((col, i: number) => {
987
+ if (!hasAggregate(col)) {
988
+ addInlineColumn(col, i);
989
+ }
990
+ });
991
+
992
+ // columns with aggregate
993
+ columns.forEach((col, i: number) => {
994
+ if (hasAggregate(col)) {
995
+ addInlineColumn(col, i);
996
+ }
997
+ });
998
+
999
+ // related tables
1000
+ if (isDetailed) {
1001
+ this.generateRelatedList(tuple).forEach((rel, i) => {
1002
+ requests.push({ related: true, index: i });
1003
+
1004
+ if (rel.pseudoColumn) {
1005
+ rel.pseudoColumn.waitFor.forEach((wf) => {
1006
+ addColToActiveList(wf, true, RELATED_TYPE, i);
1007
+ });
1008
+ }
1009
+ });
1010
+ }
1011
+
1012
+ //fkeys
1013
+ sds.fkeys.forEach((fk) => {
1014
+ if (fk.name in consideredOutbounds) return;
1015
+ consideredOutbounds[fk.name] = true;
1016
+ allOutBounds.push(new ForeignKeyPseudoColumn(this, fk));
1017
+ });
1018
+
1019
+ this._activeList = {
1020
+ requests: requests,
1021
+ allOutBounds: allOutBounds,
1022
+ selfLinks: selfLinks,
1023
+ };
1024
+
1025
+ return this._activeList;
1026
+ }
1027
+
1028
+ /**
1029
+ * List of columns that are used for search
1030
+ * if it's false, then we're using all the columns for search
1031
+ */
1032
+ get searchColumns(): Array<VisibleColumn> | false {
1033
+ if (this._searchColumns === undefined) {
1034
+ this._searchColumns = false;
1035
+ if (this.table.searchSourceDefinition && Array.isArray(this.table.searchSourceDefinition.columns)) {
1036
+ this._searchColumns = this.table.searchSourceDefinition.columns.map((sd) => {
1037
+ return createPseudoColumn(this, sd);
1038
+ });
1039
+ }
1040
+ }
1041
+ return this._searchColumns;
1042
+ }
1043
+
1044
+ /**
1045
+ * Will return the expor templates that are available for this reference.
1046
+ * It will validate the templates that are defined in annotations.
1047
+ * If its `detailed` context and annotation was missing,
1048
+ * it will return the default export template.
1049
+ * @param useDefault whether we should use default template or not
1050
+ */
1051
+ getExportTemplates(useDefault?: boolean) {
1052
+ const helpers = _exportHelpers;
1053
+
1054
+ // either null or array
1055
+ const templates = helpers.getExportAnnotTemplates(this.table, this.context);
1056
+
1057
+ // annotation is missing
1058
+ if (!Array.isArray(templates)) {
1059
+ const canUseDefault = useDefault && this.context === _contexts.DETAILED && this.defaultExportTemplate != null;
1060
+
1061
+ return canUseDefault ? [this.defaultExportTemplate] : [];
1062
+ }
1063
+
1064
+ const finalRes = helpers.replaceFragments(templates, helpers.getExportFragmentObject(this.table, this.defaultExportTemplate));
1065
+
1066
+ // validate the templates
1067
+ return finalRes.filter(validateExportTemplate);
1068
+ }
1069
+
1070
+ /**
1071
+ * Returns a object, that can be used as a default export template.
1072
+ * NOTE SHOULD ONLY BE USED IN DETAILED CONTEXT
1073
+ * It will include:
1074
+ * - csv of the main table.
1075
+ * - csv of all the related entities
1076
+ * - fetch all the assets. For fetch, we need to provide url, length, and md5 (or other checksum types).
1077
+ * if these columns are missing from the asset annotation, they won't be added.
1078
+ * - fetch all the assetes of related tables.
1079
+ * @type {string}
1080
+ */
1081
+ get defaultExportTemplate() {
1082
+ if (this._defaultExportTemplate === undefined) {
1083
+ this._defaultExportTemplate = _getDefaultExportTemplate(this);
1084
+ }
1085
+ return this._defaultExportTemplate;
1086
+ }
1087
+
1088
+ /**
1089
+ * Find a column given its name. It will search in this order:
1090
+ * 1. Visible columns
1091
+ * 2. Table columns
1092
+ * 3. search by constraint name in visible foreignkey and keys (backward compatibility)
1093
+ * Will throw an error if column is not found
1094
+ * @param name name of column
1095
+ * @returns ReferenceColumn
1096
+ */
1097
+ getColumnByName(name: string): VisibleColumn {
1098
+ // given an array of columns, find column by name
1099
+ const findCol = (list: any[]): any => {
1100
+ for (let i = 0; i < list.length; i++) {
1101
+ if (list[i].name === name) {
1102
+ return list[i];
1103
+ }
1104
+ }
1105
+ return false;
1106
+ };
1107
+
1108
+ // search in visible columns
1109
+ let c = findCol(this.columns);
1110
+ if (c) {
1111
+ return c;
1112
+ }
1113
+
1114
+ // search in table columns
1115
+ c = findCol(this.table.columns.all());
1116
+ if (c) {
1117
+ return new ReferenceColumn(this, [c]);
1118
+ }
1119
+
1120
+ // backward compatibility, look at fks and keys using constraint name
1121
+ for (const c of this.columns) {
1122
+ if (
1123
+ c.isPseudo &&
1124
+ (((c as KeyPseudoColumn).isKey && (c as KeyPseudoColumn).key._constraintName === name) ||
1125
+ ((c as ForeignKeyPseudoColumn).isForeignKey && (c as ForeignKeyPseudoColumn).foreignKey._constraintName === name))
1126
+ ) {
1127
+ return c;
1128
+ }
1129
+ }
1130
+
1131
+ throw new NotFoundError('', 'Column ' + name + ' not found in table ' + this.table.name + '.');
1132
+ }
1133
+
1134
+ /**
1135
+ * Given a page, will return a reference that has
1136
+ * - the sorting and paging of the given page.
1137
+ * - the merged facets of the based reference and given page's facet.
1138
+ * to match the page.
1139
+ * NOTE: The given page must be based on the same table that this current table is based on.
1140
+ */
1141
+ setSamePaging(page: Page) {
1142
+ const pageRef = page.reference;
1143
+
1144
+ /*
1145
+ * It only works when page's table and current table are the same.
1146
+ */
1147
+ if (pageRef.table !== this.table) {
1148
+ throw new InvalidInputError('Given page is not from the same table.');
1149
+ }
1150
+
1151
+ const newRef = this.copy();
1152
+ newRef._location = this._location._clone(newRef);
1153
+
1154
+ // same facets
1155
+ if (pageRef.location.facets) {
1156
+ const andFilters = newRef.location.facets ? newRef.location.facets.andFilters : [];
1157
+ Array.prototype.push.apply(andFilters, pageRef.location.facets.andFilters);
1158
+ newRef._location.facets = { and: andFilters };
1159
+ }
1160
+
1161
+ /*
1162
+ * This case is not possible in the current implementation,
1163
+ * page object is being created from read, and therefore always the
1164
+ * attached reference will have sortObject.
1165
+ * But if it didn't have any sort, we should just return the reference.
1166
+ */
1167
+ if (!pageRef._location.sortObject) {
1168
+ return newRef;
1169
+ }
1170
+
1171
+ // same sort
1172
+ newRef._location.sortObject = simpleDeepCopy(pageRef._location.sortObject);
1173
+
1174
+ // same pagination
1175
+ newRef._location.afterObject = pageRef._location.afterObject ? simpleDeepCopy(pageRef._location.afterObject) : null;
1176
+ newRef._location.beforeObject = pageRef._location.beforeObject ? simpleDeepCopy(pageRef._location.beforeObject) : null;
1177
+
1178
+ // if we have extra data, and one of before/after is not available
1179
+ if (page.extraData && (!pageRef._location.beforeObject || !pageRef._location.afterObject)) {
1180
+ const pageValues = _getPagingValues(newRef, page.extraData, page.extraLinkedData);
1181
+
1182
+ // add before based on extra data
1183
+ if (!pageRef._location.beforeObject) {
1184
+ newRef._location.beforeObject = pageValues;
1185
+ }
1186
+ // add after based on extra data
1187
+ else {
1188
+ newRef._location.afterObject = pageValues;
1189
+ }
1190
+ }
1191
+ return newRef;
1192
+ }
1193
+
1194
+ /**
1195
+ * Remove all the filters, facets, and custom-facets from the reference
1196
+ * @param {boolean} sameFilter By default we're removing filters, if this is true filters won't be changed.
1197
+ * @param {boolean} sameCustomFacet By default we're removing custom-facets, if this is true custom-facets won't be changed.
1198
+ * @param {boolean} sameFacet By default we're removing facets, if this is true facets won't be changed.
1199
+ *
1200
+ * @return {reference} A reference without facet filters
1201
+ */
1202
+ removeAllFacetFilters(sameFilter?: boolean, sameCustomFacet?: boolean, sameFacet?: boolean) {
1203
+ verify(!(sameFilter && sameCustomFacet && sameFacet), 'at least one of the options must be false.');
1204
+
1205
+ const newReference = this.copy();
1206
+
1207
+ // update the facetColumns list
1208
+ // NOTE there are two reasons for this:
1209
+ // 1. avoid computing the facet columns logic each time that we are removing facets.
1210
+ // 2. we don't want the list of facetColumns to be changed because of a change in the facets.
1211
+ // Some facetColumns are only in the list because they had an initial filter, and if we
1212
+ // compute that logic again, those facets will disappear.
1213
+ newReference._facetColumns = [];
1214
+ this.facetColumns.forEach((fc) => {
1215
+ newReference._facetColumns!.push(new FacetColumn(newReference, fc.index, fc.sourceObjectWrapper, sameFacet ? fc.filters.slice() : []));
1216
+ });
1217
+
1218
+ // update the location objectcd
1219
+ newReference._location = this._location._clone(newReference);
1220
+ newReference._location.beforeObject = null;
1221
+ newReference._location.afterObject = null;
1222
+
1223
+ if (!sameFacet) {
1224
+ // the hidden filters should still remain
1225
+ const andFilters = this.location.facets ? this.location.facets.andFilters : [];
1226
+ const newFilters: unknown[] = [];
1227
+ andFilters.forEach((f) => {
1228
+ if (f.hidden) {
1229
+ newFilters.push(f);
1230
+ }
1231
+ });
1232
+ newReference._location.facets = newFilters.length > 0 ? { and: newFilters } : null;
1233
+ }
1234
+
1235
+ if (!sameCustomFacet) {
1236
+ newReference._location.customFacets = null;
1237
+ }
1238
+
1239
+ if (!sameFilter) {
1240
+ newReference._location.removeFilters();
1241
+ }
1242
+
1243
+ return newReference;
1244
+ }
1245
+
1246
+ /**
1247
+ * Given a list of facet and filters, will add them to the existing conjunctive facet filters.
1248
+ *
1249
+ * @param facetAndFilters - an array of facets that will be added
1250
+ * @param customFacets - the custom facets object
1251
+ */
1252
+ addFacets(facetAndFilters: unknown[], customFacets: unknown) {
1253
+ verify(Array.isArray(facetAndFilters) && facetAndFilters.length > 0, 'given input must be an array');
1254
+
1255
+ const loc = this.location;
1256
+
1257
+ // keep a copy of existing facets
1258
+ const existingFilters = loc.facets ? simpleDeepCopy(loc.facets.andFilters) : [];
1259
+
1260
+ // create a new copy
1261
+ const newReference = this.copy();
1262
+
1263
+ // clone the location object
1264
+ newReference._location = loc._clone(newReference);
1265
+ newReference._location.beforeObject = null;
1266
+ newReference._location.afterObject = null;
1267
+
1268
+ // merge the existing facets with the input
1269
+ newReference._location.facets = { and: facetAndFilters.concat(existingFilters) };
1270
+
1271
+ if (isObjectAndNotNull(customFacets)) {
1272
+ newReference._location.customFacets = customFacets;
1273
+ }
1274
+
1275
+ return newReference;
1276
+ }
1277
+
1278
+ /**
1279
+ * Will return a reference with the same facets but hidden.
1280
+ */
1281
+ hideFacets() {
1282
+ const andFilters = this.location.facets ? this.location.facets.andFilters : [];
1283
+
1284
+ verify(andFilters.length > 0, "Reference doesn't have any facets.");
1285
+
1286
+ const newReference = this.copy();
1287
+
1288
+ const newFilters: unknown[] = [];
1289
+ // update the location object
1290
+ newReference._location = this._location._clone(newReference);
1291
+ newReference._location.beforeObject = null;
1292
+ newReference._location.afterObject = null;
1293
+
1294
+ // make all the facets as hidden
1295
+ andFilters.forEach((f) => {
1296
+ const newFilter = simpleDeepCopy(f);
1297
+ newFilter.hidden = true;
1298
+ newFilters.push(newFilter);
1299
+ });
1300
+
1301
+ newReference._location.facets = { and: newFilters };
1302
+
1303
+ return newReference;
1304
+ }
1305
+
1306
+ /**
1307
+ *
1308
+ * @param table
1309
+ */
1310
+ setNewTable(table: Table) {
1311
+ this._table = table;
1312
+ this._shortestKey = table.shortestKey;
1313
+ this._displayname = table.displayname;
1314
+ delete this._referenceColumns;
1315
+ delete this._activeList;
1316
+ delete this._related;
1317
+ delete this._canCreate;
1318
+ delete this._canRead;
1319
+ delete this._canUpdate;
1320
+ delete this._canDelete;
1321
+ delete this._canUseTRS;
1322
+ delete this._canUseTCRS;
1323
+ delete this._display;
1324
+ delete this._csvDownloadLink;
1325
+ delete this._readAttributeGroupPathProps_cached;
1326
+ }
1327
+
1328
+ /**
1329
+ * If annotation is defined and has the required attributes, will return
1330
+ * a Citation object that can be used to generate citation.
1331
+ */
1332
+ get citation(): Citation | null {
1333
+ if (this._citation === undefined) {
1334
+ const table = this.table;
1335
+ if (!table.annotations.contains(_annotations.CITATION)) {
1336
+ this._citation = null;
1337
+ } else {
1338
+ const citationAnno = table.annotations.get(_annotations.CITATION).content;
1339
+ if (!citationAnno.journal_pattern || !citationAnno.year_pattern || !citationAnno.url_pattern) {
1340
+ this._citation = null;
1341
+ } else {
1342
+ this._citation = new Citation(this, citationAnno);
1343
+ }
1344
+ }
1345
+ }
1346
+ return this._citation;
1347
+ }
1348
+
1349
+ /**
1350
+ * If annotation is defined and has the required attributes, will return
1351
+ * a Metadata object
1352
+ */
1353
+ get googleDatasetMetadata(): GoogleDatasetMetadata | null {
1354
+ if (this._googleDatasetMetadata === undefined) {
1355
+ const table = this.table;
1356
+ if (!table.annotations.contains(_annotations.GOOGLE_DATASET_METADATA)) {
1357
+ this._googleDatasetMetadata = null;
1358
+ } else {
1359
+ const metadataAnnotation = _getRecursiveAnnotationValue(
1360
+ this.context,
1361
+ this.table.annotations.get(_annotations.GOOGLE_DATASET_METADATA).content,
1362
+ );
1363
+ if (!isObjectAndNotNull(metadataAnnotation) || !isObjectAndNotNull(metadataAnnotation.dataset)) {
1364
+ this._googleDatasetMetadata = null;
1365
+ } else {
1366
+ this._googleDatasetMetadata = new GoogleDatasetMetadata(this, metadataAnnotation);
1367
+ }
1368
+ }
1369
+ }
1370
+ return this._googleDatasetMetadata;
1371
+ }
1372
+
1373
+ /**
1374
+ * The related reference or tables that might be deleted as a result of deleting the current table.
1375
+ */
1376
+ get cascadingDeletedItems(): Array<Table | RelatedReference | Reference> {
1377
+ if (this._cascadingDeletedItems === undefined) {
1378
+ const res: Array<Table | RelatedReference | Reference> = [];
1379
+ const consideredFKs: Record<string, number> = {};
1380
+ const detailedRef = this._context === _contexts.DETAILED ? this : this.contextualize.detailed;
1381
+
1382
+ // inline tables
1383
+ detailedRef.columns.forEach((col) => {
1384
+ if (isRelatedColumn(col)) {
1385
+ const typedCol = col as PseudoColumn | InboundForeignKeyPseudoColumn;
1386
+
1387
+ // col.foreignKey is available for non-source syntax, while the other one is used for source syntax
1388
+ let fk: ForeignKeyRef | undefined;
1389
+ if ((typedCol as InboundForeignKeyPseudoColumn).foreignKey) {
1390
+ fk = (typedCol as InboundForeignKeyPseudoColumn).foreignKey;
1391
+ } else if (typedCol.firstForeignKeyNode) {
1392
+ fk = typedCol.firstForeignKeyNode.nodeObject;
1393
+ }
1394
+ // this check is not needed and only added for sanity check
1395
+ if (!fk) return;
1396
+ consideredFKs[fk.name] = 1;
1397
+ if (fk.onDeleteCascade) {
1398
+ res.push(typedCol.reference);
1399
+ }
1400
+ }
1401
+ });
1402
+
1403
+ // related tables
1404
+ detailedRef.related.forEach((ref) => {
1405
+ // col.origFKR is available for non-source syntax, while the other one is used for source syntax
1406
+ let fk;
1407
+
1408
+ // TODO kept this check for backwards compatibility, but it shouldn't be needed.
1409
+ if (ref.origFKR) {
1410
+ fk = ref.origFKR;
1411
+ } else if (ref.pseudoColumn && ref.pseudoColumn.firstForeignKeyNode) {
1412
+ fk = ref.pseudoColumn.firstForeignKeyNode.nodeObject;
1413
+ }
1414
+ // this check is not needed and only added for sanity check
1415
+ if (!fk) return;
1416
+ consideredFKs[fk.name] = 1;
1417
+ if (fk.onDeleteCascade) {
1418
+ res.push(ref);
1419
+ }
1420
+ });
1421
+
1422
+ // list the tables that are not added yet
1423
+ this.table.referredBy.all().forEach((fk) => {
1424
+ if (fk.name in consideredFKs || !fk.onDeleteCascade) return;
1425
+
1426
+ res.push(fk.table);
1427
+ });
1428
+
1429
+ this._cascadingDeletedItems = res;
1430
+ }
1431
+
1432
+ return this._cascadingDeletedItems;
1433
+ }
1434
+
1435
+ /**
1436
+ * If prefill object is defined and has the required attributes, will return
1437
+ * a BulkCreateForeignKeyObject object with the necessary objects used for a association modal picker
1438
+ */
1439
+ get bulkCreateForeignKeyObject(): BulkCreateForeignKeyObject | null {
1440
+ if (this._bulkCreateForeignKeyObject === undefined) {
1441
+ this._bulkCreateForeignKeyObject = null;
1442
+ }
1443
+ return this._bulkCreateForeignKeyObject;
1444
+ }
1445
+
1446
+ /**
1447
+ * Will compute and return a BulkCreateForeignKeyObject if:
1448
+ * - the prefillObject is defined
1449
+ * - there are only 2 foreign key columns for this table that are not system columns
1450
+ * - using the prefill object, we can determine the main column for prefilling and leaf column for bulk selection
1451
+ *
1452
+ * @param prefillObject computed prefill object from chaise
1453
+ */
1454
+ computeBulkCreateForeignKeyObject(prefillObject: any) {
1455
+ if (this._bulkCreateForeignKeyObject === undefined) {
1456
+ if (!prefillObject) {
1457
+ this._bulkCreateForeignKeyObject = null;
1458
+ } else {
1459
+ // ignore the fks that are simple and their constituent column is system col
1460
+ const nonSystemColumnFks = this.table.foreignKeys.all().filter((fk) => {
1461
+ return !(fk.simple && _systemColumns.indexOf(fk.colset.columns[0].name) !== -1);
1462
+ });
1463
+
1464
+ // set of foreignkey columns (they might be overlapping so we're not using array)
1465
+ const fkCols: Record<string, boolean> = {};
1466
+ nonSystemColumnFks.forEach((fk) => {
1467
+ fk.colset.columns.forEach((col) => {
1468
+ fkCols[col.name] = true;
1469
+ });
1470
+ });
1471
+
1472
+ let mainColumn: ForeignKeyPseudoColumn | null = null;
1473
+ // find main column in the visible columns list
1474
+ for (let k = 0; k < this.columns.length; k++) {
1475
+ const column = this.columns[k];
1476
+ // column should be a foreignkey pseudo column
1477
+ if (!(column as ForeignKeyPseudoColumn).isForeignKey) continue;
1478
+ if (prefillObject.fkColumnNames.indexOf(column.name) !== -1) {
1479
+ // mainColumn is the column being prefilled, this should ALWAYS be in the visible columns list
1480
+ mainColumn = column as ForeignKeyPseudoColumn;
1481
+ break;
1482
+ }
1483
+ }
1484
+
1485
+ if (!mainColumn) {
1486
+ this._bulkCreateForeignKeyObject = null;
1487
+ return;
1488
+ }
1489
+
1490
+ /**
1491
+ * Using the given constraintName, determines the leaf column to be used for bulk foreign key create from the annotation value.
1492
+ * If no constraintName is provided, uses the other foreign key found on the table as the leaf
1493
+ * NOTE: when no constraintName, this is only called if there are 2 foreign keys and we know the main column
1494
+ *
1495
+ * @param constraintNameProp constraint name of the foreingkey from annotation or array of constraint names
1496
+ * @returns BulkCreateForeignKeyObject if leaf column can be found, null otherwise
1497
+ */
1498
+ const findLeafColumnAndSetBulkCreate = (constraintNameProp?: string[][] | string[]) => {
1499
+ /**
1500
+ *
1501
+ * @param col foreign key column to check if it's in the list of visible columns and matches the constraint name
1502
+ * @returns the foreign key column that represents the leaf table
1503
+ */
1504
+ const findLeafColumn = (col: ForeignKeyPseudoColumn, constraintName?: string) => {
1505
+ let foundColumn = null;
1506
+
1507
+ for (let k = 0; k < nonSystemColumnFks.length; k++) {
1508
+ const fk = nonSystemColumnFks[k];
1509
+ // make sure column is in visible columns and is in foreign key list
1510
+ // column and foreign key `.name` property is a hash value
1511
+ if (col.name === fk.name) {
1512
+ if (constraintName && constraintName === col._constraintName) {
1513
+ // use the constraint name to check each column and ensure it's the one we want from annotation
1514
+ foundColumn = col;
1515
+ } else if (!constraintName && col.name !== mainColumn.name) {
1516
+ // make sure the current column is the NOT the main column
1517
+ // assume there are only 2 foreign keys and we know mainColumn already
1518
+ foundColumn = col;
1519
+ }
1520
+
1521
+ if (foundColumn) break;
1522
+ }
1523
+ }
1524
+
1525
+ return foundColumn;
1526
+ };
1527
+
1528
+ let leafCol = null;
1529
+ // use for loop so we can break if we find the leaf column
1530
+ for (let i = 0; i < this.columns.length; i++) {
1531
+ const column = this.columns[i] as ForeignKeyPseudoColumn;
1532
+
1533
+ // column should be a simple foreignkey pseudo column
1534
+ // return if it's not a foreign key or the column is a foreign key but it's not simple
1535
+ if (!column.isForeignKey || !column.foreignKey.simple) continue;
1536
+
1537
+ // if constraintNameProp is string[][], it's from bulk_create_foreign_key_candidates
1538
+ // we need to iterate over the set to find the first matching column
1539
+ if (Array.isArray(constraintNameProp) && Array.isArray(constraintNameProp[0])) {
1540
+ for (let j = 0; j < constraintNameProp.length; j++) {
1541
+ const name = constraintNameProp[j];
1542
+ if (_isValidForeignKeyName(name) && Array.isArray(name)) leafCol = findLeafColumn(column, name.join('_'));
1543
+
1544
+ if (leafCol) break;
1545
+ }
1546
+ } else if (Array.isArray(constraintNameProp)) {
1547
+ // constraintNameProp should be a string[]
1548
+ leafCol = findLeafColumn(column, constraintNameProp.join('_'));
1549
+ } else {
1550
+ // no constraintName
1551
+ leafCol = findLeafColumn(column);
1552
+ }
1553
+
1554
+ if (leafCol) break;
1555
+ }
1556
+
1557
+ if (!leafCol) return null;
1558
+ return new BulkCreateForeignKeyObject(this, prefillObject, fkCols, mainColumn, leafCol);
1559
+ };
1560
+
1561
+ if (!mainColumn) {
1562
+ // if no mainColumn, this API can't be used
1563
+ this._bulkCreateForeignKeyObject = null;
1564
+ } else if (mainColumn.display.bulkForeignKeyCreateConstraintName === false) {
1565
+ // don't use heuristics
1566
+ this._bulkCreateForeignKeyObject = null;
1567
+ } else if (mainColumn.display.bulkForeignKeyCreateConstraintName) {
1568
+ // see if the leaf column to use is determined by an annotation by setting `true`
1569
+ // if a leaf column is determined, call BulkCreateForeignKeyObject constructor and set the generated value
1570
+ this._bulkCreateForeignKeyObject = findLeafColumnAndSetBulkCreate(mainColumn.display.bulkForeignKeyCreateConstraintName);
1571
+ } else {
1572
+ // use heuristics instead
1573
+ // There have to be 2 foreign key columns
1574
+ if (nonSystemColumnFks.length !== 2) {
1575
+ this._bulkCreateForeignKeyObject = null;
1576
+ } else {
1577
+ // both foreign keys have to be simple
1578
+ if (!nonSystemColumnFks[0].simple || !nonSystemColumnFks[1].simple) {
1579
+ this._bulkCreateForeignKeyObject = null;
1580
+ } else {
1581
+ // leafColumn will be set no matter what since the check above ensures there are 2 FK columns
1582
+ this._bulkCreateForeignKeyObject = findLeafColumnAndSetBulkCreate();
1583
+ }
1584
+ }
1585
+ }
1586
+ }
1587
+ }
1588
+ return this._bulkCreateForeignKeyObject;
1589
+ }
1590
+
1591
+ /**
1592
+ * Indicates whether the client has the permission to _create_
1593
+ * the referenced resource(s). Reporting a `true` value DOES NOT
1594
+ * guarantee the user right since some policies may be undecidable until
1595
+ * query execution.
1596
+ */
1597
+ get canCreate(): boolean {
1598
+ if (this._canCreate === undefined) {
1599
+ // can create if all are true
1600
+ // 1) user has write permission
1601
+ // 2) table is not generated
1602
+ // 3) not all visible columns in the table are generated
1603
+ const pm = _permissionMessages;
1604
+ const ref = this._context === _contexts.CREATE ? this : this.contextualize.entryCreate;
1605
+
1606
+ if (ref._table.kind === _tableKinds.VIEW) {
1607
+ this._canCreate = false;
1608
+ this._canCreateReason = pm.TABLE_VIEW;
1609
+ } else if (ref._table.isGenerated) {
1610
+ this._canCreate = false;
1611
+ this._canCreateReason = pm.TABLE_GENERATED;
1612
+ } else if (!ref.checkPermissions(_ERMrestACLs.INSERT)) {
1613
+ this._canCreate = false;
1614
+ this._canCreateReason = pm.NO_CREATE;
1615
+ } else {
1616
+ this._canCreate = true;
1617
+ }
1618
+
1619
+ if (this._canCreate === true) {
1620
+ const allColumnsDisabled = ref.columns.every((col) => col.inputDisabled !== false);
1621
+
1622
+ if (allColumnsDisabled) {
1623
+ this._canCreate = false;
1624
+ this._canCreateReason = pm.DISABLED_COLUMNS;
1625
+ }
1626
+ }
1627
+ }
1628
+ return this._canCreate;
1629
+ }
1630
+
1631
+ /**
1632
+ * Indicates the reason as to why a user cannot create the
1633
+ * referenced resource(s).
1634
+ */
1635
+ get canCreateReason(): string {
1636
+ if (this._canCreateReason === undefined) {
1637
+ // will set the _canCreateReason property
1638
+ void this.canCreate;
1639
+ }
1640
+ return this._canCreateReason!;
1641
+ }
1642
+
1643
+ /**
1644
+ * Indicates whether the client has the permission to _read_
1645
+ * the referenced resource(s). Reporting a `true` value DOES NOT
1646
+ * guarantee the user right since some policies may be undecidable until
1647
+ * query execution.
1648
+ */
1649
+ get canRead(): boolean {
1650
+ if (this._canRead === undefined) {
1651
+ this._canRead = this.checkPermissions(_ERMrestACLs.SELECT);
1652
+ }
1653
+ return this._canRead;
1654
+ }
1655
+
1656
+ /**
1657
+ * Indicates whether the client has the permission to _update_
1658
+ * the referenced resource(s). Reporting a `true` value DOES NOT
1659
+ * guarantee the user right since some policies may be undecidable until
1660
+ * query execution.
1661
+ */
1662
+ get canUpdate(): boolean {
1663
+ // can update if all are true
1664
+ // 1) user has write permission
1665
+ // 2) table is not generated
1666
+ // 3) table is not immutable
1667
+ // 4) not all visible columns in the table are generated/immutable
1668
+ if (this._canUpdate === undefined) {
1669
+ const pm = _permissionMessages;
1670
+ const ref = this._context === _contexts.EDIT ? this : this.contextualize.entryEdit;
1671
+
1672
+ if (ref._table.kind === _tableKinds.VIEW) {
1673
+ this._canUpdate = false;
1674
+ this._canUpdateReason = pm.TABLE_VIEW;
1675
+ // if table specifically says that it's not immutable, then it's not!
1676
+ } else if (ref._table.isGenerated && ref._table.isImmutable !== false) {
1677
+ this._canUpdate = false;
1678
+ this._canUpdateReason = pm.TABLE_GENERATED;
1679
+ } else if (ref._table.isImmutable) {
1680
+ this._canUpdate = false;
1681
+ this._canUpdateReason = pm.TABLE_IMMUTABLE;
1682
+ } else if (!ref.checkPermissions(_ERMrestACLs.UPDATE)) {
1683
+ this._canUpdate = false;
1684
+ this._canUpdateReason = pm.NO_UPDATE;
1685
+ } else {
1686
+ this._canUpdate = true;
1687
+ }
1688
+
1689
+ if (this._canUpdate) {
1690
+ const allColumnsDisabled = ref.columns.every((col) => col.inputDisabled !== false);
1691
+
1692
+ if (allColumnsDisabled) {
1693
+ this._canUpdate = false;
1694
+ this._canUpdateReason = pm.DISABLED_COLUMNS;
1695
+ }
1696
+ }
1697
+ }
1698
+ return this._canUpdate;
1699
+ }
1700
+
1701
+ /**
1702
+ * Indicates the reason as to why a user cannot update the
1703
+ * referenced resource(s).
1704
+ */
1705
+ get canUpdateReason(): string {
1706
+ if (this._canUpdateReason === undefined) {
1707
+ // will set the _canUpdateReason property
1708
+ void this.canUpdate;
1709
+ }
1710
+ return this._canUpdateReason!;
1711
+ }
1712
+
1713
+ /**
1714
+ * Indicates whether the client has the permission to _delete_
1715
+ * the referenced resource(s). Reporting a `true` value DOES NOT
1716
+ * guarantee the user right since some policies may be undecidable until
1717
+ * query execution.
1718
+ */
1719
+ get canDelete(): boolean {
1720
+ // can delete if all are true
1721
+ // 1) table is not non-deletable
1722
+ // 2) user has write permission
1723
+ if (this._canDelete === undefined) {
1724
+ this._canDelete = this._table.kind !== _tableKinds.VIEW && !this._table.isNonDeletable && this.checkPermissions(_ERMrestACLs.DELETE);
1725
+ }
1726
+ return this._canDelete;
1727
+ }
1728
+
1729
+ /**
1730
+ * Returns true if
1731
+ * - ermrest supports trs, and
1732
+ * - table has dynamic acls, and
1733
+ * - table has RID column, and
1734
+ * - table is not marked non-deletable non-updatable by annotation
1735
+ */
1736
+ get canUseTRS(): boolean {
1737
+ if (this._canUseTRS === undefined) {
1738
+ const rightKey = _ERMrestFeatures.TABLE_RIGHTS_SUMMARY;
1739
+ this._canUseTRS =
1740
+ this.table.schema.catalog.features[rightKey] === true &&
1741
+ // eslint-disable-next-line eqeqeq
1742
+ (this.table.rights[_ERMrestACLs.UPDATE] == null || this.table.rights[_ERMrestACLs.DELETE] == null) &&
1743
+ this.table.columns.has('RID') &&
1744
+ (this.canUpdate || this.canDelete);
1745
+ }
1746
+ return this._canUseTRS;
1747
+ }
1748
+
1749
+ /**
1750
+ * Returns true if
1751
+ * - ermrest supports tcrs, and
1752
+ * - table has dynamic acls, and
1753
+ * - table has RID column, and
1754
+ * - table is not marked non-updatable by annotation
1755
+ */
1756
+ get canUseTCRS(): boolean {
1757
+ if (this._canUseTCRS === undefined) {
1758
+ const rightKey = _ERMrestFeatures.TABLE_COL_RIGHTS_SUMMARY;
1759
+ this._canUseTCRS =
1760
+ this.table.schema.catalog.features[rightKey] === true &&
1761
+ // eslint-disable-next-line eqeqeq
1762
+ this.table.rights[_ERMrestACLs.UPDATE] == null &&
1763
+ this.table.columns.has('RID') &&
1764
+ this.canUpdate;
1765
+ }
1766
+ return this._canUseTCRS;
1767
+ }
1768
+
1769
+ /**
1770
+ * This is a private function that checks the user permissions for modifying the affiliated entity, record or table
1771
+ * Sets a property on the reference object used by canCreate/canUpdate/canDelete
1772
+ */
1773
+ checkPermissions(permission: string): boolean {
1774
+ // Return true if permission is null
1775
+ if (this._table.rights[permission] === null) return true;
1776
+ return this._table.rights[permission];
1777
+ }
1778
+
1779
+ _generateContextHeader(contextHeaderParams: Record<string, unknown>): Record<string, unknown> {
1780
+ if (!contextHeaderParams || !isObject(contextHeaderParams)) {
1781
+ contextHeaderParams = {};
1782
+ }
1783
+
1784
+ for (const key in this.defaultLogInfo) {
1785
+ // only add the values that are not defined.
1786
+ if (key in contextHeaderParams) continue;
1787
+ contextHeaderParams[key] = this.defaultLogInfo[key as keyof typeof this.defaultLogInfo];
1788
+ }
1789
+
1790
+ const headers: Record<string, unknown> = {};
1791
+ headers[contextHeaderName] = contextHeaderParams;
1792
+ return headers;
1793
+ }
1794
+
1795
+ /**
1796
+ * create a new reference with the new search
1797
+ * by copying this reference and clears previous search filters
1798
+ * search term can be:
1799
+ * a) A string with no space: single term or regular expression
1800
+ * b) A single term with space using ""
1801
+ * c) use space for conjunction of terms
1802
+ */
1803
+ search(term: string) {
1804
+ if (term) {
1805
+ if (typeof term === 'string') term = term.trim();
1806
+ else throw new InvalidInputError('Invalid input. Seach expects a string.');
1807
+ }
1808
+
1809
+ // make a Reference copy
1810
+ const newReference = this.copy();
1811
+
1812
+ newReference._location = this._location._clone(newReference);
1813
+ newReference._location.beforeObject = null;
1814
+ newReference._location.afterObject = null;
1815
+ newReference._location.search(term);
1816
+
1817
+ // if facet columns are already computed, just update them.
1818
+ // if we don't do this here, then facet columns will recomputed after each search
1819
+ // TODO can be refactored
1820
+ if (this._facetColumns !== undefined) {
1821
+ newReference._facetColumns = [];
1822
+ this.facetColumns.forEach((fc) => {
1823
+ newReference._facetColumns!.push(new FacetColumn(newReference, fc.index, fc.sourceObjectWrapper, fc.filters.slice()));
1824
+ });
1825
+ }
1826
+
1827
+ return newReference;
1828
+ }
1829
+
1830
+ /**
1831
+ * Return a new Reference with the new sorting
1832
+ * TODO this should validate the given sort objects,
1833
+ * but I'm not sure how much adding that validation will affect other apis and client
1834
+ *
1835
+ * @param sort an array of objects in the format
1836
+ * {"column":columname, "descending":true|false}
1837
+ * in order of priority. Undfined, null or Empty array to use default sorting.
1838
+ *
1839
+ */
1840
+ sort(sort: Array<{ column: string; descending?: boolean }> | null) {
1841
+ if (sort) {
1842
+ verify(sort instanceof Array, 'input should be an array');
1843
+ verify(sort.every(_isValidSortElement), 'invalid arguments in array');
1844
+ }
1845
+
1846
+ // make a Reference copy
1847
+ const newReference = this.copy();
1848
+
1849
+ newReference._location = this._location._clone(newReference);
1850
+ newReference._location.sortObject = sort;
1851
+ newReference._location.beforeObject = null;
1852
+ newReference._location.afterObject = null;
1853
+
1854
+ // if facet columns are already computed, just update them.
1855
+ // if we don't do this here, then facet columns will recomputed after each sort
1856
+ // TODO can be refactored
1857
+ if (this._facetColumns !== undefined) {
1858
+ newReference._facetColumns = [];
1859
+ this.facetColumns.forEach((fc) => {
1860
+ newReference._facetColumns!.push(new FacetColumn(newReference, fc.index, fc.sourceObjectWrapper, fc.filters.slice()));
1861
+ });
1862
+ }
1863
+
1864
+ return newReference;
1865
+ }
1866
+
1867
+ /**
1868
+ *
1869
+ * @param {ColumnAggregateFn[]} aggregateList - list of aggregate functions to apply to GET uri
1870
+ * @return Promise contains an array of the aggregate values in the same order as the supplied aggregate list
1871
+ */
1872
+ getAggregates(aggregateList: ColumnAggregateFn[], contextHeaderParams?: Record<string, unknown>): Promise<unknown[]> {
1873
+ return new Promise((resolve, reject) => {
1874
+ let url = '';
1875
+
1876
+ // create the context header params for logging
1877
+ if (!contextHeaderParams || !isObject(contextHeaderParams)) {
1878
+ contextHeaderParams = { action: 'aggregate' };
1879
+ }
1880
+ const config = {
1881
+ headers: this._generateContextHeader(contextHeaderParams),
1882
+ };
1883
+
1884
+ const urlSet = [];
1885
+ const baseUri = this.location.ermrestCompactPath + '/';
1886
+ // create a url: ../aggregate/../0:=fn(),1:=fn()..
1887
+ // TODO could be re-written
1888
+ for (let i = 0; i < aggregateList.length; i++) {
1889
+ const agg = aggregateList[i];
1890
+
1891
+ // if this is the first aggregate, begin with the baseUri
1892
+ if (i === 0) {
1893
+ url = baseUri;
1894
+ } else {
1895
+ url += ',';
1896
+ }
1897
+
1898
+ // if adding the next aggregate to the url will push it past url length limit, push url onto the urlSet and reset the working url
1899
+ if ((url + i + ':=' + agg).length > URL_PATH_LENGTH_LIMIT) {
1900
+ // if cannot even add the first one
1901
+ if (i === 0) {
1902
+ reject(new InvalidInputError('Cannot send the request because of URL length limit.'));
1903
+ return;
1904
+ }
1905
+
1906
+ // strip off an extra ','
1907
+ if (url.charAt(url.length - 1) === ',') {
1908
+ url = url.substring(0, url.length - 1);
1909
+ }
1910
+
1911
+ urlSet.push(url);
1912
+ url = baseUri;
1913
+ }
1914
+
1915
+ // use i as the alias
1916
+ url += i + ':=' + agg;
1917
+
1918
+ // We are at the end of the aggregate list
1919
+ if (i + 1 === aggregateList.length) {
1920
+ urlSet.push(url);
1921
+ }
1922
+ }
1923
+
1924
+ const aggregatePromises = [];
1925
+ const http = this._server.http;
1926
+ for (let j = 0; j < urlSet.length; j++) {
1927
+ aggregatePromises.push(http.get(this.location.service + '/catalog/' + this.location.catalog + '/aggregate/' + urlSet[j], config));
1928
+ }
1929
+
1930
+ Promise.all(aggregatePromises)
1931
+ .then((response) => {
1932
+ // all response rows merged into one object
1933
+ const singleResponse: Record<string, unknown> = {};
1934
+
1935
+ // collect all the data in one object so we can map it to an array
1936
+ for (let k = 0; k < response.length; k++) {
1937
+ Object.assign(singleResponse, response[k].data[0]);
1938
+ }
1939
+
1940
+ const responseArray: unknown[] = [];
1941
+ for (let m = 0; m < aggregateList.length; m++) {
1942
+ responseArray.push(singleResponse[m]);
1943
+ }
1944
+
1945
+ resolve(responseArray);
1946
+ return;
1947
+ })
1948
+ .catch((err) => {
1949
+ reject(ErrorService.responseToError(err));
1950
+ });
1951
+ });
1952
+ }
1953
+
1954
+ /**
1955
+ * Reads the referenced resources and returns a promise for a page of
1956
+ * tuples. The `limit` parameter is required and must be a positive
1957
+ * integer. The page of tuples returned will be described by the
1958
+ * {@link ERMrest.Reference#columns} array of column definitions.
1959
+ *
1960
+ * Usage:
1961
+ * ```
1962
+ * // assumes the client holds a reference
1963
+ * reference.read(10).then(
1964
+ * function(page) {
1965
+ * // we now have a page of tuples
1966
+ * ...
1967
+ * },
1968
+ * function(error) {
1969
+ * // an error occurred
1970
+ * ...
1971
+ * });
1972
+ * ```
1973
+ *
1974
+ * @param {!number} limit The limit of results to be returned by the
1975
+ * read request. __required__
1976
+ * @param {Object} contextHeaderParams the object that we want to log.
1977
+ * @param {Boolean=} useEntity whether we should use entity api or not (if true, we won't get foreignkey data)
1978
+ * @param {Boolean=} dontCorrectPage whether we should modify the page.
1979
+ * If there's a @before in url and the number of results is less than the
1980
+ * given limit, we will remove the @before and run the read again. Setting
1981
+ * dontCorrectPage to true, will not do this extra check.
1982
+ * @param {Boolean=} getTRS whether we should fetch the table-level row acls (if table supports it)
1983
+ * @param {Boolean=} getTCRS whether we should fetch the table-level and column-level row acls (if table supports it)
1984
+ * @param {Boolean=} getUnlinkTRS whether we should fetch the acls of association
1985
+ * table. Use this only if the association is based on facet syntax
1986
+ *
1987
+ * NOTE setting useEntity to true, will ignore any sort that is based on
1988
+ * pseduo-columns.
1989
+ * TODO we might want to chagne the above statement, so useEntity can
1990
+ * be used more generally.
1991
+ *
1992
+ * NOTE getUnlinkTRS can only be used on related references that are generated
1993
+ * after calling `generateRelatedReference` or `generateActiveList` with the main
1994
+ * tuple data. As part of generating related references, if the main tuple is available
1995
+ * we will use a facet filter and the alias is added in there. Without the main tuple,
1996
+ * the alias is not added to the path and therefore `getUnlinkTRS` cannot be used.
1997
+ * TODO this is a bit hacky and should be refactored
1998
+ *
1999
+ * @returns A promise resolved with {@link Page} of results,
2000
+ * or rejected with any of these errors:
2001
+ * - {@link InvalidInputError}: If `limit` is invalid.
2002
+ * - {@link BadRequestError}: If asks for sorting based on columns that are not sortable.
2003
+ * - {@link NotFoundError}: If asks for sorting based on columns that are not valid.
2004
+ * - ERMrestjs corresponding http errors, if ERMrest returns http error.
2005
+ */
2006
+ async read(
2007
+ limit: number,
2008
+ contextHeaderParams?: Record<string, unknown>,
2009
+ useEntity?: boolean,
2010
+ dontCorrectPage?: boolean,
2011
+ getTRS?: boolean,
2012
+ getTCRS?: boolean,
2013
+ getUnlinkTRS?: boolean,
2014
+ ): Promise<Page> {
2015
+ verify(limit, "'limit' must be specified");
2016
+ verify(typeof limit === 'number', "'limit' must be a number");
2017
+ verify(limit > 0, "'limit' must be greater than 0");
2018
+
2019
+ if (!isStringAndNotEmpty(this._context)) {
2020
+ $log.warn('Uncontextualized Reference usage detected. For more consistent behavior always contextualize Reference objects.');
2021
+ }
2022
+
2023
+ let uri = [this._location.service, 'catalog', this._location.catalog].join('/');
2024
+ const readPath = computeReadPath(this, useEntity, getTRS, getTCRS, getUnlinkTRS);
2025
+ if (readPath.isAttributeGroup) {
2026
+ uri += '/attributegroup/' + readPath.value;
2027
+ } else {
2028
+ uri += '/entity/' + readPath.value;
2029
+ }
2030
+ // add limit
2031
+ uri = uri + '?limit=' + (limit + 1); // read extra row, for determining whether the returned page has next/previous page
2032
+
2033
+ // attach `this` (Reference) to a variable
2034
+ // `this` inside the Promise request is a Window object
2035
+ let action = 'read';
2036
+ if (!contextHeaderParams || !isObject(contextHeaderParams)) {
2037
+ contextHeaderParams = { action: action };
2038
+ } else if (typeof contextHeaderParams.action === 'string') {
2039
+ action = contextHeaderParams.action;
2040
+ }
2041
+ const config = {
2042
+ headers: this._generateContextHeader(contextHeaderParams),
2043
+ };
2044
+
2045
+ let response;
2046
+ try {
2047
+ response = await this._server.http.get(uri, config);
2048
+ } catch (e) {
2049
+ throw ErrorService.responseToError(e);
2050
+ }
2051
+ if (!Array.isArray(response.data)) {
2052
+ throw new InvalidServerResponse(uri, response.data, action);
2053
+ }
2054
+
2055
+ const etag = HTTPService.getResponseHeader(response).etag;
2056
+
2057
+ let hasPrevious,
2058
+ hasNext = false;
2059
+ if (!this._location.paging) {
2060
+ // first page
2061
+ hasPrevious = false;
2062
+ hasNext = response.data.length > limit;
2063
+ } else if (this._location.beforeObject) {
2064
+ // has @before()
2065
+ hasPrevious = response.data.length > limit;
2066
+ hasNext = true;
2067
+ } else {
2068
+ // has @after()
2069
+ hasPrevious = true;
2070
+ hasNext = response.data.length > limit;
2071
+ }
2072
+
2073
+ // Because read() reads one extra row to determine whether the new page has previous or next
2074
+ // We need to remove those extra row of data from the result
2075
+ let extraData = {};
2076
+ if (response.data.length > limit) {
2077
+ // if no paging or @after, remove last row
2078
+ if (!this._location.beforeObject) {
2079
+ extraData = response.data[response.data.length - 1];
2080
+ response.data.splice(response.data.length - 1);
2081
+ } else {
2082
+ // @before, remove first row
2083
+ extraData = response.data[0];
2084
+ response.data.splice(0, 1);
2085
+ }
2086
+ }
2087
+ const page = new Page(this, etag, response.data, hasPrevious, hasNext, extraData);
2088
+
2089
+ // we are paging based on @before (user navigated backwards in the set of data)
2090
+ // AND there is less data than limit implies (beginning of set) OR we got the right set of data (tuples.length == pageLimit) but there's no previous set (beginning of set)
2091
+ if (dontCorrectPage !== true && !this._location.afterObject && this._location.beforeObject && (page.tuples.length < limit || !page.hasPrevious)) {
2092
+ const referenceWithoutPaging = this.copy();
2093
+ referenceWithoutPaging._location.beforeObject = null;
2094
+
2095
+ // remove the function and replace it with auto-reload
2096
+ const actionVerb = action.substring(action.lastIndexOf(';') + 1);
2097
+ let newActionVerb = 'auto-reload';
2098
+ // TODO (could be optimized)
2099
+ if (['load-domain', 'reload-domain'].indexOf(actionVerb) !== -1) {
2100
+ newActionVerb = 'auto-reload-domain';
2101
+ }
2102
+ contextHeaderParams.action = action.substring(0, action.lastIndexOf(';') + 1) + newActionVerb;
2103
+ return referenceWithoutPaging.read(limit, contextHeaderParams, useEntity, true);
2104
+ } else {
2105
+ return page;
2106
+ }
2107
+ }
2108
+
2109
+ /**
2110
+ * Creates a set of tuples in the references relation. Note, this
2111
+ * operation sets the `defaults` list according to the table
2112
+ * specification, and not according to the contents of in the input
2113
+ * tuple.
2114
+ * @param data The array of data to be created as new tuples.
2115
+ * @param contextHeaderParams the object that we want to log.
2116
+ * @param skipOnConflict if true, it will not complain about conflict
2117
+ * @returns A promise resolved with a object containing `successful` and `failure` attributes.
2118
+ * Both are {@link Page} of results.
2119
+ * or rejected with any of the following errors:
2120
+ * - {@link InvalidInputError}: If `data` is not valid, or reference is not in `entry/create` context.
2121
+ * - {@link InvalidInputError}: If `limit` is invalid.
2122
+ * - ERMrestjs corresponding http errors, if ERMrest returns http error.
2123
+ */
2124
+ create(
2125
+ data: Record<string, unknown>[],
2126
+ contextHeaderParams?: any,
2127
+ skipOnConflict?: boolean,
2128
+ ): Promise<{ successful: Page; failed: Page | null; disabled: Page | null }> {
2129
+ try {
2130
+ // verify: data is not null, data has non empty tuple set
2131
+ verify(data, "'data' must be specified");
2132
+ verify(data.length > 0, "'data' must have at least one row to create");
2133
+ verify(this._context === _contexts.CREATE, "reference must be in 'entry/create' context.");
2134
+
2135
+ return new Promise((resolve, reject) => {
2136
+ // get the defaults list for the referenced relation's table
2137
+ const defaults = this.getDefaults(data);
2138
+
2139
+ // construct the uri
2140
+ let uri = this._location.ermrestCompactUri;
2141
+ for (let i = 0; i < defaults.length; i++) {
2142
+ uri += (i === 0 ? '?defaults=' : ',') + fixedEncodeURIComponent(defaults[i]);
2143
+ }
2144
+ if (skipOnConflict) {
2145
+ const qCharacter = defaults.length > 0 ? '&' : '?';
2146
+ uri += qCharacter + 'onconflict=skip';
2147
+ }
2148
+
2149
+ if (isObject(contextHeaderParams) && Array.isArray(contextHeaderParams.stack)) {
2150
+ const stack = contextHeaderParams.stack;
2151
+ stack[stack.length - 1].num_created = data.length;
2152
+ }
2153
+ // create the context header params for log
2154
+ else if (!contextHeaderParams || !isObject(contextHeaderParams)) {
2155
+ contextHeaderParams = { action: 'create' };
2156
+ }
2157
+ const config = {
2158
+ headers: this._generateContextHeader(contextHeaderParams),
2159
+ };
2160
+
2161
+ // do the 'post' call
2162
+ this._server.http
2163
+ .post(uri, data, config)
2164
+ .then((response: any) => {
2165
+ const etag = HTTPService.getResponseHeader(response).etag;
2166
+ // new page will have a new reference (uri that filters on a disjunction of ids of these tuples)
2167
+ let uri = this._location.compactUri + '/';
2168
+ let keyName: string;
2169
+
2170
+ // loop through each returned Row and get the key value
2171
+ for (let j = 0; j < response.data.length; j++) {
2172
+ if (j !== 0) uri += ';';
2173
+ // shortest key is made up from one column
2174
+ if (this._shortestKey.length == 1) {
2175
+ keyName = this._shortestKey[0].name;
2176
+ uri += fixedEncodeURIComponent(keyName) + '=' + fixedEncodeURIComponent(response.data[j][keyName]);
2177
+ } else {
2178
+ uri += '(';
2179
+ for (let k = 0; k < this._shortestKey.length; k++) {
2180
+ if (k !== 0) uri += '&';
2181
+ keyName = this._shortestKey[k].name;
2182
+ uri += fixedEncodeURIComponent(keyName) + '=' + fixedEncodeURIComponent(response.data[j][keyName]);
2183
+ }
2184
+ uri += ')';
2185
+ }
2186
+ }
2187
+
2188
+ const ref = new Reference(parse(uri), this._table.schema.catalog);
2189
+ const contextualizedRef = response.data.length > 1 ? ref.contextualize.compactEntry : ref.contextualize.compact;
2190
+ // make a page of tuples of the results (unless error)
2191
+ const page = new Page(contextualizedRef, etag, response.data, false, false);
2192
+
2193
+ // resolve the promise, passing back the page
2194
+ resolve({
2195
+ successful: page,
2196
+ failed: null,
2197
+ disabled: null,
2198
+ });
2199
+ })
2200
+ .catch((error: unknown) => {
2201
+ reject(ErrorService.responseToError(error, this));
2202
+ });
2203
+ });
2204
+ } catch (e) {
2205
+ return Promise.reject(e);
2206
+ }
2207
+ }
2208
+
2209
+ /**
2210
+ * Updates a set of resources.
2211
+ * @param tuples array of tuple objects so that the new data nd old data can be used to determine key changes.
2212
+ * tuple.data has the new data
2213
+ * tuple._oldData has the data before changes were made
2214
+ * @param contextHeaderParams the object that we want to log.
2215
+ * @returns A promise resolved with a object containing:
2216
+ * - `successful`: Page of results that were stored.
2217
+ * - `failed`: Page of results that failed to be stored.
2218
+ * - `disabled`: Page of results that were not sent to ermrest (because of acl)
2219
+ * or rejected with any of these errors:
2220
+ * - {@link InvalidInputError}: If `limit` is invalid or reference is not in `entry/edit` context.
2221
+ * - ERMrestjs corresponding http errors, if ERMrest returns http error.
2222
+ */
2223
+ async update(tuples: Tuple[], contextHeaderParams?: any): Promise<{ successful: Page; failed: Page | null; disabled: Page | null }> {
2224
+ verify(Array.isArray(tuples), "'tuples' must be specified");
2225
+
2226
+ // store the ones that cannot be updated and filter them out
2227
+ // from the tuples list that we use to generate the update request
2228
+ const disabledPageData: any[] = [];
2229
+ tuples = tuples.filter((t) => {
2230
+ if (!t.canUpdate) {
2231
+ disabledPageData.push(t.data);
2232
+ }
2233
+ return t.canUpdate;
2234
+ });
2235
+
2236
+ verify(tuples.length > 0, "'tuples' must have at least one row to update");
2237
+ verify(this._context === _contexts.EDIT, "reference must be in 'entry/edit' context.");
2238
+
2239
+ const urlEncode = fixedEncodeURIComponent;
2240
+ const oldAlias = '_o';
2241
+ const newAlias = '_n';
2242
+
2243
+ const shortestKeyNames = this._shortestKey.map((column) => column.name);
2244
+ const encodedSchemaTableName = `${urlEncode(this.table.schema.name)}:${urlEncode(this.table.name)}`;
2245
+
2246
+ let updateRequestURL = `${this.location.service}/catalog/${this.table.schema.catalog.id}/attributegroup/${encodedSchemaTableName}/`;
2247
+
2248
+ const submissionData: any[] = []; // the list of submission data for updating
2249
+ const columnProjections: string[] = []; // the list of column names to use in the uri projection list
2250
+ let oldData: any;
2251
+ const allOldData: any[] = [];
2252
+ let newData: any;
2253
+ const allNewData: any[] = [];
2254
+ let keyName: string;
2255
+
2256
+ // add column name into list of column projections if not in column projections set and data has changed
2257
+ const addProjection = (colName: string, colType: Type) => {
2258
+ // don't add a column name in if it's already there
2259
+ // this can be the case for multi-edit
2260
+ // and if the data is unchanged, no need to add the column name to the projections list
2261
+ if (columnProjections.indexOf(colName) !== -1) return;
2262
+
2263
+ const oldVal = oldData[colName];
2264
+ const newVal = newData[colName];
2265
+
2266
+ const typename = colType.rootName;
2267
+ const compareWithMoment = typename === 'date' || typename === 'timestamp' || typename === 'timestamptz';
2268
+ // test with moment if datetime column type and one of the 2 values are defined
2269
+ // NOTE: moment will test 2 null values as different even though they are both null
2270
+ if (compareWithMoment && (oldVal || newVal)) {
2271
+ const oldMoment = moment(oldData[colName]);
2272
+ const newMoment = moment(newData[colName]);
2273
+
2274
+ if (!oldMoment.isSame(newMoment)) {
2275
+ columnProjections.push(colName);
2276
+ }
2277
+ } else if (oldData[colName] != newData[colName]) {
2278
+ columnProjections.push(colName);
2279
+ }
2280
+ };
2281
+
2282
+ const addProjectionForColumnObject = (column: VisibleColumn) => {
2283
+ // If columns is a pusedo column
2284
+ if (column.isPseudo) {
2285
+ // If a column is an asset column then set values for
2286
+ // dependent properties like filename, bytes_count_column, md5 and sha
2287
+ if ((column as AssetPseudoColumn).isAsset) {
2288
+ const asset = column as AssetPseudoColumn;
2289
+ const isNull = newData[column.name] === null ? true : false;
2290
+
2291
+ /* Populate all values in row depending on column from current asset */
2292
+ [asset.filenameColumn, asset.byteCountColumn, asset.md5, asset.sha256].forEach((assetColumn) => {
2293
+ // some metadata columns might not be defined.
2294
+ if (assetColumn && typeof assetColumn === 'object') {
2295
+ // If asset url is null then set the metadata also null
2296
+ if (isNull) newData[assetColumn.name] = null;
2297
+ addProjection(assetColumn.name, assetColumn.type);
2298
+ }
2299
+ });
2300
+ addProjection(column.name, column.type);
2301
+ } else if ((column as KeyPseudoColumn).isKey) {
2302
+ (column as KeyPseudoColumn).key.colset.columns.forEach((keyColumn) => {
2303
+ addProjection(keyColumn.name, keyColumn.type);
2304
+ });
2305
+ } else if ((column as ForeignKeyPseudoColumn).isForeignKey) {
2306
+ (column as ForeignKeyPseudoColumn).foreignKey.colset.columns.forEach((foreignKeyColumn) => {
2307
+ addProjection(foreignKeyColumn.name, foreignKeyColumn.type);
2308
+ });
2309
+ }
2310
+ } else {
2311
+ addProjection(column.name, column.type);
2312
+ }
2313
+ };
2314
+
2315
+ // add data into submission data if in column projection set
2316
+ const addSubmissionData = (index: number, colName: string) => {
2317
+ // if the column is in the column projections list, add the data to submission data
2318
+ if (columnProjections.indexOf(colName) > -1) {
2319
+ submissionData[index][colName + newAlias] = newData[colName];
2320
+ }
2321
+ };
2322
+
2323
+ const addSubmissionDataForColumnObject = (index: number, column: VisibleColumn) => {
2324
+ // If columns is a pusedo column
2325
+ if (column.isPseudo) {
2326
+ // If a column is an asset column then set values for
2327
+ // dependent properties like filename, bytes_count_column, md5 and sha
2328
+ if ((column as AssetPseudoColumn).isAsset) {
2329
+ const asset = column as AssetPseudoColumn;
2330
+ /* Populate all values in row depending on column from current asset */
2331
+ [asset.filenameColumn, asset.byteCountColumn, asset.md5, asset.sha256].forEach((assetColumn) => {
2332
+ // some metadata columns might not be defined.
2333
+ if (assetColumn && typeof assetColumn === 'object') addSubmissionData(index, assetColumn.name);
2334
+ });
2335
+
2336
+ addSubmissionData(index, column.name);
2337
+ } else if ((column as KeyPseudoColumn).isKey) {
2338
+ (column as KeyPseudoColumn).key.colset.columns.forEach((keyColumn) => {
2339
+ addSubmissionData(index, keyColumn.name);
2340
+ });
2341
+ } else if ((column as ForeignKeyPseudoColumn).isForeignKey) {
2342
+ (column as ForeignKeyPseudoColumn).foreignKey.colset.columns.forEach((foreignKeyColumn) => {
2343
+ addSubmissionData(index, foreignKeyColumn.name);
2344
+ });
2345
+ }
2346
+ } else {
2347
+ addSubmissionData(index, column.name);
2348
+ }
2349
+ };
2350
+
2351
+ // gets the key value based on which way the key was aliased.
2352
+ // use the new alias value for the shortest key first meaning the key was changed
2353
+ // if the new alias value is null, key wasn't changed so we can use the old alias
2354
+ const getAliasedKeyVal = (responseRowData: any, keyName: string) => {
2355
+ const isNotDefined = responseRowData[keyName + newAlias] === null || responseRowData[keyName + newAlias] === undefined;
2356
+ return isNotDefined ? responseRowData[keyName + oldAlias] : responseRowData[keyName + newAlias];
2357
+ };
2358
+
2359
+ // loop through each tuple and the visible columns list for each to determine what columns to add to the projection set
2360
+ // If a column is changed in one tuple but not another, that column's value for every tuple needs to be present in the submission data
2361
+ tuples.forEach((t, tupleIndex) => {
2362
+ newData = t.data;
2363
+ oldData = t._oldData;
2364
+
2365
+ // Collect all old and new data from all tuples to use in the event of a 412 error later
2366
+ allOldData.push(oldData);
2367
+ allNewData.push(newData);
2368
+
2369
+ // Loop throught the visible columns list and see what data the user changed
2370
+ // if we saw changes to data, add the constituent columns to the projections list
2371
+ this.columns.forEach((column, colIndex) => {
2372
+ // if the column is disabled (generated or immutable), no need to add the column name to the projections list
2373
+ if (column.inputDisabled) {
2374
+ return;
2375
+ }
2376
+
2377
+ // if column cannot be updated, no need to add the column to the projection list
2378
+ if (!tuples[tupleIndex].canUpdateValues[colIndex]) {
2379
+ return;
2380
+ }
2381
+
2382
+ if (column.isInputIframe) {
2383
+ addProjectionForColumnObject(column);
2384
+ // make sure the columns in the input_iframe column mapping are also added to the projection list
2385
+ column.inputIframeProps?.columns.forEach((iframeColumn) => {
2386
+ addProjectionForColumnObject(iframeColumn);
2387
+ });
2388
+ } else {
2389
+ addProjectionForColumnObject(column);
2390
+ }
2391
+ });
2392
+ });
2393
+
2394
+ if (columnProjections.length < 1) {
2395
+ throw new NoDataChangedError('No data was changed in the update request. Please check the form content and resubmit the data.');
2396
+ }
2397
+
2398
+ /* This loop manages adding the values based on the columnProjections set and setting columns associated with asset columns properly */
2399
+ // loop through each tuple again and set the data value from the tuple in submission data for each column projection
2400
+ tuples.forEach((t, tupleIndex) => {
2401
+ newData = t.data;
2402
+ oldData = t._oldData;
2403
+
2404
+ submissionData[tupleIndex] = {};
2405
+
2406
+ shortestKeyNames.forEach((shortestKey) => {
2407
+ // shortest key should always be aliased in case that key value was changed
2408
+ // use a suffix of '_o' to represent the old value for the shortest key everything else gets '_n'
2409
+ submissionData[tupleIndex][shortestKey + oldAlias] = oldData[shortestKey];
2410
+ });
2411
+
2412
+ // Loop through the columns, check if it's in columnProjections, and collect the necessary data for submission
2413
+ this.columns.forEach((column, colIndex) => {
2414
+ // if the column is disabled (generated or immutable), skip it
2415
+ if (column.inputDisabled) {
2416
+ return;
2417
+ }
2418
+
2419
+ // if column cannot be updated for this tuple, skip it
2420
+ if (!tuples[tupleIndex].canUpdateValues[colIndex]) {
2421
+ return;
2422
+ }
2423
+
2424
+ if (column.isInputIframe) {
2425
+ addSubmissionDataForColumnObject(tupleIndex, column);
2426
+ // make sure the values for columns in the input_iframe column mapping are also added
2427
+ column.inputIframeProps?.columns.forEach((iframeColumn) => {
2428
+ addSubmissionDataForColumnObject(tupleIndex, iframeColumn);
2429
+ });
2430
+ } else {
2431
+ addSubmissionDataForColumnObject(tupleIndex, column);
2432
+ }
2433
+ });
2434
+ });
2435
+
2436
+ // always alias the keyset for the key data
2437
+ shortestKeyNames.forEach((shortestKey, j) => {
2438
+ if (j !== 0) updateRequestURL += ',';
2439
+ // alias all the columns for the key set
2440
+ updateRequestURL += fixedEncodeURIComponent(shortestKey) + oldAlias + ':=' + fixedEncodeURIComponent(shortestKey);
2441
+ });
2442
+
2443
+ // the keyset is always aliased with the old alias, so make sure to include the new alias in the column projections
2444
+ columnProjections.forEach((colP, k) => {
2445
+ // Important NOTE: separator for denoting where the keyset ends and the update column set begins. The shortest key is used as the keyset
2446
+ updateRequestURL += k === 0 ? ';' : ',';
2447
+ // alias all the columns for the key set
2448
+ updateRequestURL += fixedEncodeURIComponent(colP) + newAlias + ':=' + fixedEncodeURIComponent(colP);
2449
+ });
2450
+
2451
+ /**
2452
+ * We are going to add the following to the last element of stack in the logs:
2453
+ * {
2454
+ * "num_updated": "number of updated rows"
2455
+ * "updated_keys": "the summary of what's updated"
2456
+ * }
2457
+ */
2458
+ if (isObject(contextHeaderParams) && Array.isArray(contextHeaderParams.stack)) {
2459
+ const stack = contextHeaderParams.stack,
2460
+ numUpdated = submissionData.length;
2461
+ stack[stack.length - 1].num_updated = numUpdated;
2462
+
2463
+ stack[stack.length - 1].updated_keys = {
2464
+ cols: shortestKeyNames,
2465
+ vals: allNewData.map((d) => shortestKeyNames.map((kname) => d[kname])),
2466
+ };
2467
+ } else if (!contextHeaderParams || !isObject(contextHeaderParams)) {
2468
+ contextHeaderParams = { action: 'update' };
2469
+ }
2470
+
2471
+ const config = {
2472
+ headers: this._generateContextHeader(contextHeaderParams),
2473
+ };
2474
+
2475
+ let response;
2476
+ try {
2477
+ response = await this._server.http.put(updateRequestURL, submissionData, config);
2478
+ } catch (error) {
2479
+ throw ErrorService.responseToError(error, this);
2480
+ }
2481
+
2482
+ // Some data was not updated
2483
+ if (response.status === 200 && response.data.length < submissionData.length) {
2484
+ const updatedRows = response.data;
2485
+ // no data updated
2486
+ if (updatedRows.length === 0) {
2487
+ throw new ForbiddenError('403', 'Editing records for table: ' + this.table.name + ' is not allowed.');
2488
+ }
2489
+ }
2490
+
2491
+ const etag = HTTPService.getResponseHeader(response).etag;
2492
+ const pageData: any[] = [];
2493
+
2494
+ // loop through each returned Row and get the key value
2495
+ for (let j = 0; j < response.data.length; j++) {
2496
+ // response.data is sometimes in a different order
2497
+ // so collecting the data could be incorrect if we don't make sure the response data and tuple data are in the same order
2498
+ // the entity is updated properly just the data returned from this request is in a different order sometimes
2499
+ let rowIndexInSubData = -1;
2500
+
2501
+ for (let t = 0; t < tuples.length && rowIndexInSubData === -1; t++) {
2502
+ // used to verify the number of matches for each shortest key value
2503
+ let matchCt = 0;
2504
+ for (let n = 0; n < shortestKeyNames.length; n++) {
2505
+ const shortKey = shortestKeyNames[n];
2506
+ const responseVal = getAliasedKeyVal(response.data[j], shortKey);
2507
+
2508
+ // if the value is the same, use this t index for the pageData object
2509
+ if (tuples[t].data[shortKey] == responseVal) {
2510
+ // this comes into play when the shortest key is a set of column names
2511
+ // if the values match increase te counter
2512
+ matchCt++;
2513
+ }
2514
+ }
2515
+ // if our counter is the same length as the list of shortest key names, it's an exact match to the t tuple
2516
+ if (matchCt === shortestKeyNames.length) {
2517
+ rowIndexInSubData = t;
2518
+ }
2519
+ }
2520
+
2521
+ pageData[rowIndexInSubData] = {};
2522
+
2523
+ // unalias the keys for the page data
2524
+ Object.keys(response.data[j]).forEach((columnAlias) => {
2525
+ if (columnAlias.endsWith(newAlias)) {
2526
+ // alias is always at end and length 2
2527
+ const columnName = columnAlias.slice(0, columnAlias.length - newAlias.length);
2528
+ pageData[rowIndexInSubData][columnName] = response.data[j][columnAlias];
2529
+ }
2530
+ });
2531
+ }
2532
+
2533
+ // NOTE: ermrest returns only some of the column data.
2534
+ // make sure that pageData has all the submitted and updated data
2535
+ for (let i = 0; i < tuples.length; i++) {
2536
+ for (const j in tuples[i]._oldData) {
2537
+ if (!Object.prototype.hasOwnProperty.call(tuples[i]._oldData, j)) continue;
2538
+ if (j in pageData[i]) continue; // pageData already has this data
2539
+ pageData[i][j] = tuples[i]._oldData![j]; // add the missing data
2540
+ }
2541
+ }
2542
+
2543
+ // build the url using the helper function
2544
+ const keyValueRes = generateKeyValueFilters(
2545
+ this.table.shortestKey,
2546
+ pageData,
2547
+ this.table.schema.catalog,
2548
+ -1, // we don't want to check the url length here, chaise will check it
2549
+ this.table.displayname.value,
2550
+ );
2551
+ // NOTE this will not happen since ermrest only accepts not-null keys,
2552
+ // but added here for completeness
2553
+ if (!keyValueRes.successful) {
2554
+ throw new InvalidInputError(keyValueRes.message as string);
2555
+ }
2556
+
2557
+ let refUri = `${this._location.service}/catalog/${this.table.schema.catalog.id}/entity/${encodedSchemaTableName}`;
2558
+ refUri += '/' + keyValueRes.filters!.map((f) => f.path).join('/');
2559
+ let ref = new Reference(parse(refUri), this._table.schema.catalog).contextualize.compactEntry;
2560
+ ref = response.data.length > 1 ? ref.contextualize.compactEntry : ref.contextualize.compact;
2561
+ const successfulPage = new Page(ref, etag, pageData, false, false);
2562
+ let failedPage = null,
2563
+ disabledPage = null;
2564
+
2565
+ // if the returned page is smaller than the initial page,
2566
+ // then some of the columns failed to update.
2567
+ if (tuples.length > successfulPage.tuples.length) {
2568
+ const failedPageData = [];
2569
+ for (let i = 0; i < tuples.length; i++) {
2570
+ let rowMatch = false;
2571
+
2572
+ for (let j = 0; j < successfulPage.tuples.length; j++) {
2573
+ let keyMatch = true;
2574
+
2575
+ for (let k = 0; k < shortestKeyNames.length; k++) {
2576
+ keyName = shortestKeyNames[k];
2577
+ if (tuples[i].data[keyName] === successfulPage.tuples[j].data[keyName]) {
2578
+ // these keys don't match, go for the next tuple
2579
+ keyMatch = false;
2580
+ break;
2581
+ }
2582
+ }
2583
+
2584
+ if (keyMatch) {
2585
+ // all the key columns match, then rows match.
2586
+ rowMatch = true;
2587
+ break;
2588
+ }
2589
+ }
2590
+
2591
+ if (!rowMatch) {
2592
+ // didn't find a match, so add as failed
2593
+ failedPageData.push(tuples[i].data);
2594
+ }
2595
+ }
2596
+ failedPage = new Page(ref, etag, failedPageData, false, false);
2597
+ }
2598
+
2599
+ if (disabledPageData.length > 0) {
2600
+ disabledPage = new Page(ref, etag, disabledPageData, false, false);
2601
+ }
2602
+
2603
+ return {
2604
+ successful: successfulPage,
2605
+ failed: failedPage,
2606
+ disabled: disabledPage,
2607
+ };
2608
+ }
2609
+
2610
+ /**
2611
+ * Deletes the referenced resources or the given tuples.
2612
+ * NOTE This will ignore the provided sort and paging on the reference, make
2613
+ * sure you are calling this on specific set or rows (filtered).
2614
+ *
2615
+ * @param tuples (optional) the tuples that should be deleted
2616
+ * @param contextHeaderParams (optional) the object that we want to log.
2617
+ * @returns A promise resolved with empty object or rejected with any of these errors:
2618
+ * - ERMrestjs corresponding http errors, if ERMrest returns http error.
2619
+ */
2620
+ delete(tuples?: Tuple[], contextHeaderParams?: any): Promise<void | BatchDeleteResponse> {
2621
+ const delFlag = _operationsFlag.DELETE;
2622
+ const useTuples = Array.isArray(tuples) && tuples.length > 0;
2623
+
2624
+ /**
2625
+ * NOTE: previous implemenation of delete with 412 logic is here:
2626
+ * https://github.com/informatics-isi-edu/ermrestjs/commit/5fe854118337e0a63c6f91b4f3e139e7eadc42ac
2627
+ *
2628
+ * We decided to drop the support for 412, because the etag that we get from the read function
2629
+ * is different than the one delete expects. The reason for that is because we are getting etag
2630
+ * in read with joins in the request, which affects the etag. etag is in response to any change
2631
+ * to the returned data and since join introduces extra data it is different than a request
2632
+ * without any joins.
2633
+ *
2634
+ * github issue: #425
2635
+ */
2636
+
2637
+ return new Promise((resolve, reject) => {
2638
+ /**
2639
+ * We are going to add the following to the last element of stack in the logs:
2640
+ * {
2641
+ * "num_deleted": "number of deleted rows"
2642
+ * "deleted_keys": "the summary of what's deleted"
2643
+ * }
2644
+ */
2645
+ if (isObject(contextHeaderParams) && Array.isArray(contextHeaderParams.stack) && useTuples) {
2646
+ const stack = contextHeaderParams.stack;
2647
+ const shortestKeyNames = this._shortestKey.map((column) => column.name);
2648
+ stack[stack.length - 1].num_deleted = tuples.length;
2649
+ stack[stack.length - 1].deleted_keys = {
2650
+ cols: shortestKeyNames,
2651
+ vals: tuples.map((t) => shortestKeyNames.map((kname) => t.data[kname])),
2652
+ };
2653
+ } else if (!contextHeaderParams || !isObject(contextHeaderParams)) {
2654
+ contextHeaderParams = { action: 'delete' };
2655
+ }
2656
+ const config = {
2657
+ headers: this._generateContextHeader(contextHeaderParams),
2658
+ };
2659
+
2660
+ if (!useTuples) {
2661
+ // delete the reference
2662
+ this._server.http
2663
+ .delete(this.location.ermrestCompactUri, config)
2664
+ .then(() => {
2665
+ resolve();
2666
+ })
2667
+ .catch((catchError: unknown) => {
2668
+ reject(ErrorService.responseToError(catchError, this, delFlag));
2669
+ });
2670
+
2671
+ return;
2672
+ } else {
2673
+ // construct the url based on the given tuples
2674
+ let successTupleData: any[] = [];
2675
+ let failedTupleData: any[] = [];
2676
+ const deleteSubmessage: string[] = [];
2677
+
2678
+ const encode = fixedEncodeURIComponent;
2679
+ const schemaTable = encode(this.table.schema.name) + ':' + encode(this.table.name);
2680
+ const deletableData: any[] = [];
2681
+ const nonDeletableTuples: string[] = [];
2682
+
2683
+ tuples.forEach((t, index) => {
2684
+ if (t.canDelete) {
2685
+ deletableData.push(t.data);
2686
+ } else {
2687
+ failedTupleData.push(t.data);
2688
+ nonDeletableTuples.push('- Record number ' + (index + 1) + ': ' + t.displayname.value);
2689
+ }
2690
+ });
2691
+
2692
+ if (nonDeletableTuples.length > 0) {
2693
+ deleteSubmessage.push('The following records could not be deleted based on your permissions:\n' + nonDeletableTuples.join('\n'));
2694
+ }
2695
+
2696
+ // if none of the rows could be deleted, just return now.
2697
+ if (deletableData.length === 0) {
2698
+ resolve(new BatchDeleteResponse(successTupleData, failedTupleData, deleteSubmessage.join('\n')));
2699
+ return;
2700
+ }
2701
+
2702
+ // might throw an error
2703
+ const keyValueRes = generateKeyValueFilters(
2704
+ this.table.shortestKey,
2705
+ deletableData,
2706
+ this.table.schema.catalog,
2707
+ schemaTable.length + 1,
2708
+ this.displayname.value as string,
2709
+ );
2710
+ if (!keyValueRes.successful) {
2711
+ return reject(new InvalidInputError(keyValueRes.message ? keyValueRes.message : ''));
2712
+ }
2713
+
2714
+ const recursiveDelete = (index: number) => {
2715
+ const currFilter = keyValueRes.filters![index];
2716
+ const url = [this.location.service, 'catalog', this.location.catalog, 'entity', schemaTable, currFilter.path].join('/');
2717
+
2718
+ this._server.http
2719
+ .delete(url, config)
2720
+ .then(() => {
2721
+ successTupleData = successTupleData.concat(currFilter.keyData);
2722
+ })
2723
+ .catch((err: unknown) => {
2724
+ failedTupleData = failedTupleData.concat(currFilter.keyData);
2725
+ deleteSubmessage.push(ErrorService.responseToError(err, this, delFlag).message);
2726
+ })
2727
+ .finally(() => {
2728
+ if (index < keyValueRes.filters!.length - 1) {
2729
+ recursiveDelete(index + 1);
2730
+ } else {
2731
+ resolve(new BatchDeleteResponse(successTupleData, failedTupleData, deleteSubmessage.join('\n')));
2732
+ return;
2733
+ }
2734
+ });
2735
+ };
2736
+
2737
+ recursiveDelete(0);
2738
+ }
2739
+ });
2740
+ }
2741
+
2742
+ /**
2743
+ * create a new instance with the same properties.
2744
+ *
2745
+ * you can customized the properties of the new instance by passing new ones to this function.
2746
+ * You must pass undefined for other props that you don't want to change.
2747
+ */
2748
+ copy(displayname?: DisplayName, comment?: CommentType, pseudoColumn?: VisibleColumn): Reference {
2749
+ const ref = new Reference(
2750
+ this.location,
2751
+ this.location.catalogObject,
2752
+ typeof displayname !== 'undefined' ? displayname : this._displayname,
2753
+ typeof comment !== 'undefined' ? comment : this._comment,
2754
+ typeof pseudoColumn !== 'undefined' ? pseudoColumn : this._pseudoColumn,
2755
+ );
2756
+
2757
+ ref.setContext(this.context);
2758
+ return ref;
2759
+ }
2760
+
2761
+ /**
2762
+ * return the columns that we should add to the defaults list during creation
2763
+ * @private
2764
+ */
2765
+ private getDefaults(data: Record<string, unknown>[]): string[] {
2766
+ const defaults: string[] = [];
2767
+ this.table.columns.all().forEach((col) => {
2768
+ // ignore the columns that user doesn't have insert access for.
2769
+ // This is to avoid ermrest from throwing any errors.
2770
+ //
2771
+ // NOTE At first we were ignoring any disabled inputs.
2772
+ // While ignoring value for disabled inputs might sound logical,
2773
+ // there are some disabled inputs that chaise is actually going to generate
2774
+ // value for and we need to store them. At the time of writing this comment,
2775
+ // this is only true for the asset's filename, md5, etc. columns.
2776
+ // In most deployments they are marked as generated and the expectation
2777
+ // would be that chaise/ermrestjs should generate the value.
2778
+ // The misconception here is the generated definition in the annotation.
2779
+ // by generated we mean chaise/ERMrestjs generated not necessarily database generated.
2780
+ // the issue: https://github.com/informatics-isi-edu/ermrestjs/issues/722
2781
+ if (col.rights.insert === false) {
2782
+ defaults.push(col.name);
2783
+ return;
2784
+ }
2785
+
2786
+ // if default is null, don't add it.
2787
+ // this is just to reduce the size of defaults. adding them
2788
+ // is harmless but it's not necessary. so we're just not going
2789
+ // to add them to make the default list shorter
2790
+ // NOTE we added nullok check because of a special case that we found.
2791
+ // In this case the column was not-null, no default value, and the value
2792
+ // was being populated by trigger. So we have to add the column to the default
2793
+ // list so ermrest doesn't throw the error.
2794
+ // if a column is not-null, we need to actually check for the value. if the value
2795
+ // is null, not adding it to the default list will always result in ermrest error.
2796
+ // But adding it to the default list might succeed (if the column has trigger for value)
2797
+ if (col.ermrestDefault == null && col.nullok) return;
2798
+
2799
+ // add columns that their value is missing in all the rows
2800
+ let missing = true;
2801
+ for (let i = 0; i < data.length; i++) {
2802
+ // at least one of the rows has value for it
2803
+ if (data[i][col.name] !== undefined && data[i][col.name] !== null) {
2804
+ missing = false;
2805
+ break;
2806
+ }
2807
+ }
2808
+ if (missing) defaults.push(col.name);
2809
+ });
2810
+
2811
+ return defaults;
2812
+ }
2813
+ }