Skip to content

Commit 14305ce

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21464-s-metric
2 parents 072d08d + 5c9138b commit 14305ce

33 files changed

Lines changed: 2260 additions & 188 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Liveness ledger: a permission set's row-level security policy `label` and `description` (`rowLevelSecurity[].label` / `.description`) are now `live`, not `dead`. Studio's permission editor shows both on every policy. Ledger data and its generated count shard only.
6+
7+
Clause-②: no
8+
9+
- **What shows them.** These are display keys, so under the ledger's "Designer previews count as consumers" ruling, being shown to a human is the whole of their claimed effect. The Row-Level Security section of the permission editor (`PermissionAdvancedFacets` in objectui) now heads each policy card with the policy's `label` and, beneath it, its `description`, exactly as written. Both rows cite that reader at the `.objectui-sha` pin `89cad75d557`. The registered permission preview also draws both, but no route mounts it for `permission`, so it is not cited.
10+
- **Where the values come from.** Each row names its producer: the `permission` edit page registration and the Studio edit route that mounts it, the editor's `GET /api/v1/meta/permission/:name/layers` read, and this repo's shared layered answer (`createMetaLayeredAnswer`), which serves a permission set whole. The showcase's contributor permission set authors both keys on all three of its policies.
11+
- **Author-facing effect.** `os lint` / `os validate` no longer warn `liveness-dead-property` on a policy that sets `label` or `description`. A warning is not a refusal, so the accept set is unchanged.
12+
- **Still kept.** The re-grade reverses no ADR-0033 decision. Both rows stay docs-shaped annotation, deliberately kept and not `authorWarn`'d.
13+
- The regenerated liveness count is the `liveness/state-counts/permission.md` shard: `permission` has 38 live and 4 dead (was 36 and 6). The `view` container's own `label` stays `dead`.
14+
- ⛔ No schema, parse, `.describe()`, export or accept-set change.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/metadata-core': minor
3+
'@objectstack/metadata-protocol': patch
4+
'@objectstack/rest': patch
5+
---
6+
7+
Public forms on a walled tenancy posture: saving or publishing a view whose public form cannot take anonymous intake now tells the author why, on the response.
8+
9+
Clause-②: yes (widening)
10+
11+
On a walled posture (`group` or `isolated` in force), an open public form whose object is walled by an organization column cannot take an anonymous submission: the submission carries no organization, and an insert without one into a walled object is refused. The two anonymous form endpoints already answer such a form as a withdrawn one (`404 FORM_NOT_FOUND`), and the administrator's read of the view (`GET /meta/view/:name`) already states why in `_diagnostics.warnings`.
12+
13+
- **`@objectstack/metadata-protocol`**: saving the view (`PUT /meta/view/:name`) or publishing its draft (`POST /meta/view/:name/publish`, and a package's batch publish) now answers success with one `warning` advisory per such form, under `advisories`, with rule `public-form-intake-unavailable`. It is located at the form's `sharing` (for example `views[0].formViews.contact.sharing`), its `message` is the same text the administrator's read states, and its `hint` is the remedy: if the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it. The write is never refused. The advisory reads the posture in force from the `tenancy` service, which is what the anonymous endpoints read: a single-posture deployment, a deployment whose walled posture is degraded to `single`, a deployment with no tenancy service, and a form bound to a tenancy-disabled object get no advisory, and a draft save is not judged. The publish refusal for an unstamped platform schedule flow still reads the requested posture, as before.
14+
- **`@objectstack/metadata-core`**: the intake-availability rule moved here from `@objectstack/rest` and is exported, so the anonymous endpoints, the administrator's read and the publish advisory read one answer: `anonymousFormIntakeUnavailability(object, posture, readObjectSchema)` (`null` when the form can take intake, otherwise the object, the posture and the wall column; it judges the object's effective schema, with the injected `organization_id`), `anonymousFormIntakePosture(tenancy)` (the posture in force, as a tenancy service reports it), `anonymousFormIntakeUnavailableMessage` and `anonymousFormIntakeUnavailableRemedy` (the reason and its remedy), `anonymousFormSharingPath` and `anonymousFormObjectName`, and the type `AnonymousFormIntakeUnavailable`.
15+
- **`@objectstack/rest`**: the anonymous form endpoints and the administrator's read import that rule instead of holding their own copy. Their answers are unchanged.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/driver-sql': patch
3+
---
4+
5+
A `Field.date` grouped by `day`, `week`, `month`, `quarter` or `year` buckets as its own calendar day on PostgreSQL and MySQL, whatever zone the server or the session is in (#21485).
6+
7+
Clause-②: no
8+
9+
- **What was wrong.** The PostgreSQL bucket cast every column to `timestamptz` and the MySQL bucket passed every column through `convert_tz`. A `date` has no instant, so both invented midnight in the session's zone, and on a session east of UTC the conversion to UTC read the previous day. On PostgreSQL with the server at `Asia/Shanghai`, `2026-06-01` grouped into month `2026-05`, and `2026-01-01` into year `2025`. MySQL did the same once the session zone was `+08:00`; the driver pins its own sessions to UTC, so there it took a host `pool.afterCreate` that sets the session zone.
10+
- **What it does now.** A declared `Field.date` buckets its calendar day with no zone conversion. A `Field.datetime`, and a column with no declaration, keep the UTC-instant expression, byte for byte. SQLite already bucketed a `date` as its calendar day and is unchanged.
11+
- **Where it shows.** `aggregate()` with a `dateGranularity` group, and the expression `SqlDriver.dateBucketSql()` renders for the analytics SQL echo, which reads the same expression.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@objectstack/objectql': minor
3+
---
4+
5+
fix(objectql)!: a read with no projection serves the object's declared fields and the platform's system columns, never a column no metadata declares (#21571)
6+
7+
**BREAKING (narrowing)** — what a released read door serves shrinks. A column that
8+
no metadata declares, typically a field retired in an upgrade whose column additive
9+
schema sync leaves in the table until `os migrate apply --allow-destructive`, is no
10+
longer returned by any read through the engine.
11+
12+
| read | before | now |
13+
| --- | --- | --- |
14+
| `POST /api/v1/data/:object/query` or `GET /api/v1/data/:object` with no `fields` | every column of the table, retired ones and their values included | the declared fields, the registry's system columns, `id`, `created_at`, `updated_at` |
15+
| `GET /api/v1/data/:object/:id`, export, search hits, the RPC dispatcher, `expand`ed records | the same whole row | the same declared set |
16+
| `engine.find` / `engine.findOne` in process (hooks, flows, plugins), no `fields` | the whole row | the declared set |
17+
| an explicit `fields` naming a declared field whose column does not exist yet (driver-sql retries `select('*')`) | the whole row, retired columns included | the declared set |
18+
| `POST /api/v1/data/:object/:id/clone` of a record whose table carries a retired column | refused `INVALID_FIELD` (the copy carried the retired column into the insert) | cloned |
19+
20+
**Unchanged:** naming a retired column in `fields` still answers `400 INVALID_FIELD`
21+
on the data door. Declared fields keep their treatment: `internal: true` omission,
22+
credential masking, formula evaluation and the hidden `__search` strip run as
23+
before, and the registry-injected tenant, owner and audit columns are still served.
24+
No driver changed: the engine shapes the rows any driver returns, so the answer is
25+
the same on every driver and every door. Writes, and the rows a write returns, are
26+
not changed by this release.
27+
28+
**If you still read a retired column's values** (for example a one-time conversion
29+
that copies the old columns into their replacement field): run that conversion
30+
BEFORE upgrading to this release, while the old field is still declared, or, once
31+
it lands, read the unmapped columns through the operator-only `os migrate` read
32+
(objectstack#21573). There is no flag that re-opens undeclared columns on a runtime
33+
door. An in-process reader that needs a column must declare it as a field.
34+
35+
Clause-②: no (narrowing)
36+
37+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key, spelling, export, config field or stored shape is removed or renamed, and no stored row is read or rewritten: the change narrows which physical columns the engine's read verbs return, to the field map the metadata already declares. A retired column's values stay in the table; what an app does with them is an operator action stated above (convert before upgrading, or the operator-only os migrate read), not a FROM to TO mapping that objectstack migrate meta could apply. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a read projection (not registered / already-registered); and the change is runtime behaviour, not a declaration (not runtime-interface-only / type-surface-only). -->

‎content/docs/data-modeling/queries.mdx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,13 @@ pattern the metadata-revision / flow-run / notification list endpoints already u
285285
}
286286
```
287287

288+
Without `fields`, a read returns the object's **declared** fields plus the platform's
289+
system columns (`id`, `created_at`, `updated_at`, and the tenant, owner and audit columns
290+
the registry adds). A column no metadata declares is never returned — for example one a
291+
retired field left in the table until `os migrate apply --allow-destructive` drops it — and
292+
naming it in `fields` on the data API is refused with `400 INVALID_FIELD`. To read such a column's values
293+
for a one-time conversion, run the conversion before the field is retired.
294+
288295
### Nested / Related Fields
289296

290297
{/* os:check */}

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ orthogonal to both, and no cell here can carry it; it is written out in
478478
| Declared enforcement that cannot run, **declared on the object being written** — a validation rule's regex / JSON Schema (#4762) and its `format` names (#5178) | ✓ | ✓ | ✓ | ✓ᵒ |
479479
| Declared enforcement that cannot run, **declared on another collection** — sharing-rule conditions (#4698), row-level-security predicates (#4983) | ✓ | ✓ | ✓ | — |
480480
| Platform-schedule `create_record` organization (#6285) | — | — | — | ✓ᶠ |
481+
| Public-form anonymous intake on this deployment's tenancy posture — advisory only (#21476) | — | — | — | ✓ᵛ |
481482
| Autonumber `{field}` interpolation | ✓ | ✓ | ✓ | ✓ᵒ |
482483
| View references — form targets, view-key collisions (#2554) | ✓ | ✓ | ✓ | — |
483484
| Flow authoring anti-patterns (#1874) | ✓ | ✓ | ✓ | ✓ᶠ |
@@ -594,11 +595,17 @@ The fourth door does not weaken that, because it is held to the CLI's verdicts
594595
rather than to its own: a test fails if a rule runs at the runtime publish gate
595596
but not on `os build` — the two publish verbs must not disagree. What that
596597
column narrows is which *types* it judges, never which *verdict* it reaches. The
597-
one deliberate exception is the platform-schedule row (#6285), runtime-only by ruling:
598-
both of its inputs are facts about the **deployment** (the organization this
599-
write lands in, and whether this deployment walls organizations), and a build
600-
machine's environment is a false signal for them — so `os build` must not judge
601-
it at all.
598+
deliberate exceptions are the two rows whose inputs are facts about the
599+
**deployment**, and a build machine's environment is a false signal for those —
600+
so `os build` must not judge them at all. The platform-schedule row (#6285) is
601+
runtime-only by ruling: its inputs are the organization this write lands in and
602+
whether this deployment walls organizations. The public-form intake row (#21476)
603+
reads the tenancy posture **in force**: on a walled posture, an open public form
604+
whose object is walled by an organization column cannot take an anonymous
605+
submission, so the anonymous form endpoints do not offer it, and a save or
606+
publish of the view answers success with a `public-form-intake-unavailable`
607+
warning in `advisories`, located at the form's `sharing`. It never refuses the
608+
write.
602609

603610
Some rows are deliberately not universal across the three commands, and each is
604611
one-directional (none lets a stack through a gate another command enforces):

‎content/docs/ui/forms.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export default defineView({
100100
> - Anything not in the `sections[].fields[]` whitelist is silently stripped at submit time. Treat the whitelist as the form's authoritative "what the public is allowed to set" list.
101101
> - A form whose sections declare **no** fields collects nothing, so the submit is **refused** (`400 VALIDATION_ERROR`) rather than accepting whatever the caller sent (#6920). Its `GET /forms/:slug` publishes no schema either (#6601) — declare the fields and both planes come alive together.
102102
> - Multiple form views per object are fine — only the one(s) with `sharing.enabled === true` and `sharing.allowAnonymous === true` are exposed.
103+
> - On a **walled** tenancy posture (`group` or `isolated` in force), a form whose object is walled by an organization column is **not offered**. An anonymous submission carries no organization, and an insert without one into a walled object is refused, so both anonymous endpoints answer the form exactly as they answer a withdrawn one (`404 FORM_NOT_FOUND`). The administrator is told why, at the form's `sharing`: the view's read (`GET /api/v1/meta/view/:name`) carries it in `_diagnostics.warnings`, and a save or publish of the view answers success with a `public-form-intake-unavailable` warning in `advisories`. If the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it and the form is offered again.
103104
104105
## 2. (Optional) Create the `guest_portal` permission set
105106

0 commit comments

Comments
 (0)