@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,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;
|