@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 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 {Object} [contextHeaderParams={cid:'null'}] An optional server header parameters for context logging
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
- self._sourceDefinitions = new TableSourceDefinitions(this, res.columns, res.fkeys, res.sources, res.sourceMapping, res.sourceDependencies);
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 {Object} defaultExportTemplate
85
+ * @param {any} defaultExportTemplate
86
86
  * @returns An object that can be used in combination with export annotation
87
87
  * @private
88
88
  * @ignore
@@ -1325,7 +1325,7 @@ import AuthnService from '@isrd-isi-edu/ermrestjs/src/services/authn';
1325
1325
  });
1326
1326
  }
1327
1327
 
1328
- return {successful: true, filters: result};
1328
+ return {successful: true, filters: result, usedQuantified: canUseQuantified};
1329
1329
  };
1330
1330
 
1331
1331
  export function _stringToDate(_date, _format, _delimiter) {
@@ -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 seconadry
694
+ * - hasWaitFor: whether any of the waitFor columns is secondary
695
695
  * - hasWaitForAggregate: whether any of the waitfor columns are aggregate
696
- * @param {Array|String} waitFor - the waitfor definition
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@isrd-isi-edu/ermrestjs",
3
3
  "description": "ERMrest client library in JavaScript",
4
- "version": "2.4.3",
4
+ "version": "2.6.0",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
7
7
  "node": ">= 20.0.0",
@@ -50,7 +50,7 @@
50
50
  "@types/lodash-es": "^4.17.12",
51
51
  "@types/markdown-it": "^14.1.2",
52
52
  "@types/q": "^1.5.8",
53
- "axios": "1.14.0",
53
+ "axios": "1.15.0",
54
54
  "handlebars": "4.7.9",
55
55
  "lodash-es": "^4.18.1",
56
56
  "lz-string": "^1.5.0",
package/src/index.ts CHANGED
@@ -64,6 +64,7 @@ import {
64
64
  processMarkdownPattern,
65
65
  renderMustacheTemplate,
66
66
  _validateMustacheTemplate,
67
+ generateKeyValueFilters,
67
68
  } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
68
69
  import {
69
70
  AttributeGroupColumn,
@@ -137,6 +138,7 @@ export {
137
138
  setHTTP401Handler,
138
139
  getElapsedTime,
139
140
  onload,
141
+ generateKeyValueFilters,
140
142
 
141
143
  // markdown/template/print
142
144
  processMarkdownPattern,
@@ -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
+ }