@object-ui/core 16.1.0 → 17.2.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 (60) hide show
  1. package/CHANGELOG.md +1881 -4
  2. package/dist/actions/ActionEngine.d.ts +11 -1
  3. package/dist/actions/ActionEngine.js +14 -3
  4. package/dist/actions/ActionRunner.d.ts +199 -24
  5. package/dist/actions/ActionRunner.js +189 -84
  6. package/dist/actions/actionKeys.d.ts +135 -0
  7. package/dist/actions/actionKeys.js +283 -0
  8. package/dist/actions/index.d.ts +1 -0
  9. package/dist/actions/index.js +1 -0
  10. package/dist/adapters/ApiDataSource.js +10 -1
  11. package/dist/adapters/ValueDataSource.d.ts +2 -2
  12. package/dist/evaluator/fieldRules.d.ts +0 -2
  13. package/dist/evaluator/fieldRules.js +3 -2
  14. package/dist/evaluator/optionLint.js +5 -2
  15. package/dist/evaluator/optionRules.d.ts +13 -6
  16. package/dist/evaluator/optionRules.js +0 -23
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.js +7 -0
  19. package/dist/query/query-ast.d.ts +6 -6
  20. package/dist/query/query-ast.js +2 -2
  21. package/dist/registry/PluginSystem.d.ts +15 -5
  22. package/dist/registry/PluginSystem.js +1 -1
  23. package/dist/registry/Registry.d.ts +81 -2
  24. package/dist/registry/Registry.js +102 -2
  25. package/dist/registry/WidgetRegistry.d.ts +7 -7
  26. package/dist/registry/WidgetRegistry.js +1 -1
  27. package/dist/registry/public-blocks.js +52 -1
  28. package/dist/styling/scoped-styles.d.ts +14 -8
  29. package/dist/theme/ThemeEngine.js +0 -12
  30. package/dist/utils/chart-series.d.ts +42 -2
  31. package/dist/utils/chart-series.js +55 -0
  32. package/dist/utils/column-identity.d.ts +110 -0
  33. package/dist/utils/column-identity.js +210 -0
  34. package/dist/utils/dashboard-filters.d.ts +11 -2
  35. package/dist/utils/dashboard-filters.js +99 -4
  36. package/dist/utils/dataset-format.d.ts +13 -1
  37. package/dist/utils/dataset-format.js +10 -2
  38. package/dist/utils/expand-fields.js +8 -8
  39. package/dist/utils/filter-converter.d.ts +47 -0
  40. package/dist/utils/filter-converter.js +94 -7
  41. package/dist/utils/filter-tokens.d.ts +108 -0
  42. package/dist/utils/filter-tokens.js +180 -0
  43. package/dist/utils/freeze-schema.d.ts +14 -6
  44. package/dist/utils/freeze-schema.js +14 -6
  45. package/dist/utils/managedBy.d.ts +97 -51
  46. package/dist/utils/managedBy.js +117 -53
  47. package/dist/utils/normalize-list-view.d.ts +109 -0
  48. package/dist/utils/normalize-list-view.js +255 -0
  49. package/dist/utils/sort-values.d.ts +52 -0
  50. package/dist/utils/sort-values.js +125 -0
  51. package/dist/validation/index.d.ts +10 -1
  52. package/dist/validation/index.js +10 -1
  53. package/dist/validation/required-presence.d.ts +31 -0
  54. package/dist/validation/required-presence.js +36 -0
  55. package/dist/validation/schema-validator.d.ts +24 -8
  56. package/dist/validation/validators/index.d.ts +7 -1
  57. package/dist/validation/validators/index.js +7 -1
  58. package/dist/validation/validators/object-validation-engine.d.ts +151 -31
  59. package/dist/validation/validators/object-validation-engine.js +275 -142
  60. package/package.json +4 -4
@@ -11,6 +11,32 @@
11
11
  * @param operator - MongoDB-style operator (e.g., '$gte', '$in')
12
12
  * @returns ObjectStack operator or null if not recognized
13
13
  */
14
+ /**
15
+ * A filter operator this layer will not translate.
16
+ *
17
+ * Carries the data API's own code and status for the same refusal so a failed
18
+ * list renders "the filter is malformed" rather than "check your connection" —
19
+ * `classifyLoadError` reads these, and a bare `Error` classifies as a network
20
+ * fault, which is the one thing this is definitely not.
21
+ */
22
+ export class FilterOperatorError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ Object.defineProperty(this, "code", {
26
+ enumerable: true,
27
+ configurable: true,
28
+ writable: true,
29
+ value: 'INVALID_FILTER'
30
+ });
31
+ Object.defineProperty(this, "httpStatus", {
32
+ enumerable: true,
33
+ configurable: true,
34
+ writable: true,
35
+ value: 400
36
+ });
37
+ this.name = 'FilterOperatorError';
38
+ }
39
+ }
14
40
  export function convertOperatorToAST(operator) {
15
41
  // Spec reference: framework/packages/spec/src/data/filter.zod.ts
16
42
  // Canonical MongoDB-style keys are camelCase ($startsWith, $endsWith, $notContains).
@@ -69,14 +95,22 @@ export function convertFiltersToAST(filter) {
69
95
  if (typeof value === 'object' && !Array.isArray(value)) {
70
96
  // Handle operator-based filters
71
97
  for (const [operator, operatorValue] of Object.entries(value)) {
72
- // Special handling for $regex - warn users about limited support
98
+ // `$regex` is refused, not downgraded. It used to become `contains`
99
+ // behind a `console.warn` — but substring matching is a DIFFERENT
100
+ // QUESTION, not a weaker version of the same one: `$regex: 'a.c'`
101
+ // matches "abc", `contains 'a.c'` does not, and neither result looks
102
+ // wrong on screen. A warning is not an error channel; nobody reads the
103
+ // console of a deployed app.
104
+ //
105
+ // The spec has no `$regex` (`FILTER_OPERATORS`, data/filter.zod.ts), so
106
+ // there is nothing to translate it INTO. Same treatment the unknown
107
+ // operator below already gets, and for the same stated reason.
73
108
  if (operator === '$regex') {
74
- console.warn(`[ObjectUI] Warning: $regex operator is not fully supported. ` +
75
- `Converting to 'contains' which only supports substring matching, not regex patterns. ` +
109
+ throw new FilterOperatorError(`[ObjectUI] The '$regex' filter operator is not supported. It used to be ` +
110
+ `converted to 'contains', which matches a literal substring rather than a ` +
111
+ `pattern — a different result, not a degraded one. ` +
76
112
  `Field: '${field}', Value: ${JSON.stringify(operatorValue)}. ` +
77
- `Consider using $contains or $startsWith instead.`);
78
- conditions.push([field, 'contains', operatorValue]);
79
- continue;
113
+ `Use $contains, $startsWith or $endsWith.`);
80
114
  }
81
115
  // $null / $exists translate based on their boolean value (per spec semantics).
82
116
  // $null: true → IS NULL | $null: false → IS NOT NULL
@@ -95,7 +129,7 @@ export function convertFiltersToAST(filter) {
95
129
  }
96
130
  else {
97
131
  // Unknown operator - throw error to avoid silent failure
98
- throw new Error(`[ObjectUI] Unknown filter operator '${operator}' for field '${field}'. ` +
132
+ throw new FilterOperatorError(`[ObjectUI] Unknown filter operator '${operator}' for field '${field}'. ` +
99
133
  `Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $between, ` +
100
134
  `$contains, $notContains, $startsWith, $endsWith, $null, $exists. ` +
101
135
  `If you need exact object matching, use the value directly without an operator.`);
@@ -118,3 +152,56 @@ export function convertFiltersToAST(filter) {
118
152
  // Multiple conditions: combine with 'and'
119
153
  return ['and', ...conditions];
120
154
  }
155
+ /**
156
+ * Normalize ONE filter source into a single filter node.
157
+ *
158
+ * A "source" is whatever a view hands a renderer, and there are three shapes in
159
+ * circulation, all legitimate:
160
+ *
161
+ * - `[{ field, operator, value }, ...]` a spec `ViewFilterRule[]`
162
+ * - `[['stage', '=', 'won'], ...]` an AST node / legacy flat array
163
+ * - `{ status: 'active' }` a MongoDB-style object
164
+ *
165
+ * The third is the one that kept getting lost. Renderers tested `source.length
166
+ * > 0` before using it, which is `undefined > 0` for an object — so a
167
+ * `table.defaultFilters` (declared `Record<string, any>`) was DROPPED and the
168
+ * view returned every record. Silently: no error, just a wider answer.
169
+ *
170
+ * Returns `undefined` for an absent or empty source, so callers can skip
171
+ * `$filter` rather than sending an empty array.
172
+ */
173
+ export function toFilterNode(source) {
174
+ if (source === null || source === undefined)
175
+ return undefined;
176
+ if (Array.isArray(source))
177
+ return source.length > 0 ? source : undefined;
178
+ if (typeof source !== 'object')
179
+ return undefined;
180
+ const obj = source;
181
+ if (Object.keys(obj).length === 0)
182
+ return undefined;
183
+ // MongoDB-style → AST, so it can sit beside the other shapes under one `and`.
184
+ return convertFiltersToAST(obj);
185
+ }
186
+ /**
187
+ * Combine filter sources under a single `and`, each as its OWN child.
188
+ *
189
+ * Wrapping rather than spreading, on purpose. `['and', ...rules]` looks
190
+ * equivalent and is not: spreading a `ViewFilterRule[]` puts bare rule OBJECTS
191
+ * where the AST expects nodes, and the server neither understands nor rejects
192
+ * that cleanly — `isFilterAST` says no (a 400 since objectstack#4121), while
193
+ * `parseFilterAST` reads the rule as a Mongo condition and filters on columns
194
+ * literally named `field` / `operator` / `value`. Spreading is only correct
195
+ * when the source happens to be an array of nodes, which is why it survived.
196
+ *
197
+ * Sources that normalize to nothing are skipped; one surviving source is
198
+ * returned as-is rather than wrapped in a pointless `and`.
199
+ */
200
+ export function mergeFilterNodes(...sources) {
201
+ const nodes = sources.map(toFilterNode).filter((n) => n !== undefined);
202
+ if (nodes.length === 0)
203
+ return undefined;
204
+ if (nodes.length === 1)
205
+ return nodes[0];
206
+ return ['and', ...nodes];
207
+ }
@@ -0,0 +1,108 @@
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
+ import { CONTEXT_TOKENS } from '@objectstack/spec/data';
9
+ /**
10
+ * Session-scoped filter placeholders — the sibling vocabulary to
11
+ * `{date-macros}`.
12
+ *
13
+ * Filter values travel as JSON, so a user-scoped slice cannot call
14
+ * `currentUser().id` inline; it writes a placeholder that this module expands
15
+ * immediately before the filter reaches the data source:
16
+ *
17
+ * { owner_id: '{current_user_id}' }
18
+ *
19
+ * ## Why this module exists (framework #3574)
20
+ *
21
+ * There used to be no shared resolver. Three ad-hoc implementations had grown
22
+ * up independently — one in `ObjectView` for list views, one in
23
+ * `ObjectDataPage` for URL filter triples, one in `NavigationRenderer` for
24
+ * hrefs — and each handled only the shape its own surface happened to use.
25
+ * Dashboard widgets never got one at all, so `{current_user_id}` in a widget
26
+ * filter reached SQL as a literal, matched no row, and the widget rendered
27
+ * `0` with no error in the console or the server log.
28
+ *
29
+ * The failure mode is the point: a metric reading `0` is indistinguishable
30
+ * from a metric that is legitimately zero, so the bug survived review and
31
+ * shipped. Any *new* surface that resolves placeholders itself will
32
+ * eventually reproduce it, which is why resolution lives here, once, and
33
+ * every surface calls {@link resolveFilterPlaceholders}.
34
+ *
35
+ * ## Presentation scope, NOT a security boundary
36
+ *
37
+ * These tokens scope what a surface *shows*. They do not decide what a caller
38
+ * is *allowed* to read — that is RLS, enforced server-side against a
39
+ * different vocabulary rooted at `current_user` (`owner_id = current_user.id`).
40
+ * Dropping a `{current_user_id}` filter widens a view; it must never widen
41
+ * access. Never use a context token as an access control.
42
+ *
43
+ * ## Contract source
44
+ *
45
+ * The canonical vocabulary is published as part of the platform contract at
46
+ * `@objectstack/spec/data` → `CONTEXT_TOKENS`. This module used to carry a
47
+ * hand-written copy of that tuple under the spec's own name, with a comment
48
+ * saying the duplication was "temporary until the next coordinated release"
49
+ * — the installed spec (17.0.0-rc.0) has exported it for some time, so the
50
+ * copy was simply a fork wearing the spec's name (objectstack#4115).
51
+ *
52
+ * It is now a **re-export**, which is the only form that cannot drift: the
53
+ * copy was byte-identical, so every value comparison and every behavioural
54
+ * test passed while it sat here. Reference identity is the one check that
55
+ * distinguishes a re-export from a fork (objectui#3003).
56
+ */
57
+ /** The complete set of session-scoped filter tokens (spec-owned, re-exported). */
58
+ export { CONTEXT_TOKENS };
59
+ export type ContextTokenName = (typeof CONTEXT_TOKENS)[number];
60
+ /** Session values a filter placeholder can resolve against. */
61
+ export interface FilterTokenScope {
62
+ /** The signed-in user's id. */
63
+ currentUserId?: string | null;
64
+ /** The active organization id. */
65
+ currentOrgId?: string | null;
66
+ /**
67
+ * Called when a placeholder cannot be resolved — either a recognised token
68
+ * with no value in scope (signed out), or a near-miss spelling that resolves
69
+ * in no vocabulary. Defaults to a `console.warn`; pass `null` to silence.
70
+ */
71
+ onUnresolved?: ((message: string) => void) | null;
72
+ }
73
+ /**
74
+ * Expand `{current_user_id}` / `{current_org_id}` inside a filter.
75
+ *
76
+ * Walks arrays and plain objects recursively, which is what makes one
77
+ * resolver cover both platform filter shapes: the MongoDB-style object a
78
+ * dashboard widget carries (`{ owner_id: '{current_user_id}' }`) and the
79
+ * condition/triple arrays a list view carries (`[{ field, operator, value }]`
80
+ * / `[['owner','=','…']]`). The predecessor helper in `ObjectView` bailed on
81
+ * non-arrays (`if (!Array.isArray(filter)) return filter`), which is why
82
+ * lifting it into the dashboard would have been a silent no-op.
83
+ *
84
+ * Only whole-string placeholders are substituted — an id is an opaque value,
85
+ * so embedding it in a larger string (`'user-{current_user_id}'`) is never
86
+ * what an author means, and substituting there would silently produce a value
87
+ * that matches nothing. Unknown tokens are passed through untouched so that
88
+ * date macros and nav-only `AppContextSelector` ids survive this pass.
89
+ *
90
+ * An unresolvable *known* token is deliberately left as-is rather than
91
+ * dropped: leaving it yields an empty result, whereas dropping the clause
92
+ * would widen the result set — and a filter that silently widens is far worse
93
+ * than one that silently narrows.
94
+ */
95
+ export declare function resolveContextTokens<T = any>(filter: T, scope?: FilterTokenScope): T;
96
+ /**
97
+ * Resolve **every** placeholder vocabulary a filter value understands — date
98
+ * macros and context tokens — in one call.
99
+ *
100
+ * This is the function surfaces should call. Calling only one of the two
101
+ * resolvers is the defect behind framework #3574: dashboard widgets called
102
+ * `resolveDateMacros` alone, so `{today}` worked and `{current_user_id}`
103
+ * silently did not.
104
+ *
105
+ * Order is irrelevant (the vocabularies are disjoint) but fixed here so
106
+ * behaviour is identical everywhere.
107
+ */
108
+ export declare function resolveFilterPlaceholders<T = any>(filter: T, scope?: FilterTokenScope, now?: Date): T;
@@ -0,0 +1,180 @@
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
+ import { CONTEXT_TOKENS } from '@objectstack/spec/data';
9
+ import { resolveDateMacros } from './date-macros.js';
10
+ /**
11
+ * Session-scoped filter placeholders — the sibling vocabulary to
12
+ * `{date-macros}`.
13
+ *
14
+ * Filter values travel as JSON, so a user-scoped slice cannot call
15
+ * `currentUser().id` inline; it writes a placeholder that this module expands
16
+ * immediately before the filter reaches the data source:
17
+ *
18
+ * { owner_id: '{current_user_id}' }
19
+ *
20
+ * ## Why this module exists (framework #3574)
21
+ *
22
+ * There used to be no shared resolver. Three ad-hoc implementations had grown
23
+ * up independently — one in `ObjectView` for list views, one in
24
+ * `ObjectDataPage` for URL filter triples, one in `NavigationRenderer` for
25
+ * hrefs — and each handled only the shape its own surface happened to use.
26
+ * Dashboard widgets never got one at all, so `{current_user_id}` in a widget
27
+ * filter reached SQL as a literal, matched no row, and the widget rendered
28
+ * `0` with no error in the console or the server log.
29
+ *
30
+ * The failure mode is the point: a metric reading `0` is indistinguishable
31
+ * from a metric that is legitimately zero, so the bug survived review and
32
+ * shipped. Any *new* surface that resolves placeholders itself will
33
+ * eventually reproduce it, which is why resolution lives here, once, and
34
+ * every surface calls {@link resolveFilterPlaceholders}.
35
+ *
36
+ * ## Presentation scope, NOT a security boundary
37
+ *
38
+ * These tokens scope what a surface *shows*. They do not decide what a caller
39
+ * is *allowed* to read — that is RLS, enforced server-side against a
40
+ * different vocabulary rooted at `current_user` (`owner_id = current_user.id`).
41
+ * Dropping a `{current_user_id}` filter widens a view; it must never widen
42
+ * access. Never use a context token as an access control.
43
+ *
44
+ * ## Contract source
45
+ *
46
+ * The canonical vocabulary is published as part of the platform contract at
47
+ * `@objectstack/spec/data` → `CONTEXT_TOKENS`. This module used to carry a
48
+ * hand-written copy of that tuple under the spec's own name, with a comment
49
+ * saying the duplication was "temporary until the next coordinated release"
50
+ * — the installed spec (17.0.0-rc.0) has exported it for some time, so the
51
+ * copy was simply a fork wearing the spec's name (objectstack#4115).
52
+ *
53
+ * It is now a **re-export**, which is the only form that cannot drift: the
54
+ * copy was byte-identical, so every value comparison and every behavioural
55
+ * test passed while it sat here. Reference identity is the one check that
56
+ * distinguishes a re-export from a fork (objectui#3003).
57
+ */
58
+ /** The complete set of session-scoped filter tokens (spec-owned, re-exported). */
59
+ export { CONTEXT_TOKENS };
60
+ /**
61
+ * Near-miss spellings → the token the author meant.
62
+ *
63
+ * Every entry is a real authoring mistake, and each is a correct spelling
64
+ * *somewhere else* in the platform, which is exactly why authors reach for it:
65
+ * `current_user` is the RLS expression root, `{user_id}` is valid
66
+ * `titleFormat` field interpolation, `organization_id` is a real column name.
67
+ *
68
+ * Mirrors `CONTEXT_TOKEN_SUGGESTIONS` in `@objectstack/spec`. Used only to
69
+ * make the runtime warning actionable; the authoring-time gate
70
+ * (`validateFilterTokens` in `@objectstack/lint`) is what actually prevents
71
+ * these from shipping.
72
+ */
73
+ const CONTEXT_TOKEN_SUGGESTIONS = {
74
+ current_user: 'current_user_id',
75
+ current_user_email: 'current_user_id',
76
+ user_id: 'current_user_id',
77
+ userid: 'current_user_id',
78
+ me: 'current_user_id',
79
+ current_organization_id: 'current_org_id',
80
+ org_id: 'current_org_id',
81
+ organization_id: 'current_org_id',
82
+ current_tenant_id: 'current_org_id',
83
+ };
84
+ /** Whole-string placeholder: `{token}` or `${token}`, anchored. */
85
+ const WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/;
86
+ function isContextToken(token) {
87
+ return CONTEXT_TOKENS.includes(token);
88
+ }
89
+ /**
90
+ * Expand `{current_user_id}` / `{current_org_id}` inside a filter.
91
+ *
92
+ * Walks arrays and plain objects recursively, which is what makes one
93
+ * resolver cover both platform filter shapes: the MongoDB-style object a
94
+ * dashboard widget carries (`{ owner_id: '{current_user_id}' }`) and the
95
+ * condition/triple arrays a list view carries (`[{ field, operator, value }]`
96
+ * / `[['owner','=','…']]`). The predecessor helper in `ObjectView` bailed on
97
+ * non-arrays (`if (!Array.isArray(filter)) return filter`), which is why
98
+ * lifting it into the dashboard would have been a silent no-op.
99
+ *
100
+ * Only whole-string placeholders are substituted — an id is an opaque value,
101
+ * so embedding it in a larger string (`'user-{current_user_id}'`) is never
102
+ * what an author means, and substituting there would silently produce a value
103
+ * that matches nothing. Unknown tokens are passed through untouched so that
104
+ * date macros and nav-only `AppContextSelector` ids survive this pass.
105
+ *
106
+ * An unresolvable *known* token is deliberately left as-is rather than
107
+ * dropped: leaving it yields an empty result, whereas dropping the clause
108
+ * would widen the result set — and a filter that silently widens is far worse
109
+ * than one that silently narrows.
110
+ */
111
+ export function resolveContextTokens(filter, scope = {}) {
112
+ if (filter == null)
113
+ return filter;
114
+ const { currentUserId, currentOrgId } = scope;
115
+ const warn = scope.onUnresolved === null
116
+ ? () => { }
117
+ : scope.onUnresolved ??
118
+ ((message) => {
119
+ // eslint-disable-next-line no-console
120
+ console.warn(`[object-ui] ${message}`);
121
+ });
122
+ const values = {
123
+ current_user_id: currentUserId,
124
+ current_org_id: currentOrgId,
125
+ };
126
+ const walk = (value) => {
127
+ if (value == null)
128
+ return value;
129
+ if (typeof value === 'string') {
130
+ const m = value.match(WHOLE_TOKEN_RE);
131
+ if (!m)
132
+ return value;
133
+ const token = m[1];
134
+ if (isContextToken(token)) {
135
+ const resolved = values[token];
136
+ if (resolved != null && resolved !== '')
137
+ return resolved;
138
+ warn(`Filter placeholder "{${token}}" could not be resolved — no ${token === 'current_user_id' ? 'signed-in user' : 'active organization'} in scope. The filter will match no records.`);
139
+ return value;
140
+ }
141
+ // Not ours. Warn only for near-misses, which resolve in no vocabulary
142
+ // at all and would otherwise fail silently; genuine date macros and
143
+ // nav context-selector ids must pass through quietly.
144
+ const suggestion = CONTEXT_TOKEN_SUGGESTIONS[token.toLowerCase()];
145
+ if (suggestion) {
146
+ warn(`Filter placeholder "{${token}}" is not a recognised token — did you mean ` +
147
+ `"{${suggestion}}"? It is sent to the server as a literal string and will ` +
148
+ `match no records.`);
149
+ }
150
+ return value;
151
+ }
152
+ if (Array.isArray(value))
153
+ return value.map(walk);
154
+ if (typeof value === 'object') {
155
+ const out = {};
156
+ for (const k of Object.keys(value))
157
+ out[k] = walk(value[k]);
158
+ return out;
159
+ }
160
+ return value;
161
+ };
162
+ return walk(filter);
163
+ }
164
+ /**
165
+ * Resolve **every** placeholder vocabulary a filter value understands — date
166
+ * macros and context tokens — in one call.
167
+ *
168
+ * This is the function surfaces should call. Calling only one of the two
169
+ * resolvers is the defect behind framework #3574: dashboard widgets called
170
+ * `resolveDateMacros` alone, so `{today}` worked and `{current_user_id}`
171
+ * silently did not.
172
+ *
173
+ * Order is irrelevant (the vocabularies are disjoint) but fixed here so
174
+ * behaviour is identical everywhere.
175
+ */
176
+ export function resolveFilterPlaceholders(filter, scope = {}, now = new Date()) {
177
+ if (filter == null)
178
+ return filter;
179
+ return resolveContextTokens(resolveDateMacros(filter, now), scope);
180
+ }
@@ -9,7 +9,7 @@
9
9
  * System View immutability layer.
10
10
  *
11
11
  * A "System View" is any UI schema authored in source code (imported `.ts`/`.json`,
12
- * `as const` literals, or returned from a `defineView()` call). Such schemas are
12
+ * `as const` literals, or returned from a `defineSystemView()` call). Such schemas are
13
13
  * part of the product contract and MUST NOT be mutated at runtime. Mutation
14
14
  * would cause behavior drift, break TypeScript inference, and bypass code review.
15
15
  *
@@ -37,7 +37,7 @@ export type DeepReadonly<T> = T extends (...args: any[]) => any ? T : T extends
37
37
  readonly [K in keyof T]: DeepReadonly<T[K]>;
38
38
  } : T;
39
39
  /**
40
- * A schema that has been frozen by `defineView()`. The marker symbol is
40
+ * A schema that has been frozen by `defineSystemView()`. The marker symbol is
41
41
  * non-enumerable and therefore invisible to consumers, but its presence
42
42
  * lets us discriminate at runtime.
43
43
  */
@@ -52,11 +52,19 @@ export declare function deepFreeze<T>(value: T, seen?: WeakSet<object>): DeepRea
52
52
  /**
53
53
  * Mark and freeze a code-loaded schema as a System View.
54
54
  *
55
+ * Named `defineView` until objectstack#4115, which is a name `@objectstack/spec`
56
+ * owns for something else entirely: the spec's `defineView(config)` is the
57
+ * VIEW-DOCUMENT factory — it parses a `ViewSchema` (`{ list, form, listViews,
58
+ * formViews }`) and returns a validated `View`. This one takes an arbitrary
59
+ * object, stamps the System-View marker and deep-freezes it; it validates
60
+ * nothing. An app-shell comment already conflated the two, which is precisely
61
+ * the planted-premise failure the guard exists to stop.
62
+ *
55
63
  * @example
56
64
  * ```ts
57
- * import { defineView } from '@object-ui/core';
65
+ * import { defineSystemView } from '@object-ui/core';
58
66
  *
59
- * export const userListView = defineView({
67
+ * export const userListView = defineSystemView({
60
68
  * type: 'list',
61
69
  * data: { object: 'User' },
62
70
  * columns: [{ name: 'email' }],
@@ -69,9 +77,9 @@ export declare function deepFreeze<T>(value: T, seen?: WeakSet<object>): DeepRea
69
77
  * To produce a mutable derivative (Tenant or User View), call
70
78
  * `cloneAsOverride(userListView)`.
71
79
  */
72
- export declare function defineView<T extends object>(schema: T): SystemView<T>;
80
+ export declare function defineSystemView<T extends object>(schema: T): SystemView<T>;
73
81
  /**
74
- * Runtime check: was this object produced by `defineView()` (or loaded from a
82
+ * Runtime check: was this object produced by `defineSystemView()` (or loaded from a
75
83
  * source that forwards the marker)?
76
84
  */
77
85
  export declare function isSystemView(value: unknown): boolean;
@@ -9,7 +9,7 @@
9
9
  * System View immutability layer.
10
10
  *
11
11
  * A "System View" is any UI schema authored in source code (imported `.ts`/`.json`,
12
- * `as const` literals, or returned from a `defineView()` call). Such schemas are
12
+ * `as const` literals, or returned from a `defineSystemView()` call). Such schemas are
13
13
  * part of the product contract and MUST NOT be mutated at runtime. Mutation
14
14
  * would cause behavior drift, break TypeScript inference, and bypass code review.
15
15
  *
@@ -86,11 +86,19 @@ export function deepFreeze(value, seen = new WeakSet()) {
86
86
  /**
87
87
  * Mark and freeze a code-loaded schema as a System View.
88
88
  *
89
+ * Named `defineView` until objectstack#4115, which is a name `@objectstack/spec`
90
+ * owns for something else entirely: the spec's `defineView(config)` is the
91
+ * VIEW-DOCUMENT factory — it parses a `ViewSchema` (`{ list, form, listViews,
92
+ * formViews }`) and returns a validated `View`. This one takes an arbitrary
93
+ * object, stamps the System-View marker and deep-freezes it; it validates
94
+ * nothing. An app-shell comment already conflated the two, which is precisely
95
+ * the planted-premise failure the guard exists to stop.
96
+ *
89
97
  * @example
90
98
  * ```ts
91
- * import { defineView } from '@object-ui/core';
99
+ * import { defineSystemView } from '@object-ui/core';
92
100
  *
93
- * export const userListView = defineView({
101
+ * export const userListView = defineSystemView({
94
102
  * type: 'list',
95
103
  * data: { object: 'User' },
96
104
  * columns: [{ name: 'email' }],
@@ -103,9 +111,9 @@ export function deepFreeze(value, seen = new WeakSet()) {
103
111
  * To produce a mutable derivative (Tenant or User View), call
104
112
  * `cloneAsOverride(userListView)`.
105
113
  */
106
- export function defineView(schema) {
114
+ export function defineSystemView(schema) {
107
115
  if (schema == null || typeof schema !== 'object') {
108
- throw new TypeError('[ObjectUI] defineView() expects a non-null object schema.');
116
+ throw new TypeError('[ObjectUI] defineSystemView() expects a non-null object schema.');
109
117
  }
110
118
  // Stamp the marker on the root only — nested nodes share origin via lineage.
111
119
  // Non-enumerable keeps it out of JSON, spreads, and Object.keys.
@@ -120,7 +128,7 @@ export function defineView(schema) {
120
128
  return deepFreeze(schema);
121
129
  }
122
130
  /**
123
- * Runtime check: was this object produced by `defineView()` (or loaded from a
131
+ * Runtime check: was this object produced by `defineSystemView()` (or loaded from a
124
132
  * source that forwards the marker)?
125
133
  */
126
134
  export function isSystemView(value) {