Skip to content

Commit dc345ae

Browse files
authored
Merge branch 'main' into claude/issue-18224-wire-issue-citations-to-ci
2 parents 6d1272b + b1d3945 commit dc345ae

161 files changed

Lines changed: 9351 additions & 239748 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/runtime': minor
4+
---
5+
6+
**BREAKING for action handlers**`ActionEngineFacade.find` takes the engine's query ENVELOPE; the bare-filter parameter shape is withdrawn (#15124)
7+
8+
Clause-②: yes (narrowing)
9+
10+
`ctx.engine.find(object, query)` now takes `EngineQueryOptions` — the same
11+
options bag `IDataEngine.find` and ObjectQL's own `engine.find` take, named by
12+
identity rather than restated. **One platform, one query shape.**
13+
14+
### Migration — FROM → TO
15+
16+
| You wrote | Write instead |
17+
| --- | --- |
18+
| `ctx.engine.find('task', { status: 'open' })` | `ctx.engine.find('task', { where: { status: 'open' } })` |
19+
| `ctx.engine.find('task', { amount: { $gt: 100 } })` | `ctx.engine.find('task', { where: { amount: { $gt: 100 } } })` |
20+
| `ctx.engine.find('task', {})` | unchanged — an empty envelope is still the unfiltered read |
21+
22+
The rewrite is lossless and mechanical: the filter moves under `where`, verbatim.
23+
`tsc --noEmit` over your handlers finds every unmigrated call — see below.
24+
25+
### Why the shape was withdrawn rather than the bar closed
26+
27+
Until now this parameter was the `where` HALF of a query while every other
28+
`find` on the platform took the whole envelope, and the runtime wrapped what it
29+
was given. That made the most natural spelling the wrong one, silently: an
30+
author who passed the engine's own envelope reached the engine as
31+
`{ where: { where: … } }` — a filter on a field named `where` — which matches no
32+
row and resolves to `[]` with **no error at all**. A handler that made the
33+
mistake ran to completion over zero rows for as long as it shipped, and its own
34+
hand-written test double, written to the same belief, passed every assertion.
35+
Because an empty `{}` skipped the wrap, one unfiltered read kept working under
36+
either belief, so a dead handler looked partially alive.
37+
38+
Refusing `where` at the top level instead — intersecting the old parameter with
39+
`{ where?: never }` — was rejected: it asserts a vocabulary fact the spec
40+
declares nowhere, reserving the field name `where` across every customer's data
41+
model to buy one parameter's compile-time check. Aligning the parameter removes
42+
the ambiguity at its root and reserves nothing.
43+
44+
### What the new declaration refuses, measured
45+
46+
If your handler is typed with the published `ActionHandlerContext`, a bare filter
47+
no longer type-checks on **either** path you can reach it by:
48+
49+
- an object literal (`{ status: 'completed' }`) fails the excess-property check —
50+
a field name is not an envelope key;
51+
- a filter held in a `FilterCondition` variable fails **TS2559** — every envelope
52+
key is optional, so a bag of field names has no property in common with it.
53+
54+
The envelope's own keys are typed too: `where: 'a = b'`, `fields: 'id,subject'`
55+
and `limit: '50'` are each refused.
56+
57+
**If your handler is NOT typed with it** — a handler in an `objectstack.config.js`
58+
/ `.mjs`, one annotated with your own copy of the context type, or a `(ctx: any)`
59+
handler — nothing above reaches you, so the facade refuses the withdrawn shape at
60+
**runtime** instead, before the engine, with the same prescription:
61+
62+
```
63+
find('task') was given a key 'status' the query envelope does not carry.
64+
ctx.engine.find(object, query) takes the engine QUERY ENVELOPE, not a bare
65+
filter — move the filter under `where`: find(object, { where: { … } }).
66+
Envelope keys: context, cursor, distinct, expand, fields, limit, offset,
67+
orderBy, search, searchFields, top, where.
68+
```
69+
70+
⚠️ **That refusal matters most for a filter whose value is `null`.** The engine's
71+
own unknown-option check exempts a `null` value, because on an option bag a
72+
`null` is a withdrawal. On a filter it is the "rows with no X" idiom, so
73+
`{ deleted_at: null }` would have been dropped unexecuted and the read would have
74+
widened to **every row** — including the ones you were excluding — with no error
75+
at all. It is refused instead.
76+
77+
### What this opens
78+
79+
`fields`, `orderBy`, `limit`, `offset` and `expand` are reachable from an action
80+
handler for the first time — under the old parameter there was nowhere to carry
81+
them. A caller-supplied `context` is **ignored**: this facade is trusted and
82+
context-less by design, and the runtime stamps its own elevated
83+
`ExecutionContext` last. Do not write one — it reads as authorization and is
84+
none.
85+
86+
### Checking a migrated handler
87+
88+
Do not settle for "it still resolves". A handler that had been passing the
89+
envelope was returning `[]` on **every** call, so a suite written against the
90+
mistake passes and the row count is the only witness. Re-run each migrated
91+
handler against seeded data and assert it returns the rows its filter selects.
92+
93+
<!-- adr-0087: registered action-engine-facade-find-query-envelope -->

.changeset/16045-spec-declaration-text-snapshots.md

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`SelectOptionSchema`'s six row properties carry a JSON Schema `title`, so Studio's property panel stops printing raw machine keys as the column headers of a field's `options` table (#17506).
6+
7+
Clause-②: no
8+
9+
Studio renders a `type: 'repeater'` form field as a table whose column headers read `items.properties[k].title ?? k` off the JSON Schema derived from the metadata type schema. `SelectOptionSchema` carried no `title` on any row property, so the fallback arm ran and the maker saw `label` / `value` / `description` / `color` / `default` / `visibleWhen` inside an otherwise translated panel — **in every locale, English included**. Titles are hard-coded English by design: `system/translation.zod.ts` states that a row property renders from `items.properties[k].title`, and `resolveMetadataFormSchemaTitles` only ever REPLACES a title that is already there, so an untitled property has no layer for a translation to overlay.
10+
11+
- **One edit clears two carriers.** `field:options` and `object:fields.options` resolve to the *same* `SelectOptionSchema` object — `FieldSchema.options` is `z.array(SelectOptionSchema)` and `object.fields` is a `z.record(..., FieldSchema)` of that same `FieldSchema` — verified by object identity (`===`) against the schemas `getMetadataTypeSchema('field')` and `getMetadataTypeSchema('object')` actually return, with `FormSelectOptionSchema` as the firing control that the probe can tell two schemas apart. Both entries are deleted from the shrink-only `repeater-item-titles` ledger in the same change; `object.zod.ts` needed no edit.
12+
- **Nothing the schema accepts or refuses moved.** `.meta({ title })` is presentation metadata: the generated `authorable-surface/` artifacts are byte-identical, and the pinned accept/refuse suites for this shape (`editability-boundary`, `visible-when-alias-guidance`, `form-select-option`, `evaluated-slot-population`) pass unchanged.
13+
- **The form-view face inherits the titles for free.** `FormSelectOptionSchema` is a shape-level Omit that reuses the same property schema instances, so the five keys it keeps arrive titled too, and its `default`-refusal is untouched.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING** for authored metadata — `ObjectSchema.fields` refuses a key named `__proto__`, `constructor` or `prototype`, and `AssignmentConfigSchema.assignments` (the `assignment` flow node's variable map) refuses a key named `__proto__` — both refused with a named, located error at parse time, rather than silently accepted and then silently mishandled (objectstack#17852, objectstack#18847).
6+
7+
## Why
8+
9+
zod's `z.record()` skips a `__proto__` own key entirely, above its own key schema — the record parser's `if (key === "__proto__") continue;` runs before `def.keyType._zod.run`, so no key grammar (a regex, `.refine()`, `.superRefine()`, even a key schema that rejects every string) can ever see that key. A document whose `fields` (or `assignments`) carried a `__proto__` own key — which `JSON.parse` produces routinely — used to parse as SUCCESS with that key silently missing from the output: the validator accepted a document and handed back a *different* document. `os build` writes the release artifact from that returned document, so the failure shape is success, silent, and irreversible into the shipped artifact.
10+
11+
Two independent mechanisms close this, one per name class, because they are not reachable the same way:
12+
13+
- `__proto__` is refused by a **pre-parse guard** that reads the raw input's own keys before the record ever parses, at both `ObjectSchema.fields` and `AssignmentConfigSchema.assignments`.
14+
- `constructor` and `prototype` — which, unlike `__proto__`, DO reach the key schema unskipped — are refused by `ObjectSchema.fields`' own key grammar (they were ordinary lowercase words its regex already admitted). They are **not** refused at `AssignmentConfigSchema.assignments`: that slot's key type carries no grammar at all (`z.string().min(1)`), both names are legal flow-VARIABLE names measured to survive parse intact today, and no ruling narrows that slot's accept set for them — only its `__proto__` half moves.
15+
16+
Measured: zero authored use of any of the three names as a `fields` key or an `assignments` variable name, across this repo, `examples/` and `objectui`.
17+
18+
## Known gap, left open on purpose
19+
20+
The guard runs at parse time only. It does not project into the published JSON Schema (`packages/spec/json-schema/**`) — the general gap that closes is tracked separately (objectstack#18670) and stays open after this change.
21+
22+
Clause-②: yes (narrowing)
23+
24+
<!-- adr-0087: not-required (no-migration-prescription) zero authored use of `__proto__`, `constructor` or `prototype` as a `fields` key or an `assignments` variable name across this repo, examples/ and objectui — nobody has anything to rewrite, so there is no prescription to give. -->

0 commit comments

Comments
 (0)