Skip to content

Commit 86cb4fb

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/drop-require-auth
# Conflicts: # packages/rest/src/rest.test.ts
2 parents d97bb04 + e4c61a7 commit 86cb4fb

109 files changed

Lines changed: 4933 additions & 410 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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
---
3+
4+
Docs-only: record in ADR-0105's status line that Phase 2 D8 (scoped
5+
invitations, #3645/#3663/#3674/#3695/#3722/#3767) and D9 (cross-org approval
6+
targeting, #3824/#3873) are implemented, and that the membership-role channel
7+
D8 replaces is closed by ADR-0108. Releases nothing.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/plugin-sharing": minor
4+
"@objectstack/plugin-security": minor
5+
---
6+
7+
feat(sharing): hierarchy managers may manage shares within their write DEPTH (ADR-0111 D1 DEPTH)
8+
9+
`canManageShares` gains its named DEPTH extension: a caller whose effective
10+
WRITE scope on the object is a hierarchy scope (`unit` / `unit_and_below` /
11+
`own_and_reports`) may now manage shares on a record whose owner falls within
12+
that scope's owner set — the same set the write filter and `canEdit` already
13+
honour, resolved by the enterprise `hierarchy-scope-resolver`. This lets a
14+
manager grant/revoke/list shares on a subordinate's record, matching
15+
Salesforce (roles above the owner) and Dataverse (the `Share` privilege's BU
16+
depth), without expanding the MVP owner + Modify-All authority.
17+
18+
- New `ISecurityService.resolveWriteScope(object, context)` — the effective
19+
write scope, resolved by the same evaluator the CRUD middleware uses; fails
20+
closed to `own`. Mirrored on the sharing plugin's structural probe.
21+
- The gate honours only the three hierarchy scopes. `org` from the probe is
22+
deliberately ignored: it means both a genuine Modify-All holder (already
23+
granted via `hasWriteBypass`) AND the fail-OPEN "no permission set mentions
24+
this object" default, so honouring it here would reopen the hole
25+
`hasWriteBypass` was chosen to avoid.
26+
- Fails closed with no security service or no enterprise resolver — the open
27+
edition stays owner + Modify-All, exactly as before.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/objectql": major
4+
"@objectstack/driver-sql": major
5+
---
6+
7+
feat!: ADR-0113 — `required` is a write contract; the column constraint becomes the explicit `storage.notNull`
8+
9+
`field.required` bound three meanings to one knob (write check, `NOT NULL` DDL,
10+
drift expectation), so tightening any invariant on a deployed object was a
11+
destructive migration blocked by the very legacy nulls that motivated it — the
12+
reason `criteria_json`'s mandatory-in-substance contract lived in three
13+
imperative guards instead of one declaration.
14+
15+
Split, with the **non-regression invariant** as the unifying rule — *a write
16+
may not take a record from compliant to violating; a pre-existing violation
17+
does not block writes that leave it in place*:
18+
19+
- `required: true` = the write contract, uniformly on new and deployed objects:
20+
insert must provide; **an update PATCHing `null` into a required field is now
21+
rejected** (it silently passed before); omitted fields never block, so legacy
22+
null rows rest. The column stays nullable.
23+
- `storage: { notNull: true }` = the explicit physical constraint, owning the
24+
DDL (`sql-driver` `createColumn`) and the destructive drift ceremony.
25+
Orthogonal to `required` — all four combinations are legitimate, including
26+
the engine-populated column (`storage.notNull` without `required`).
27+
- `requiredWhen` inherits the same invariant: flipping the condition true
28+
without providing the field is rejected (the write *creates* the violation);
29+
a row violating since before the rule tightened no longer locks out
30+
unrelated edits (#3929's objection, cured). `storage.notNull` ×
31+
`requiredWhen` rejects at parse (`FieldSchema.superRefine`).
32+
- **Pre-17 sources keep their exact meaning** via the migration-chain-only
33+
`field-required-notnull-explicit` conversion: `os migrate meta` stamps
34+
`storage.notNull` onto every previously-required field — writing down what
35+
the old text already meant. The loader never infers semantics from the
36+
physical column.
37+
- Drift compares nullability against `storage.notNull`; a column stricter than
38+
its declaration is `needs_confirm` (never auto-applied — dev auto-reconcile
39+
no longer silently strips a stray `NOT NULL`), and silent when the field is
40+
write-gated by `required`.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/rest": minor
4+
"@objectstack/objectql": minor
5+
---
6+
7+
feat(spec,rest,objectql)!: a closed field-level error catalog, and Zod stops leaking onto the wire (#3977)
8+
9+
Settles the vocabulary ADR-0112 D6 deferred, per [ADR-0114](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0114-field-level-error-code-catalog.md).
10+
11+
**`FieldErrorCode` — a closed, lowercase catalog.** 27 members covering what the
12+
six emitters already emit. `FieldErrorSchema.code` tightens from `z.string()` to
13+
this enum, so a validation body's per-field codes are validated for the first time.
14+
`FieldValidationError.code` (objectql) and `FieldCoerceError.code` (rest) stop
15+
being a hand-listed union and a bare `string` respectively and reference the
16+
catalog, so the three cannot drift apart.
17+
18+
Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING_SNAKE: a
19+
top-level code names the condition the *request* hit, while a field-level code
20+
names the *constraint* the value violated — and constraints are declared in the
21+
metadata's own snake_case, so `max_length` the code and `max_length: 50` the
22+
property are the same word on purpose.
23+
24+
**Zod issue codes no longer reach the wire (wire-visible).** Routes that validate
25+
with Zod passed its vocabulary straight through, so `fields[]` spoke a different
26+
language depending on which route served it, and `too_small` was ambiguous between
27+
a short string, a small number and a short array. `zodIssuesToFields` now maps
28+
using Zod's `origin`/`format`:
29+
30+
| Was | Now |
31+
|:---|:---|
32+
| `too_small` | `min_length` / `min_value` / `min_items` |
33+
| `too_big` | `max_length` / `max_value` / `max_items` |
34+
| `invalid_format` | `invalid_email` / `invalid_url` / `invalid_format` |
35+
| `invalid_value` | `invalid_option` |
36+
| `unrecognized_keys` | `unknown_field` |
37+
| `invalid_union`, `invalid_element`, `invalid_key` | `invalid_shape` |
38+
39+
**A missing required property now reports `required`, not `invalid_type`.** Zod
40+
spells "absent" as a type mismatch against `undefined`, so passing it through made
41+
a form mark a *missing* input as the wrong *type*. The two are indistinguishable on
42+
the issue alone, so the mapper takes the parsed input as an optional argument and
43+
walks the issue path; a caller that cannot supply it keeps `invalid_type` rather
44+
than guessing.
45+
46+
**`unknown_param` → `unknown_field`.** `ActionParamIssue.code` references the
47+
catalog instead of its own literal union; the `param` key beside it already says
48+
what was addressed.
49+
50+
**Not changed:** `EnhancedApiErrorSchema.fieldErrors` keeps its name even though
51+
every producer emits `fields`. Retiring an authorable key needs a tombstone plus a
52+
migration (ADR-0104's contract guard), so it lands on its own — the property now
53+
carries a banner saying which name the wire uses.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@objectstack/core": minor
3+
"@objectstack/driver-sql": patch
4+
"@objectstack/driver-sqlite-wasm": patch
5+
"@objectstack/service-analytics": patch
6+
---
7+
8+
fix(driver-sql,service-analytics): a bare-day upper bound covers the whole day on `Field.datetime` (#3777)
9+
10+
A bare `YYYY-MM-DD` comparand anchors to midnight UTC. That is right for a
11+
lower bound and was silently wrong for an upper one: the dashboard date-range
12+
filter compiles `{ $gte: from, $lte: to }` with bare-day bounds, so on a
13+
`datetime` column every row created after 00:00 of the `to` day vanished from
14+
the result — no error, the chart renders, the numbers are just smaller. The
15+
default configuration hit it: the filter's default field is `created_at`
16+
(a system-injected `Field.datetime`) and 7 of the 13 presets end "today".
17+
18+
The translation is operator-sensitive and half-open, applied at every
19+
comparison emitter:
20+
21+
- `SqlDriver` (and `SqliteWasmDriver` by inheritance): `$lte`/`<=` with a
22+
bare-day comparand on a `datetime` column compiles to `< next-day-midnight`
23+
in the column's storage form; `$between [min, max]` with a bare-day max
24+
decomposes to `>= min AND < next-day(max)`. Both the plain and the
25+
legacy-repair (mixed-storage) column paths, both `where` spellings.
26+
- `NativeSQLStrategy`: `dateRange` windows and `lte` filters bind `< next-day`
27+
instead of an inclusive `BETWEEN`/`<=` when the bound is a bare day.
28+
- The `/analytics/sql` rendering and the dataset preview evaluator apply the
29+
same rule, so the echoed SQL and drafted numbers reproduce execution.
30+
31+
`@objectstack/core` gains the shared primitive `nextUtcCalendarDay(value)`:
32+
the next calendar day of a valid bare `YYYY-MM-DD` (else `null` — instants,
33+
`Date`s and impossible days are never widened).
34+
35+
Unchanged on purpose, per the semantics table on #3777: `date`/`time` columns
36+
(`<= day` is already whole-day-correct there), full-ISO/`Date` comparands
37+
(instant semantics), and `$gte`/`$gt`/`$lt` (midnight anchoring is correct for
38+
those). No authored metadata changes: a dashboard's existing
39+
`{ $gte, $lte }` window now simply includes its final day.

‎.changeset/console-96ee72e85439.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/console": minor
3+
---
4+
5+
Console (objectui) refreshed to `96ee72e85439`. Frontend changes in this range:
6+
7+
- fix(console): render the redaction notice on the enveloped resolve body (objectstack#3983) (#2980)
8+
- feat(sdui): guard the public contract against silent drift (#2979)
9+
- fix(sdui): lazy public blocks reach a kind:'react' page scope; ReactRunner keeps its errors (#2976)
10+
- fix(list,data): bridge every spec view operator onto the filter AST (#2901) (#2974)
11+
- fix(errors): error-code branches survive the framework's ADR-0112 rename (objectstack#3841) (#2977)
12+
- fix(fields): a select no longer wipes itself when its value outruns its options (#2968) (#2969)
13+
- fix(approvals): decision outputs reach both decision surfaces (#2955) (#2961)
14+
15+
objectui range: `e651c936870e...96ee72e85439`
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/plugin-sharing": patch
3+
---
4+
5+
feat(sharing): `sys_sharing_rule.criteria_json` is declaratively required (ADR-0113 P2)
6+
7+
The field the ADR was written for: `required: true` as the write contract —
8+
insert must provide, update may not null out, legacy null rows rest, an admin
9+
can still `active: false` an over-broad legacy rule. Deliberately NO
10+
`storage.notNull`: deployed tenants' legacy nulls are the case the split
11+
exists for. The Setup form's required marker and client validation now derive
12+
from the declaration.
13+
14+
Not breaking: a rule without criteria was already rejected by the #3929 hook
15+
guard; the guard narrows to the non-null match-all shapes `required` cannot
16+
express ('{}', vacuous $and/$or, unparsable JSON), `defineRule` keeps the API
17+
seam, and the evaluator stays fail-closed (ADR-0049).

‎.changeset/date-now-default-utc.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/driver-sql": patch
3+
---
4+
5+
fix(driver-sql): `Field.date` + `defaultValue: 'NOW()'` records the UTC calendar day on Postgres/MySQL (#4022)
6+
7+
The bare `CURRENT_TIMESTAMP` default resolved the calendar day in the SERVER's
8+
timezone on Postgres — measured: a UTC-12 server recorded yesterday; an
9+
Asia/Shanghai server records tomorrow for every default after 16:00 UTC — and
10+
MySQL 8.0 rejects it on a DATE column outright (MariaDB is merely permissive,
11+
and the driver's UTC-pinned session masked the semantic half there).
12+
`nowColumnDefault` now emits a UTC expression default on both dialects, the
13+
#3994 D-C3 construction one type over. Defaults only govern newly created
14+
columns; existing columns keep their legacy default, per the standing D-B3
15+
policy.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/driver-memory": minor
3+
"@objectstack/driver-sql": patch
4+
---
5+
6+
fix(driver-sql,driver-memory): an uncompilable filter now throws instead of matching everything (#3948)
7+
8+
A filter the driver could not compile was **skipped**, not rejected. No predicate
9+
was emitted and the query returned every row — the caller asked to filter and
10+
silently received the unfiltered set.
11+
12+
The reachable shape is a bare comparison triple. `['close_date','before','2024-01-01']`
13+
arrives at a driver only when `isFilterAST()` refused it — its operator is outside
14+
`VALID_AST_OPERATORS`, so `parseFilterAST()` never converted it and the raw array
15+
was assigned to `where`. `driver-sql`'s loop then saw three *strings*, matched
16+
neither `and` nor `or`, and `continue`d past all three. `driver-memory` was worse:
17+
it cast every string to a logic keyword, opening three empty groups and returning
18+
`{}` — a filter matching every record.
19+
20+
This is reachable from ordinary authoring, not just malformed input: `before` and
21+
`after` are canonical `VIEW_FILTER_OPERATORS` members that `VALID_AST_OPERATORS`
22+
does not accept. Eight of the nineteen canonical view operators are in that
23+
position, including `equals`; the others were masked only because ObjectUI's
24+
adapter alias table happened to cover them.
25+
26+
**Behaviour change.** Both drivers now throw on a filter element that is neither a
27+
logical keyword (`and`/`or`) nor a condition array, and `driver-memory` throws on
28+
an operator it cannot express rather than dropping the condition. The nested and
29+
`$`-object paths already threw on the same input, so this makes the three paths
30+
agree. A caller that was relying on the old silence was receiving wrong results;
31+
the error names the operator and the offending filter.
32+
33+
**`driver-memory` also gains seven operators it silently ignored:** `not_in`,
34+
`is_null`, `is_not_null`, `isnull`, `isnotnull`, `is_empty`, `is_not_empty` — all
35+
members of `VALID_AST_OPERATORS`, all previously falling through to
36+
`default: return null`. `is_null` narrowed nothing instead of matching null rows.
37+
Alias sets and semantics mirror `driver-sql`'s `whereNull`/`whereNotNull` arms so
38+
the two backends accept one vocabulary.
39+
40+
Migration: none for well-formed filters. If a query now throws, the filter was
41+
never being applied — fix the operator (the message names it), or lower it to an
42+
AST spelling. `before` → `<`, `after` → `>`, `'not in'` → `nin`.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@objectstack/lint': minor
5+
---
6+
7+
Validate the expression slots a flow node's `configSchema` declares (#4027).
8+
9+
A node type's designer `configSchema` and the keys its validators traverse were
10+
two unreconciled lists. Both the engine's `registerFlow` pass and the author-time
11+
`objectstack validate` pass hardcoded `config.condition` / `edge.condition` and
12+
assumed every other node string was a `{var}` template — so a declared expression
13+
property outside that hardcoded set was validated by nobody.
14+
15+
That is how #3528 shipped. `screen.fields[].visibleWhen` has been on the `screen`
16+
descriptor since #3304, typed `xExpression: 'expression'` (bare CEL) and offered
17+
to authors in Studio, but no validator traversed it. An app authored the
18+
predicate in the *other* dialect — `'{createOpportunity} == true'` — and it passed
19+
`tsc`, `objectstack validate` and registration in silence. Because `required` *is*
20+
enforced, a field the author had made conditional rendered unconditionally and
21+
blocked Submit on an input the user was never shown: the run paused forever and no
22+
resume was ever issued.
23+
24+
Now:
25+
26+
- **`FLOW_NODE_EXPRESSION_PATHS`** (`@objectstack/spec`) is the declared ledger of
27+
expression-bearing node config paths, each recording the dialect it takes.
28+
- **Both validators read it.** A malformed `visibleWhen` is a located, quoted
29+
error at `registerFlow` *and* at `objectstack validate` — `node 'screen_1'
30+
(screen) screen field visibleWhen at config.fields[1].visibleWhen`.
31+
- **A reconciliation ratchet** derives the expression properties from the live
32+
descriptors and fails CI in both directions: a new `xExpression` property with
33+
no ledger entry, or a stale entry no descriptor declares. It walks every
34+
registered builtin, not just `screen`.
35+
36+
Dialects are recorded rather than assumed because there are three, and two of them
37+
disagree about braces: bare CEL (`{…}` is the #1491 brace-trap), single-brace
38+
`{var}` flow interpolation (`{…}` is correct), and the ADR-0032 §3 double-brace
39+
text template. Only bare-CEL slots are checked — `loop.collection` and
40+
`map.collection` are recorded as `flow-template` and deliberately left alone,
41+
since no validator implements their dialect and checking them under either of the
42+
other two would reject every currently-valid flow.
43+
44+
`ActionDescriptor.configSchema`'s TSDoc no longer claims `registerFlow()`
45+
validates `config` against it. It never did: `FlowNodeSchema.config` is
46+
`z.record(z.unknown())`, so types, `required`, `enum` and unknown keys are still
47+
unenforced. The doc now states exactly what is checked and what is designer-facing
48+
only, so nothing relies on a guard that does not exist.

0 commit comments

Comments
 (0)