@object-ui/core 16.1.0 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +1355 -4
  2. package/dist/actions/ActionRunner.d.ts +82 -14
  3. package/dist/actions/ActionRunner.js +164 -81
  4. package/dist/actions/actionKeys.d.ts +118 -0
  5. package/dist/actions/actionKeys.js +243 -0
  6. package/dist/actions/index.d.ts +1 -0
  7. package/dist/actions/index.js +1 -0
  8. package/dist/adapters/ApiDataSource.js +10 -1
  9. package/dist/adapters/ValueDataSource.d.ts +2 -2
  10. package/dist/evaluator/fieldRules.d.ts +0 -2
  11. package/dist/evaluator/fieldRules.js +3 -2
  12. package/dist/evaluator/optionLint.js +5 -2
  13. package/dist/evaluator/optionRules.d.ts +13 -6
  14. package/dist/evaluator/optionRules.js +0 -23
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.js +7 -0
  17. package/dist/registry/Registry.d.ts +81 -2
  18. package/dist/registry/Registry.js +102 -2
  19. package/dist/registry/public-blocks.js +52 -1
  20. package/dist/theme/ThemeEngine.js +0 -12
  21. package/dist/utils/chart-series.d.ts +28 -0
  22. package/dist/utils/chart-series.js +55 -0
  23. package/dist/utils/column-identity.d.ts +110 -0
  24. package/dist/utils/column-identity.js +210 -0
  25. package/dist/utils/dashboard-filters.d.ts +2 -2
  26. package/dist/utils/expand-fields.js +8 -8
  27. package/dist/utils/filter-converter.d.ts +47 -0
  28. package/dist/utils/filter-converter.js +94 -7
  29. package/dist/utils/filter-tokens.d.ts +101 -0
  30. package/dist/utils/filter-tokens.js +173 -0
  31. package/dist/utils/managedBy.d.ts +21 -3
  32. package/dist/utils/managedBy.js +45 -4
  33. package/dist/utils/normalize-list-view.d.ts +99 -0
  34. package/dist/utils/normalize-list-view.js +255 -0
  35. package/dist/utils/sort-values.d.ts +52 -0
  36. package/dist/utils/sort-values.js +125 -0
  37. package/dist/validation/index.d.ts +9 -1
  38. package/dist/validation/index.js +9 -1
  39. package/dist/validation/validators/index.d.ts +7 -1
  40. package/dist/validation/validators/index.js +7 -1
  41. package/dist/validation/validators/object-validation-engine.d.ts +151 -31
  42. package/dist/validation/validators/object-validation-engine.js +275 -142
  43. package/package.json +4 -4
@@ -0,0 +1,243 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * What an action is allowed to carry — objectstack#4075 step 1.
10
+ *
11
+ * `ActionDef` ends with `[key: string]: any`, so it accepts any key of any type.
12
+ * Concretely (objectui#2990): deleting `ActionDef.execute` produced ZERO compile
13
+ * errors even though the field had just been removed, and stale metadata still
14
+ * authoring `execute: 'markDone'` type-checks today. The same deletion against
15
+ * `@object-ui/types`' `ActionSchema` — which has no index signature — correctly
16
+ * produced `TS2353` at the authoring site. One of the two readers can catch a
17
+ * retired key; the other is structurally incapable.
18
+ *
19
+ * That asymmetry is the issue. An open key set on a DECLARED METADATA CONTRACT
20
+ * is what lets a typo (`targt`, `exectue`) and a tombstoned key (`execute`) both
21
+ * sail through to a runner that then silently does nothing — the #2169 "Mark
22
+ * Done does nothing" shape.
23
+ *
24
+ * This module is step 1 of the staged narrowing: it makes the key set VISIBLE
25
+ * and warns on anything outside it, without changing a single type. Nothing
26
+ * breaks, and an invisible failure becomes an audible one. Steps 2 and 3 —
27
+ * promoting the legitimate keys to explicit optional fields, then removing the
28
+ * index signature — are what finally let `tsc` catch typos and retired keys.
29
+ *
30
+ * The lists are pinned by `__tests__/actionKeys.pin.test.ts`, which re-derives
31
+ * each one from its actual source. A hand-maintained list that drifts from the
32
+ * interface it mirrors would be the same "declared ≠ enforced" trap this whole
33
+ * thread is about.
34
+ *
35
+ * ── What the inventory found, i.e. step 2's worklist ─────────────────────────
36
+ * `ActionDef` declares 34 keys, the spec's `ActionSchema` declares 36, and they
37
+ * overlap on 17. The two halves of the difference are different problems:
38
+ *
39
+ * 18 keys the SPEC owns that `ActionDef` never declared — `ai`, `aria`,
40
+ * `bodyExtra`, `bodyShape`, `bulkEnabled`, `component`, `icon`, `locations`,
41
+ * `mode`, `objectName`, `order`, `recordIdField`, `recordIdParam`,
42
+ * `requiredPermissions`, `requiresFeature`, `shortcut`, `variant`, `visible`.
43
+ * These are authored today and reach readers through the index signature.
44
+ * `ActionEngine` reads two of them (`visible` at the location filter, `locations`
45
+ * at registration) through an `as any` cast — a cast that exists ONLY because the
46
+ * field is undeclared, and that goes away when step 2 promotes them.
47
+ *
48
+ * 17 keys `ActionDef` declares that the spec does not own — `actionType`, `api`,
49
+ * `chain`, `chainMode`, `close`, `condition`, `confirm`, `endpoint`, `modal`,
50
+ * `navigate`, `onClick`, `onFailure`, `onSuccess`, `redirect`, `reload`, `toast`,
51
+ * `actionParams`. Several are already marked legacy at their declaration. These
52
+ * are objectui's own dialect and need the #4115 treatment: kept and documented as
53
+ * deliberate, or retired — but not silently carried.
54
+ */
55
+ /**
56
+ * Every property `ActionDef` declares, in declaration order.
57
+ *
58
+ * Kept as data because TypeScript types do not survive to runtime — and
59
+ * `keyof ActionDef` cannot substitute for this list: the index signature widens
60
+ * it to `string | number`, which is precisely the problem being worked around.
61
+ */
62
+ export const ACTION_DEF_KEYS = [
63
+ 'type',
64
+ 'actionType',
65
+ 'name',
66
+ 'label',
67
+ 'confirmText',
68
+ 'confirm',
69
+ 'condition',
70
+ 'disabled',
71
+ 'api',
72
+ 'endpoint',
73
+ 'method',
74
+ 'navigate',
75
+ 'onClick',
76
+ 'reload',
77
+ 'close',
78
+ 'redirect',
79
+ 'toast',
80
+ 'successMessage',
81
+ 'errorMessage',
82
+ 'refreshAfter',
83
+ 'undoable',
84
+ 'params',
85
+ 'actionParams',
86
+ 'resultDialog',
87
+ 'target',
88
+ 'body',
89
+ 'openIn',
90
+ 'modal',
91
+ 'chain',
92
+ 'chainMode',
93
+ 'onSuccess',
94
+ 'onFailure',
95
+ 'opensInNewTab',
96
+ 'newTabUrl',
97
+ ];
98
+ /**
99
+ * Every property `@objectstack/spec`'s `ActionSchema` declares.
100
+ *
101
+ * `ActionDef` mirrors that schema, so a key the spec owns is legitimate even
102
+ * when `ActionDef` has not declared it yet — several are read today only through
103
+ * an `as any` cast (`visible` and `locations` in `ActionEngine`), which is the
104
+ * cast this list exists to eventually retire.
105
+ *
106
+ * Pinned rather than read off the schema at runtime: the spec exports
107
+ * `ActionSchema` as a lazy proxy that does not forward `.shape`, so resolving it
108
+ * means walking zod internals (`_def.in` / `_def.innerType`). That is fine in a
109
+ * test and wrong in shipped code — `@object-ui/types` made the same call for
110
+ * `ActionComponent`. The pin test does the walk and fails the day this drifts.
111
+ */
112
+ export const SPEC_ACTION_KEYS = [
113
+ 'ai',
114
+ 'aria',
115
+ 'body',
116
+ 'bodyExtra',
117
+ 'bodyShape',
118
+ 'bulkEnabled',
119
+ 'component',
120
+ 'confirmText',
121
+ 'disabled',
122
+ 'errorMessage',
123
+ 'execute',
124
+ 'icon',
125
+ 'label',
126
+ 'locations',
127
+ 'method',
128
+ 'mode',
129
+ 'name',
130
+ 'newTabUrl',
131
+ 'objectName',
132
+ 'openIn',
133
+ 'opensInNewTab',
134
+ 'order',
135
+ 'params',
136
+ 'recordIdField',
137
+ 'recordIdParam',
138
+ 'refreshAfter',
139
+ 'requiredPermissions',
140
+ 'requiresFeature',
141
+ 'resultDialog',
142
+ 'shortcut',
143
+ 'successMessage',
144
+ 'target',
145
+ 'type',
146
+ 'undoable',
147
+ 'variant',
148
+ 'visible',
149
+ ];
150
+ /**
151
+ * The `navigation` alias's own spelling of a target, read off the action itself
152
+ * when no nested `navigate` object is present (`ActionRunner.executeNavigation`).
153
+ *
154
+ * A declared objectui-side dialect, not spec vocabulary: `replace` is documented
155
+ * at its read site as "the one thing the `navigation` shape carries that
156
+ * `ActionSchema` has no field for". Listing it here is what keeps it from being
157
+ * silent dialect — the alternative is a warning that cries wolf on a shape the
158
+ * runner itself supports.
159
+ */
160
+ export const NAVIGATION_ALIAS_KEYS = ['to', 'external', 'newTab', 'replace'];
161
+ /**
162
+ * Keys the spec has TOMBSTONED: still present in `ActionSchema` so the parser can
163
+ * reject them BY NAME with a rename prescription, rather than fail with a bare
164
+ * "unrecognized key".
165
+ *
166
+ * Warned about separately from unknown keys, and more loudly: an unknown key is
167
+ * probably a typo, while a retired key is metadata that used to work. That
168
+ * distinction is why `executeScript` carries a runtime branch returning the
169
+ * rename prescription — a branch that exists solely to compensate for the index
170
+ * signature, and that can retire with it in step 3.
171
+ */
172
+ export const RETIRED_ACTION_KEYS = {
173
+ execute: '`execute` was removed in @objectstack/spec 17 (#3855) — rename the key to `target`. ' +
174
+ 'The value (a script name or expression) is unchanged. ' +
175
+ 'Run `os migrate meta --from 16` to rewrite it automatically.',
176
+ };
177
+ /** Every key an action may legitimately carry today. */
178
+ export const KNOWN_ACTION_KEYS = new Set([
179
+ ...ACTION_DEF_KEYS,
180
+ ...SPEC_ACTION_KEYS,
181
+ ...NAVIGATION_ALIAS_KEYS,
182
+ ].filter((key) => !(key in RETIRED_ACTION_KEYS)));
183
+ /** Split an action's own keys into the two things worth saying out loud. */
184
+ export function classifyActionKeys(action) {
185
+ const unknown = [];
186
+ const retired = [];
187
+ if (!action || typeof action !== 'object')
188
+ return { unknown, retired };
189
+ for (const key of Object.keys(action)) {
190
+ if (key in RETIRED_ACTION_KEYS)
191
+ retired.push(key);
192
+ else if (!KNOWN_ACTION_KEYS.has(key))
193
+ unknown.push(key);
194
+ }
195
+ return { unknown, retired };
196
+ }
197
+ // Warn once per (action, problem), not once per execution: an unrecognized key
198
+ // usually sits in metadata driving a button that gets clicked repeatedly, and a
199
+ // warning that floods the console is a warning that gets muted.
200
+ //
201
+ // Keyed by action name as well as by the keys, deliberately. Keying on the keys
202
+ // alone would report the FIRST action carrying `targt` and stay silent about
203
+ // every other one — sending the author to fix a button that was only the first
204
+ // symptom. The memo is bounded by the number of authored actions either way.
205
+ const warned = new Set();
206
+ /** Reset the warn-once memo. Exported for tests. */
207
+ export function resetActionKeyWarnings() {
208
+ warned.clear();
209
+ }
210
+ const isDev = () => globalThis.process?.env?.NODE_ENV !==
211
+ 'production';
212
+ /**
213
+ * Dev-mode only: name the keys an action carries that no reader will ever look
214
+ * at. Non-breaking by construction — it changes no types and rejects nothing.
215
+ *
216
+ * Silently binding no handler is the #2169 "Mark Done does nothing" shape, and
217
+ * the compiler cannot help while the index signature stands. Until it comes
218
+ * down, this is the audible half.
219
+ */
220
+ export function warnOnUnknownActionKeys(action, where = 'ActionRunner') {
221
+ if (!isDev())
222
+ return;
223
+ const { unknown, retired } = classifyActionKeys(action);
224
+ const name = action?.name ?? action?.type ?? '(unnamed)';
225
+ for (const key of retired) {
226
+ const memo = `retired:${String(name)}:${key}`;
227
+ if (warned.has(memo))
228
+ continue;
229
+ warned.add(memo);
230
+ console.warn(`[${where}] action "${String(name)}" carries the retired key \`${key}\`. ${RETIRED_ACTION_KEYS[key]}`);
231
+ }
232
+ if (unknown.length === 0)
233
+ return;
234
+ const memo = `unknown:${String(name)}:${unknown.slice().sort().join(',')}`;
235
+ if (warned.has(memo))
236
+ return;
237
+ warned.add(memo);
238
+ console.warn(`[${where}] action "${String(name)}" carries ${unknown.length === 1 ? 'a key' : 'keys'} no reader ` +
239
+ `recognizes: \`${unknown.join('`, `')}\`. Nothing will read ${unknown.length === 1 ? 'it' : 'them'} — ` +
240
+ 'check for a typo, or promote the key to an explicit field on `ActionDef` ' +
241
+ '(packages/core/src/actions/actionKeys.ts). Warned rather than rejected because `ActionDef` ' +
242
+ 'still carries `[key: string]: any`, so the compiler cannot see this (objectstack#4075).');
243
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  export * from './ActionRunner.js';
9
9
  export * from './ActionEngine.js';
10
+ export * from './actionKeys.js';
10
11
  export * from './TransactionManager.js';
11
12
  export * from './UndoManager.js';
12
13
  export * from './bulkFastPath.js';
@@ -7,6 +7,7 @@
7
7
  */
8
8
  export * from './ActionRunner.js';
9
9
  export * from './ActionEngine.js';
10
+ export * from './actionKeys.js';
10
11
  export * from './TransactionManager.js';
11
12
  export * from './UndoManager.js';
12
13
  export * from './bulkFastPath.js';
@@ -44,7 +44,16 @@ function queryParamsToRecord(params) {
44
44
  out.$filter = JSON.stringify(params.$filter);
45
45
  }
46
46
  if (params.$orderby) {
47
- if (Array.isArray(params.$orderby)) {
47
+ if (typeof params.$orderby === 'string') {
48
+ // Already an OData order clause ('name asc', 'a asc,b desc'). It MUST be
49
+ // passed through verbatim: the `Object.entries` branch below would walk a
50
+ // string index-by-index and emit `0 n,1 a,2 m,…`, which the server dutifully
51
+ // sorts by — columns that don't exist — and the whole page comes back empty
52
+ // (objectstack#3821). Callers that build the clause by hand (ObjectGrid's
53
+ // schema `sort`, RecipientPickerField) hit exactly that.
54
+ out.$orderby = params.$orderby;
55
+ }
56
+ else if (Array.isArray(params.$orderby)) {
48
57
  if (typeof params.$orderby[0] === 'string') {
49
58
  out.$orderby = params.$orderby.join(',');
50
59
  }
@@ -8,7 +8,7 @@
8
8
  * A DataSource adapter for the `provider: 'value'` ViewData mode.
9
9
  * Operates entirely on an in-memory array — no network requests.
10
10
  */
11
- import type { DataSource, BatchTransactionOperation, MutationEvent, QueryParams, QueryResult, AggregateParams, AggregateResult } from '@object-ui/types';
11
+ import type { DataSource, BatchTransactionOperation, DataSourceMutationEvent, QueryParams, QueryResult, AggregateParams, AggregateResult } from '@object-ui/types';
12
12
  export interface ValueDataSourceConfig<T = any> {
13
13
  /** The static data array */
14
14
  items: T[];
@@ -60,7 +60,7 @@ export declare class ValueDataSource<T = any> implements DataSource<T> {
60
60
  getView(_objectName: string, _viewId: string): Promise<any | null>;
61
61
  getApp(_appId: string): Promise<any | null>;
62
62
  aggregate(_resource: string, params: AggregateParams): Promise<AggregateResult[]>;
63
- onMutation(callback: (event: MutationEvent<T>) => void): () => void;
63
+ onMutation(callback: (event: DataSourceMutationEvent<T>) => void): () => void;
64
64
  /** Get the current number of items */
65
65
  get count(): number;
66
66
  /** Get a snapshot of all items (cloned) */
@@ -37,8 +37,6 @@ export declare function resolveFieldRuleState(rules: {
37
37
  visibleWhen?: FieldRulePredicate;
38
38
  readonlyWhen?: FieldRulePredicate;
39
39
  requiredWhen?: FieldRulePredicate;
40
- /** Back-compat alias of `requiredWhen` (spec @deprecated). */
41
- conditionalRequired?: FieldRulePredicate;
42
40
  }, record: Record<string, unknown>, statics: {
43
41
  required?: boolean;
44
42
  readonly?: boolean;
@@ -79,8 +79,9 @@ export function resolveFieldRuleState(rules, record, statics, previous, scope) {
79
79
  (rules.readonlyWhen != null
80
80
  ? evalFieldPredicate(rules.readonlyWhen, record, false, previous, scope)
81
81
  : false);
82
- const requiredPred = rules.requiredWhen ?? rules.conditionalRequired;
83
82
  const required = statics.required === true ||
84
- (requiredPred != null ? evalFieldPredicate(requiredPred, record, false, previous, scope) : false);
83
+ (rules.requiredWhen != null
84
+ ? evalFieldPredicate(rules.requiredWhen, record, false, previous, scope)
85
+ : false);
85
86
  return { visible, readonly, required };
86
87
  }
@@ -109,7 +109,10 @@ export function lintOptionPredicates(fields) {
109
109
  continue;
110
110
  knownFields.add(f.name);
111
111
  if (OPTION_FIELD_TYPES.has(fieldType(f)) && f.options && f.options.length > 0) {
112
- domainByField.set(f.name, new Set(f.options.map((o) => o.value)));
112
+ // Values stringify at this boundary (#3090 widened OptionLike.value):
113
+ // the domain is compared against CEL literal TEXT, so the canonical
114
+ // string form is the right footing for numeric/boolean options too.
115
+ domainByField.set(f.name, new Set(f.options.map((o) => String(o.value))));
113
116
  }
114
117
  }
115
118
  const issues = [];
@@ -123,7 +126,7 @@ export function lintOptionPredicates(fields) {
123
126
  const source = predicateSource(opt.visibleWhen);
124
127
  if (!source.trim())
125
128
  continue;
126
- const option = opt.value;
129
+ const option = String(opt.value); // report key — display form (#3090)
127
130
  // 1. Syntax — canonical CEL parse (no schema hint = no scope false-positives).
128
131
  const parsed = validateExpression('predicate', source);
129
132
  if (!parsed.ok) {
@@ -28,6 +28,7 @@
28
28
  * security boundary: when an option is gated for access-control reasons the
29
29
  * server must also reject writes of its value.
30
30
  */
31
+ import type { DependsOnInput } from '@object-ui/types';
31
32
  import { type FieldRulePredicate } from './fieldRules.js';
32
33
  /**
33
34
  * Minimal shape of a select/radio option this module reads. Deliberately has no
@@ -36,15 +37,21 @@ import { type FieldRulePredicate } from './fieldRules.js';
36
37
  */
37
38
  export interface OptionLike {
38
39
  label: string;
39
- value: string;
40
+ /** Widened with `SelectOption.value` (#3090): the option engines treat the
41
+ * value opaquely (gate/filter by `visibleWhen`, compare by identity), so a
42
+ * numeric/boolean authored value flows through unchanged. */
43
+ value: string | number | boolean;
40
44
  /** Per-option visibility predicate (CEL). Omit = always available. */
41
45
  visibleWhen?: FieldRulePredicate;
42
46
  }
43
- /** A field's `dependsOn` as authored: a bare name, or a list of names / `{field,param}`. */
44
- export type DependsOnInput = string | Array<string | {
45
- field: string;
46
- param?: string;
47
- }> | undefined | null;
47
+ /**
48
+ * A field's `dependsOn` as authored — imported from `@object-ui/types`, where
49
+ * `FormField.dependsOn` is declared with it (framework#4074; formerly a local
50
+ * declaration that only the reader knew about, while the public
51
+ * `FormField.dependsOn` said `string`). Re-exported for this module's existing
52
+ * consumers.
53
+ */
54
+ export type { DependsOnInput } from '@object-ui/types';
48
55
  /**
49
56
  * Normalize a field's `dependsOn` to the list of sibling field names it reacts
50
57
  * to. The lookup-only `{field,param}` form contributes just its `field`.
@@ -5,29 +5,6 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- /**
9
- * Client-side resolution of per-option visibility for `select` / `radio` /
10
- * `multiselect` fields — cascading (dependent) options and role/context gating.
11
- *
12
- * An option carries an optional `visibleWhen` CEL predicate; the option is
13
- * offered only when it evaluates TRUE against the live record + `current_user`
14
- * (the SAME engine and binding environment as a field-level `visibleWhen`, via
15
- * {@link evalFieldPredicate}). This expresses both:
16
- * - cascades — `record.country == 'cn'` (the dependent list narrows as the
17
- * controlling field changes), and
18
- * - role/context gating — `'admin' in current_user.positions`.
19
- *
20
- * A field declares which sibling fields its option list reacts to via
21
- * `dependsOn` (aligns with `@objectstack/spec` Field.dependsOn — the same knob
22
- * lookups use). While any dependency is empty the list is *gated*: we surface a
23
- * "select the parent first" hint instead of an unfiltered set, mirroring the
24
- * dependent-lookup UX (#2215).
25
- *
26
- * Evaluation is fail-open (a broken/absent predicate keeps the option) — the
27
- * same safe default as field visibility. Client-side hiding is UX, not a
28
- * security boundary: when an option is gated for access-control reasons the
29
- * server must also reject writes of its value.
30
- */
31
8
  import { evalFieldPredicate } from './fieldRules.js';
32
9
  /**
33
10
  * Normalize a field's `dependsOn` to the list of sibling field names it reacts
package/dist/index.d.ts CHANGED
@@ -17,6 +17,8 @@ export * from './utils/filter-converter.js';
17
17
  export * from './utils/managedBy.js';
18
18
  export * from './utils/extract-records.js';
19
19
  export * from './utils/expand-fields.js';
20
+ export * from './utils/column-identity.js';
21
+ export * from './utils/sort-values.js';
20
22
  export * from './evaluator/index.js';
21
23
  export * from './actions/index.js';
22
24
  export * from './query/index.js';
@@ -40,6 +42,7 @@ export * from './runtime/capabilities.js';
40
42
  export { composeStacks } from '@objectstack/spec';
41
43
  export * from './utils/drill-down.js';
42
44
  export * from './utils/date-macros.js';
45
+ export * from './utils/filter-tokens.js';
43
46
  export * from './utils/dashboard-filters.js';
44
47
  export * from './utils/merge-filters.js';
45
48
  export * from './utils/compare-to.js';
@@ -48,3 +51,4 @@ export * from './utils/dataset-format.js';
48
51
  export * from './utils/record-title.js';
49
52
  export * from './utils/export-filename.js';
50
53
  export * from './utils/reference-keys.js';
54
+ export * from './utils/normalize-list-view.js';
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ export * from './utils/filter-converter.js';
16
16
  export * from './utils/managedBy.js';
17
17
  export * from './utils/extract-records.js';
18
18
  export * from './utils/expand-fields.js';
19
+ export * from './utils/column-identity.js';
20
+ export * from './utils/sort-values.js';
19
21
  export * from './evaluator/index.js';
20
22
  export * from './actions/index.js';
21
23
  export * from './query/index.js';
@@ -39,6 +41,10 @@ export * from './runtime/capabilities.js';
39
41
  export { composeStacks } from '@objectstack/spec';
40
42
  export * from './utils/drill-down.js';
41
43
  export * from './utils/date-macros.js';
44
+ // Session-scoped filter placeholders ({current_user_id} / {current_org_id})
45
+ // plus `resolveFilterPlaceholders`, the single entry point every surface
46
+ // should call so no vocabulary is silently skipped (framework #3574).
47
+ export * from './utils/filter-tokens.js';
42
48
  export * from './utils/dashboard-filters.js';
43
49
  export * from './utils/merge-filters.js';
44
50
  export * from './utils/compare-to.js';
@@ -47,3 +53,4 @@ export * from './utils/dataset-format.js';
47
53
  export * from './utils/record-title.js';
48
54
  export * from './utils/export-filename.js';
49
55
  export * from './utils/reference-keys.js';
56
+ export * from './utils/normalize-list-view.js';
@@ -62,6 +62,22 @@ export type ComponentConfig<T = any> = ComponentMeta & {
62
62
  type: string;
63
63
  component: ComponentRenderer<T>;
64
64
  };
65
+ /**
66
+ * A CONTRACT-surface entry (ADR-0080), as returned by
67
+ * {@link Registry.getPublicConfigs}.
68
+ *
69
+ * Same shape as {@link ComponentConfig} except `component` is absent while the
70
+ * entry is still a pending `registerLazy` stub: the plugin module has not been
71
+ * imported yet, so there is no renderer to hand out. Consumers render such an
72
+ * entry through `SchemaRenderer`, which triggers the loader and shows a
73
+ * placeholder in the meantime (objectui#2953).
74
+ */
75
+ export type PublicComponentConfig<T = any> = ComponentMeta & {
76
+ type: string;
77
+ component?: ComponentRenderer<T>;
78
+ /** True while this entry is a `registerLazy` stub whose loader has not run. */
79
+ lazy?: boolean;
80
+ };
65
81
  /**
66
82
  * Lazy loader function used by `Registry.registerLazy`. The loader is invoked
67
83
  * the first time a missing component type is requested through `getAsync`/the
@@ -79,6 +95,14 @@ export declare class Registry<T = any> {
79
95
  * load completes.
80
96
  */
81
97
  private listeners;
98
+ /**
99
+ * Bumped whenever the set of KNOWN types changes — a registration, an
100
+ * unregistration, or a new lazy stub. A cheap cache key for consumers that
101
+ * derive something from the whole registry (whitelists, manifests, palettes)
102
+ * and need to rebuild when it grows. Counting types is not a substitute: a
103
+ * registration paired with an unregistration leaves the count untouched.
104
+ */
105
+ private version;
82
106
  /**
83
107
  * Register a component with optional namespace support.
84
108
  * If namespace is provided in meta, the component will be registered as "namespace:type".
@@ -141,6 +165,11 @@ export declare class Registry<T = any> {
141
165
  */
142
166
  subscribe(listener: () => void): () => void;
143
167
  private notify;
168
+ /**
169
+ * Monotonic counter of changes to the set of known types. Rebuild anything
170
+ * derived from the whole registry when this moves — see {@link getKnownTypes}.
171
+ */
172
+ getVersion(): number;
144
173
  /**
145
174
  * Get a component by type. Supports both namespaced and non-namespaced lookups.
146
175
  *
@@ -164,6 +193,13 @@ export declare class Registry<T = any> {
164
193
  /**
165
194
  * Get component configuration by type with namespace support.
166
195
  *
196
+ * LOADED registrations only — a type that exists solely as a `registerLazy`
197
+ * stub returns `undefined` here, because callers read `.component` off the
198
+ * result and a stub has no renderer until its loader runs. The *contract*
199
+ * question ("is this tag part of the public surface?") must not depend on
200
+ * load order, so {@link getPublicConfigs} resolves lazy stubs separately
201
+ * (objectui#2953). Use {@link hasLazy} / {@link loadLazy} to reach a stub.
202
+ *
167
203
  * @param type - Component type (e.g., 'button' or 'ui:button')
168
204
  * @param namespace - Optional namespace for lookup priority
169
205
  * @returns Component configuration or undefined
@@ -178,23 +214,66 @@ export declare class Registry<T = any> {
178
214
  */
179
215
  has(type: string, namespace?: string): boolean;
180
216
  /**
181
- * Get all registered component types.
217
+ * Get all LOADED component types — what can be rendered right now.
218
+ *
219
+ * Pending `registerLazy` stubs are not included; use {@link getKnownTypes}
220
+ * for the question "is this a type this app knows about", which is what a
221
+ * whitelist or manifest wants (objectui#2953).
182
222
  *
183
223
  * @returns Array of all component type identifiers
184
224
  */
185
225
  getAllTypes(): string[];
226
+ /**
227
+ * Every type the registry can resolve — loaded registrations PLUS pending
228
+ * lazy stubs, deduped. Both the bare and namespaced keys are included, as
229
+ * they are in {@link getAllTypes}.
230
+ *
231
+ * This is the set to build a whitelist or a manifest from. Keying one off
232
+ * `getAllTypes()` instead makes it depend on which plugin chunks happen to
233
+ * have loaded: a lazily-registered block gets rejected as unknown, and
234
+ * whether it does varies by session (objectui#2953).
235
+ */
236
+ getKnownTypes(): string[];
237
+ /**
238
+ * Metadata for `type` — from the loaded registration when there is one,
239
+ * otherwise from a pending `registerLazy` stub.
240
+ *
241
+ * Use this when you need to know what a type IS (namespace, container-ness,
242
+ * declared inputs). {@link getConfig} answers a different question — "can I
243
+ * render this right now" — and is deliberately loaded-only, since callers
244
+ * read `.component` off it.
245
+ *
246
+ * A stub carries only what `registerLazy` was given, so `inputs` is usually
247
+ * absent until the chunk loads. Consumers should treat that as "not yet
248
+ * known", not as "declares no props".
249
+ */
250
+ getMeta(type: string, namespace?: string): ComponentMeta | undefined;
186
251
  /**
187
252
  * Get all registered component configurations.
188
253
  *
189
254
  * @returns Array of all component configurations
190
255
  */
191
256
  getAllConfigs(): ComponentConfig<T>[];
257
+ /**
258
+ * Resolve `tag` for the CONTRACT surface: the loaded registration when the
259
+ * component is already in the registry, otherwise the metadata of a pending
260
+ * `registerLazy` stub.
261
+ *
262
+ * A block registered lazily is a first-class member of the contract — the
263
+ * loader is recorded at boot and the plugin chunk is imported on first use —
264
+ * so contract membership must not hinge on whether that import happened to
265
+ * have run yet (objectui#2953).
266
+ */
267
+ private getContractConfig;
192
268
  /**
193
269
  * Get the curated PUBLIC-tier component configs (ADR-0080) — those registered
194
270
  * with `tier: 'public'`. This is the contract/AI-vocabulary surface, a subset
195
271
  * of the full rendering capability returned by {@link getAllConfigs}.
272
+ *
273
+ * Includes blocks that are only lazily registered so far; those come back
274
+ * with `lazy: true` and no `component` (see {@link PublicComponentConfig}).
196
275
  */
197
- getPublicConfigs(): ComponentConfig<T>[];
276
+ getPublicConfigs(): PublicComponentConfig<T>[];
198
277
  /**
199
278
  * Get all components in a specific namespace.
200
279
  *