@isrd-isi-edu/ermrestjs 2.5.0 → 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/pseudocolumn_helpers.js +2 -2
- package/package.json +1 -1
- 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
package/js/core.js
CHANGED
|
@@ -81,7 +81,7 @@ import {
|
|
|
81
81
|
* @memberof ERMrest
|
|
82
82
|
* @function
|
|
83
83
|
* @param {string} uri URI of the ERMrest service.
|
|
84
|
-
* @param {
|
|
84
|
+
* @param {any} [contextHeaderParams={cid:'null'}] An optional server header parameters for context logging
|
|
85
85
|
* appended to the end of any request to the server.
|
|
86
86
|
* @return {Server} Returns a server instance.
|
|
87
87
|
* @throws {InvalidInputError} URI is missing
|
|
@@ -1532,6 +1532,7 @@ import {
|
|
|
1532
1532
|
* Returns an object with
|
|
1533
1533
|
* - fkeys: array of ForeignKeyRef objects
|
|
1534
1534
|
* - columns: Array of columns
|
|
1535
|
+
* - conditions: hash-map of sourcekey to the condition's unprocessed column-directive.
|
|
1535
1536
|
* - sources: hash-map of name to the SourceObjectWrapper object.
|
|
1536
1537
|
* - sourceMapping: hashname to all the names
|
|
1537
1538
|
* - sourceDependencies: for each sourcekey, what are the other sourcekeys that it depends on (includes self as well)
|
|
@@ -1549,7 +1550,7 @@ import {
|
|
|
1549
1550
|
var self = this;
|
|
1550
1551
|
var sd = _annotations.SOURCE_DEFINITIONS;
|
|
1551
1552
|
var hasAnnot = self.annotations.contains(sd);
|
|
1552
|
-
var res = {columns: [], fkeys: [], sources: {}, sourceMapping: {}, sourceDependencies: {}};
|
|
1553
|
+
var res = {columns: [], fkeys: [], sources: {}, sourceMapping: {}, sourceDependencies: {}, conditions: {}};
|
|
1553
1554
|
var addedCols = {}, addedFks = {}, processedSources = {};
|
|
1554
1555
|
var allColumns = self.columns.all(),
|
|
1555
1556
|
allForeignKeys = self.foreignKeys.all();
|
|
@@ -1693,7 +1694,7 @@ import {
|
|
|
1693
1694
|
if (!hasAnnot) {
|
|
1694
1695
|
res.columns = allColumns;
|
|
1695
1696
|
res.fkeys = allForeignKeys;
|
|
1696
|
-
self._sourceDefinitions = new TableSourceDefinitions(this, res.columns, res.fkeys, res.sources, res.sourceMapping, res.sourceDependencies);
|
|
1697
|
+
self._sourceDefinitions = new TableSourceDefinitions(this, res.columns, res.fkeys, res.sources, res.sourceMapping, res.sourceDependencies, res.conditions);
|
|
1697
1698
|
return;
|
|
1698
1699
|
}
|
|
1699
1700
|
|
|
@@ -1731,7 +1732,32 @@ import {
|
|
|
1731
1732
|
}
|
|
1732
1733
|
}
|
|
1733
1734
|
|
|
1734
|
-
|
|
1735
|
+
// conditions
|
|
1736
|
+
if (annot.conditions && typeof annot.conditions === "object") {
|
|
1737
|
+
for (var cKey in annot.conditions) {
|
|
1738
|
+
if (!Object.prototype.hasOwnProperty.call(annot.conditions, cKey)) continue;
|
|
1739
|
+
var condDef = annot.conditions[cKey];
|
|
1740
|
+
if (typeof condDef !== "object" || condDef === null) {
|
|
1741
|
+
$log.info("condition definition, table =" + self.name + ", condition=" + cKey + ": must be an object.");
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
// must have source or sourcekey
|
|
1745
|
+
if (!condDef.source && !isStringAndNotEmpty(condDef.sourcekey)) {
|
|
1746
|
+
$log.info("condition definition, table =" + self.name + ", condition=" + cKey + ": must have `source` or `sourcekey`.");
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
// if sourcekey, it must exist in the sources map
|
|
1750
|
+
if (isStringAndNotEmpty(condDef.sourcekey) && !(condDef.sourcekey in res.sources)) {
|
|
1751
|
+
$log.info("condition definition, table =" + self.name + ", condition=" + cKey + ": sourcekey `" + condDef.sourcekey + "` not found in sources.");
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
// just capture and don't process
|
|
1755
|
+
// we have to reprocess these again anyways because they might rely on tuple.
|
|
1756
|
+
res.conditions[cKey] = condDef;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
self._sourceDefinitions = new TableSourceDefinitions(this, res.columns, res.fkeys, res.sources, res.sourceMapping, res.sourceDependencies, res.conditions);
|
|
1735
1761
|
},
|
|
1736
1762
|
|
|
1737
1763
|
/**
|
package/js/export.js
CHANGED
|
@@ -82,7 +82,7 @@ export const _exportHelpers = {
|
|
|
82
82
|
/**
|
|
83
83
|
* Return the export fragments that should be used with export annotation.
|
|
84
84
|
* @param {Table} table
|
|
85
|
-
* @param {
|
|
85
|
+
* @param {any} defaultExportTemplate
|
|
86
86
|
* @returns An object that can be used in combination with export annotation
|
|
87
87
|
* @private
|
|
88
88
|
* @ignore
|
|
@@ -691,9 +691,9 @@ import { parse, _convertSearchTermToFilter } from '@isrd-isi-edu/ermrestjs/js/pa
|
|
|
691
691
|
* - will ignore normal columns.
|
|
692
692
|
* will return an object with the following attributes:
|
|
693
693
|
* - waitForList: an array of pseudo-columns
|
|
694
|
-
* - hasWaitFor: whether any of the waitFor columns is
|
|
694
|
+
* - hasWaitFor: whether any of the waitFor columns is secondary
|
|
695
695
|
* - hasWaitForAggregate: whether any of the waitfor columns are aggregate
|
|
696
|
-
* @param {
|
|
696
|
+
* @param {unknown} waitFor - the waitfor definition
|
|
697
697
|
* @param {Reference} baseReference - the reference that this waitfor is based on
|
|
698
698
|
* @param {Table} currentTable - the current table.
|
|
699
699
|
* @param {ReferenceColumn=} currentColumn - if this is defined on a column.
|
package/package.json
CHANGED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// models
|
|
2
|
+
import type { Reference, VisibleColumn, 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 { ConditionDefinition } from '@isrd-isi-edu/ermrestjs/src/models/table-source-definitions';
|
|
5
|
+
import SourceObjectWrapper from '@isrd-isi-edu/ermrestjs/src/models/source-object-wrapper';
|
|
6
|
+
|
|
7
|
+
// utils
|
|
8
|
+
import { buildSelfTemplateVariables } from '@isrd-isi-edu/ermrestjs/src/utils/template-utils';
|
|
9
|
+
import { isStringAndNotEmpty } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
10
|
+
import { _warningMessages } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
11
|
+
import { createPseudoColumn } from '@isrd-isi-edu/ermrestjs/src/utils/column-utils';
|
|
12
|
+
|
|
13
|
+
// legacy
|
|
14
|
+
import { _renderTemplate } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
15
|
+
import { _processWaitForList } from '@isrd-isi-edu/ermrestjs/js/utils/pseudocolumn_helpers';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Encapsulates condition evaluation logic for conditionally visible
|
|
19
|
+
* columns and related entities in the record page.
|
|
20
|
+
*/
|
|
21
|
+
export default class ActiveListCondition {
|
|
22
|
+
/** The PseudoColumn for fetching condition data */
|
|
23
|
+
column: VisibleColumn;
|
|
24
|
+
|
|
25
|
+
/** Whether the condition source requires a secondary request (has inbound path or aggregate) */
|
|
26
|
+
isAsync: boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* if the condition definition is shared across multiple columns/entities,
|
|
30
|
+
* this key can be used to link them together for efficient evaluation.
|
|
31
|
+
*/
|
|
32
|
+
conditionKey?: string;
|
|
33
|
+
|
|
34
|
+
private _condDef: ConditionDefinition;
|
|
35
|
+
private _reference: Reference;
|
|
36
|
+
private _tuple?: Tuple;
|
|
37
|
+
|
|
38
|
+
private _waitFor?: ReferenceColumn[];
|
|
39
|
+
private _hasWaitFor?: boolean;
|
|
40
|
+
private _hasWaitForAggregate?: boolean;
|
|
41
|
+
|
|
42
|
+
constructor(condDef: ConditionDefinition, reference: Reference, tuple?: Tuple, conditionKey?: string) {
|
|
43
|
+
this._condDef = condDef;
|
|
44
|
+
this._reference = reference;
|
|
45
|
+
this._tuple = tuple;
|
|
46
|
+
this.conditionKey = conditionKey;
|
|
47
|
+
|
|
48
|
+
const wm = _warningMessages;
|
|
49
|
+
|
|
50
|
+
if (typeof condDef !== 'object' || (!condDef.source && !isStringAndNotEmpty(condDef.sourcekey))) {
|
|
51
|
+
throw new Error(wm.CONDITION.INVALID_SOURCE);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let condSourceWrapper: SourceObjectWrapper;
|
|
55
|
+
try {
|
|
56
|
+
const sds = this._reference.table.sourceDefinitions;
|
|
57
|
+
if (condDef.sourcekey) {
|
|
58
|
+
const sd = sds.getSource(condDef.sourcekey as string);
|
|
59
|
+
if (!sd) {
|
|
60
|
+
throw new Error('condition sourcekey `' + condDef.sourcekey + '` could not be resolved.');
|
|
61
|
+
}
|
|
62
|
+
// SourceObjectWrapper.clone mutates its first argument (deletes `source`,
|
|
63
|
+
// copies in keys from the resolved source). condDef may be the shared
|
|
64
|
+
// object stored in TableSourceDefinitions.conditions (for condition_key
|
|
65
|
+
// references), so pass a shallow copy to avoid corrupting the shared def.
|
|
66
|
+
condSourceWrapper = sd.clone({ ...condDef }, this._reference.table, false, this._tuple);
|
|
67
|
+
} else {
|
|
68
|
+
condSourceWrapper = new SourceObjectWrapper(condDef, this._reference.table, false, undefined, this._tuple);
|
|
69
|
+
}
|
|
70
|
+
this.column = createPseudoColumn(this._reference, condSourceWrapper!, this._tuple);
|
|
71
|
+
} catch (e: unknown) {
|
|
72
|
+
throw new Error('failed to create condition column: ' + (e instanceof Error ? e.message : String(e)));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.isAsync = condSourceWrapper.hasInbound || condSourceWrapper.hasAggregate;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether there are aggregate or entity set columns in the wait_for list of this condition.
|
|
80
|
+
*/
|
|
81
|
+
get hasWaitFor(): boolean {
|
|
82
|
+
if (this._hasWaitFor === undefined) {
|
|
83
|
+
this.waitFor; // eslint-disable-line @typescript-eslint/no-unused-expressions
|
|
84
|
+
}
|
|
85
|
+
return this._hasWaitFor!;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether there are aggregate columns in the wait_for list of this condition.
|
|
90
|
+
*/
|
|
91
|
+
get hasWaitForAggregate(): boolean {
|
|
92
|
+
if (this._hasWaitForAggregate === undefined) {
|
|
93
|
+
this.waitFor; // eslint-disable-line @typescript-eslint/no-unused-expressions
|
|
94
|
+
}
|
|
95
|
+
return this._hasWaitForAggregate!;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Array of columns that the condition evaluation depends on.
|
|
100
|
+
* Follows the same pattern as ReferenceColumn.waitFor.
|
|
101
|
+
*/
|
|
102
|
+
get waitFor(): ReferenceColumn[] {
|
|
103
|
+
if (this._waitFor === undefined) {
|
|
104
|
+
const res = _processWaitForList(this._condDef.wait_for, this._reference, this._reference.table, null, this._tuple, 'condition');
|
|
105
|
+
this._waitFor = res.waitForList;
|
|
106
|
+
this._hasWaitFor = res.hasWaitFor;
|
|
107
|
+
this._hasWaitForAggregate = res.hasWaitForAggregate;
|
|
108
|
+
}
|
|
109
|
+
return this._waitFor!;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Decide whether the conditioned content should be shown.
|
|
114
|
+
*
|
|
115
|
+
* Three internal paths, dispatched on `condition_pattern` and `this.isAsync`:
|
|
116
|
+
*
|
|
117
|
+
* 1. **`condition_pattern` set.** Render the pattern. `$self` / `$_self` come
|
|
118
|
+
* from {@link buildSelfTemplateVariables}, which sources from `mainTuple`
|
|
119
|
+
* for sync sources and from `conditionValue` for async sources. Blank
|
|
120
|
+
* rendered output ⇒ empty.
|
|
121
|
+
* 2. **No pattern + `isAsync === true`.** Caller already fetched the data;
|
|
122
|
+
* emptiness is computed from `conditionValue` by {@link _isConditionValueEmpty}.
|
|
123
|
+
* 3. **No pattern + `isAsync === false`.** `conditionValue` is ignored (sync
|
|
124
|
+
* sources have no fetched value). The raw value is pulled from `mainTuple`
|
|
125
|
+
* via {@link buildSelfTemplateVariables}: scalar sources are empty when
|
|
126
|
+
* `$_self` is `null`/`undefined`/`''`; entity-mode sources are empty when
|
|
127
|
+
* `$self` is undefined (the joined row didn't load).
|
|
128
|
+
*
|
|
129
|
+
* The `on_empty` flag then maps `isEmpty` to "show": `"hide"` (default) shows
|
|
130
|
+
* when not empty; `"show"` shows when empty.
|
|
131
|
+
*
|
|
132
|
+
* @param templateVariables - accumulated `$x`/`$_x` template variables for
|
|
133
|
+
* every other source (only consulted by the pattern branch).
|
|
134
|
+
* @param conditionValue - the fetched value for an async source.
|
|
135
|
+
* - **entityset** (Reference.read result): a {@link Page} object — has
|
|
136
|
+
* `length` (used by `_isConditionValueEmpty`) and `templateVariables`
|
|
137
|
+
* (used by `buildSelfTemplateVariables` when there is a pattern).
|
|
138
|
+
* - **aggregate** (column.getAggregatedValue result): the unwrapped first
|
|
139
|
+
* element, shaped `{ value, templateVariables }` — `value` is the
|
|
140
|
+
* scalar; `templateVariables` carries `$self`/`$_self`.
|
|
141
|
+
* - **null** if the request failed or there's no fetched value (treated
|
|
142
|
+
* as empty).
|
|
143
|
+
* Ignored entirely for sync sources (path 3).
|
|
144
|
+
* @param mainTuple - the main record tuple. Always required: the pattern
|
|
145
|
+
* branch and the sync no-pattern branch both look up `$self`/`$_self`
|
|
146
|
+
* from it.
|
|
147
|
+
* @returns whether the conditioned content should be shown.
|
|
148
|
+
*/
|
|
149
|
+
evaluateCondition(templateVariables: any, conditionValue: any, mainTuple: Tuple): { shouldShow: boolean } {
|
|
150
|
+
let isEmpty: boolean;
|
|
151
|
+
const condDef = this._condDef;
|
|
152
|
+
|
|
153
|
+
if (condDef.condition_pattern) {
|
|
154
|
+
const selfTemplateVariables = buildSelfTemplateVariables(this.column as ReferenceColumn, mainTuple, conditionValue);
|
|
155
|
+
const keyValues: any = {};
|
|
156
|
+
Object.assign(keyValues, templateVariables, selfTemplateVariables);
|
|
157
|
+
|
|
158
|
+
// evaluate template (no markdown rendering)
|
|
159
|
+
const rendered = _renderTemplate(condDef.condition_pattern as string, keyValues, this.column.table.schema.catalog, {
|
|
160
|
+
templateEngine: condDef.template_engine,
|
|
161
|
+
});
|
|
162
|
+
isEmpty = !rendered || rendered.trim() === '';
|
|
163
|
+
} else if (this.isAsync) {
|
|
164
|
+
// no pattern, async: caller fetched the value
|
|
165
|
+
isEmpty = this._isConditionValueEmpty(conditionValue);
|
|
166
|
+
} else {
|
|
167
|
+
// no pattern, sync: derive the raw value from the tuple via the same
|
|
168
|
+
// helper sourceFormatPresentation uses, then decide emptiness directly.
|
|
169
|
+
const selfVars = buildSelfTemplateVariables(this.column as ReferenceColumn, mainTuple, null);
|
|
170
|
+
if ('$_self' in selfVars) {
|
|
171
|
+
// scalar source (local column or all-outbound scalar)
|
|
172
|
+
const v = selfVars.$_self;
|
|
173
|
+
isEmpty = v === null || v === undefined || v === '';
|
|
174
|
+
} else {
|
|
175
|
+
// entity-mode source (key, foreign-key, all-outbound entity):
|
|
176
|
+
// the row exists iff buildSelfTemplateVariables returned a $self.
|
|
177
|
+
isEmpty = selfVars.$self === undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// on_empty: "hide" (default) -> hide when empty -> show when NOT empty
|
|
182
|
+
// on_empty: "show" -> show when empty -> hide when NOT empty
|
|
183
|
+
const shouldShow = condDef.on_empty === 'show' ? isEmpty : !isEmpty;
|
|
184
|
+
return { shouldShow };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if a fetched async condition value is "empty".
|
|
189
|
+
* Only called from the no-pattern async branch of evaluateCondition; expects
|
|
190
|
+
* one of the shapes produced by Chaise's secondary-fetch:
|
|
191
|
+
* - page object (entityset) — empty iff `length === 0`
|
|
192
|
+
* - `{ value, templateVariables }` — empty iff `value` is null/''/0
|
|
193
|
+
* - `null`/`undefined` — empty (request failed / missing)
|
|
194
|
+
*/
|
|
195
|
+
private _isConditionValueEmpty(conditionValue: any): boolean {
|
|
196
|
+
if (conditionValue === null || conditionValue === undefined) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// page object (entityset)
|
|
201
|
+
if (typeof conditionValue.length === 'number') {
|
|
202
|
+
return conditionValue.length === 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// aggregate single value: { value, templateVariables }
|
|
206
|
+
if (typeof conditionValue === 'object') {
|
|
207
|
+
const inner = (conditionValue as { value?: unknown }).value;
|
|
208
|
+
if (inner === null || inner === undefined || inner === '') return true;
|
|
209
|
+
// count-style aggregates may serialize as a numeric string ("0"); coerce.
|
|
210
|
+
const num = typeof inner === 'number' ? inner : Number(inner);
|
|
211
|
+
return !isNaN(num) && num === 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|