@isrd-isi-edu/ermrestjs 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +55 -0
  3. package/dist/ermrest.d.ts +3481 -0
  4. package/dist/ermrest.js +45 -0
  5. package/dist/ermrest.js.gz +0 -0
  6. package/dist/ermrest.js.map +1 -0
  7. package/dist/ermrest.min.js +45 -0
  8. package/dist/ermrest.min.js.gz +0 -0
  9. package/dist/ermrest.min.js.map +1 -0
  10. package/dist/ermrest.ver.txt +1 -0
  11. package/dist/stats.html +4949 -0
  12. package/js/ag_reference.js +1483 -0
  13. package/js/core.js +4931 -0
  14. package/js/datapath.js +336 -0
  15. package/js/export.js +956 -0
  16. package/js/filters.js +192 -0
  17. package/js/format.js +344 -0
  18. package/js/hatrac.js +1130 -0
  19. package/js/json_ld_validator.js +285 -0
  20. package/js/parser.js +2320 -0
  21. package/js/setup/node.js +27 -0
  22. package/js/utils/helpers.js +2300 -0
  23. package/js/utils/json_ld_schema.js +680 -0
  24. package/js/utils/pseudocolumn_helpers.js +2196 -0
  25. package/package.json +79 -0
  26. package/src/index.ts +204 -0
  27. package/src/models/comment.ts +14 -0
  28. package/src/models/deferred-promise.ts +16 -0
  29. package/src/models/display-name.ts +5 -0
  30. package/src/models/errors.ts +408 -0
  31. package/src/models/path-prefix-alias-mapping.ts +130 -0
  32. package/src/models/reference/bulk-create-foreign-key-object.ts +133 -0
  33. package/src/models/reference/citation.ts +98 -0
  34. package/src/models/reference/contextualize.ts +535 -0
  35. package/src/models/reference/google-dataset-metadata.ts +72 -0
  36. package/src/models/reference/index.ts +14 -0
  37. package/src/models/reference/page.ts +520 -0
  38. package/src/models/reference/reference-aggregate-fn.ts +37 -0
  39. package/src/models/reference/reference.ts +2813 -0
  40. package/src/models/reference/related-reference.ts +467 -0
  41. package/src/models/reference/tuple.ts +652 -0
  42. package/src/models/reference-column/asset-pseudo-column.ts +498 -0
  43. package/src/models/reference-column/column-aggregate.ts +313 -0
  44. package/src/models/reference-column/facet-column.ts +1380 -0
  45. package/src/models/reference-column/foreign-key-pseudo-column.ts +626 -0
  46. package/src/models/reference-column/inbound-foreign-key-pseudo-column.ts +131 -0
  47. package/src/models/reference-column/index.ts +13 -0
  48. package/src/models/reference-column/key-pseudo-column.ts +236 -0
  49. package/src/models/reference-column/pseudo-column.ts +850 -0
  50. package/src/models/reference-column/reference-column.ts +740 -0
  51. package/src/models/source-object-node.ts +156 -0
  52. package/src/models/source-object-wrapper.ts +694 -0
  53. package/src/models/table-source-definitions.ts +98 -0
  54. package/src/services/authn.ts +43 -0
  55. package/src/services/catalog.ts +37 -0
  56. package/src/services/config.ts +202 -0
  57. package/src/services/error.ts +247 -0
  58. package/src/services/handlebars.ts +607 -0
  59. package/src/services/history.ts +136 -0
  60. package/src/services/http.ts +536 -0
  61. package/src/services/logger.ts +70 -0
  62. package/src/services/mustache.ts +0 -0
  63. package/src/utils/column-utils.ts +308 -0
  64. package/src/utils/constants.ts +526 -0
  65. package/src/utils/markdown-utils.ts +855 -0
  66. package/src/utils/reference-utils.ts +1658 -0
  67. package/src/utils/template-utils.ts +0 -0
  68. package/src/utils/type-utils.ts +89 -0
  69. package/src/utils/value-utils.ts +127 -0
  70. package/tsconfig.json +30 -0
  71. package/vite.config.mts +104 -0
@@ -0,0 +1,694 @@
1
+ /* eslint-disable prettier/prettier */
2
+
3
+ // models
4
+ import SourceObjectNode from '@isrd-isi-edu/ermrestjs/src/models/source-object-node';
5
+ import { ReferenceColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference-column';
6
+ import { Tuple, Reference } from '@isrd-isi-edu/ermrestjs/src/models/reference';
7
+
8
+ // services
9
+ // import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
10
+
11
+ // utils
12
+ import { isObjectAndNotNull, isStringAndNotEmpty } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
13
+ import { _contexts, _facetFilterTypes, _pseudoColAggregateFns, _sourceDefinitionAttributes, _warningMessages } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
14
+ import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils';
15
+ import { createPseudoColumn } from '@isrd-isi-edu/ermrestjs/src/utils/column-utils';
16
+
17
+ // legacy imports that need to be accessed
18
+ import { _sourceColumnHelpers } from '@isrd-isi-edu/ermrestjs/js/utils/pseudocolumn_helpers';
19
+ import { Column, Table } from '@isrd-isi-edu/ermrestjs/js/core';
20
+
21
+ export type FilterPropsType = {
22
+ /**
23
+ * whether the filter is processed or not
24
+ */
25
+ isFilterProcessed: boolean;
26
+ /**
27
+ * whether there's any filter applied to the root (the first node)
28
+ */
29
+ hasRootFilter: boolean;
30
+ /**
31
+ * whether there's any filter applied to the nodes in between
32
+ * (i.e. not the root and not the leaf)
33
+ */
34
+ hasFilterInBetween: boolean;
35
+ /**
36
+ * the leaf filter string
37
+ */
38
+ leafFilterString: string;
39
+ };
40
+
41
+ export type InputIframePropsType = {
42
+ /**
43
+ * the url pattern that should be used to generate the iframe url
44
+ */
45
+ urlPattern: string;
46
+ /**
47
+ * the template engine that should be used to generate the url
48
+ */
49
+ urlTemplateEngine?: string;
50
+ /**
51
+ * the columns used in the mapping
52
+ */
53
+ columns: ReferenceColumn[];
54
+ /**
55
+ * an object from field name to column.
56
+ */
57
+ fieldMapping: Record<string, ReferenceColumn>;
58
+ /**
59
+ * name of optional fields
60
+ */
61
+ optionalFieldNames: string[];
62
+ /**
63
+ * the message that we should show when user wants to submit empty.
64
+ */
65
+ emptyFieldConfirmMessage: string;
66
+ };
67
+
68
+ /**
69
+ * Represents a column-directive
70
+ */
71
+ class SourceObjectWrapper {
72
+ /**
73
+ * the source object
74
+ */
75
+ public sourceObject: Record<string, unknown>;
76
+ /**
77
+ * the column that this source object refers to
78
+ * undefined if it's a virtual column
79
+ */
80
+ public column?: Column;
81
+ /**
82
+ * whether the source object has a prefix or not
83
+ */
84
+ public hasPrefix = false;
85
+ /**
86
+ * whether the source object has a foreign key path or not
87
+ */
88
+ public hasPath = false;
89
+ /**
90
+ * the length of the foreign key path
91
+ * this is the number of foreign key nodes in the path
92
+ */
93
+ public foreignKeyPathLength = 0;
94
+ /**
95
+ * whether the source object has an inbound foreign key path or not
96
+ */
97
+ public hasInbound = false;
98
+ /**
99
+ * whether the source object has an aggregate function or not
100
+ */
101
+ public hasAggregate = false;
102
+ /**
103
+ * whether any of the source object nodes is filtered or not
104
+ */
105
+ public isFiltered = false;
106
+ public filterProps?: FilterPropsType = {
107
+ isFilterProcessed: true,
108
+ hasRootFilter: false,
109
+ hasFilterInBetween: false,
110
+ leafFilterString: ''
111
+ };
112
+
113
+ /**
114
+ * whether this is an entity mode or not
115
+ */
116
+ public isEntityMode = false;
117
+ /**
118
+ * whether this represents a unique row or not
119
+ */
120
+ public isUnique = false;
121
+ /**
122
+ * whether this is unique and filtered
123
+ */
124
+ public isUniqueFiltered = false;
125
+ /**
126
+ * returns true if all foreign keys are outbound and all the columns involved are not null.
127
+ */
128
+ public isAllOutboundNotNullPerModel = false;
129
+ /**
130
+ *
131
+ */
132
+ public isAllOutboundNotNull = false;
133
+ public lastForeignKeyNode?: SourceObjectNode;
134
+ public firstForeignKeyNode?: SourceObjectNode;
135
+ public name = '';
136
+ public isHash = false;
137
+ public sourceObjectNodes: SourceObjectNode[] = [];
138
+ public isInputIframe = false;
139
+ public inputIframeProps?: InputIframePropsType;
140
+
141
+
142
+ /**
143
+ * used for facets
144
+ * TODO is there a better way to manage this?
145
+ */
146
+ public entityChoiceFilterTuples?: Tuple[];
147
+
148
+ /**
149
+ * @param sourceObject the column directive object
150
+ * @param table the root (starting) table
151
+ * @param isFacet whether this is a facet or not
152
+ * @param sources already generated source (only useful for source-def generation)
153
+ * @param mainTuple the main tuple that is used for filters
154
+ * @param skipProcessingFilters whether we should skip processing filters or not
155
+ */
156
+ constructor(
157
+ sourceObject: Record<string, unknown>,
158
+ table?: Table,
159
+ isFacet?: boolean,
160
+ sources?: unknown,
161
+ mainTuple?: Tuple,
162
+ skipProcessingFilters?: boolean,
163
+ ) {
164
+ this.sourceObject = sourceObject;
165
+
166
+ // if the extra objects are not passed, we cannot process
167
+ if (isObjectAndNotNull(table)) {
168
+ const res = this._process(table!, isFacet, sources, mainTuple, skipProcessingFilters);
169
+ if (typeof res === 'object' && res.error) {
170
+ throw new Error(res.message);
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * return a new sourceObjectWrapper that is created by merging the given sourceObject and existing object.
177
+ *
178
+ * Useful when we have an object with sourcekey and want to find the actual definition. You can then call
179
+ * clone on the source-def and pass the object.
180
+ *
181
+ * const myCol = {"sourcekey": "some_key"};
182
+ * const sd = table.sourceDefinitions.getSource(myCol.sourcekey);
183
+ * if (sd) {
184
+ * const wrapper = sd.clone(myCol, table);
185
+ * }
186
+ *
187
+ * - attributes in sourceObject will override the similar ones in the current object.
188
+ * - "source" of sourceObject will be ignored. so "sourcekey" always has priority over "source".
189
+ * - mainTuple should be passed in detailed context so we can use it for filters.
190
+ *
191
+ * @param sourceObject the source object
192
+ * @param table the table that these sources belong to.
193
+ * @param isFacet whether this is for a facet or not
194
+ */
195
+ clone(sourceObject: Record<string, unknown>, table: Table, isFacet?: boolean, mainTuple?: Tuple): SourceObjectWrapper {
196
+ let key: string;
197
+
198
+ // remove the definition attributes
199
+ _sourceDefinitionAttributes.forEach(function (attr: string) {
200
+ delete sourceObject[attr];
201
+ });
202
+
203
+ for (key in this.sourceObject) {
204
+ if (!Object.prototype.hasOwnProperty.call(this.sourceObject, key)) continue;
205
+
206
+ // only add the attributes that are not defined again
207
+ if (!Object.prototype.hasOwnProperty.call(sourceObject, key)) {
208
+ sourceObject[key] = this.sourceObject[key];
209
+ }
210
+ }
211
+
212
+ return new SourceObjectWrapper(sourceObject, table, isFacet, undefined, mainTuple);
213
+ }
214
+
215
+ /**
216
+ * Parse the given sourceobject and create the attributes
217
+ * @param table
218
+ * @param isFacet -- validation is differrent if it's a facet
219
+ * @param sources already generated source (only useful for source-def generation)
220
+ * @param mainTuple the main tuple that is used for filters
221
+ * @param skipProcessingFilters whether we should skip processing filters or not
222
+ * @returns
223
+ */
224
+ private _process(
225
+ table: Table,
226
+ isFacet?: boolean,
227
+ sources?: any,
228
+ mainTuple?: Tuple,
229
+ skipProcessingFilters?: boolean
230
+ ): true | { error: boolean; message: string } {
231
+ const sourceObject = this.sourceObject;
232
+ const wm = _warningMessages;
233
+
234
+ const returnError = (message: string) => {
235
+ return { error: true, message: message };
236
+ };
237
+
238
+ if (typeof sourceObject !== 'object' || !sourceObject.source) {
239
+ return returnError(wm.INVALID_SOURCE);
240
+ }
241
+
242
+ let colName: string;
243
+ let col: Column | undefined = undefined;
244
+ let colTable = table;
245
+ const source = sourceObject.source;
246
+ let sourceObjectNodes: SourceObjectNode[] = [];
247
+ let hasPath = false;
248
+ let hasInbound = false;
249
+ let hasPrefix = false;
250
+ let isAllOutboundNotNull = false;
251
+ let isAllOutboundNotNullPerModel = false;
252
+ let lastForeignKeyNode: SourceObjectNode | undefined = undefined;
253
+ let firstForeignKeyNode: SourceObjectNode | undefined = undefined;
254
+ let foreignKeyPathLength = 0;
255
+ let isFiltered = false;
256
+ let filterProps: FilterPropsType | undefined = undefined;
257
+
258
+ // just the column name
259
+ if (isStringAndNotEmpty(source) && typeof source === 'string') {
260
+ colName = source;
261
+ }
262
+ // from 0 to source.length-1 we have paths
263
+ else if (Array.isArray(source) && source.length === 1 && isStringAndNotEmpty(source[0])) {
264
+ colName = source[0];
265
+ } else if (Array.isArray(source) && source.length > 1) {
266
+ const res = _sourceColumnHelpers.processDataSourcePath(
267
+ source,
268
+ table,
269
+ table.name,
270
+ table.schema.catalog.id,
271
+ sources,
272
+ mainTuple,
273
+ skipProcessingFilters
274
+ ) as any;
275
+
276
+ if (res.error) {
277
+ return res;
278
+ }
279
+
280
+ hasPath = res.foreignKeyPathLength > 0;
281
+ hasInbound = res.hasInbound;
282
+ hasPrefix = res.hasPrefix;
283
+ firstForeignKeyNode = res.firstForeignKeyNode;
284
+ lastForeignKeyNode = res.lastForeignKeyNode;
285
+ colTable = res.column.table;
286
+ foreignKeyPathLength = res.foreignKeyPathLength;
287
+ sourceObjectNodes = res.sourceObjectNodes;
288
+ colName = res.column.name;
289
+ isFiltered = res.isFiltered;
290
+ filterProps = res.filterProps;
291
+ isAllOutboundNotNull = res.isAllOutboundNotNull;
292
+ isAllOutboundNotNullPerModel = res.isAllOutboundNotNullPerModel;
293
+ } else {
294
+ return returnError('Invalid source definition');
295
+ }
296
+
297
+ // we need this check here to make sure the column is in the table
298
+ try {
299
+ col = colTable.columns.get(colName);
300
+ } catch {
301
+ return returnError(wm.INVALID_COLUMN_IN_SOURCE_PATH);
302
+ }
303
+ const isEntity = hasPath && sourceObject.entity !== false && col.isUniqueNotNull;
304
+
305
+ // validate aggregate fn
306
+ if (isFacet !== true && typeof sourceObject.aggregate === 'string' && _pseudoColAggregateFns.indexOf(sourceObject.aggregate) === -1) {
307
+ return returnError(wm.INVALID_AGG);
308
+ }
309
+
310
+ this.column = col;
311
+
312
+ this.hasPrefix = hasPrefix;
313
+ // NOTE hasPath only means foreign key path and not filter
314
+ this.hasPath = hasPath;
315
+ this.foreignKeyPathLength = foreignKeyPathLength;
316
+ this.hasInbound = hasInbound;
317
+ this.hasAggregate = typeof sourceObject.aggregate === 'string';
318
+ this.isFiltered = isFiltered;
319
+ this.filterProps = filterProps;
320
+ this.isEntityMode = isEntity;
321
+ this.isUnique = !this.hasAggregate && !this.isFiltered && (!hasPath || !hasInbound);
322
+
323
+ // TODO FILTER_IN_SOURCE better name...
324
+ /**
325
+ * these type of columns would be very similiar to aggregate columns.
326
+ * but it requires more changes in both chaise and ermrestjs
327
+ * (most probably a new column type or at least more api to fetch their values is needed)
328
+ * (in chaise we would have to add a new type of secondary requests to active list)
329
+ * (not sure if these type of pseudo-columns are even useful or not)
330
+ * so for now we're not going to allow these type of pseudo-columns in visible-columns
331
+ */
332
+ this.isUniqueFiltered = !this.hasAggregate && this.isFiltered && (!hasPath || !hasInbound);
333
+
334
+ this.isAllOutboundNotNullPerModel = isAllOutboundNotNullPerModel;
335
+ this.isAllOutboundNotNull = isAllOutboundNotNull;
336
+
337
+ // attach last fk
338
+ if (lastForeignKeyNode !== undefined && lastForeignKeyNode !== null) {
339
+ this.lastForeignKeyNode = lastForeignKeyNode;
340
+ }
341
+
342
+ // attach first fk
343
+ if (firstForeignKeyNode !== undefined && firstForeignKeyNode !== null) {
344
+ this.firstForeignKeyNode = firstForeignKeyNode;
345
+ }
346
+
347
+ // generate name:
348
+ // TODO maybe we shouldn't even allow aggregate in faceting (for now we're ignoring it)
349
+ if (
350
+ sourceObject.self_link === true ||
351
+ this.isFiltered ||
352
+ this.hasPath ||
353
+ this.isEntityMode ||
354
+ (isFacet !== true && this.hasAggregate)
355
+ ) {
356
+ this.name = _sourceColumnHelpers.generateSourceObjectHashName(sourceObject, !!isFacet, sourceObjectNodes) as string;
357
+
358
+ this.isHash = true;
359
+
360
+ if (table.columns.has(this.name)) {
361
+ return returnError(
362
+ 'Generated Hash `' + this.name + '` for pseudo-column exists in table `' + table.name + '`.'
363
+ );
364
+ }
365
+ } else {
366
+ this.name = col.name;
367
+ this.isHash = false;
368
+ }
369
+
370
+ this.sourceObjectNodes = sourceObjectNodes;
371
+
372
+ return true;
373
+ }
374
+
375
+ /**
376
+ * Return the string representation of this foreignkey path
377
+ * returned format:
378
+ * if not isLeft and outAlias is not passed: ()=()/.../()=()
379
+ * if isLeft: left()=()/.../left()=()
380
+ * if outAlias defined: ()=()/.../outAlias:=()/()
381
+ * used in:
382
+ * - export default
383
+ * - column.getAggregate
384
+ * - reference.read
385
+ * @param reverse whether we want the reverse path
386
+ * @param isLeft use left join
387
+ * @param outAlias the alias that should be added to the output
388
+ */
389
+ toString(reverse?: boolean, isLeft?: boolean, outAlias?: string, isReverseRightJoin?: boolean): string {
390
+ return this.sourceObjectNodes.reduce((prev: string, sn: any, i: number) => {
391
+ if (sn.isFilter) {
392
+ if (reverse) {
393
+ return sn.toString() + (i > 0 ? '/' : '') + prev;
394
+ } else {
395
+ return prev + (i > 0 ? '/' : '') + sn.toString();
396
+ }
397
+ }
398
+
399
+ // it will always be the first one
400
+ if (sn.isPathPrefix) {
401
+ // if we're reversing, we have to add alias to the first one,
402
+ // otherwise we only need to add alias if this object only has a prefix and nothing else
403
+ if (reverse) {
404
+ return sn.toString(reverse, isLeft, outAlias, isReverseRightJoin);
405
+ } else {
406
+ return sn.toString(
407
+ reverse,
408
+ isLeft,
409
+ this.foreignKeyPathLength === sn.nodeObject.foreignKeyPathLength ? outAlias : null,
410
+ isReverseRightJoin
411
+ );
412
+ }
413
+ }
414
+
415
+ const fkStr = sn.toString(reverse, isLeft);
416
+ const addAlias =
417
+ outAlias &&
418
+ ((reverse && sn === this.firstForeignKeyNode) || (!reverse && sn === this.lastForeignKeyNode));
419
+
420
+ // NOTE alias on each node is ignored!
421
+ // currently we've added alias only for the association and
422
+ // therefore it's not really needed here anyways
423
+ let res = '';
424
+ if (reverse) {
425
+ if (i > 0) {
426
+ res += fkStr + '/';
427
+ } else {
428
+ if (addAlias) {
429
+ res += outAlias + ':=';
430
+ if (isReverseRightJoin) {
431
+ res += 'right';
432
+ }
433
+ }
434
+ res += fkStr;
435
+ }
436
+
437
+ res += prev;
438
+ return res;
439
+ } else {
440
+ return prev + (i > 0 ? '/' : '') + (addAlias ? outAlias + ':=' : '') + fkStr;
441
+ }
442
+ }, '');
443
+ }
444
+
445
+ /**
446
+ * Turn this into a raw source path without any path prefix
447
+ * NOTE the returned array is not a complete path as it
448
+ * doesn't include the last column
449
+ * currently used in two places:
450
+ * - generating hashname for a sourcedef that uses path prefix
451
+ * - generating the reverse path for a related entitty
452
+ * @param reverse
453
+ * @param outAlias alias that will be added to the last fk
454
+ * regardless of reversing or not
455
+ */
456
+ getRawSourcePath(reverse?: boolean, outAlias?: string): any[] {
457
+ const path: any[] = [];
458
+ const len = this.sourceObjectNodes.length;
459
+ const isLast = (index: number) => {
460
+ return reverse ? index >= 0 : index < len;
461
+ };
462
+
463
+ let i = reverse ? len - 1 : 0;
464
+ while (isLast(i)) {
465
+ const sn = this.sourceObjectNodes[i];
466
+ if (sn.isPathPrefix) {
467
+ // if this is the last element, we have to add the alias to this
468
+ path.push(
469
+ ...sn.nodeObject.getRawSourcePath(
470
+ reverse,
471
+ this.foreignKeyPathLength == sn.nodeObject.foreignKeyPathLength ? outAlias : null
472
+ )
473
+ );
474
+ } else if (sn.isFilter) {
475
+ path.push(sn.nodeObject);
476
+ } else {
477
+ let obj: any;
478
+ if ((reverse && sn.isInbound) || (!reverse && !sn.isInbound)) {
479
+ obj = { outbound: sn.nodeObject.constraint_names[0] };
480
+ } else {
481
+ obj = { inbound: sn.nodeObject.constraint_names[0] };
482
+ }
483
+
484
+ // add alias to the last element
485
+ if (isStringAndNotEmpty(outAlias) && sn == this.lastForeignKeyNode) {
486
+ obj.alias = outAlias;
487
+ }
488
+
489
+ path.push(obj);
490
+ }
491
+
492
+ i = i + (reverse ? -1 : 1);
493
+ }
494
+ return path;
495
+ }
496
+
497
+ /**
498
+ * Return the reverse path as facet with the value of shortestkey
499
+ * currently used in two places:
500
+ * - column.refernece
501
+ * - reference.generateRelatedReference
502
+ * both are for generating the reverse related entity path
503
+ * @param tuple
504
+ * @param rootTable
505
+ * @param outAlias
506
+ */
507
+ getReverseAsFacet(tuple: Tuple, rootTable: Table, outAlias?: string): any {
508
+ if (!isObjectAndNotNull(tuple)) return null;
509
+ let i: number;
510
+ const filters: any[] = [];
511
+ let filterSource: any[] = [];
512
+
513
+ // create the reverse path
514
+ filterSource = this.getRawSourcePath(true, outAlias);
515
+
516
+ // add the filter data
517
+ for (i = 0; i < rootTable.shortestKey.length; i++) {
518
+ const col: Column = rootTable.shortestKey[i];
519
+ if (!tuple.data || !tuple.data[col.name]) {
520
+ return null;
521
+ }
522
+ const filter: any = {
523
+ source: filterSource.concat(col.name),
524
+ };
525
+ filter[_facetFilterTypes.CHOICE] = [tuple.data[col.name]];
526
+ filters.push(filter);
527
+ }
528
+
529
+ if (filters.length == 0) {
530
+ return null;
531
+ }
532
+
533
+ return { and: filters };
534
+ }
535
+
536
+ /**
537
+ * if sourceObject has the required input_iframe properties, will attach `inputIframeProps` and `isInputIframe`
538
+ * to the sourceObject.
539
+ * The returned value of this function summarizes whether processing was successful or not.
540
+ *
541
+ * var res = processInputIframe(reference, tuple, usedColumnMapping);
542
+ * if (res.error) {
543
+ * console.log(res.message);
544
+ * } else {
545
+ * // success
546
+ * }
547
+ *
548
+ * @param reference the reference object of the parent
549
+ * @param usedIframeInputMappings an object capturing columns used in other mappings. used to avoid overlapping
550
+ * @param tuple the tuple object
551
+ */
552
+ processInputIframe(reference: Reference, usedIframeInputMappings: any, tuple?: Tuple): {
553
+ success?: boolean;
554
+ error?: boolean;
555
+ message?: string;
556
+ columns?: ReferenceColumn[];
557
+ } {
558
+ const context = reference.context;
559
+ const annot: any = this.sourceObject.input_iframe;
560
+ if (!isObjectAndNotNull(annot) || !isStringAndNotEmpty(annot.url_pattern)) {
561
+ return { error: true, message: 'url_pattern not defined.' };
562
+ }
563
+
564
+ if (!isObjectAndNotNull(annot.field_mapping)) {
565
+ return { error: true, message: 'field_mapping not defined.' };
566
+ }
567
+
568
+ const optionalFieldNames: string[] = [];
569
+ if (Array.isArray(annot.optional_fields)) {
570
+ annot.optional_fields.forEach(function (f: unknown) {
571
+ if (isStringAndNotEmpty(f) && typeof f === 'string') optionalFieldNames.push(f);
572
+ });
573
+ }
574
+
575
+ let emptyFieldConfirmMessage = '';
576
+ if (isStringAndNotEmpty(annot.empty_field_confirm_message_markdown)) {
577
+ emptyFieldConfirmMessage = renderMarkdown(annot.empty_field_confirm_message_markdown);
578
+ }
579
+
580
+ const columns: ReferenceColumn[] = [];
581
+ const fieldMapping: Record<string, ReferenceColumn> = {};
582
+ for (const f in annot.field_mapping) {
583
+ const colName = annot.field_mapping[f];
584
+
585
+ // column already used in another mapping
586
+ if (colName in usedIframeInputMappings) {
587
+ return {
588
+ error: true,
589
+ message: 'column `' + colName + '` already used in another field_mapping.',
590
+ };
591
+ }
592
+
593
+ try {
594
+ const c = this.column!.table.columns.get(colName);
595
+ const isSerial = c.type.name.indexOf('serial') === 0;
596
+
597
+ // we cannot use getInputDisabled since we just want to do this based on ACLs
598
+ if (
599
+ context === _contexts.CREATE &&
600
+ (c.isSystemColumn || c.isGeneratedPerACLs || isSerial)
601
+ ) {
602
+ if (colName in optionalFieldNames) continue;
603
+ return {
604
+ error: true,
605
+ message: 'column `' + colName + '` cannot be modified by this user.',
606
+ };
607
+ }
608
+ if (
609
+ (context === _contexts.EDIT || context === _contexts.ENTRY) &&
610
+ (c.isSystemColumn || c.isImmutablePerACLs || isSerial)
611
+ ) {
612
+ if (colName in optionalFieldNames) continue;
613
+ return {
614
+ error: true,
615
+ message: 'column `' + colName + '` cannot be modified by this user.',
616
+ };
617
+ }
618
+
619
+ // create a pseudo-column will make sure we're also handling assets
620
+ const wrapper = new SourceObjectWrapper({ source: colName }, reference.table);
621
+ const refCol = createPseudoColumn(reference, wrapper, tuple);
622
+
623
+ fieldMapping[f] = refCol;
624
+ columns.push(refCol);
625
+ } catch {
626
+ if (colName in optionalFieldNames) continue;
627
+ return { error: true, message: 'column `' + colName + '` not found.' };
628
+ }
629
+ }
630
+
631
+ this.isInputIframe = true;
632
+ this.inputIframeProps = {
633
+ /**
634
+ * can be used for finding the location of iframe
635
+ */
636
+ urlPattern: annot.url_pattern,
637
+ urlTemplateEngine: annot.template_engine,
638
+ /**
639
+ * the columns used in the mapping
640
+ */
641
+ columns: columns,
642
+ /**
643
+ * an object from field name to column.
644
+ */
645
+ fieldMapping: fieldMapping,
646
+ /**
647
+ * name of optional fields
648
+ */
649
+ optionalFieldNames: optionalFieldNames,
650
+ /**
651
+ * the message that we should show when user wants to submit empty.
652
+ */
653
+ emptyFieldConfirmMessage: emptyFieldConfirmMessage,
654
+ };
655
+
656
+ return { success: true, columns: columns };
657
+ }
658
+
659
+ /**
660
+ * @param mainTuple
661
+ * @param dontThrowError if set to true, will not throw an error if the filters are not valid
662
+ */
663
+ processFilterNodes(mainTuple?: Tuple, dontThrowError?: boolean): {
664
+ success: boolean;
665
+ error?: boolean;
666
+ message?: string;
667
+ } {
668
+ if (!this.filterProps || this.filterProps.isFilterProcessed) {
669
+ return { success: true };
670
+ }
671
+
672
+ try {
673
+ for (const sn of this.sourceObjectNodes) {
674
+ if (sn.isPathPrefix) {
675
+ sn.nodeObject.processFilterNodes(mainTuple);
676
+ } else if (sn.isFilter) {
677
+ sn.processFilter(mainTuple);
678
+ }
679
+ }
680
+
681
+ this.filterProps.isFilterProcessed = true;
682
+
683
+ return { success: true };
684
+ } catch (exp: unknown) {
685
+ if (dontThrowError) {
686
+ return { success: false, error: true, message: (exp as Error).message };
687
+ } else {
688
+ throw exp;
689
+ }
690
+ }
691
+ }
692
+ }
693
+
694
+ export default SourceObjectWrapper;