@isrd-isi-edu/ermrestjs 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +55 -0
  3. package/dist/ermrest.d.ts +3481 -0
  4. package/dist/ermrest.js +45 -0
  5. package/dist/ermrest.js.gz +0 -0
  6. package/dist/ermrest.js.map +1 -0
  7. package/dist/ermrest.min.js +45 -0
  8. package/dist/ermrest.min.js.gz +0 -0
  9. package/dist/ermrest.min.js.map +1 -0
  10. package/dist/ermrest.ver.txt +1 -0
  11. package/dist/stats.html +4949 -0
  12. package/js/ag_reference.js +1483 -0
  13. package/js/core.js +4931 -0
  14. package/js/datapath.js +336 -0
  15. package/js/export.js +956 -0
  16. package/js/filters.js +192 -0
  17. package/js/format.js +344 -0
  18. package/js/hatrac.js +1130 -0
  19. package/js/json_ld_validator.js +285 -0
  20. package/js/parser.js +2320 -0
  21. package/js/setup/node.js +27 -0
  22. package/js/utils/helpers.js +2300 -0
  23. package/js/utils/json_ld_schema.js +680 -0
  24. package/js/utils/pseudocolumn_helpers.js +2196 -0
  25. package/package.json +79 -0
  26. package/src/index.ts +204 -0
  27. package/src/models/comment.ts +14 -0
  28. package/src/models/deferred-promise.ts +16 -0
  29. package/src/models/display-name.ts +5 -0
  30. package/src/models/errors.ts +408 -0
  31. package/src/models/path-prefix-alias-mapping.ts +130 -0
  32. package/src/models/reference/bulk-create-foreign-key-object.ts +133 -0
  33. package/src/models/reference/citation.ts +98 -0
  34. package/src/models/reference/contextualize.ts +535 -0
  35. package/src/models/reference/google-dataset-metadata.ts +72 -0
  36. package/src/models/reference/index.ts +14 -0
  37. package/src/models/reference/page.ts +520 -0
  38. package/src/models/reference/reference-aggregate-fn.ts +37 -0
  39. package/src/models/reference/reference.ts +2813 -0
  40. package/src/models/reference/related-reference.ts +467 -0
  41. package/src/models/reference/tuple.ts +652 -0
  42. package/src/models/reference-column/asset-pseudo-column.ts +498 -0
  43. package/src/models/reference-column/column-aggregate.ts +313 -0
  44. package/src/models/reference-column/facet-column.ts +1380 -0
  45. package/src/models/reference-column/foreign-key-pseudo-column.ts +626 -0
  46. package/src/models/reference-column/inbound-foreign-key-pseudo-column.ts +131 -0
  47. package/src/models/reference-column/index.ts +13 -0
  48. package/src/models/reference-column/key-pseudo-column.ts +236 -0
  49. package/src/models/reference-column/pseudo-column.ts +850 -0
  50. package/src/models/reference-column/reference-column.ts +740 -0
  51. package/src/models/source-object-node.ts +156 -0
  52. package/src/models/source-object-wrapper.ts +694 -0
  53. package/src/models/table-source-definitions.ts +98 -0
  54. package/src/services/authn.ts +43 -0
  55. package/src/services/catalog.ts +37 -0
  56. package/src/services/config.ts +202 -0
  57. package/src/services/error.ts +247 -0
  58. package/src/services/handlebars.ts +607 -0
  59. package/src/services/history.ts +136 -0
  60. package/src/services/http.ts +536 -0
  61. package/src/services/logger.ts +70 -0
  62. package/src/services/mustache.ts +0 -0
  63. package/src/utils/column-utils.ts +308 -0
  64. package/src/utils/constants.ts +526 -0
  65. package/src/utils/markdown-utils.ts +855 -0
  66. package/src/utils/reference-utils.ts +1658 -0
  67. package/src/utils/template-utils.ts +0 -0
  68. package/src/utils/type-utils.ts +89 -0
  69. package/src/utils/value-utils.ts +127 -0
  70. package/tsconfig.json +30 -0
  71. package/vite.config.mts +104 -0
package/js/export.js ADDED
@@ -0,0 +1,956 @@
1
+ /* eslint-disable @typescript-eslint/no-this-alias */
2
+
3
+ // models
4
+ import { InvalidInputError } from '@isrd-isi-edu/ermrestjs/src/models/errors';
5
+ // import DeferredPromise from '@isrd-isi-edu/ermrestjs/src/models/deferred-promise';
6
+
7
+ // services
8
+ import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
9
+ import ErrorService from '@isrd-isi-edu/ermrestjs/src/services/error';
10
+ import ConfigService from '@isrd-isi-edu/ermrestjs/src/services/config';
11
+
12
+ // utils
13
+ import { isObjectAndNotNull, isStringAndNotEmpty, ObjectHasAllKeys } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
14
+ import { fixedEncodeURIComponent, trimSlashes, simpleDeepCopy } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
15
+ import { _annotations, contextHeaderName, _contexts, _exportKnownAPIs, URL_PATH_LENGTH_LIMIT } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
16
+
17
+ // legacy
18
+ import {
19
+ _getAnnotationValueByContext,
20
+ _getRecursiveAnnotationValue,
21
+ _getCandidateRowNameColumn,
22
+ _sanitizeFilename,
23
+ } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
24
+
25
+ export const _exportHelpers = {
26
+ /**
27
+ *
28
+ * Returns the export templates that are defined on this table.
29
+ * NOTE If this returns `null`, then the exportTemplates is not defined on the table or schema
30
+ * NOTE The returned array should not be directly used as it might be using fragments
31
+ * @param {Table} table
32
+ * @param {string} context
33
+ * @private
34
+ * @ignore
35
+ */
36
+ getExportAnnotTemplates: function (table, context) {
37
+ var exp = _annotations.EXPORT,
38
+ expCtx = _annotations.EXPORT_CONTEXTED,
39
+ annotDefinition = {},
40
+ hasAnnot = false,
41
+ chosenAnnot,
42
+ templates = [];
43
+
44
+ // start from table, then try schema, and then catalog
45
+ [table, table.schema, table.schema.catalog].forEach(function (el) {
46
+ if (hasAnnot) return;
47
+
48
+ // get from table annotation
49
+ if (el.annotations.contains(exp)) {
50
+ annotDefinition = { '*': el.annotations.get(exp).content };
51
+ hasAnnot = true;
52
+ }
53
+
54
+ // get from table contextualized annotation
55
+ if (el.annotations.contains(expCtx)) {
56
+ annotDefinition = Object.assign({}, annotDefinition, el.annotations.get(expCtx).content);
57
+ hasAnnot = true;
58
+ }
59
+
60
+ if (hasAnnot) {
61
+ // find the annotation defined for the context
62
+ chosenAnnot = _getAnnotationValueByContext(context, annotDefinition);
63
+
64
+ // not defined for the context
65
+ if (chosenAnnot === -1) {
66
+ hasAnnot = false;
67
+ }
68
+ // make sure it's the correct format
69
+ else if (isObjectAndNotNull(chosenAnnot) && 'templates' in chosenAnnot && Array.isArray(chosenAnnot.templates)) {
70
+ templates = chosenAnnot.templates;
71
+ }
72
+ }
73
+ });
74
+
75
+ if (hasAnnot) {
76
+ return templates;
77
+ }
78
+
79
+ return null;
80
+ },
81
+
82
+ /**
83
+ * Return the export fragments that should be used with export annotation.
84
+ * @param {Table} table
85
+ * @param {Object} defaultExportTemplate
86
+ * @returns An object that can be used in combination with export annotation
87
+ * @private
88
+ * @ignore
89
+ */
90
+ getExportFragmentObject: function (table, defaultExportTemplate) {
91
+ var exportFragments = {
92
+ $chaise_default_bdbag_template: {
93
+ type: 'BAG',
94
+ displayname: { fragment_key: '$chaise_default_bdbag_displayname' },
95
+ outputs: [{ fragment_key: '$chaise_default_bdbag_outputs' }],
96
+ },
97
+ $chaise_default_bdbag_displayname: 'BDBag',
98
+ $chaise_default_bdbag_outputs: defaultExportTemplate ? defaultExportTemplate.outputs : null,
99
+ };
100
+ var annotKey = _annotations.EXPORT_FRAGMENT_DEFINITIONS,
101
+ annot;
102
+ [table.schema.catalog, table.schema, table].forEach(function (el) {
103
+ if (!el.annotations.contains(annotKey)) return;
104
+
105
+ annot = el.annotations.get(annotKey).content;
106
+ if (isObjectAndNotNull(annot)) {
107
+ // remove the keys that start with $
108
+ Object.keys(annot)
109
+ .filter(function (k) {
110
+ return k.startsWith('$');
111
+ })
112
+ .forEach(function (k) {
113
+ $log.warn('Export: ignoring `' + k + '` fragment as it cannot start with $.');
114
+ delete annot[k];
115
+ });
116
+ Object.assign(exportFragments, annot);
117
+ }
118
+ });
119
+
120
+ return exportFragments;
121
+ },
122
+
123
+ /**
124
+ * Replace the fragments used in templates with actual definition
125
+ * @param {Array} templates the template definitions
126
+ * @param {*} exportFragments the fragment object
127
+ * @returns An array of templates that should be validated and used
128
+ * @private
129
+ * @ignore
130
+ */
131
+ replaceFragments: function (templates, exportFragments) {
132
+ var hasError;
133
+
134
+ // traverse through the object and replace fragment with the actual definition
135
+ var _replaceFragments = function (obj, usedFragments) {
136
+ if (hasError) return null;
137
+
138
+ if (!usedFragments) {
139
+ usedFragments = {};
140
+ }
141
+
142
+ var res, intRes;
143
+
144
+ // if it's an array, then we have to process each individual value
145
+ if (Array.isArray(obj)) {
146
+ res = [];
147
+ obj.forEach(function (item) {
148
+ // flatten the values and just concat with each other
149
+ intRes = _replaceFragments(item, usedFragments);
150
+ if (intRes == null || hasError) {
151
+ res = null;
152
+ return;
153
+ }
154
+ res = res.concat(intRes);
155
+ });
156
+
157
+ return res;
158
+ }
159
+
160
+ // if it's an object, we have to see whether it's fragment or not
161
+ if (isObjectAndNotNull(obj)) {
162
+ if ('fragment_key' in obj) {
163
+ var fragmentKey = obj.fragment_key;
164
+
165
+ // there was a cycle, so just set the variables and abort
166
+ if (fragmentKey in usedFragments) {
167
+ $log.warn(`Export: circular dependency detected in the defined templates and therefore ignored (caused by ${fragmentKey} key)`);
168
+ hasError = true;
169
+ return null;
170
+ }
171
+
172
+ // fragment_key is invalid
173
+ if (!(fragmentKey in exportFragments)) {
174
+ hasError = true;
175
+ $log.warn('Export: the given fragment_key `' + fragmentKey + '` is not valid and template will be ignored.');
176
+ return null;
177
+ }
178
+
179
+ // replace with actual definition
180
+ var modified = simpleDeepCopy(usedFragments);
181
+ modified[fragmentKey] = true;
182
+ return _replaceFragments(exportFragments[fragmentKey], modified);
183
+ }
184
+
185
+ // run the function for each value
186
+ res = {};
187
+ for (var k in obj) {
188
+ intRes = _replaceFragments(obj[k], usedFragments);
189
+ if (intRes == null || hasError) {
190
+ res = null;
191
+ return;
192
+ }
193
+ res[k] = intRes;
194
+ }
195
+ return res;
196
+ }
197
+
198
+ // for other data types, just return the input without any change
199
+ return obj;
200
+ };
201
+
202
+ var finalRes = [];
203
+
204
+ templates.forEach(function (t) {
205
+ hasError = false;
206
+ var tempRes = _replaceFragments(t, {});
207
+ // if there was an issue, the whole thing should return null
208
+ if (tempRes != null) {
209
+ finalRes = finalRes.concat(tempRes);
210
+ }
211
+ });
212
+
213
+ return finalRes;
214
+ },
215
+ };
216
+
217
+ /**
218
+ * Given an object, returns a boolean that indicates whether it is a valid template or not
219
+ * NOTE This will only validate the structure and not the values
220
+ * @param {Object} template
221
+ * @return {boolean}
222
+ */
223
+ export const validateExportTemplate = function (template) {
224
+ var errMessage = function (reason) {
225
+ $log.info('export template ignored with displayname=`' + template.displayname + '`. Reason: ' + reason);
226
+ };
227
+
228
+ // template is not an object
229
+ if (template !== Object(template) || Array.isArray(template) || !template) {
230
+ $log.info("export template ignored. Reason: it's not an object.");
231
+ return false;
232
+ }
233
+
234
+ // doesn't have the expected attributes
235
+ if (!ObjectHasAllKeys(template, ['displayname', 'type'])) {
236
+ $log.info('export template ignored. Reason: first level required attributes are missing.');
237
+ return false;
238
+ }
239
+
240
+ //type must be either FILE or BAG
241
+ if (['BAG', 'FILE'].indexOf(template.type) === -1) {
242
+ $log.info('export template ignored. Reason: template.type must be either `BAG` or `FILE`.');
243
+ return false;
244
+ }
245
+
246
+ // in FILE, outputs must be properly defined
247
+ if (!Array.isArray(template.outputs) || template.outputs.length === 0) {
248
+ errMessage('outputs must be a non-empty array.');
249
+ return false;
250
+ }
251
+
252
+ var output;
253
+ for (var i = 0; i < template.outputs.length; i++) {
254
+ output = template.outputs[i];
255
+
256
+ //output must be an object
257
+ if (output !== Object(output) || Array.isArray(output) || !output) {
258
+ errMessage('output index=' + i + ' is not an object.');
259
+ return false;
260
+ }
261
+
262
+ // output must have source and destination
263
+ if (!ObjectHasAllKeys(output, ['source', 'destination'])) {
264
+ errMessage('output index=' + i + ' has missing required attributes.');
265
+ return false;
266
+ }
267
+
268
+ // output.source must have api
269
+ if (!ObjectHasAllKeys(output.source, ['api'])) {
270
+ errMessage('output.source index=' + i + ' has missing required attributes.');
271
+ return false;
272
+ }
273
+
274
+ // output.destination must have at least a type
275
+ if (!ObjectHasAllKeys(output.destination, ['type'])) {
276
+ errMessage('output.destination index=' + i + ' has missing required attributes.');
277
+ return false;
278
+ }
279
+ }
280
+
281
+ return true;
282
+ };
283
+
284
+ /**
285
+ * @desc Export Object
286
+ *
287
+ * @memberof ERMrest
288
+ * @class
289
+ * @param {Reference} reference
290
+ * @param {String} bagName the name that will be used for the bag
291
+ * @param {Object} template the tempalte must be in the valid format.
292
+ * @param {String} servicePath the path to the service, i.e. "/deriva/export/"
293
+ *
294
+ *
295
+ * @returns {Export}
296
+ * @constructor
297
+ */
298
+ export function Exporter(reference, bagName, template, servicePath) {
299
+ if (!validateExportTemplate(template)) {
300
+ throw new InvalidInputError('Given Template is not valid.');
301
+ }
302
+ if (typeof servicePath !== 'string' || servicePath.length === 0) {
303
+ throw new InvalidInputError('Given service path is not valid.');
304
+ }
305
+
306
+ this.reference = reference;
307
+ this.template = template;
308
+ this.servicePath = trimSlashes(servicePath);
309
+ this.formatOptions = {
310
+ BAG: {
311
+ name: bagName,
312
+ algs: ['md5'],
313
+ archiver: 'zip',
314
+ metadata: {},
315
+ table_format: 'csv',
316
+ },
317
+ };
318
+ }
319
+
320
+ Exporter.prototype = {
321
+ constructor: Exporter,
322
+
323
+ /**
324
+ * TODO: add description
325
+ */
326
+ get exportParameters() {
327
+ if (this._exportParameters === undefined) {
328
+ var exportParameters = {};
329
+ var bagOptions = this.formatOptions.BAG;
330
+ var template = this.template;
331
+
332
+ var bagParameters = {
333
+ bag_name: bagOptions.name,
334
+ bag_algorithms: bagOptions.algs,
335
+ bag_archiver: bagOptions.archiver,
336
+ bag_metadata: bagOptions.metadata,
337
+ };
338
+
339
+ exportParameters.bag = bagParameters;
340
+
341
+ var base_url = this.reference.location.service;
342
+ var queries = [];
343
+ var catalogParameters = {
344
+ host: base_url.substring(0, base_url.lastIndexOf('/')),
345
+ catalog_id: this.reference.location.catalog,
346
+ query_processors: queries,
347
+ };
348
+
349
+ exportParameters.catalog = catalogParameters;
350
+
351
+ if (!template) {
352
+ // this is basically the same as a single file CSV or JSON export but packaged as a bag
353
+ var query = {
354
+ processor: bagOptions.table_format,
355
+ processor_params: {
356
+ output_path: bagOptions.name,
357
+ query_path: '/' + this.reference.location.api + '/' + this.reference.location.ermrestCompactPath + '?limit=none',
358
+ },
359
+ };
360
+
361
+ queries.push(query);
362
+ } else {
363
+ var outputs = template.outputs;
364
+ var predicate = this.reference.location.ermrestCompactPath;
365
+ var table = this.reference.table.name;
366
+ var output_path = _sanitizeFilename(this.reference.displayname.unformatted);
367
+
368
+ outputs.forEach(function (output, index) {
369
+ var source = output.source,
370
+ dest = output.destination;
371
+ var query = {},
372
+ queryParams = {};
373
+
374
+ // <api>/<current reference path>/<path>
375
+ var queryFrags = [];
376
+ if (isStringAndNotEmpty(source.api)) {
377
+ queryFrags.push(source.api);
378
+ }
379
+ if (!source.skip_root_path) {
380
+ queryFrags.push(predicate);
381
+ }
382
+ if (isStringAndNotEmpty(source.path)) {
383
+ // remove the first and last slash if it has one
384
+ const addedPath = trimSlashes(source.path);
385
+ // make sure the path is not empty
386
+ if (addedPath.length > 0) {
387
+ queryFrags.push(addedPath);
388
+ }
389
+ }
390
+
391
+ var queryStr = queryFrags.join('/');
392
+ if (queryStr.length > URL_PATH_LENGTH_LIMIT) {
393
+ $log.warn(
394
+ 'Cannot send the output index `' + index + '` for table `' + table + '` to ermrest (URL LENGTH ERROR). Generated query:',
395
+ queryStr,
396
+ );
397
+ return;
398
+ }
399
+
400
+ // find the character that should be used for added q params
401
+ var qParamCharacter = queryStr.indexOf('?') !== -1 ? '&' : '?';
402
+
403
+ /**
404
+ * add limit q param if all the following are set
405
+ * - skip_limit is not set to true
406
+ * - API is known.
407
+ * - it's not part of the url
408
+ */
409
+ var addLimit =
410
+ !source.skip_limit &&
411
+ isStringAndNotEmpty(queryStr) &&
412
+ _exportKnownAPIs.some(function (api) {
413
+ return queryStr.startsWith(api + '/');
414
+ });
415
+ // if limit is already part of the query, don't add it.
416
+ if (addLimit) {
417
+ addLimit = !/[?]([^&=]*=[^&]*[&])*limit=/.test(queryStr);
418
+ }
419
+ if (addLimit) {
420
+ queryStr += qParamCharacter + 'limit=none';
421
+ }
422
+
423
+ queryParams.query_path = '/' + queryStr;
424
+ queryParams.output_path = dest.name || output_path;
425
+ if (dest.impl != null) {
426
+ query.processor_type = dest.impl;
427
+ }
428
+ if (dest.params != null) {
429
+ Object.assign(queryParams, dest.params);
430
+ }
431
+ query.processor = dest.type || bagOptions.table_format;
432
+ query.processor_params = queryParams;
433
+ queries.push(query);
434
+ });
435
+ }
436
+ if (template.transforms != null) {
437
+ exportParameters.transform_processors = template.transforms;
438
+ }
439
+ if (template.postprocessors != null) {
440
+ exportParameters.post_processors = template.postprocessors;
441
+ }
442
+ if (template.public != null) {
443
+ exportParameters.public = template.public;
444
+ }
445
+ if (template.bag_archiver != null) {
446
+ exportParameters.bag.bag_archiver = template.bag_archiver;
447
+ }
448
+ if (template.bag_idempotent != null) {
449
+ exportParameters.bag.bag_idempotent = template.bag_idempotent;
450
+ }
451
+ this._exportParameters = exportParameters;
452
+ }
453
+
454
+ return this._exportParameters;
455
+ },
456
+
457
+ /**
458
+ * sends the export request to ioboxd
459
+ * @param {Object} contextHeaderParams the object that will be logged
460
+ * @returns {Promise}
461
+ */
462
+ run: function (contextHeaderParams) {
463
+ var defer = ConfigService.q.defer(),
464
+ self = this;
465
+ try {
466
+ var serviceUrl = [self.exportParameters.catalog.host, self.servicePath, self.template.type == 'BAG' ? 'bdbag' : 'file'].join('/');
467
+
468
+ // log parameters
469
+ var headers = {};
470
+ if (!contextHeaderParams || typeof contextHeaderParams !== 'object') {
471
+ contextHeaderParams = { action: 'export' };
472
+ }
473
+ // add the reference information
474
+ for (var key in self.reference.defaultLogInfo) {
475
+ if (key in contextHeaderParams) continue;
476
+ contextHeaderParams[key] = self.reference.defaultLogInfo[key];
477
+ }
478
+ headers[contextHeaderName] = contextHeaderParams;
479
+
480
+ self.canceled = false;
481
+ if (self.exportParameters.public != null) {
482
+ serviceUrl += '?public=' + self.exportParameters.public;
483
+ }
484
+ self.reference._server.http
485
+ .post(serviceUrl, self.exportParameters, { headers: headers })
486
+ .then(function success(response) {
487
+ defer.resolve({ data: response.data.split('\n'), canceled: self.canceled });
488
+ })
489
+ .catch(function (err) {
490
+ var error = ErrorService.responseToError(err);
491
+ defer.reject(error);
492
+ });
493
+ } catch (e) {
494
+ defer.reject(e);
495
+ }
496
+ return defer.promise;
497
+ },
498
+
499
+ /**
500
+ * Will set the canceled flag so when the datat comes back, we can tell the client
501
+ * to ignore the value. If it is already canceled it won't do anything.
502
+ * @return {boolean} returns false if the export is already canceled
503
+ */
504
+ cancel: function () {
505
+ if (this.canceled) return false;
506
+ this.canceled = true;
507
+ return true;
508
+ },
509
+ };
510
+
511
+ /**
512
+ * Try export/<context> then export then 'detailed'
513
+ * @param {reference} ref
514
+ * @param {Boolean} useCompact - whether the current context is compact or not
515
+ */
516
+ export const _getExportReference = function (ref, useCompact) {
517
+ var detCtx = _contexts.DETAILED,
518
+ expCompCtx = _contexts.EXPORT_COMPACT,
519
+ expDetCtx = _contexts.EXPORT_DETAILED;
520
+
521
+ var isContext = function (context) {
522
+ return context == ref._context;
523
+ };
524
+
525
+ var hasColumns = function (ctx) {
526
+ var res = _getRecursiveAnnotationValue(ctx, ref.table.annotations.get(_annotations.VISIBLE_COLUMNS).content, true);
527
+ return res !== -1 && Array.isArray(res);
528
+ };
529
+
530
+ var useMainContext = function () {
531
+ return isContext(detCtx) ? ref : ref.contextualize.detailed;
532
+ };
533
+
534
+ if (ref.table.annotations.contains(_annotations.VISIBLE_COLUMNS)) {
535
+ // export/<context>
536
+ // NOTE even if only export context is defined, the visible-columns logic will handle it
537
+ if (useCompact) {
538
+ if (hasColumns(expCompCtx)) {
539
+ return isContext(expCompCtx) ? ref : ref.contextualize.exportCompact;
540
+ }
541
+ } else {
542
+ if (hasColumns(expDetCtx)) {
543
+ return isContext(expDetCtx) ? ref : ref.contextualize.exportDetailed;
544
+ }
545
+ }
546
+ }
547
+
548
+ // <context> or no annot
549
+ return useMainContext();
550
+ };
551
+
552
+ /**
553
+ * Given a reference object, will return the appropriate output object.
554
+ * It might use attributegroup or entity apis based on the situation.
555
+ *
556
+ * given a reference and the path from main table to it (it might be empty),
557
+ * will generate the appropriate output.
558
+ * - If addMainKey is true,
559
+ * it will add the shortestkey of main table to the projection list.
560
+ * - will go based on visible columns defined for `export` (if not `detailed`):
561
+ * - Normal Columns: add to the projection list.
562
+ * - ForeignKey pseudo-column: Add the constituent columns alongside extra "candidate" columns (if needed).
563
+ * - Key pseudo-column: add the constituent columns.
564
+ * - Asset pseudo-column: add all the metadata columns alongside the url value.
565
+ * - Inline table: not applicable. Because of the one-to-many nature of the relationship this is not feasible.
566
+ * - other pseudo-columns (aggregate): not applicable.
567
+ * If the genarated attributegroup path is long, will fall back to the entity api
568
+ *
569
+ * In case of attributegroup, we will change the projection list.
570
+ * Assume that this is the model:
571
+ * main_table <- t1 <- t2
572
+ * the key list will be based on:
573
+ * - shortestkey of main_table and t2 (with alias name in `<tablename>.<shortestkey_column_name>` format)
574
+ * the projection list will be based on:
575
+ * - visible columns of t2
576
+ * - "candidate" columns of the foreignkeys (with alias name in `<tablename>.<candidate_column_name>` format)
577
+ *
578
+ * by "candidate" we mean columns that might make more sense to user instead of the typical "RID" or "ID".
579
+ * These are the same column names that we are using for row-name generation.
580
+ *
581
+ * @private
582
+ * @param {reference} ref the reference that we want the output for
583
+ * @param {String} tableAlias the alias that is used for projecting table (last table in path)
584
+ * @param {String=} path the string that will be prepended to the path
585
+ * @param {boolean=} addMainKey whether we want to add the key of the main table.
586
+ * if this is true, the next parameter is required.
587
+ * @param {Reference=} mainRef The main reference
588
+ * @return {any} the output object
589
+ */
590
+ export const _referenceExportOutput = function (ref, tableAlias, path, addMainKey, mainRef, useCompact) {
591
+ var projectionList = [],
592
+ keyList = [],
593
+ name,
594
+ i = 0,
595
+ consideredFks = {},
596
+ addedCols = {},
597
+ usedNames = {},
598
+ shortestKeyCols = {},
599
+ fkeys = [],
600
+ fkAlias,
601
+ candidate,
602
+ addedColPrefix;
603
+
604
+ var encode = fixedEncodeURIComponent;
605
+
606
+ // find the candidate column of the table
607
+ var getCandidateColumn = function (table) {
608
+ return _getCandidateRowNameColumn(
609
+ table.columns.all().map(function (col) {
610
+ return col.name;
611
+ }),
612
+ );
613
+ };
614
+
615
+ // check whether the given column is a candidate column
616
+ var isCandidateColumn = function (column) {
617
+ return _getCandidateRowNameColumn([column.name]) !== false;
618
+ };
619
+
620
+ var addColumn = function (c) {
621
+ var columns = Array.isArray(c) ? c : [c];
622
+ columns.forEach(function (col) {
623
+ if (col == null || typeof col !== 'object' || addedCols[col.name]) return;
624
+ addedCols[col.name] = true;
625
+ projectionList.push(encode(col.name));
626
+ });
627
+ };
628
+
629
+ // don't use any of the table column names
630
+ ref.table.columns.all().forEach(function (col) {
631
+ usedNames[col.name] = true;
632
+ });
633
+
634
+ // shortestkey of the current reference
635
+ ref.table.shortestKey.forEach(function (col) {
636
+ keyList.push(encode(col.name));
637
+ addedCols[col.name] = true;
638
+ });
639
+
640
+ // if it's a related entity and we need to key of the main table
641
+ if (addMainKey) {
642
+ // we have to add the shortestkey of main table
643
+ addedColPrefix = encode(mainRef.table.name) + '.';
644
+ mainRef.table.shortestKey.forEach(function (col) {
645
+ shortestKeyCols[col.name] = true;
646
+ name = addedColPrefix + encode(col.name);
647
+ // make sure the alias doesn't exist in the table
648
+ while (name in usedNames) {
649
+ name = addedColPrefix + encode(col.name) + '_' + ++i;
650
+ }
651
+ usedNames[name] = true;
652
+ keyList.push(name + ':=' + mainRef.location.mainTableAlias + ':' + encode(col.name));
653
+ });
654
+
655
+ //add the candidate column of main table too
656
+ candidate = getCandidateColumn(mainRef.table);
657
+ if (candidate && !(candidate in shortestKeyCols)) {
658
+ name = addedColPrefix + encode(candidate);
659
+ // make sure the alias doesn't exist in the table
660
+ while (name in usedNames) {
661
+ name = addedColPrefix + encode(candidate) + '_' + ++i;
662
+ }
663
+ usedNames[name] = true;
664
+ }
665
+ }
666
+
667
+ var exportRef = _getExportReference(ref, useCompact);
668
+
669
+ if (exportRef.columns.length === 0) {
670
+ return null;
671
+ }
672
+
673
+ exportRef.columns.forEach(function (col) {
674
+ if (!col.isPseudo) {
675
+ addColumn(col);
676
+ return;
677
+ }
678
+
679
+ if (col.isForeignKey || (col.isPathColumn && col.isUnique && col.isEntityMode)) {
680
+ if (consideredFks[col.name]) return;
681
+ consideredFks[col.name] = true;
682
+
683
+ // add the constituent columns
684
+ var hasCandidate = false;
685
+ var firstFk = col.firstForeignKeyNode.nodeObject;
686
+ firstFk.colset.columns.forEach(function (fkeyCol) {
687
+ addColumn(fkeyCol);
688
+ if (!hasCandidate && col.foreignKeyPathLength === 1 && isCandidateColumn(firstFk.mapping.get(fkeyCol))) {
689
+ hasCandidate = true;
690
+ }
691
+ });
692
+
693
+ // if any of the constituent columns is candidate, don't add fk projection
694
+ if (hasCandidate) return;
695
+
696
+ // find the candidate column in the referred table;
697
+ candidate = getCandidateColumn(col.lastForeignKeyNode.nodeObject.key.table);
698
+
699
+ // we couldn't find any candidate columns
700
+ if (!candidate) return;
701
+
702
+ // add the fkey
703
+ fkAlias = 'F' + (fkeys.length + 1);
704
+
705
+ var fkeyPath = [];
706
+ col.sourceObjectNodes.forEach(function (f) {
707
+ if (f.isFilter) {
708
+ fkeyPath.push(f.toString());
709
+ } else {
710
+ fkeyPath.push((f === col.lastForeignKeyNode ? fkAlias + ':=' : '') + f.toString(false, true));
711
+ }
712
+ });
713
+
714
+ // path to the foreignkey + reset the path to the main table
715
+ fkeys.push(fkeyPath.join('/') + '/$' + tableAlias);
716
+
717
+ // add to projectionList
718
+ addedColPrefix = encode(col.table.name) + '.';
719
+ name = addedColPrefix + encode(candidate);
720
+ i = 0;
721
+ while (name in usedNames) {
722
+ name = addedColPrefix + encode(candidate) + '_' + ++i;
723
+ }
724
+ usedNames[name] = true;
725
+ name = name + ':=' + fkAlias + ':' + candidate;
726
+
727
+ projectionList.push(name);
728
+
729
+ return;
730
+ }
731
+
732
+ if (col.isKey) {
733
+ // add constituent columns
734
+ col.key.colset.columns.forEach(addColumn);
735
+ return;
736
+ }
737
+
738
+ if (col.isAsset) {
739
+ // add the column alongside the metadata columns
740
+ addColumn([col, col.filenameColumn, col.byteCountColumn, col.md5, col.sha256]);
741
+ return;
742
+ }
743
+
744
+ // other pseudo-columns won't be added
745
+ });
746
+
747
+ // generate the path, based on given values.
748
+ var exportPath = typeof path === 'string' ? path + '/' : '';
749
+ if (fkeys.length > 0) {
750
+ exportPath += fkeys.join('/') + '/';
751
+ }
752
+ exportPath += keyList.join(',') + ';' + projectionList.join(',');
753
+
754
+ if (exportPath.length > URL_PATH_LENGTH_LIMIT) {
755
+ $log.warn('Cannot use attributegroup api for exporting `' + ref.table.name + '` because of url limitation.');
756
+ return _referenceExportEntityOutput(ref, path);
757
+ }
758
+
759
+ return {
760
+ destination: {
761
+ name: _sanitizeFilename(ref.displayname.unformatted),
762
+ type: 'csv',
763
+ },
764
+ source: {
765
+ api: 'attributegroup',
766
+ path: exportPath,
767
+ },
768
+ };
769
+ };
770
+
771
+ /**
772
+ * Given a reference object, will return the appropriate output object using entity api
773
+ * @private
774
+ * @param {Reference} ref the reference object
775
+ * @param {String} path the string that will be prepended to the path
776
+ * @return {Object} the output object
777
+ */
778
+ export const _referenceExportEntityOutput = function (ref, path) {
779
+ var source = {
780
+ api: 'entity',
781
+ };
782
+
783
+ if (path) {
784
+ source.path = path;
785
+ }
786
+
787
+ return {
788
+ destination: {
789
+ name: _sanitizeFilename(ref.displayname.unformatted),
790
+ type: 'csv',
791
+ },
792
+ source: source,
793
+ };
794
+ };
795
+
796
+ /**
797
+ * Given a column will return the appropriate output object for asset.
798
+ * It will return null if column is not an asset.
799
+ * @private
800
+ * @param {Column} col the column object
801
+ * @param {String} destinationPath the string that will be prepended to destination path
802
+ * @param {String} sourcePath the string that will be prepended to source path
803
+ * @return {Object}
804
+ */
805
+ export const _getAssetExportOutput = function (col, destinationPath, sourcePath) {
806
+ if (!col.isAsset) return null;
807
+
808
+ var path = [],
809
+ key;
810
+ var sanitize = _sanitizeFilename,
811
+ encode = fixedEncodeURIComponent;
812
+
813
+ // attributes
814
+ var attributes = {
815
+ byteCountColumn: 'length',
816
+ filenameColumn: 'filename',
817
+ md5: 'md5',
818
+ sha256: 'sha256',
819
+ };
820
+
821
+ // add the url
822
+ path.push('url:=' + encode(col.name));
823
+
824
+ // add the attributes (ignore the ones that are not defined)
825
+ for (key in attributes) {
826
+ if (col[key] == null) continue;
827
+ path.push(attributes[key] + ':=' + encode(col[key].name));
828
+ }
829
+
830
+ return {
831
+ destination: {
832
+ name: 'assets/' + (destinationPath ? sanitize(destinationPath) + '/' : '') + sanitize(col.name),
833
+ type: 'fetch',
834
+ },
835
+ source: {
836
+ api: 'attribute',
837
+ // exporter will throw an error if the url is null, so we are adding the check for not-null.
838
+ path: (sourcePath ? sourcePath + '/' : '') + '!(' + encode(col.name) + '::null::)/' + path.join(','),
839
+ },
840
+ };
841
+ };
842
+
843
+ /**
844
+ * Returns a object, that can be used as a default export template.
845
+ * NOTE SHOULD ONLY BE USED IN DETAILED CONTEXT
846
+ * It will include:
847
+ * - csv of the main table.
848
+ * - csv of all the related entities
849
+ * - fetch all the assets. For fetch, we need to provide url, length, and md5 (or other checksum types).
850
+ * if these columns are missing from the asset annotation, they won't be added.
851
+ * - fetch all the assetes of related tables.
852
+ * @param {Reference} reference the reference object
853
+ */
854
+ export const _getDefaultExportTemplate = function (reference) {
855
+ const outputs = [],
856
+ relatedTableAlias = 'R';
857
+
858
+ const getTableOutput = _referenceExportOutput,
859
+ getAssetOutput = _getAssetExportOutput;
860
+
861
+ const addOutput = function (output) {
862
+ if (output != null) {
863
+ outputs.push(output);
864
+ }
865
+ };
866
+
867
+ // create a csv + fetch all the assets
868
+ const processRelatedReference = function (rel) {
869
+ // the path that will be used for assets of related entities
870
+ const destinationPath = rel.displayname.unformatted;
871
+ // this will be used for source path
872
+ let sourcePath;
873
+ if (rel.pseudoColumn && !rel.pseudoColumn.isInboundForeignKey) {
874
+ // const lastFk = rel.pseudoColumn.sourceObjectWrapper.lastForeignKeyNode;
875
+ // path from main to the related reference
876
+ sourcePath = rel.pseudoColumn.sourceObjectWrapper.toString(false, false, relatedTableAlias);
877
+
878
+ // path more than length one, we need to add the main table fkey
879
+ addOutput(getTableOutput(rel, relatedTableAlias, sourcePath, rel.pseudoColumn.foreignKeyPathLength >= 2, reference));
880
+ }
881
+ // association table
882
+ else if (rel.derivedAssociationReference) {
883
+ const assoc = rel.derivedAssociationReference;
884
+ sourcePath = assoc.origFKR.toString() + '/' + relatedTableAlias + ':=' + assoc.associationToRelatedFKR.toString(true);
885
+ addOutput(getTableOutput(rel, relatedTableAlias, sourcePath, true, reference));
886
+ }
887
+ // single inbound related
888
+ else {
889
+ sourcePath = relatedTableAlias + ':=' + rel.origFKR.toString(false, false);
890
+ addOutput(getTableOutput(rel, relatedTableAlias, sourcePath));
891
+ }
892
+
893
+ // add asset of the related table
894
+ const expRef = _getExportReference(rel);
895
+
896
+ // alternative table, don't add asset
897
+ if (expRef.table !== rel.table) return;
898
+
899
+ expRef.columns.forEach(function (col) {
900
+ const output = getAssetOutput(col, destinationPath, sourcePath);
901
+ addOutput(output);
902
+ });
903
+ };
904
+
905
+ // main entity
906
+ addOutput(getTableOutput(reference, reference.location.mainTableAlias));
907
+
908
+ const exportRef = _getExportReference(reference);
909
+
910
+ // we're not supporting alternative tables
911
+ if (exportRef.table.name === reference.table.name) {
912
+ // main assets
913
+ exportRef.columns.forEach(function (col) {
914
+ const output = getAssetOutput(col, '', '');
915
+ addOutput(output);
916
+ });
917
+
918
+ // inline entities
919
+ exportRef.columns.forEach(function (col) {
920
+ if (col.isInboundForeignKey || (col.isPathColumn && col.hasPath && !col.isUnique && !col.hasAggregate)) {
921
+ return processRelatedReference(col.reference);
922
+ }
923
+ });
924
+ }
925
+
926
+ // related entities (use the export context otherwise detailed)
927
+ let hasRelatedExport = false;
928
+ if (reference.table.annotations.contains(_annotations.VISIBLE_FOREIGN_KEYS)) {
929
+ const exportRelated = _getRecursiveAnnotationValue(
930
+ _contexts.EXPORT,
931
+ reference.table.annotations.get(_annotations.VISIBLE_FOREIGN_KEYS).content,
932
+ true,
933
+ );
934
+ hasRelatedExport = exportRelated !== -1 && Array.isArray(exportRelated);
935
+ }
936
+
937
+ // if export context is defined in visible-foreign-keys, use it, otherwise fallback to detailed
938
+ const exportRefForRelated = hasRelatedExport
939
+ ? reference.contextualize.export
940
+ : reference._context === _contexts.DETAILED
941
+ ? reference
942
+ : reference.contextualize.detailed;
943
+ if (exportRefForRelated.table.name === reference.table.name) {
944
+ exportRefForRelated.related.forEach(processRelatedReference);
945
+ }
946
+
947
+ if (outputs.length === 0) {
948
+ return null;
949
+ }
950
+
951
+ return {
952
+ displayname: 'BDBag',
953
+ type: 'BAG',
954
+ outputs: outputs,
955
+ };
956
+ };