@isrd-isi-edu/ermrestjs 2.4.3 → 2.6.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/js/core.js +30 -4
- package/js/export.js +1 -1
- package/js/utils/helpers.js +1 -1
- package/js/utils/pseudocolumn_helpers.js +2 -2
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/models/active-list-condition.ts +216 -0
- package/src/models/reference/reference.ts +70 -175
- package/src/models/reference-column/asset-pseudo-column.ts +1 -1
- package/src/models/reference-column/foreign-key-pseudo-column.ts +4 -8
- package/src/models/reference-column/key-pseudo-column.ts +3 -4
- package/src/models/reference-column/pseudo-column.ts +3 -34
- package/src/models/reference-column/reference-column.ts +79 -8
- package/src/models/source-object-wrapper.ts +1 -1
- package/src/models/table-source-definitions.ts +37 -0
- package/src/services/active-list.ts +432 -0
- package/src/utils/constants.ts +3 -0
- package/src/utils/reference-utils.ts +6 -1
- package/src/utils/template-utils.ts +117 -0
|
@@ -8,6 +8,27 @@ import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
|
|
|
8
8
|
// legacy
|
|
9
9
|
import { Column, ForeignKeyRef, Table } from '@isrd-isi-edu/ermrestjs/js/core';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Defines a condition that controls visibility of a column or related entity.
|
|
13
|
+
* Used in source-definitions annotation under the `conditions` key, or inline
|
|
14
|
+
* on a column-directive via `condition` / `condition_key`.
|
|
15
|
+
*/
|
|
16
|
+
// export type ConditionDefinition = {
|
|
17
|
+
// /** reference to an existing sourcekey whose data determines the condition */
|
|
18
|
+
// sourcekey?: string;
|
|
19
|
+
// /** inline source path (alternative to sourcekey) */
|
|
20
|
+
// source?: unknown;
|
|
21
|
+
// /** behavior when condition source returns empty: "hide" (default) or "show" */
|
|
22
|
+
// on_empty: 'show' | 'hide';
|
|
23
|
+
// /** optional template evaluated with $self, no markdown rendering */
|
|
24
|
+
// condition_pattern?: string;
|
|
25
|
+
// /** template engine for condition_pattern: "mustache" (default) or "handlebars" */
|
|
26
|
+
// template_engine?: string;
|
|
27
|
+
// /** optional list of sourcekeys whose data should be available when evaluating condition_pattern */
|
|
28
|
+
// wait_for?: string[];
|
|
29
|
+
// };
|
|
30
|
+
export type ConditionDefinition = Record<string, unknown>;
|
|
31
|
+
|
|
11
32
|
/**
|
|
12
33
|
* Result of Table.sourceDefinitions
|
|
13
34
|
*/
|
|
@@ -39,6 +60,10 @@ class TableSourceDefinitions {
|
|
|
39
60
|
* this has been added because of path prefix where a sourcekey might rely on other sourcekeys
|
|
40
61
|
*/
|
|
41
62
|
sourceDependencies: Record<string, string[]> = {};
|
|
63
|
+
/**
|
|
64
|
+
* reusable condition definitions defined in the source-definitions annotation
|
|
65
|
+
*/
|
|
66
|
+
private conditions: Record<string, ConditionDefinition> = {};
|
|
42
67
|
|
|
43
68
|
constructor(
|
|
44
69
|
table: Table,
|
|
@@ -47,6 +72,7 @@ class TableSourceDefinitions {
|
|
|
47
72
|
sources: Record<string, SourceObjectWrapper>,
|
|
48
73
|
sourceMapping: Record<string, string[]>,
|
|
49
74
|
sourceDependencies: Record<string, string[]>,
|
|
75
|
+
conditions: Record<string, ConditionDefinition> = {},
|
|
50
76
|
) {
|
|
51
77
|
this.table = table;
|
|
52
78
|
this.columns = columns;
|
|
@@ -54,6 +80,17 @@ class TableSourceDefinitions {
|
|
|
54
80
|
this.sources = sources;
|
|
55
81
|
this.sourceMapping = sourceMapping;
|
|
56
82
|
this.sourceDependencies = sourceDependencies;
|
|
83
|
+
this.conditions = conditions;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get a condition definition by its key.
|
|
88
|
+
* NOTE: the returned definition is not processed and might be invalid.
|
|
89
|
+
* @param key The key of the condition definition.
|
|
90
|
+
* @returns The condition definition or undefined if not found.
|
|
91
|
+
*/
|
|
92
|
+
getCondition(key: string): ConditionDefinition | undefined {
|
|
93
|
+
return this.conditions[key];
|
|
57
94
|
}
|
|
58
95
|
|
|
59
96
|
/**
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// models
|
|
2
|
+
import ActiveListCondition from '@isrd-isi-edu/ermrestjs/src/models/active-list-condition';
|
|
3
|
+
import type { Reference, VisibleColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference/reference';
|
|
4
|
+
import type { Tuple } from '@isrd-isi-edu/ermrestjs/src/models/reference';
|
|
5
|
+
import type { PseudoColumn, ForeignKeyPseudoColumn, KeyPseudoColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference-column';
|
|
6
|
+
|
|
7
|
+
// services
|
|
8
|
+
|
|
9
|
+
// utils
|
|
10
|
+
import { isAllOutboundColumn, isRelatedColumn } from '@isrd-isi-edu/ermrestjs/src/utils/column-utils';
|
|
11
|
+
|
|
12
|
+
// legacy
|
|
13
|
+
import { _isEntryContext } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// TYPES
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export type ActiveListRequestObject = {
|
|
20
|
+
/** The index of the object in the column's values array */
|
|
21
|
+
index?: number;
|
|
22
|
+
/** whether this is for a visible column */
|
|
23
|
+
column?: boolean;
|
|
24
|
+
/** whether this is for a related entity */
|
|
25
|
+
related?: boolean;
|
|
26
|
+
/** whether this is for an inline related entity */
|
|
27
|
+
inline?: boolean;
|
|
28
|
+
/** whether this is for the citation */
|
|
29
|
+
citation?: boolean;
|
|
30
|
+
/** whether this is for a condition evaluation (index refers to the conditionalGroups index) */
|
|
31
|
+
condition?: boolean;
|
|
32
|
+
|
|
33
|
+
isWaitFor: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ActiveListRequest = {
|
|
37
|
+
column: VisibleColumn;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* whether the request is "first outbound" type.
|
|
41
|
+
* These booleans are used for figuring out how the data should be fetched.
|
|
42
|
+
*/
|
|
43
|
+
firstOutbound?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* whether the request is for an aggregate column.
|
|
46
|
+
*/
|
|
47
|
+
aggregate?: boolean;
|
|
48
|
+
entity?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* whether the request is an entityset.
|
|
51
|
+
*/
|
|
52
|
+
entityset?: boolean;
|
|
53
|
+
|
|
54
|
+
/** where this request is needed. */
|
|
55
|
+
objects: Array<ActiveListRequestObject>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type ActiveListRelatedEntityRequest = {
|
|
59
|
+
/**
|
|
60
|
+
* whether the request is for an inline related entity.
|
|
61
|
+
* the .index indicated which related entity it is
|
|
62
|
+
*/
|
|
63
|
+
inline?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* whether the request is for a related entity.
|
|
66
|
+
* the .index indicated which related entity it is
|
|
67
|
+
*/
|
|
68
|
+
related?: boolean;
|
|
69
|
+
/** the index of the related entity in the reference.related array */
|
|
70
|
+
index: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Identifies a single conditioned item: the column/inline-related column/related-entity
|
|
75
|
+
* whose visibility is gated by the group's condition. Multiple items share a group when
|
|
76
|
+
* they reference the same `condition_key`.
|
|
77
|
+
*/
|
|
78
|
+
export type ActiveListConditionedItem = {
|
|
79
|
+
column?: boolean;
|
|
80
|
+
inline?: boolean;
|
|
81
|
+
related?: boolean;
|
|
82
|
+
index: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type ActiveListConditionalGroup = {
|
|
86
|
+
/** The condition object (class with evaluateCondition method) */
|
|
87
|
+
condition: ActiveListCondition;
|
|
88
|
+
/**
|
|
89
|
+
* The column / inline / related entries gated by this condition. Used by chaise to
|
|
90
|
+
* mark the user's columns/related-models as conditioned (and to flip their hide flag
|
|
91
|
+
* when the condition resolves).
|
|
92
|
+
* Distinct from `dependentRequests`: `conditionedItems` covers EVERY conditioned
|
|
93
|
+
* item — including sync scalar columns whose `dependentRequests` entry is a no-op —
|
|
94
|
+
* while `dependentRequests` is just the secondary fetches gated behind the condition.
|
|
95
|
+
*/
|
|
96
|
+
conditionedItems: ActiveListConditionedItem[];
|
|
97
|
+
/** Requests gated behind this condition (main content + its wait-fors) */
|
|
98
|
+
dependentRequests: Array<ActiveListRequest | ActiveListRelatedEntityRequest>;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type ActiveList = {
|
|
102
|
+
/**
|
|
103
|
+
* The list of requests that need to be made.
|
|
104
|
+
* Already ordered based on the priority needed for the page load (top to bottom).
|
|
105
|
+
*/
|
|
106
|
+
requests: Array<ActiveListRequest | ActiveListRelatedEntityRequest>;
|
|
107
|
+
/** The list of conditional groups that need to be evaluated. */
|
|
108
|
+
conditionalGroups: ActiveListConditionalGroup[];
|
|
109
|
+
allOutBounds: Array<PseudoColumn | ForeignKeyPseudoColumn>;
|
|
110
|
+
selfLinks: Array<KeyPseudoColumn>;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export enum ActiveListRequestTypes {
|
|
114
|
+
COLUMN = 'column',
|
|
115
|
+
RELATED = 'related',
|
|
116
|
+
INLINE = 'inline',
|
|
117
|
+
CITATION = 'citation',
|
|
118
|
+
CONDITION = 'condition',
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// BUILDER
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Builds an ActiveList for a Reference. Encapsulates the dedup state and
|
|
127
|
+
* the various add* helpers that used to live as nested closures inside
|
|
128
|
+
* Reference.generateActiveList.
|
|
129
|
+
*
|
|
130
|
+
* Behavior is intended to be 1:1 with the previous nested implementation.
|
|
131
|
+
*/
|
|
132
|
+
export class ActiveListBuilder {
|
|
133
|
+
// result buckets (public so the driver in reference.ts can push directly
|
|
134
|
+
// for the related-entity case, matching the original code).
|
|
135
|
+
requests: Array<ActiveListRequest | ActiveListRelatedEntityRequest> = [];
|
|
136
|
+
allOutBounds: Array<PseudoColumn | ForeignKeyPseudoColumn> = [];
|
|
137
|
+
selfLinks: Array<KeyPseudoColumn> = [];
|
|
138
|
+
conditionalGroups: ActiveListConditionalGroup[] = [];
|
|
139
|
+
|
|
140
|
+
// dedup maps
|
|
141
|
+
private consideredUniqueFiltered: { [key: string]: number } = {};
|
|
142
|
+
private consideredSets: { [key: string]: number } = {};
|
|
143
|
+
private consideredOutbounds: { [key: string]: boolean } = {};
|
|
144
|
+
private consideredAggregates: { [key: string]: number } = {};
|
|
145
|
+
private consideredSelfLinks: { [key: string]: boolean } = {};
|
|
146
|
+
private consideredEntryWaitFors: { [key: string]: number } = {};
|
|
147
|
+
|
|
148
|
+
private isEntry: boolean;
|
|
149
|
+
|
|
150
|
+
constructor(
|
|
151
|
+
private reference: Reference,
|
|
152
|
+
/**
|
|
153
|
+
* The main request tuple.
|
|
154
|
+
*/
|
|
155
|
+
private tuple: Tuple | undefined,
|
|
156
|
+
/**
|
|
157
|
+
* Whether the active list is being built for detailed context
|
|
158
|
+
*/
|
|
159
|
+
private isDetailed: boolean,
|
|
160
|
+
) {
|
|
161
|
+
this.isEntry = _isEntryContext(reference.context);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Whether the given column is aggregate (either hasWaitForAggregate or is a path column with aggregate)
|
|
166
|
+
*/
|
|
167
|
+
hasAggregate(col: VisibleColumn): boolean {
|
|
168
|
+
return col.hasWaitForAggregate || ((col as PseudoColumn).isPathColumn && (col as PseudoColumn).hasAggregate);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* col: the column that we need its data
|
|
173
|
+
* isWaitFor: whether it was part of waitFor or just visible
|
|
174
|
+
* type: where in the page it belongs to
|
|
175
|
+
* index: the container index (column index, or related index) (optional)
|
|
176
|
+
*/
|
|
177
|
+
addCol(col: VisibleColumn, isWaitFor: boolean, type: ActiveListRequestTypes, index?: number): void {
|
|
178
|
+
const obj: ActiveListRequestObject = { isWaitFor: isWaitFor };
|
|
179
|
+
|
|
180
|
+
// add the type
|
|
181
|
+
obj[type] = true;
|
|
182
|
+
|
|
183
|
+
// add index if available (not available in citation)
|
|
184
|
+
if (Number.isInteger(index)) {
|
|
185
|
+
obj.index = index;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// entry wait_fors (asset wait_for)
|
|
189
|
+
if (isWaitFor && this.isEntry) {
|
|
190
|
+
if (col.name in this.consideredEntryWaitFors) {
|
|
191
|
+
(this.requests[this.consideredEntryWaitFors[col.name]] as ActiveListRequest).objects.push(obj);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.consideredEntryWaitFors[col.name] = this.requests.length;
|
|
195
|
+
this.requests.push({ firstOutbound: true, column: col, objects: [obj] });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// unique filtered
|
|
200
|
+
// TODO FILTER_IN_SOURCE chaise should use this type of column as well?
|
|
201
|
+
// TODO FILTER_IN_SOURCE should be added to documentation as well
|
|
202
|
+
if ((col as PseudoColumn).sourceObjectWrapper?.isUniqueFiltered) {
|
|
203
|
+
// duplicate
|
|
204
|
+
if (col.name in this.consideredUniqueFiltered) {
|
|
205
|
+
(this.requests[this.consideredUniqueFiltered[col.name]] as ActiveListRequest).objects.push(obj);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// new
|
|
210
|
+
this.consideredUniqueFiltered[col.name] = this.requests.length;
|
|
211
|
+
this.requests.push({ entity: true, column: col, objects: [obj] });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// aggregates
|
|
216
|
+
if ((col as PseudoColumn).isPathColumn && (col as PseudoColumn).hasAggregate) {
|
|
217
|
+
// duplicate
|
|
218
|
+
if (col.name in this.consideredAggregates) {
|
|
219
|
+
(this.requests[this.consideredAggregates[col.name]] as ActiveListRequest).objects.push(obj);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// new
|
|
224
|
+
this.consideredAggregates[col.name] = this.requests.length;
|
|
225
|
+
this.requests.push({ aggregate: true, column: col, objects: [obj] });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// entitysets
|
|
230
|
+
if (isRelatedColumn(col)) {
|
|
231
|
+
if (!this.isDetailed) {
|
|
232
|
+
return; // only acceptable in detailed
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (col.name in this.consideredSets) {
|
|
236
|
+
(this.requests[this.consideredSets[col.name]] as ActiveListRequest).objects.push(obj);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.consideredSets[col.name] = this.requests.length;
|
|
241
|
+
this.requests.push({ entityset: true, column: col, objects: [obj] });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// all outbounds
|
|
245
|
+
if (isAllOutboundColumn(col)) {
|
|
246
|
+
if (col.name in this.consideredOutbounds) return;
|
|
247
|
+
this.consideredOutbounds[col.name] = true;
|
|
248
|
+
this.allOutBounds.push(col as PseudoColumn | ForeignKeyPseudoColumn);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// self-links
|
|
252
|
+
if ((col as KeyPseudoColumn).isKey) {
|
|
253
|
+
if (col.name in this.consideredSelfLinks) return;
|
|
254
|
+
this.consideredSelfLinks[col.name] = true;
|
|
255
|
+
this.selfLinks.push(col as KeyPseudoColumn);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Same as addCol, but adds to a dependent requests array
|
|
261
|
+
* instead of the main requests array. Used for conditional groups.
|
|
262
|
+
*/
|
|
263
|
+
addColToDependent(
|
|
264
|
+
dependentRequests: Array<ActiveListRequest | ActiveListRelatedEntityRequest>,
|
|
265
|
+
col: VisibleColumn,
|
|
266
|
+
isWaitFor: boolean,
|
|
267
|
+
type: ActiveListRequestTypes,
|
|
268
|
+
index?: number,
|
|
269
|
+
): void {
|
|
270
|
+
const obj: ActiveListRequestObject = { isWaitFor: isWaitFor };
|
|
271
|
+
obj[type] = true;
|
|
272
|
+
if (Number.isInteger(index)) {
|
|
273
|
+
obj.index = index;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// unique filtered (mirrors addCol branch; dedup is scoped to dependentRequests)
|
|
277
|
+
if ((col as PseudoColumn).sourceObjectWrapper?.isUniqueFiltered) {
|
|
278
|
+
const existing = dependentRequests.find((r) => (r as ActiveListRequest).column?.name === col.name);
|
|
279
|
+
if (existing) {
|
|
280
|
+
(existing as ActiveListRequest).objects.push(obj);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
dependentRequests.push({ entity: true, column: col, objects: [obj] });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// aggregates
|
|
288
|
+
if ((col as PseudoColumn).isPathColumn && (col as PseudoColumn).hasAggregate) {
|
|
289
|
+
// check if already in dependentRequests
|
|
290
|
+
const existing = dependentRequests.find((r) => (r as ActiveListRequest).column?.name === col.name && (r as ActiveListRequest).aggregate) as
|
|
291
|
+
| ActiveListRequest
|
|
292
|
+
| undefined;
|
|
293
|
+
if (existing) {
|
|
294
|
+
existing.objects.push(obj);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
dependentRequests.push({ aggregate: true, column: col, objects: [obj] });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// entitysets
|
|
302
|
+
if (isRelatedColumn(col)) {
|
|
303
|
+
const existing = dependentRequests.find((r) => (r as ActiveListRequest).column?.name === col.name && (r as ActiveListRequest).entityset) as
|
|
304
|
+
| ActiveListRequest
|
|
305
|
+
| undefined;
|
|
306
|
+
if (existing) {
|
|
307
|
+
existing.objects.push(obj);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
dependentRequests.push({ entityset: true, column: col, objects: [obj] });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// all outbounds — still add to main allOutBounds
|
|
315
|
+
if (isAllOutboundColumn(col)) {
|
|
316
|
+
if (!(col.name in this.consideredOutbounds)) {
|
|
317
|
+
this.consideredOutbounds[col.name] = true;
|
|
318
|
+
this.allOutBounds.push(col as PseudoColumn | ForeignKeyPseudoColumn);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// self-links — still add to main selfLinks
|
|
323
|
+
if ((col as KeyPseudoColumn).isKey) {
|
|
324
|
+
if (!(col.name in this.consideredSelfLinks)) {
|
|
325
|
+
this.consideredSelfLinks[col.name] = true;
|
|
326
|
+
this.selfLinks.push(col as KeyPseudoColumn);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Add a visible column and its waitFors to the active list as inline requests (with deduplication).
|
|
333
|
+
*/
|
|
334
|
+
addInline(col: VisibleColumn, i: number): void {
|
|
335
|
+
if (isRelatedColumn(col)) {
|
|
336
|
+
this.requests.push({ inline: true, index: i });
|
|
337
|
+
} else {
|
|
338
|
+
this.addCol(col, false, ActiveListRequestTypes.COLUMN, i);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
col.waitFor.forEach((wf) => {
|
|
342
|
+
this.addCol(wf, true, isRelatedColumn(col) ? ActiveListRequestTypes.INLINE : ActiveListRequestTypes.COLUMN, i);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Same as addInline but adds to dependent requests in a conditional group */
|
|
347
|
+
addInlineToDependent(dependentRequests: Array<ActiveListRequest | ActiveListRelatedEntityRequest>, col: VisibleColumn, i: number): void {
|
|
348
|
+
if (isRelatedColumn(col)) {
|
|
349
|
+
dependentRequests.push({ inline: true, index: i });
|
|
350
|
+
} else {
|
|
351
|
+
this.addColToDependent(dependentRequests, col, false, ActiveListRequestTypes.COLUMN, i);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
col.waitFor.forEach((wf) => {
|
|
355
|
+
this.addColToDependent(dependentRequests, wf, true, isRelatedColumn(col) ? ActiveListRequestTypes.INLINE : ActiveListRequestTypes.COLUMN, i);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Add a column / related entity that has a condition into a conditional group.
|
|
361
|
+
* Always handles the item via the group flow (regardless of whether the
|
|
362
|
+
* condition source is sync or async). The caller does NOT add the user's
|
|
363
|
+
* column directly — its identity goes into the group's `conditionedItems` so
|
|
364
|
+
* chaise knows to gate its visibility, and any secondary fetches go into
|
|
365
|
+
* `dependentRequests`.
|
|
366
|
+
*
|
|
367
|
+
* For sync sources, `addCol` is a no-op on `requests` (sync sources are
|
|
368
|
+
* not entityset/aggregate by construction), so the source isn't fetched —
|
|
369
|
+
* but its all-outbound joins / self-link keys still get registered into
|
|
370
|
+
* `allOutBounds` / `selfLinks`. Chaise evaluates the condition immediately
|
|
371
|
+
* after the main entity is read.
|
|
372
|
+
*
|
|
373
|
+
* For async sources, `addCol` pushes the source (and any async wait-fors)
|
|
374
|
+
* into `requests`, tagged with `condition: true` so chaise can dispatch
|
|
375
|
+
* `evaluateCondition` once the data lands.
|
|
376
|
+
*/
|
|
377
|
+
processConditionedItem(
|
|
378
|
+
condition: ActiveListCondition,
|
|
379
|
+
conditionedItem: ActiveListConditionedItem,
|
|
380
|
+
addToDependent: (dependentRequests: Array<ActiveListRequest | ActiveListRelatedEntityRequest>) => void,
|
|
381
|
+
): void {
|
|
382
|
+
// dedup: if this condition was already grouped (via condition_key), reuse
|
|
383
|
+
// that group's dependent list and add this item to its conditionedItems list.
|
|
384
|
+
if (condition.conditionKey) {
|
|
385
|
+
const existingGroup = this.conditionalGroups.find((g) => g.condition.conditionKey === condition.conditionKey);
|
|
386
|
+
if (existingGroup) {
|
|
387
|
+
addToDependent(existingGroup.dependentRequests);
|
|
388
|
+
existingGroup.conditionedItems.push(conditionedItem);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const conditionIndex = this.conditionalGroups.length;
|
|
394
|
+
|
|
395
|
+
// register the condition's own column and its wait-fors. For async sources
|
|
396
|
+
// this populates `requests` (with `condition: true, index: conditionIndex`
|
|
397
|
+
// so chaise routes the completion to evaluateCondition); for sync sources
|
|
398
|
+
// it's a no-op on `requests` but still populates `allOutBounds` / `selfLinks`.
|
|
399
|
+
this.addCol(condition.column, false, ActiveListRequestTypes.CONDITION, conditionIndex);
|
|
400
|
+
condition.waitFor.forEach((wf) => {
|
|
401
|
+
this.addCol(wf, true, ActiveListRequestTypes.CONDITION, conditionIndex);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// build the dependent list and stash the new group.
|
|
405
|
+
const dependentRequests: Array<ActiveListRequest | ActiveListRelatedEntityRequest> = [];
|
|
406
|
+
addToDependent(dependentRequests);
|
|
407
|
+
this.conditionalGroups.push({
|
|
408
|
+
condition,
|
|
409
|
+
conditionedItems: [conditionedItem],
|
|
410
|
+
dependentRequests,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Add a fkey from sourceDefinitions to allOutBounds (with dedup). */
|
|
415
|
+
addAllOutBound(fkCol: ForeignKeyPseudoColumn): void {
|
|
416
|
+
if (fkCol.name in this.consideredOutbounds) return;
|
|
417
|
+
this.consideredOutbounds[fkCol.name] = true;
|
|
418
|
+
this.allOutBounds.push(fkCol);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Return the built ActiveList object. After calling this method, the builder should not be used anymore.
|
|
423
|
+
*/
|
|
424
|
+
build(): ActiveList {
|
|
425
|
+
return {
|
|
426
|
+
requests: this.requests,
|
|
427
|
+
conditionalGroups: this.conditionalGroups,
|
|
428
|
+
allOutBounds: this.allOutBounds,
|
|
429
|
+
selfLinks: this.selfLinks,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
package/src/utils/constants.ts
CHANGED
|
@@ -405,6 +405,9 @@ export const _warningMessages = Object.freeze({
|
|
|
405
405
|
FILTER_NOT_ALLOWED: 'filter in source is only supported in `filter` context of visible-columns',
|
|
406
406
|
FILTER_NO_PATH_NOT_ALLOWED: 'filter in source is not supported with local columns or all-outbound paths.',
|
|
407
407
|
USED_IN_IFRAME_INPUT: 'the column already used in another column mapping.',
|
|
408
|
+
CONDITION: {
|
|
409
|
+
INVALID_SOURCE: 'condition definition is invalid. `source` or `sourcekey` is required and must be valid.',
|
|
410
|
+
},
|
|
408
411
|
});
|
|
409
412
|
|
|
410
413
|
export const _permissionMessages = Object.freeze({
|
|
@@ -507,7 +507,12 @@ export function computeReadPath(
|
|
|
507
507
|
/**
|
|
508
508
|
* Generate the list of columns for this reference based on context and annotations
|
|
509
509
|
*/
|
|
510
|
-
export function generateColumnsList(
|
|
510
|
+
export function generateColumnsList(
|
|
511
|
+
reference: Reference | RelatedReference,
|
|
512
|
+
tuple?: Tuple,
|
|
513
|
+
columnsList?: Array<unknown>,
|
|
514
|
+
skipLog?: boolean,
|
|
515
|
+
): VisibleColumn[] {
|
|
511
516
|
const resultColumns: VisibleColumn[] = [];
|
|
512
517
|
const consideredColumns: { [key: string]: boolean } = {}; // to avoid duplicate pseudo columns
|
|
513
518
|
const tableColumns: { [key: string]: boolean } = {}; // to make sure the hashes we generate are not clashing with table column names
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// models
|
|
2
|
+
import type { Tuple } from '@isrd-isi-edu/ermrestjs/src/models/reference';
|
|
3
|
+
import type { ReferenceColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference-column';
|
|
4
|
+
import type { PseudoColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference-column/pseudo-column';
|
|
5
|
+
import type { ForeignKeyPseudoColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference-column/foreign-key-pseudo-column';
|
|
6
|
+
import type { KeyPseudoColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference-column/key-pseudo-column';
|
|
7
|
+
|
|
8
|
+
// legacy
|
|
9
|
+
import { _getRowTemplateVariables } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build the `$self` (and where applicable `$_self`) template variables for a
|
|
13
|
+
* given column and tuple. Consolidates the derivation logic that used to be
|
|
14
|
+
* duplicated across `sourceFormatPresentation` in `ReferenceColumn`,
|
|
15
|
+
* `PseudoColumn`, `ForeignKeyPseudoColumn`, `KeyPseudoColumn`, and
|
|
16
|
+
* `ActiveListCondition`.
|
|
17
|
+
*
|
|
18
|
+
* The returned shape varies with the column:
|
|
19
|
+
* - scalar sources (local column, all-outbound scalar) ⇒ `{ $self, $_self }`
|
|
20
|
+
* where `$_self` is the raw value and `$self` is `formatvalue(...)`'d.
|
|
21
|
+
* - entity-mode sources (key, foreign-key, all-outbound entity, entityset) ⇒
|
|
22
|
+
* `{ $self }` only, where `$self` is the row template (`{ values, rowName, uri, ... }`).
|
|
23
|
+
* - aggregates ⇒ `columnValue.templateVariables` directly (already contains
|
|
24
|
+
* `$self`/`$_self`).
|
|
25
|
+
* - missing data (entity row didn't load, etc.) ⇒ `{}`.
|
|
26
|
+
*
|
|
27
|
+
* @param column - the column whose `$self` we are building.
|
|
28
|
+
* @param mainTuple - the main record tuple. Used to source `$self`/`$_self`
|
|
29
|
+
* for any path that doesn't require an async fetch (local columns,
|
|
30
|
+
* all-outbound, key, foreign-key).
|
|
31
|
+
* @param columnValue - the fetched value, only meaningful for async sources:
|
|
32
|
+
* - **entityset** (Reference.read result): a {@link Page} — `templateVariables`
|
|
33
|
+
* is read from it.
|
|
34
|
+
* - **aggregate** (column.getAggregatedValue result, unwrapped first row):
|
|
35
|
+
* `{ value, templateVariables }` — `templateVariables` is read from it.
|
|
36
|
+
* - **null/undefined** for sync sources, or before the fetch has completed
|
|
37
|
+
* (sync paths ignore it; async paths return `{}`).
|
|
38
|
+
* @returns the `$self`/`$_self` slice ready to be merged into the
|
|
39
|
+
* accumulated template variables.
|
|
40
|
+
*/
|
|
41
|
+
export function buildSelfTemplateVariables(column: ReferenceColumn, mainTuple: Tuple, columnValue?: any): Record<string, any> {
|
|
42
|
+
const context = (column as any)._context as string;
|
|
43
|
+
|
|
44
|
+
// KeyPseudoColumn: $self is row-level template variables from mainTuple.data
|
|
45
|
+
if ((column as any).isKey) {
|
|
46
|
+
const keyCol = column as KeyPseudoColumn;
|
|
47
|
+
return {
|
|
48
|
+
$self: _getRowTemplateVariables(keyCol.table, context, mainTuple.data),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ForeignKeyPseudoColumn: $self is row-level template variables from linkedData
|
|
53
|
+
if ((column as any).isForeignKey) {
|
|
54
|
+
const fkCol = column as ForeignKeyPseudoColumn;
|
|
55
|
+
if (!mainTuple.linkedData[fkCol.name]) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
$self: _getRowTemplateVariables(fkCol.table, context, mainTuple.linkedData[fkCol.name]),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// PseudoColumn (path-based source column)
|
|
64
|
+
if ((column as any).isPathColumn) {
|
|
65
|
+
const pseudoCol = column as PseudoColumn;
|
|
66
|
+
|
|
67
|
+
// aggregate: use precomputed templateVariables from the fetched value
|
|
68
|
+
if (pseudoCol.hasAggregate && columnValue) {
|
|
69
|
+
return columnValue.templateVariables || {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// all-outbound (isUnique): use mainTuple.linkedData
|
|
73
|
+
if (pseudoCol.hasPath && pseudoCol.isUnique) {
|
|
74
|
+
if (!mainTuple.linkedData[pseudoCol.name]) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
// scalar
|
|
78
|
+
if (!pseudoCol.isEntityMode) {
|
|
79
|
+
const baseCol = pseudoCol.baseColumn;
|
|
80
|
+
return {
|
|
81
|
+
$self: baseCol.formatvalue(mainTuple.linkedData[pseudoCol.name][baseCol.name], context),
|
|
82
|
+
$_self: mainTuple.linkedData[pseudoCol.name][baseCol.name],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// entity
|
|
86
|
+
return {
|
|
87
|
+
$self: _getRowTemplateVariables(pseudoCol.table, context, mainTuple.linkedData[pseudoCol.name]),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// entityset: use templateVariables from the page result
|
|
92
|
+
if (columnValue && columnValue.templateVariables) {
|
|
93
|
+
return columnValue.templateVariables;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// other paths with a base column (scalar)
|
|
97
|
+
if (pseudoCol.baseColumn) {
|
|
98
|
+
return {
|
|
99
|
+
$self: pseudoCol.baseColumn.formatvalue(mainTuple.data[pseudoCol.baseColumn.name], context),
|
|
100
|
+
$_self: mainTuple.data[pseudoCol.baseColumn.name],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Base ReferenceColumn (non-pseudo, simple column)
|
|
108
|
+
const baseCol = column.baseColumns[0];
|
|
109
|
+
if (baseCol) {
|
|
110
|
+
return {
|
|
111
|
+
$self: baseCol.formatvalue(mainTuple.data[baseCol.name], context),
|
|
112
|
+
$_self: mainTuple.data[baseCol.name],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {};
|
|
117
|
+
}
|