@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.
- package/LICENSE +202 -0
- package/README.md +55 -0
- package/dist/ermrest.d.ts +3481 -0
- package/dist/ermrest.js +45 -0
- package/dist/ermrest.js.gz +0 -0
- package/dist/ermrest.js.map +1 -0
- package/dist/ermrest.min.js +45 -0
- package/dist/ermrest.min.js.gz +0 -0
- package/dist/ermrest.min.js.map +1 -0
- package/dist/ermrest.ver.txt +1 -0
- package/dist/stats.html +4949 -0
- package/js/ag_reference.js +1483 -0
- package/js/core.js +4931 -0
- package/js/datapath.js +336 -0
- package/js/export.js +956 -0
- package/js/filters.js +192 -0
- package/js/format.js +344 -0
- package/js/hatrac.js +1130 -0
- package/js/json_ld_validator.js +285 -0
- package/js/parser.js +2320 -0
- package/js/setup/node.js +27 -0
- package/js/utils/helpers.js +2300 -0
- package/js/utils/json_ld_schema.js +680 -0
- package/js/utils/pseudocolumn_helpers.js +2196 -0
- package/package.json +79 -0
- package/src/index.ts +204 -0
- package/src/models/comment.ts +14 -0
- package/src/models/deferred-promise.ts +16 -0
- package/src/models/display-name.ts +5 -0
- package/src/models/errors.ts +408 -0
- package/src/models/path-prefix-alias-mapping.ts +130 -0
- package/src/models/reference/bulk-create-foreign-key-object.ts +133 -0
- package/src/models/reference/citation.ts +98 -0
- package/src/models/reference/contextualize.ts +535 -0
- package/src/models/reference/google-dataset-metadata.ts +72 -0
- package/src/models/reference/index.ts +14 -0
- package/src/models/reference/page.ts +520 -0
- package/src/models/reference/reference-aggregate-fn.ts +37 -0
- package/src/models/reference/reference.ts +2813 -0
- package/src/models/reference/related-reference.ts +467 -0
- package/src/models/reference/tuple.ts +652 -0
- package/src/models/reference-column/asset-pseudo-column.ts +498 -0
- package/src/models/reference-column/column-aggregate.ts +313 -0
- package/src/models/reference-column/facet-column.ts +1380 -0
- package/src/models/reference-column/foreign-key-pseudo-column.ts +626 -0
- package/src/models/reference-column/inbound-foreign-key-pseudo-column.ts +131 -0
- package/src/models/reference-column/index.ts +13 -0
- package/src/models/reference-column/key-pseudo-column.ts +236 -0
- package/src/models/reference-column/pseudo-column.ts +850 -0
- package/src/models/reference-column/reference-column.ts +740 -0
- package/src/models/source-object-node.ts +156 -0
- package/src/models/source-object-wrapper.ts +694 -0
- package/src/models/table-source-definitions.ts +98 -0
- package/src/services/authn.ts +43 -0
- package/src/services/catalog.ts +37 -0
- package/src/services/config.ts +202 -0
- package/src/services/error.ts +247 -0
- package/src/services/handlebars.ts +607 -0
- package/src/services/history.ts +136 -0
- package/src/services/http.ts +536 -0
- package/src/services/logger.ts +70 -0
- package/src/services/mustache.ts +0 -0
- package/src/utils/column-utils.ts +308 -0
- package/src/utils/constants.ts +526 -0
- package/src/utils/markdown-utils.ts +855 -0
- package/src/utils/reference-utils.ts +1658 -0
- package/src/utils/template-utils.ts +0 -0
- package/src/utils/type-utils.ts +89 -0
- package/src/utils/value-utils.ts +127 -0
- package/tsconfig.json +30 -0
- 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
|
+
}
|