@object-ui/core 17.5.0 → 17.7.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 (157) hide show
  1. package/CHANGELOG.md +7404 -0
  2. package/README.md +127 -26
  3. package/dist/actions/ActionEngine.d.ts +5 -3
  4. package/dist/actions/ActionEngine.js +14 -10
  5. package/dist/actions/ActionRunner.d.ts +363 -30
  6. package/dist/actions/ActionRunner.js +544 -58
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +75 -13
  9. package/dist/actions/actionKeys.js +131 -16
  10. package/dist/actions/actionResponse.js +3 -3
  11. package/dist/actions/index.d.ts +2 -0
  12. package/dist/actions/index.js +2 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/actions/recordIdParam.d.ts +77 -0
  16. package/dist/actions/recordIdParam.js +37 -0
  17. package/dist/actions/serverActionHandler.d.ts +1 -1
  18. package/dist/actions/serverActionHandler.js +1 -1
  19. package/dist/adapters/ValueDataSource.d.ts +6 -1
  20. package/dist/adapters/ValueDataSource.js +1090 -98
  21. package/dist/adapters/batchTransaction.d.ts +1 -1
  22. package/dist/adapters/batchTransaction.js +1 -1
  23. package/dist/builder/schema-builder.d.ts +38 -81
  24. package/dist/builder/schema-builder.js +33 -152
  25. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  26. package/dist/data-scope/ViewDataProvider.js +21 -1
  27. package/dist/data-scope/element-data-source.d.ts +261 -7
  28. package/dist/data-scope/element-data-source.js +342 -52
  29. package/dist/data-scope/index.d.ts +9 -4
  30. package/dist/data-scope/index.js +9 -4
  31. package/dist/evaluator/ExpressionEvaluator.d.ts +128 -1
  32. package/dist/evaluator/ExpressionEvaluator.js +226 -13
  33. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  34. package/dist/evaluator/FormulaFunctions.js +166 -27
  35. package/dist/evaluator/SafeExpressionParser.js +0 -4
  36. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  37. package/dist/evaluator/declaredPredicate.js +118 -17
  38. package/dist/evaluator/fieldRules.d.ts +107 -4
  39. package/dist/evaluator/fieldRules.js +231 -35
  40. package/dist/evaluator/index.d.ts +2 -0
  41. package/dist/evaluator/index.js +2 -0
  42. package/dist/evaluator/listConditional.d.ts +125 -9
  43. package/dist/evaluator/listConditional.js +167 -26
  44. package/dist/evaluator/optionRules.d.ts +126 -0
  45. package/dist/evaluator/optionRules.js +141 -4
  46. package/dist/evaluator/predicateInput.d.ts +15 -4
  47. package/dist/evaluator/predicateInput.js +35 -2
  48. package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
  49. package/dist/evaluator/rowPredicateCanon.js +158 -0
  50. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  51. package/dist/evaluator/subjectPermissions.js +44 -0
  52. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  53. package/dist/evaluator/unevaluablePredicate.js +70 -0
  54. package/dist/index.d.ts +18 -0
  55. package/dist/index.js +97 -2
  56. package/dist/protocols/index.d.ts +0 -1
  57. package/dist/protocols/index.js +9 -1
  58. package/dist/registry/Registry.d.ts +397 -48
  59. package/dist/registry/Registry.js +263 -11
  60. package/dist/registry/WidgetRegistry.js +7 -3
  61. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  62. package/dist/registry/html-tier-intrinsics.js +110 -0
  63. package/dist/registry/public-blocks.d.ts +8 -0
  64. package/dist/registry/public-blocks.js +57 -0
  65. package/dist/theme/ThemeEngine.js +1 -1
  66. package/dist/theme/index.d.ts +1 -1
  67. package/dist/theme/index.js +1 -1
  68. package/dist/types/index.d.ts +92 -13
  69. package/dist/utils/chart-category-key.d.ts +82 -0
  70. package/dist/utils/chart-category-key.js +84 -0
  71. package/dist/utils/chart-measure-key.d.ts +64 -0
  72. package/dist/utils/chart-measure-key.js +65 -0
  73. package/dist/utils/chart-presentation.d.ts +188 -0
  74. package/dist/utils/chart-presentation.js +345 -0
  75. package/dist/utils/chart-series.d.ts +271 -31
  76. package/dist/utils/chart-series.js +478 -55
  77. package/dist/utils/column-identity.d.ts +43 -0
  78. package/dist/utils/column-identity.js +47 -1
  79. package/dist/utils/column-sortability.d.ts +157 -0
  80. package/dist/utils/column-sortability.js +217 -0
  81. package/dist/utils/dashboard-filters.d.ts +38 -6
  82. package/dist/utils/dashboard-filters.js +107 -61
  83. package/dist/utils/dataset-format.d.ts +211 -30
  84. package/dist/utils/dataset-format.js +407 -52
  85. package/dist/utils/date-display.d.ts +354 -0
  86. package/dist/utils/date-display.js +463 -0
  87. package/dist/utils/date-macros.js +7 -0
  88. package/dist/utils/debug.js +7 -0
  89. package/dist/utils/drill-down.d.ts +78 -0
  90. package/dist/utils/drill-down.js +93 -0
  91. package/dist/utils/emptiness.d.ts +96 -0
  92. package/dist/utils/emptiness.js +101 -0
  93. package/dist/utils/expand-fields.d.ts +171 -11
  94. package/dist/utils/expand-fields.js +171 -11
  95. package/dist/utils/extract-records.d.ts +65 -5
  96. package/dist/utils/extract-records.js +65 -8
  97. package/dist/utils/filter-converter.d.ts +152 -4
  98. package/dist/utils/filter-converter.js +1092 -22
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -34
  101. package/dist/utils/freeze-schema.d.ts +38 -1
  102. package/dist/utils/freeze-schema.js +10 -0
  103. package/dist/utils/grouping-fields.d.ts +61 -0
  104. package/dist/utils/grouping-fields.js +84 -0
  105. package/dist/utils/humanize-label.d.ts +60 -0
  106. package/dist/utils/humanize-label.js +62 -0
  107. package/dist/utils/managedBy.d.ts +30 -5
  108. package/dist/utils/managedBy.js +5 -5
  109. package/dist/utils/native-date-value.d.ts +48 -0
  110. package/dist/utils/native-date-value.js +167 -0
  111. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  112. package/dist/utils/non-grid-row-ceiling.js +141 -0
  113. package/dist/utils/normalize-list-view.d.ts +131 -1
  114. package/dist/utils/normalize-list-view.js +248 -6
  115. package/dist/utils/number-display.d.ts +70 -17
  116. package/dist/utils/number-display.js +34 -12
  117. package/dist/utils/object-aggregate-query.d.ts +113 -0
  118. package/dist/utils/object-aggregate-query.js +50 -0
  119. package/dist/utils/parent-scope.d.ts +123 -0
  120. package/dist/utils/parent-scope.js +140 -0
  121. package/dist/utils/predicate-fields.d.ts +62 -4
  122. package/dist/utils/predicate-fields.js +169 -4
  123. package/dist/utils/record-color.d.ts +75 -0
  124. package/dist/utils/record-color.js +65 -0
  125. package/dist/utils/record-source.d.ts +298 -0
  126. package/dist/utils/record-source.js +331 -0
  127. package/dist/utils/record-title.d.ts +148 -8
  128. package/dist/utils/record-title.js +220 -26
  129. package/dist/utils/reference-keys.d.ts +21 -22
  130. package/dist/utils/reference-keys.js +532 -29
  131. package/dist/utils/retired-field-types.d.ts +90 -0
  132. package/dist/utils/retired-field-types.js +124 -0
  133. package/dist/utils/sort-query.d.ts +128 -26
  134. package/dist/utils/sort-query.js +240 -13
  135. package/dist/utils/text-comparand.d.ts +92 -0
  136. package/dist/utils/text-comparand.js +99 -0
  137. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  138. package/dist/utils/unmaterialized-fields.js +57 -0
  139. package/dist/utils/without-denied-fields.d.ts +49 -0
  140. package/dist/utils/without-denied-fields.js +52 -0
  141. package/dist/validation/index.d.ts +9 -4
  142. package/dist/validation/index.js +9 -4
  143. package/dist/validation/schema-validator.d.ts +1 -1
  144. package/dist/validation/schema-validator.js +305 -82
  145. package/dist/validation/server-owned-value.d.ts +62 -0
  146. package/dist/validation/server-owned-value.js +99 -0
  147. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  148. package/dist/validation/validators/object-validation-engine.js +14 -5
  149. package/package.json +8 -9
  150. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  151. package/dist/__benchmarks__/core.bench.js +0 -53
  152. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  153. package/dist/data-scope/DataScopeManager.js +0 -229
  154. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  155. package/dist/protocols/ResponsiveProtocol.js +0 -158
  156. package/dist/validation/validation-engine.d.ts +0 -88
  157. package/dist/validation/validation-engine.js +0 -434
@@ -16,7 +16,7 @@ import { CONTEXT_TOKENS } from '@objectstack/spec/data';
16
16
  *
17
17
  * { owner_id: '{current_user_id}' }
18
18
  *
19
- * ## Why this module exists (framework #3574)
19
+ * ## Why this module exists (objectstack-ai/objectstack#3574)
20
20
  *
21
21
  * There used to be no shared resolver. Three ad-hoc implementations had grown
22
22
  * up independently — one in `ObjectView` for list views, one in
@@ -40,6 +40,25 @@ import { CONTEXT_TOKENS } from '@objectstack/spec/data';
40
40
  * Dropping a `{current_user_id}` filter widens a view; it must never widen
41
41
  * access. Never use a context token as an access control.
42
42
  *
43
+ * ## `{record_id}` — the record in view, not the session (objectui#7297)
44
+ *
45
+ * `@objectstack/spec` declares one sibling of the two session tokens:
46
+ * `RECORD_CONTEXT_TOKENS` = `{record_id}`, the id of the record a
47
+ * `type: 'record'` page is showing (objectstack-ai/objectstack#20003). It is a
48
+ * separate list, not a member of `CONTEXT_TOKENS`, because it resolves against
49
+ * the SURFACE rather than the session, and only here: the server never knows
50
+ * which record a page shows, so it refuses a filter still carrying the token
51
+ * by name (`FILTER_TOKEN_UNRESOLVED`).
52
+ *
53
+ * Its value is `FilterTokenScope.recordId`, which `useFilterScope()` in
54
+ * `@object-ui/react` fills from the mounted `RecordContextProvider` and from
55
+ * nothing else — never a URL parameter, never a page variable. With no record
56
+ * in scope the token is refused by name through `onUnresolved`, in the voice
57
+ * an unresolved session token gets, and left as written: it never becomes
58
+ * `null` (a count about nobody) and its condition is never dropped (a count
59
+ * about everybody). Like the session tokens it scopes what a record page
60
+ * SHOWS; which of those rows the caller may read is still RLS's decision.
61
+ *
43
62
  * ## Contract source
44
63
  *
45
64
  * The canonical vocabulary is published as part of the platform contract at
@@ -53,6 +72,20 @@ import { CONTEXT_TOKENS } from '@objectstack/spec/data';
53
72
  * copy was byte-identical, so every value comparison and every behavioural
54
73
  * test passed while it sat here. Reference identity is the one check that
55
74
  * distinguishes a re-export from a fork (objectui#3003).
75
+ *
76
+ * The same argument retired the two module-local copies that sat beside it
77
+ * until objectui#7265 — the near-miss suggestion map and the membership
78
+ * predicate are imported from `@objectstack/spec/data` now, so neither can
79
+ * drift either. Both were re-measured against the RESOLVED pin (17.4.0; the
80
+ * card had measured 17.2.0) before the swap, because "byte-identical" is a
81
+ * statement about a version, not a property: the map matched on nine keys, in
82
+ * the same order, with the same values, and the two predicates agreed on every
83
+ * one of 61 real vocabulary members — the token tuple, both suggestion key
84
+ * sets, the date-macro tokens and their aliases, plus casing, whitespace and
85
+ * prototype-key spellings. Each comparison ran a lit control of the same kind
86
+ * through the same comparator (the spec's own token-description map, and its
87
+ * date-macro predicate), so "no difference" came from an instrument shown able
88
+ * to report one.
56
89
  */
57
90
  /** The complete set of session-scoped filter tokens (spec-owned, re-exported). */
58
91
  export { CONTEXT_TOKENS };
@@ -63,15 +96,26 @@ export interface FilterTokenScope {
63
96
  currentUserId?: string | null;
64
97
  /** The active organization id. */
65
98
  currentOrgId?: string | null;
99
+ /**
100
+ * The id of the record the surface is bound to, which resolves
101
+ * `{record_id}` (objectui#7297): the mounted record context of a
102
+ * `type: 'record'` page. `useFilterScope()` in `@object-ui/react` fills it
103
+ * from the nearest `RecordContextProvider`, and a host that renders no
104
+ * record leaves it unset, so the token is refused there. ⛔ Never fill it
105
+ * from a URL parameter or a page variable.
106
+ */
107
+ recordId?: string | null;
66
108
  /**
67
109
  * 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.
110
+ * with no value in scope (signed out, or no record in context), or a
111
+ * near-miss spelling that resolves in no vocabulary. Defaults to a
112
+ * `console.warn`; pass `null` to silence.
70
113
  */
71
114
  onUnresolved?: ((message: string) => void) | null;
72
115
  }
73
116
  /**
74
- * Expand `{current_user_id}` / `{current_org_id}` inside a filter.
117
+ * Expand `{current_user_id}` / `{current_org_id}`, and `{record_id}` where a
118
+ * record is in scope, inside a filter.
75
119
  *
76
120
  * Walks arrays and plain objects recursively, which is what makes one
77
121
  * resolver cover both platform filter shapes: the MongoDB-style object a
@@ -98,7 +142,7 @@ export declare function resolveContextTokens<T = any>(filter: T, scope?: FilterT
98
142
  * macros and context tokens — in one call.
99
143
  *
100
144
  * This is the function surfaces should call. Calling only one of the two
101
- * resolvers is the defect behind framework #3574: dashboard widgets called
145
+ * resolvers is the defect behind objectstack-ai/objectstack#3574: dashboard widgets called
102
146
  * `resolveDateMacros` alone, so `{today}` worked and `{current_user_id}`
103
147
  * silently did not.
104
148
  *
@@ -5,7 +5,7 @@
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
- import { CONTEXT_TOKENS } from '@objectstack/spec/data';
8
+ import { CONTEXT_TOKENS, CONTEXT_TOKEN_SUGGESTIONS, isContextToken, isRecordContextToken, } from '@objectstack/spec/data';
9
9
  import { resolveDateMacros } from './date-macros.js';
10
10
  /**
11
11
  * Session-scoped filter placeholders — the sibling vocabulary to
@@ -17,7 +17,7 @@ import { resolveDateMacros } from './date-macros.js';
17
17
  *
18
18
  * { owner_id: '{current_user_id}' }
19
19
  *
20
- * ## Why this module exists (framework #3574)
20
+ * ## Why this module exists (objectstack-ai/objectstack#3574)
21
21
  *
22
22
  * There used to be no shared resolver. Three ad-hoc implementations had grown
23
23
  * up independently — one in `ObjectView` for list views, one in
@@ -41,6 +41,25 @@ import { resolveDateMacros } from './date-macros.js';
41
41
  * Dropping a `{current_user_id}` filter widens a view; it must never widen
42
42
  * access. Never use a context token as an access control.
43
43
  *
44
+ * ## `{record_id}` — the record in view, not the session (objectui#7297)
45
+ *
46
+ * `@objectstack/spec` declares one sibling of the two session tokens:
47
+ * `RECORD_CONTEXT_TOKENS` = `{record_id}`, the id of the record a
48
+ * `type: 'record'` page is showing (objectstack-ai/objectstack#20003). It is a
49
+ * separate list, not a member of `CONTEXT_TOKENS`, because it resolves against
50
+ * the SURFACE rather than the session, and only here: the server never knows
51
+ * which record a page shows, so it refuses a filter still carrying the token
52
+ * by name (`FILTER_TOKEN_UNRESOLVED`).
53
+ *
54
+ * Its value is `FilterTokenScope.recordId`, which `useFilterScope()` in
55
+ * `@object-ui/react` fills from the mounted `RecordContextProvider` and from
56
+ * nothing else — never a URL parameter, never a page variable. With no record
57
+ * in scope the token is refused by name through `onUnresolved`, in the voice
58
+ * an unresolved session token gets, and left as written: it never becomes
59
+ * `null` (a count about nobody) and its condition is never dropped (a count
60
+ * about everybody). Like the session tokens it scopes what a record page
61
+ * SHOWS; which of those rows the caller may read is still RLS's decision.
62
+ *
44
63
  * ## Contract source
45
64
  *
46
65
  * The canonical vocabulary is published as part of the platform contract at
@@ -54,40 +73,47 @@ import { resolveDateMacros } from './date-macros.js';
54
73
  * copy was byte-identical, so every value comparison and every behavioural
55
74
  * test passed while it sat here. Reference identity is the one check that
56
75
  * distinguishes a re-export from a fork (objectui#3003).
76
+ *
77
+ * The same argument retired the two module-local copies that sat beside it
78
+ * until objectui#7265 — the near-miss suggestion map and the membership
79
+ * predicate are imported from `@objectstack/spec/data` now, so neither can
80
+ * drift either. Both were re-measured against the RESOLVED pin (17.4.0; the
81
+ * card had measured 17.2.0) before the swap, because "byte-identical" is a
82
+ * statement about a version, not a property: the map matched on nine keys, in
83
+ * the same order, with the same values, and the two predicates agreed on every
84
+ * one of 61 real vocabulary members — the token tuple, both suggestion key
85
+ * sets, the date-macro tokens and their aliases, plus casing, whitespace and
86
+ * prototype-key spellings. Each comparison ran a lit control of the same kind
87
+ * through the same comparator (the spec's own token-description map, and its
88
+ * date-macro predicate), so "no difference" came from an instrument shown able
89
+ * to report one.
57
90
  */
58
91
  /** The complete set of session-scoped filter tokens (spec-owned, re-exported). */
59
92
  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
93
  /** Whole-string placeholder: `{token}` or `${token}`, anchored. */
85
94
  const WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/;
86
- function isContextToken(token) {
87
- return CONTEXT_TOKENS.includes(token);
88
- }
89
95
  /**
90
- * Expand `{current_user_id}` / `{current_org_id}` inside a filter.
96
+ * A null-prototype copy of the spec's near-miss suggestion map (objectui#9129).
97
+ *
98
+ * `CONTEXT_TOKEN_SUGGESTIONS` is a plain object, so indexing it with an
99
+ * author-controlled, lower-cased string reaches `Object.prototype` for any
100
+ * spelling that happens to be an inherited member name — `constructor` and
101
+ * `__proto__` today, and, silently, whichever future member is added in
102
+ * lower case (see the lookup below). This is a read-only console hint, never
103
+ * an assignment, so no filter value is ever widened, narrowed or mismatched
104
+ * by it; the only externally visible effect of the underlying bug is a
105
+ * confusing suggestion string in a `warn()` call.
106
+ *
107
+ * Copying into an `Object.create(null)` base closes the whole class rather
108
+ * than special-casing the two spellings measured today: this object has no
109
+ * prototype at all, so *no* key — known or future — can resolve through it.
110
+ * `@objectstack/spec`'s own map is left untouched (out of scope here; the
111
+ * identical shape in its `classifyFilterToken` is the one objectstack `4342c9923` closed).
112
+ */
113
+ const NEAR_MISS_SUGGESTIONS = Object.assign(Object.create(null), CONTEXT_TOKEN_SUGGESTIONS);
114
+ /**
115
+ * Expand `{current_user_id}` / `{current_org_id}`, and `{record_id}` where a
116
+ * record is in scope, inside a filter.
91
117
  *
92
118
  * Walks arrays and plain objects recursively, which is what makes one
93
119
  * resolver cover both platform filter shapes: the MongoDB-style object a
@@ -111,18 +137,30 @@ function isContextToken(token) {
111
137
  export function resolveContextTokens(filter, scope = {}) {
112
138
  if (filter == null)
113
139
  return filter;
114
- const { currentUserId, currentOrgId } = scope;
140
+ const { currentUserId, currentOrgId, recordId } = scope;
115
141
  const warn = scope.onUnresolved === null
116
142
  ? () => { }
117
143
  : scope.onUnresolved ??
118
144
  ((message) => {
119
- // eslint-disable-next-line no-console
120
145
  console.warn(`[object-ui] ${message}`);
121
146
  });
147
+ // Annotated `Record<string, …>` because the spec's predicate returns a plain
148
+ // `boolean`, not the narrowing `token is ContextTokenName` the deleted local
149
+ // copy declared, so the narrowed branch below hands this lookup a `string`.
150
+ // The `satisfies` clause keeps the one thing that narrowing bought: a third
151
+ // context token in the spec reds this literal at compile time instead of
152
+ // resolving to `undefined` at runtime.
122
153
  const values = {
123
154
  current_user_id: currentUserId,
124
155
  current_org_id: currentOrgId,
125
156
  };
157
+ // The record-context sibling, kept apart for the reason the spec keeps the
158
+ // two lists apart: it is filled from the record in view, never from the
159
+ // session. The same `satisfies` ratchet: a second record-context token in
160
+ // the spec reds this literal at compile time.
161
+ const recordValues = {
162
+ record_id: recordId,
163
+ };
126
164
  const walk = (value) => {
127
165
  if (value == null)
128
166
  return value;
@@ -138,10 +176,36 @@ export function resolveContextTokens(filter, scope = {}) {
138
176
  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
177
  return value;
140
178
  }
179
+ // `{record_id}` (objectui#7297). Resolved only from the record in scope.
180
+ // With none, it is refused BY NAME, in the voice of the unresolved
181
+ // session token above, and left as written: never `null` (a count about
182
+ // nobody), never dropped (a count about everybody), and never reported as
183
+ // an unknown spelling, because the spelling is right and the surface is
184
+ // not. The ObjectStack server then refuses the same filter by name
185
+ // (`FILTER_TOKEN_UNRESOLVED`), since no server path has a record in view.
186
+ if (isRecordContextToken(token)) {
187
+ const resolved = recordValues[token];
188
+ if (resolved != null && resolved !== '')
189
+ return resolved;
190
+ warn(`Filter placeholder "{${token}}" could not be resolved — no record in context ` +
191
+ `on this surface. It resolves only on a component of a \`type: 'record'\` page, ` +
192
+ `to the id of the record that page shows. It is left as written, so the filter ` +
193
+ `never widens: the server refuses it by name, and a backend with no resolver ` +
194
+ `matches no records.`);
195
+ return value;
196
+ }
141
197
  // Not ours. Warn only for near-misses, which resolve in no vocabulary
142
198
  // at all and would otherwise fail silently; genuine date macros and
143
199
  // nav context-selector ids must pass through quietly.
144
- const suggestion = CONTEXT_TOKEN_SUGGESTIONS[token.toLowerCase()];
200
+ //
201
+ // Every spelling in the imported map is a real authoring mistake, and
202
+ // each is a correct spelling *somewhere else* in the platform, which is
203
+ // exactly why authors reach for it: `current_user` is the RLS expression
204
+ // root, `{user_id}` is valid `titleFormat` field interpolation,
205
+ // `organization_id` is a real column name. The map only makes the runtime
206
+ // warning actionable; the authoring-time gate (`validateFilterTokens` in
207
+ // `@objectstack/lint`) is what actually prevents these from shipping.
208
+ const suggestion = NEAR_MISS_SUGGESTIONS[token.toLowerCase()];
145
209
  if (suggestion) {
146
210
  warn(`Filter placeholder "{${token}}" is not a recognised token — did you mean ` +
147
211
  `"{${suggestion}}"? It is sent to the server as a literal string and will ` +
@@ -151,7 +215,14 @@ export function resolveContextTokens(filter, scope = {}) {
151
215
  }
152
216
  if (Array.isArray(value))
153
217
  return value.map(walk);
218
+ // Only a PLAIN object (prototype `Object.prototype` or `null`) is a bag to
219
+ // walk. Anything else is a leaf, returned as the same instance: rebuilding
220
+ // a `Date` from its own keys — it has none — turned a comparand the spec
221
+ // admits (`ACCEPTED_FILTER_COMPARAND_TYPES`) into `{}` (objectui#10506).
154
222
  if (typeof value === 'object') {
223
+ const proto = Object.getPrototypeOf(value);
224
+ if (proto !== Object.prototype && proto !== null)
225
+ return value;
155
226
  const out = {};
156
227
  for (const k of Object.keys(value))
157
228
  out[k] = walk(value[k]);
@@ -166,7 +237,7 @@ export function resolveContextTokens(filter, scope = {}) {
166
237
  * macros and context tokens — in one call.
167
238
  *
168
239
  * This is the function surfaces should call. Calling only one of the two
169
- * resolvers is the defect behind framework #3574: dashboard widgets called
240
+ * resolvers is the defect behind objectstack-ai/objectstack#3574: dashboard widgets called
170
241
  * `resolveDateMacros` alone, so `{today}` worked and `{current_user_id}`
171
242
  * silently did not.
172
243
  *
@@ -36,6 +36,33 @@ export declare const SYSTEM_VIEW_MARKER: unique symbol;
36
36
  export type DeepReadonly<T> = T extends (...args: any[]) => any ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends object ? {
37
37
  readonly [K in keyof T]: DeepReadonly<T[K]>;
38
38
  } : T;
39
+ /**
40
+ * The inverse of `DeepReadonly<T>`: recursively strip `readonly` at every depth.
41
+ *
42
+ * Named `DeepMutable` rather than `Mutable` for the same reason the type above
43
+ * is not called `Readonly` — TS' built-in `Readonly<T>` is SHALLOW, so a bare
44
+ * `Mutable` would understate what this does by exactly the depth gap that made
45
+ * `cloneAsOverride` wrong in the first place. A type whose name misreports its
46
+ * own reach is the defect this pair exists to close, not a naming preference.
47
+ *
48
+ * Symmetric with `DeepReadonly` arm for arm, deliberately including its two
49
+ * limits: a tuple widens to an array (`DeepReadonly` widens it the same way, so
50
+ * an inverse that preserved tuples would not be an inverse), and `Date`,
51
+ * `RegExp`, `Map` and `Set` are mapped as plain objects even though
52
+ * `isFreezableObject` skips them at runtime.
53
+ *
54
+ * One thing it deliberately does NOT do: drop the `SYSTEM_VIEW_MARKER` key.
55
+ * `cloneAsOverride` never copies the symbol, so a clone's marker is always
56
+ * absent — but the key is declared optional (`?: true`), and carrying it
57
+ * therefore states "may be absent", which is true. Excluding it needs
58
+ * `Exclude<keyof T, typeof SYSTEM_VIEW_MARKER>`, a NON-homomorphic mapped type
59
+ * that drops the `?` modifier from every other property and turns optional keys
60
+ * required — a strictly worse type, and a real break for callers, traded for
61
+ * removing a key that already reads as optional.
62
+ */
63
+ export type DeepMutable<T> = T extends (...args: any[]) => any ? T : T extends ReadonlyArray<infer U> ? DeepMutable<U>[] : T extends object ? {
64
+ -readonly [K in keyof T]: DeepMutable<T[K]>;
65
+ } : T;
39
66
  /**
40
67
  * A schema that has been frozen by `defineSystemView()`. The marker symbol is
41
68
  * non-enumerable and therefore invisible to consumers, but its presence
@@ -87,8 +114,18 @@ export declare function isSystemView(value: unknown): boolean;
87
114
  * Produce a deep, mutable clone of a System View so callers can apply
88
115
  * Tenant/User overrides without touching the source schema.
89
116
  *
117
+ * Returns `DeepMutable<T>`, not `T`. The declaration used to hand back the
118
+ * input type unchanged, which meant cloning a `SystemView<S>` returned
119
+ * something still typed deep-readonly even though the implementation had always
120
+ * produced a plain mutable object — so the override flow this function exists
121
+ * for, `draft.columns.push(...)`, did not type-check (TS2339). The return type
122
+ * now relaxes TOWARD what the runtime already does, never away from it, which
123
+ * is why the change adds capability to a caller's type rather than removing it.
124
+ *
90
125
  * Implementation note: uses `structuredClone` when available (Node 17+, all
91
126
  * evergreen browsers) and falls back to a JSON round-trip. The marker
92
127
  * symbol is intentionally NOT copied — the clone is no longer a System View.
128
+ * Neither path can copy it by accident: `structuredClone` ignores symbol keys,
129
+ * and `JSON.stringify` never sees them.
93
130
  */
94
- export declare function cloneAsOverride<T>(view: T): T;
131
+ export declare function cloneAsOverride<T>(view: T): DeepMutable<T>;
@@ -140,9 +140,19 @@ export function isSystemView(value) {
140
140
  * Produce a deep, mutable clone of a System View so callers can apply
141
141
  * Tenant/User overrides without touching the source schema.
142
142
  *
143
+ * Returns `DeepMutable<T>`, not `T`. The declaration used to hand back the
144
+ * input type unchanged, which meant cloning a `SystemView<S>` returned
145
+ * something still typed deep-readonly even though the implementation had always
146
+ * produced a plain mutable object — so the override flow this function exists
147
+ * for, `draft.columns.push(...)`, did not type-check (TS2339). The return type
148
+ * now relaxes TOWARD what the runtime already does, never away from it, which
149
+ * is why the change adds capability to a caller's type rather than removing it.
150
+ *
143
151
  * Implementation note: uses `structuredClone` when available (Node 17+, all
144
152
  * evergreen browsers) and falls back to a JSON round-trip. The marker
145
153
  * symbol is intentionally NOT copied — the clone is no longer a System View.
154
+ * Neither path can copy it by accident: `structuredClone` ignores symbol keys,
155
+ * and `JSON.stringify` never sees them.
146
156
  */
147
157
  export function cloneAsOverride(view) {
148
158
  if (view == null || typeof view !== 'object')
@@ -0,0 +1,61 @@
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
+ * The THIRD half of a view's field appetite (objectui#7179).
10
+ *
11
+ * A list view projects what it DISPLAYS — `$select` is built from `columns`.
12
+ * `predicate-fields.ts` covers the fields a view's PREDICATES read. This covers
13
+ * the fields it GROUPS BY, which is a third, independent source of demand: the
14
+ * spec's `grouping` block is a sibling of `columns`, not a subset of it, so a
15
+ * view may legitimately group by a field it never shows.
16
+ *
17
+ * ## Why it needed a fix rather than a gate
18
+ *
19
+ * `GroupingConfigSchema` accepts `grouping` with no matching column, and the
20
+ * neighbouring view kinds (kanban / gantt / timeline) already union their
21
+ * `groupByField` into the projection with no column required. Grouping by a
22
+ * field you do not want on screen is an ordinary thing to want. Refusing it at
23
+ * author time would make the grid the odd one out and reject working intent.
24
+ *
25
+ * ## The failure this closes
26
+ *
27
+ * With the grouping field absent from `$select` the server never returns it,
28
+ * so `useGroupedData` reads `undefined` on every row and `buildSegmentLabel`
29
+ * answers `(empty)` for all of them: ONE group holding every record, with no
30
+ * error, no warning and no empty state. It reads as "these records have no
31
+ * value for this field" — a plausible, wrong, actionable conclusion about the
32
+ * data rather than a visible bug in the view.
33
+ *
34
+ * ## ⛔ THE RESULT IS CANDIDATES, NOT VERIFIED FIELDS — the caller MUST gate it
35
+ *
36
+ * `GroupingFieldSchema.field` is a string that refuses a padded name
37
+ * (objectui#7347 — `grouping-field-padded-refused-7347.test.ts` in
38
+ * `@object-ui/types` pins it on every face declaring `grouping`; the trim below
39
+ * stays as defence for blocks that skipped that parse). Nothing in the schema
40
+ * requires it to name a field the object declares, and the whole premise of
41
+ * this harvest is that it has NOT been through column validation. Some backends
42
+ * answer an unknown `$select` key with an EMPTY RESULT SET rather than ignoring
43
+ * it — the cloud multi-tenant runtime does exactly that — so a single unknown
44
+ * grouping field put in the projection unguarded silently zeroes the whole
45
+ * list. That would convert this card's bug (one `(empty)` group holding every
46
+ * row) into a strictly worse one (no rows at all, still silent).
47
+ *
48
+ * So every caller intersects this result with the object's declared fields —
49
+ * {@link isProjectableField}, or the caller's equivalent known-field set —
50
+ * exactly as {@link collectPredicateFieldRefs}'s callers do, and for the same
51
+ * measured reason. Callers additionally FLS-gate it: a grouping field names a
52
+ * field just as capable of being denied as a column is, and the projection is
53
+ * what goes on the wire (objectui#6898).
54
+ *
55
+ * @param grouping - The view's `grouping` block in any authored state
56
+ * (`undefined`, malformed, or the spec shape). Anything that is not an entry
57
+ * carrying a non-empty string `field` contributes nothing, so a malformed
58
+ * block yields an empty harvest instead of a plausible wrong name.
59
+ * @returns Grouping field names in first-seen order, deduplicated.
60
+ */
61
+ export declare function collectGroupingFieldRefs(grouping: unknown): string[];
@@ -0,0 +1,84 @@
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
+ * The THIRD half of a view's field appetite (objectui#7179).
10
+ *
11
+ * A list view projects what it DISPLAYS — `$select` is built from `columns`.
12
+ * `predicate-fields.ts` covers the fields a view's PREDICATES read. This covers
13
+ * the fields it GROUPS BY, which is a third, independent source of demand: the
14
+ * spec's `grouping` block is a sibling of `columns`, not a subset of it, so a
15
+ * view may legitimately group by a field it never shows.
16
+ *
17
+ * ## Why it needed a fix rather than a gate
18
+ *
19
+ * `GroupingConfigSchema` accepts `grouping` with no matching column, and the
20
+ * neighbouring view kinds (kanban / gantt / timeline) already union their
21
+ * `groupByField` into the projection with no column required. Grouping by a
22
+ * field you do not want on screen is an ordinary thing to want. Refusing it at
23
+ * author time would make the grid the odd one out and reject working intent.
24
+ *
25
+ * ## The failure this closes
26
+ *
27
+ * With the grouping field absent from `$select` the server never returns it,
28
+ * so `useGroupedData` reads `undefined` on every row and `buildSegmentLabel`
29
+ * answers `(empty)` for all of them: ONE group holding every record, with no
30
+ * error, no warning and no empty state. It reads as "these records have no
31
+ * value for this field" — a plausible, wrong, actionable conclusion about the
32
+ * data rather than a visible bug in the view.
33
+ *
34
+ * ## ⛔ THE RESULT IS CANDIDATES, NOT VERIFIED FIELDS — the caller MUST gate it
35
+ *
36
+ * `GroupingFieldSchema.field` is a string that refuses a padded name
37
+ * (objectui#7347 — `grouping-field-padded-refused-7347.test.ts` in
38
+ * `@object-ui/types` pins it on every face declaring `grouping`; the trim below
39
+ * stays as defence for blocks that skipped that parse). Nothing in the schema
40
+ * requires it to name a field the object declares, and the whole premise of
41
+ * this harvest is that it has NOT been through column validation. Some backends
42
+ * answer an unknown `$select` key with an EMPTY RESULT SET rather than ignoring
43
+ * it — the cloud multi-tenant runtime does exactly that — so a single unknown
44
+ * grouping field put in the projection unguarded silently zeroes the whole
45
+ * list. That would convert this card's bug (one `(empty)` group holding every
46
+ * row) into a strictly worse one (no rows at all, still silent).
47
+ *
48
+ * So every caller intersects this result with the object's declared fields —
49
+ * {@link isProjectableField}, or the caller's equivalent known-field set —
50
+ * exactly as {@link collectPredicateFieldRefs}'s callers do, and for the same
51
+ * measured reason. Callers additionally FLS-gate it: a grouping field names a
52
+ * field just as capable of being denied as a column is, and the projection is
53
+ * what goes on the wire (objectui#6898).
54
+ *
55
+ * @param grouping - The view's `grouping` block in any authored state
56
+ * (`undefined`, malformed, or the spec shape). Anything that is not an entry
57
+ * carrying a non-empty string `field` contributes nothing, so a malformed
58
+ * block yields an empty harvest instead of a plausible wrong name.
59
+ * @returns Grouping field names in first-seen order, deduplicated.
60
+ */
61
+ export function collectGroupingFieldRefs(grouping) {
62
+ const fields = grouping?.fields;
63
+ if (!Array.isArray(fields))
64
+ return [];
65
+ const out = [];
66
+ const seen = new Set();
67
+ for (const entry of fields) {
68
+ // The spec shape is `{ field, order, collapsed }`. A bare string is NOT
69
+ // accepted here even though it would be a natural shorthand: `grouping` is
70
+ // a `$strict` object schema in `@objectstack/spec`, so a bare string is
71
+ // off-spec metadata, and reading it anyway would be exactly the lenient
72
+ // renderer-side alias AGENTS.md #0.1 forbids — it fossilizes a second
73
+ // de-facto contract instead of having the producer rejected at publish.
74
+ const name = entry?.field;
75
+ if (typeof name !== 'string')
76
+ continue;
77
+ const trimmed = name.trim();
78
+ if (!trimmed || seen.has(trimmed))
79
+ continue;
80
+ seen.add(trimmed);
81
+ out.push(trimmed);
82
+ }
83
+ return out;
84
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * ObjectUI — value-fallback label prettifier
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
+ * humanize-label — the ONE home for the VALUE fallback: a stored value becomes
10
+ * a display string when nothing else resolves it (objectui#5444).
11
+ *
12
+ * Two byte-identical copies preceded this module: `@object-ui/fields` (read by
13
+ * `plugin-grid`, `plugin-gantt`, `plugin-detail`, and by the field renderers in
14
+ * that package itself) and a deliberate local copy in `plugin-charts`'
15
+ * `ObjectChart.tsx`, whose comment said it existed "to avoid a dependency on
16
+ * `@object-ui/fields`". `@object-ui/core` is the shared ancestor both packages
17
+ * already depend on, so the convention gets a single home without a new
18
+ * dependency edge and without React reaching core — objectui#4389's ruling for
19
+ * this family: core-canonical logic, plugins consume; never core importing
20
+ * react. Both former sites now re-export THIS function, so there is one
21
+ * implementation and two doorways.
22
+ *
23
+ * ## Why this is NOT `humanizeFieldKey`, and why the two are kept apart
24
+ *
25
+ * The renderer carries a second prettifier — `humanizeFieldKey` in
26
+ * `@object-ui/plugin-dashboard`, the single home for the KEY fallback (a field
27
+ * key becomes a column header). It is a DIFFERENT convention, not a stale copy
28
+ * of this one: it additionally splits camelCase.
29
+ *
30
+ * input humanizeFieldKey humanizeLabel (here)
31
+ * ────────────────── ────────────────── ──────────────────
32
+ * needs_analysis Needs Analysis Needs Analysis
33
+ * NeedsAnalysis Needs Analysis NeedsAnalysis <- differ
34
+ * unitPrice Unit Price UnitPrice <- differ
35
+ * BestCase Best Case BestCase <- differ
36
+ * lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ
37
+ *
38
+ * They are fed different inputs, which is why their outputs may differ. A field
39
+ * KEY is authored in the codebase and is expected to carry a machine spelling,
40
+ * so splitting camelCase recovers words its author meant. A stored VALUE is
41
+ * arbitrary tenant data — a picklist member, an object name, a code from
42
+ * another system — where a mid-token capital is not reliably a word boundary,
43
+ * and splitting it rewrites what the tenant wrote (`McDonald` -> `Mc Donald`).
44
+ * Note also that the two conventions do not nest: on the last row above each
45
+ * leaves alone the separator the other rewrites, so "converge" would have to
46
+ * pick a winner per input class rather than adopt one side wholesale.
47
+ *
48
+ * Kept distinct DELIBERATELY, and this paragraph is the write-down whose
49
+ * absence made the split read as drift. Converging them is a decision, not a
50
+ * refactor: this function is read by grid, gantt, detail and charts, so any
51
+ * change to what it RETURNS moves rendered output in four packages at once.
52
+ * That change is explicitly out of scope for the convergence that created this
53
+ * module; it needs its own card.
54
+ *
55
+ * Examples:
56
+ * "in_progress" -> "In Progress"
57
+ * "high-priority" -> "High Priority"
58
+ * "active" -> "Active"
59
+ */
60
+ export declare function humanizeLabel(value: string): string;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * ObjectUI — value-fallback label prettifier
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
+ * humanize-label — the ONE home for the VALUE fallback: a stored value becomes
10
+ * a display string when nothing else resolves it (objectui#5444).
11
+ *
12
+ * Two byte-identical copies preceded this module: `@object-ui/fields` (read by
13
+ * `plugin-grid`, `plugin-gantt`, `plugin-detail`, and by the field renderers in
14
+ * that package itself) and a deliberate local copy in `plugin-charts`'
15
+ * `ObjectChart.tsx`, whose comment said it existed "to avoid a dependency on
16
+ * `@object-ui/fields`". `@object-ui/core` is the shared ancestor both packages
17
+ * already depend on, so the convention gets a single home without a new
18
+ * dependency edge and without React reaching core — objectui#4389's ruling for
19
+ * this family: core-canonical logic, plugins consume; never core importing
20
+ * react. Both former sites now re-export THIS function, so there is one
21
+ * implementation and two doorways.
22
+ *
23
+ * ## Why this is NOT `humanizeFieldKey`, and why the two are kept apart
24
+ *
25
+ * The renderer carries a second prettifier — `humanizeFieldKey` in
26
+ * `@object-ui/plugin-dashboard`, the single home for the KEY fallback (a field
27
+ * key becomes a column header). It is a DIFFERENT convention, not a stale copy
28
+ * of this one: it additionally splits camelCase.
29
+ *
30
+ * input humanizeFieldKey humanizeLabel (here)
31
+ * ────────────────── ────────────────── ──────────────────
32
+ * needs_analysis Needs Analysis Needs Analysis
33
+ * NeedsAnalysis Needs Analysis NeedsAnalysis <- differ
34
+ * unitPrice Unit Price UnitPrice <- differ
35
+ * BestCase Best Case BestCase <- differ
36
+ * lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ
37
+ *
38
+ * They are fed different inputs, which is why their outputs may differ. A field
39
+ * KEY is authored in the codebase and is expected to carry a machine spelling,
40
+ * so splitting camelCase recovers words its author meant. A stored VALUE is
41
+ * arbitrary tenant data — a picklist member, an object name, a code from
42
+ * another system — where a mid-token capital is not reliably a word boundary,
43
+ * and splitting it rewrites what the tenant wrote (`McDonald` -> `Mc Donald`).
44
+ * Note also that the two conventions do not nest: on the last row above each
45
+ * leaves alone the separator the other rewrites, so "converge" would have to
46
+ * pick a winner per input class rather than adopt one side wholesale.
47
+ *
48
+ * Kept distinct DELIBERATELY, and this paragraph is the write-down whose
49
+ * absence made the split read as drift. Converging them is a decision, not a
50
+ * refactor: this function is read by grid, gantt, detail and charts, so any
51
+ * change to what it RETURNS moves rendered output in four packages at once.
52
+ * That change is explicitly out of scope for the convergence that created this
53
+ * module; it needs its own card.
54
+ *
55
+ * Examples:
56
+ * "in_progress" -> "In Progress"
57
+ * "high-priority" -> "High Priority"
58
+ * "active" -> "Active"
59
+ */
60
+ export function humanizeLabel(value) {
61
+ return value.replace(/[_-]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
62
+ }