Skip to content

Commit 58b36fa

Browse files
os-billclaude
andauthored
fix(spec): project a union branch-by-branch, so five filter operators reach a published reference page (#17085)
* wip: per-branch json-schema projection * wip: regenerate manifests, docs, import baseline * wip: projection unit tests * wip: changeset * wip: declaration-map --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 23aa83c commit 58b36fa

13 files changed

Lines changed: 994 additions & 44 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): project a union branch-by-branch, so five filter operators reach a published reference page
6+
7+
`z.toJSONSchema()` refuses a whole schema the moment ONE node in it has no JSON
8+
form, and `build-schemas.ts` applied that refusal per SCHEMA. `orderingComparandSchema`
9+
is `z.union([z.number(), z.date(), z.string(), FieldReferenceSchema])`, so four
10+
`data/filter.zod.ts` exports emitted nothing at all — and `$gt`, `$gte`, `$lt`,
11+
`$lte` and `$between` reached no reference row. Not a blank Description cell: no
12+
section. The ~2000 characters of `.describe()` on those slots — the #5685 comparand
13+
contract, the #6571 endpoint contract, and the `{ "$gte": "2026-01-01" }` shape the
14+
platform's own date-macro resolver produces — reached no reader.
15+
16+
The generator now makes a third attempt when both strict directions refuse: it
17+
projects with Zod's `unrepresentable: 'any'`, marks every node that came back with
18+
no structural keyword, and DROPS the marked ones that are direct members of an
19+
`anyOf` / `oneOf`. That is not a narrowing. These artifacts describe JSON
20+
documents, a JSON document cannot carry a `Date` INSTANCE, so the set of JSON
21+
documents that union accepts is unchanged by the drop.
22+
23+
⛔ A marked node anywhere else — an object property, a record value, an array item
24+
— refuses the projection and the export is skipped with the message Zod threw, so
25+
this cannot change WHY anything is skipped. Five exports leave
26+
`unemitted-schemas.baseline.json` (23 → 18): the four filter exports, plus
27+
`data/Hook`, whose only unprojectable member was the deprecated inline-function
28+
handler branch — that puts 22 `data/Hook:` authorable keys under the key ratchet
29+
for the first time.
30+
31+
Published artifacts gain `json-schema/data/{ComparisonOperator,FieldOperators,
32+
NormalizedFilter,RangeOperator,Hook}.json`, each carrying an
33+
`x-unprojectable-branches` record naming exactly which branch the projection
34+
dropped and where.

content/docs/references/data/filter.mdx

Lines changed: 194 additions & 3 deletions
Large diffs are not rendered by default.

content/docs/references/data/hook.mdx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,73 @@ description: Hook protocol schemas
1212
## TypeScript Usage
1313

1414
```typescript
15-
import { HookContextSchema, HookEvent } from '@objectstack/spec/data';
16-
import type { HookContext } from '@objectstack/spec/data';
15+
import { HookSchema, HookContextSchema, HookEvent } from '@objectstack/spec/data';
16+
import type { Hook, HookContext } from '@objectstack/spec/data';
1717

1818
// Validate data
19-
const result = HookContextSchema.parse(data);
19+
const result = HookSchema.parse(data);
2020
```
2121

22+
---
23+
24+
## Hook
25+
26+
### Properties
27+
28+
| Property | Type | Required | Description |
29+
| :--- | :--- | :--- | :--- |
30+
| **name** | `string` || Hook unique name (snake_case) |
31+
| **label** | `string` | optional | Description of what this hook does |
32+
| **object** | `string \| string[]` || Target object(s) |
33+
| **events** | `Enum<'beforeFind' \| 'afterFind' \| 'beforeInsert' \| 'afterInsert' \| 'beforeUpdate' \| 'afterUpdate' \| 'beforeDelete' \| 'afterDelete'>[]` || Lifecycle events |
34+
| **handler** | `string` | optional | Handler function name (string, post-build) or inline function (pre-build) — DEPRECATED, prefer `body` |
35+
| **body** | `{ language: 'expression'; source: string } \| { language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Hook body — expression (L1) or sandboxed JS (L2) |
36+
| **priority** | `number` | optional (default: `100`) | Execution priority |
37+
| **async** | `boolean` | optional (default: `false`) | Run specifically as fire-and-forget |
38+
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL); hook runs only when TRUE. e.g. P`record.status == "closed" && record.amount > 1000` |
39+
| **description** | `string` | optional | Human-readable description of what this hook does |
40+
| **retryPolicy** | `{ maxRetries?: number; backoffMs?: number }` | optional | Retry policy for failed hook executions |
41+
| **timeoutMs** | `number` | optional | Maximum execution time in milliseconds before the hook is aborted |
42+
| **timeout** | `never` | optional | [REMOVED] `hook.timeout` was removed in @objectstack/spec 17 — its unit (milliseconds) lived only in the description, beside a body-level `timeoutMs` and a `retryPolicy.backoffMs` that spell theirs, so the same number read as two conventions on one surface. Rename the key to `timeoutMs`; the value (milliseconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
43+
| **onError** | `Enum<'abort' \| 'log'>` | optional (default: `"abort"`) | Error handling strategy |
44+
| **runAs** | `Enum<'system' \| 'user' \| 'inherit'>` | optional (default: `"inherit"`) | Execution identity for the hook's ctx.api data operations: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting), inherit = the context of the write that fired the hook (the pre-runAs behaviour; the default). A hook with no trigger user has no identity to scope to, so under user its ctx.api data operations are REFUSED — declare system to make the elevation explicit. This covers any hook fired by a write that carried no user (an isSystem plugin/service write; a system-elevated flow node). Scope: ctx.api only — condition evaluation, the readonly strip on ctx.input, ctx.session and async are unchanged. |
45+
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
46+
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
47+
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
48+
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
49+
| **_packageId** | `string` | optional | Owning package machine id. |
50+
| **_packageVersion** | `string` | optional | Owning package version. |
51+
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
52+
53+
### Nested Shape: `Hook.body[language='expression']`
54+
55+
L1 expression body — pure formula, no IO
56+
57+
| Property | Type | Required | Description |
58+
| :--- | :--- | :--- | :--- |
59+
| **language** | `'expression'` || |
60+
| **source** | `string` || Formula expression source |
61+
62+
### Nested Shape: `Hook.body[language='js']`
63+
64+
L2 sandboxed JS body — runs inside an isolated VM with declared capabilities
65+
66+
| Property | Type | Required | Description |
67+
| :--- | :--- | :--- | :--- |
68+
| **language** | `'js'` || |
69+
| **source** | `string` || Function body source |
70+
| **capabilities** | `Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]` | optional (default: `[]`) | Granted capability tokens |
71+
| **timeoutMs** | `integer` | optional | Per-invocation timeout (ms) |
72+
| **memoryMb** | `integer` | optional | Per-invocation memory cap (MB) |
73+
74+
### Nested Shape: `Hook.retryPolicy`
75+
76+
| Property | Type | Required | Description |
77+
| :--- | :--- | :--- | :--- |
78+
| **maxRetries** | `number` | optional (default: `3`) | Maximum retry attempts on failure |
79+
| **backoffMs** | `number` | optional (default: `1000`) | Backoff delay between retries in milliseconds |
80+
81+
2282
---
2383

2484
## HookContext

content/docs/references/index.mdx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Protocol Reference
3-
description: Every schema published by @objectstack/spec — 1577 schemas across 14 protocol modules
3+
description: Every schema published by @objectstack/spec — 1582 schemas across 14 protocol modules
44
---
55

66
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -23,7 +23,7 @@ counts are sums of the rows they head. Regenerate with
2323
| [API Protocol](/docs/references/api) | 31 | 437 | REST contracts, endpoints, routing, realtime, batch, discovery. |
2424
| [Automation Protocol](/docs/references/automation) | 13 | 73 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
2525
| [Cloud Protocol](/docs/references/cloud) | 11 | 94 | Environments, packages and versions, marketplace, developer portal, tenancy. |
26-
| [Data Protocol](/docs/references/data) | 29 | 168 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
26+
| [Data Protocol](/docs/references/data) | 29 | 173 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
2727
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
2828
| [Integration Protocol](/docs/references/integration) | 1 | 24 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
2929
| [Kernel Protocol](/docs/references/kernel) | 30 | 162 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. |
@@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with
3333
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3434
| [System Protocol](/docs/references/system) | 33 | 272 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3535
| [UI Protocol](/docs/references/ui) | 16 | 153 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
36-
| **Total** | **198** | **1577** | 14 protocol modules |
36+
| **Total** | **198** | **1582** | 14 protocol modules |
3737

3838
---
3939

@@ -149,7 +149,7 @@ Environments, packages and versions, marketplace, developer portal, tenancy.
149149

150150
## Data Protocol
151151

152-
**Source:** `packages/spec/src/data/` · **Import:** `@objectstack/spec/data` · **29 pages, 168 schemas**
152+
**Source:** `packages/spec/src/data/` · **Import:** `@objectstack/spec/data` · **29 pages, 173 schemas**
153153

154154
Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer.
155155

@@ -175,8 +175,8 @@ Objects, fields, queries, filters, datasources and drivers — the ObjectQL laye
175175
| [`feed.zod.ts`](/docs/references/data/feed) | `FeedFilterMode`, `FeedItemType` |
176176
| [`field.zod.ts`](/docs/references/data/field) | `CurrencyConfig`, `CurrencyValue`, `Field`, `FieldMaskingKeep`, `FieldMaskingRule`, `FieldType`, `InlineGridColumn`, `LocationCoordinates`, `SelectOption`, `UniqueScope` |
177177
| [`field-value.zod.ts`](/docs/references/data/field-value) | `Address`, `AddressValue`, `CalendarDateValue`, `ClockTimeValue`, `FileLikeValue`, `FileReferenceIdValue`, `FileValue`, `InstantValue`, `LocationValue`, `ReferenceIdValue` |
178-
| [`filter.zod.ts`](/docs/references/data/filter) | `EqualityOperator`, `FieldReference`, `FilterArray`, `FilterCondition`, `QueryFilter`, `SetOperator`, `SpecialOperator`, `StringOperator` |
179-
| [`hook.zod.ts`](/docs/references/data/hook) | `HookContext`, `HookEvent` |
178+
| [`filter.zod.ts`](/docs/references/data/filter) | `ComparisonOperator`, `EqualityOperator`, `FieldOperators`, `FieldReference`, `FilterArray`, `FilterCondition`, `NormalizedFilter`, `QueryFilter`, `RangeOperator`, `SetOperator`, `SpecialOperator`, `StringOperator` |
179+
| [`hook.zod.ts`](/docs/references/data/hook) | `Hook`, `HookContext`, `HookEvent` |
180180
| [`hook-body.zod.ts`](/docs/references/data/hook-body) | `ExpressionBody`, `HookBody`, `HookBodyCapability`, `ScriptBody` |
181181
| [`mapping.zod.ts`](/docs/references/data/mapping) | `ImportFieldMapping`, `Mapping`, `TransformType` |
182182
| [`object.zod.ts`](/docs/references/data/object) | `ApiMethod`, `ApiOperation`, `Index`, `Lifecycle`, `LifecycleClass`, `Object`, `ObjectAccessConfig`, `ObjectCapabilities`, `ObjectExtension`, `ObjectExternalBinding`, `ObjectFieldGroup`, `ObjectOwnershipEnum`, `ObjectRequiredPermissions`, `PerOperationRequiredPermissions`, `RowCrudActionOverride`, `TenancyConfig` |

packages/spec/authorable-defaults/data.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
"data/FullTextSearch:fuzzy = false",
5252
"data/FullTextSearch:highlight = false",
5353
"data/FullTextSearch:operator = \"or\"",
54+
"data/Hook:async = false",
55+
"data/Hook:onError = \"abort\"",
56+
"data/Hook:priority = 100",
57+
"data/Hook:runAs = \"inherit\"",
5458
"data/ImportFieldMapping:transform = \"none\"",
5559
"data/Index:unique = false",
5660
"data/JSONValidation:active = true",

packages/spec/authorable-surface/data.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
"data/AutoPersistenceConfig:path",
4242
"data/AutoPersistenceConfig:type",
4343
"data/BaseEngineOptions:context",
44+
"data/ComparisonOperator:$gt",
45+
"data/ComparisonOperator:$gte",
46+
"data/ComparisonOperator:$lt",
47+
"data/ComparisonOperator:$lte",
4448
"data/ConditionalValidation:_lock",
4549
"data/ConditionalValidation:_lockDocsUrl",
4650
"data/ConditionalValidation:_lockReason",
@@ -415,6 +419,24 @@
415419
"data/Field:widget",
416420
"data/FieldMaskingKeep:keepHead",
417421
"data/FieldMaskingKeep:keepTail",
422+
"data/FieldOperators:$between",
423+
"data/FieldOperators:$contains",
424+
"data/FieldOperators:$endsWith",
425+
"data/FieldOperators:$eq",
426+
"data/FieldOperators:$exists",
427+
"data/FieldOperators:$gt",
428+
"data/FieldOperators:$gte",
429+
"data/FieldOperators:$icontains",
430+
"data/FieldOperators:$ilike",
431+
"data/FieldOperators:$in",
432+
"data/FieldOperators:$like",
433+
"data/FieldOperators:$lt",
434+
"data/FieldOperators:$lte",
435+
"data/FieldOperators:$ne",
436+
"data/FieldOperators:$nin",
437+
"data/FieldOperators:$notContains",
438+
"data/FieldOperators:$null",
439+
"data/FieldOperators:$startsWith",
418440
"data/FieldReference:$field",
419441
"data/FieldReference:addDays",
420442
"data/FilePersistenceConfig:autoSaveInterval [RETIRED]",
@@ -455,6 +477,28 @@
455477
"data/FullTextSearch:minScore",
456478
"data/FullTextSearch:operator",
457479
"data/FullTextSearch:query",
480+
"data/Hook:_lock",
481+
"data/Hook:_lockDocsUrl",
482+
"data/Hook:_lockReason",
483+
"data/Hook:_lockSource",
484+
"data/Hook:_packageId",
485+
"data/Hook:_packageVersion",
486+
"data/Hook:_provenance",
487+
"data/Hook:async",
488+
"data/Hook:body",
489+
"data/Hook:condition",
490+
"data/Hook:description",
491+
"data/Hook:events",
492+
"data/Hook:handler",
493+
"data/Hook:label",
494+
"data/Hook:name",
495+
"data/Hook:object",
496+
"data/Hook:onError",
497+
"data/Hook:priority",
498+
"data/Hook:retryPolicy",
499+
"data/Hook:runAs",
500+
"data/Hook:timeout [RETIRED]",
501+
"data/Hook:timeoutMs",
458502
"data/HookContext:api",
459503
"data/HookContext:dispatch",
460504
"data/HookContext:event",
@@ -618,6 +662,9 @@
618662
"data/NoSQLTransactionOptions:readConcern",
619663
"data/NoSQLTransactionOptions:readPreference",
620664
"data/NoSQLTransactionOptions:writeConcern",
665+
"data/NormalizedFilter:$and",
666+
"data/NormalizedFilter:$not",
667+
"data/NormalizedFilter:$or",
621668
"data/Object:_lock",
622669
"data/Object:_lockDocsUrl",
623670
"data/Object:_lockReason",
@@ -736,6 +783,7 @@
736783
"data/Query:where",
737784
"data/Query:windowFunctions [RETIRED]",
738785
"data/QueryFilter:where",
786+
"data/RangeOperator:$between",
739787
"data/ReferenceResolution:field",
740788
"data/ReferenceResolution:fieldType",
741789
"data/ReferenceResolution:multiple",

packages/spec/declaration-map/data.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"CalendarDateValueSchema": "data/CalendarDateValue",
3333
"ClockTimeValue": "data/ClockTimeValue",
3434
"ClockTimeValueSchema": "data/ClockTimeValue",
35+
"ComparisonOperatorSchema": "data/ComparisonOperator",
3536
"ConditionalValidation": "data/ConditionalValidation",
3637
"ConditionalValidationSchema": "data/ConditionalValidation",
3738
"ConsistencyLevel": "data/ConsistencyLevel",
@@ -145,6 +146,8 @@
145146
"FieldMaskingRuleSchema": "data/FieldMaskingRule",
146147
"FieldNode": "data/FieldNode",
147148
"FieldNodeSchema": "data/FieldNode",
149+
"FieldOperators": "data/FieldOperators",
150+
"FieldOperatorsSchema": "data/FieldOperators",
148151
"FieldReference": "data/FieldReference",
149152
"FieldReferenceSchema": "data/FieldReference",
150153
"FieldSchema": "data/Field",
@@ -167,12 +170,14 @@
167170
"FullTextSearchSchema": "data/FullTextSearch",
168171
"GroupByNode": "data/GroupByNode",
169172
"GroupByNodeSchema": "data/GroupByNode",
173+
"Hook": "data/Hook",
170174
"HookBody": "data/HookBody",
171175
"HookBodyCapability": "data/HookBodyCapability",
172176
"HookBodySchema": "data/HookBody",
173177
"HookContext": "data/HookContext",
174178
"HookContextSchema": "data/HookContext",
175179
"HookEvent": "data/HookEvent",
180+
"HookSchema": "data/Hook",
176181
"ImportFieldMapping": "data/ImportFieldMapping",
177182
"ImportFieldMappingSchema": "data/ImportFieldMapping",
178183
"IndexSchema": "data/Index",
@@ -216,6 +221,8 @@
216221
"NoSQLQueryOptionsSchema": "data/NoSQLQueryOptions",
217222
"NoSQLTransactionOptions": "data/NoSQLTransactionOptions",
218223
"NoSQLTransactionOptionsSchema": "data/NoSQLTransactionOptions",
224+
"NormalizedFilter": "data/NormalizedFilter",
225+
"NormalizedFilterSchema": "data/NormalizedFilter",
219226
"ObjectAccessConfig": "data/ObjectAccessConfig",
220227
"ObjectAccessConfigSchema": "data/ObjectAccessConfig",
221228
"ObjectCapabilities": "data/ObjectCapabilities",
@@ -245,6 +252,7 @@
245252
"QueryFilter": "data/QueryFilter",
246253
"QueryFilterSchema": "data/QueryFilter",
247254
"QuerySchema": "data/Query",
255+
"RangeOperatorSchema": "data/RangeOperator",
248256
"ReferenceIdValue": "data/ReferenceIdValue",
249257
"ReferenceIdValueSchema": "data/ReferenceIdValue",
250258
"ReferenceResolution": "data/ReferenceResolution",

packages/spec/docs-import-surface.baseline.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"api/MetadataQueryRequest — no type export",
1212
"automation/FlowVariable — no type export",
1313
"automation/StateMachine — no type export",
14+
"data/ComparisonOperator — no type export",
1415
"data/DataEngineAggregateRequest — no type export",
1516
"data/DataEngineCountRequest — no type export",
1617
"data/DataEngineDeleteRequest — no type export",
@@ -24,6 +25,7 @@
2425
"data/Object — no type export",
2526
"data/ObjectOwnershipEnum — no type export",
2627
"data/Query — no type export",
28+
"data/RangeOperator — no type export",
2729
"data/SeedMode — no type export",
2830
"data/SetOperator — no type export",
2931
"data/SpecialOperator — no type export",

packages/spec/json-schema.manifest/data.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"data/BaseEngineOptions",
1919
"data/CalendarDateValue",
2020
"data/ClockTimeValue",
21+
"data/ComparisonOperator",
2122
"data/ConditionalValidation",
2223
"data/ConsistencyLevel",
2324
"data/ContextToken",
@@ -81,6 +82,7 @@
8182
"data/FieldMaskingKeep",
8283
"data/FieldMaskingRule",
8384
"data/FieldNode",
85+
"data/FieldOperators",
8486
"data/FieldReference",
8587
"data/FieldType",
8688
"data/FileLikeValue",
@@ -92,6 +94,7 @@
9294
"data/FormatValidation",
9395
"data/FullTextSearch",
9496
"data/GroupByNode",
97+
"data/Hook",
9598
"data/HookBody",
9699
"data/HookBodyCapability",
97100
"data/HookContext",
@@ -118,6 +121,7 @@
118121
"data/NoSQLOperationType",
119122
"data/NoSQLQueryOptions",
120123
"data/NoSQLTransactionOptions",
124+
"data/NormalizedFilter",
121125
"data/Object",
122126
"data/ObjectAccessConfig",
123127
"data/ObjectCapabilities",
@@ -134,6 +138,7 @@
134138
"data/PostgresConfig",
135139
"data/Query",
136140
"data/QueryFilter",
141+
"data/RangeOperator",
137142
"data/ReferenceIdValue",
138143
"data/ReferenceResolution",
139144
"data/ReferenceResolutionError",

0 commit comments

Comments
 (0)