Skip to content

Commit 561ae05

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15232-plugin-dev-i18n-package-order
2 parents e3aca15 + 460134a commit 561ae05

34 files changed

Lines changed: 2649 additions & 114 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `BulkDataEvent` names the one organization a predicate write's affected records belong to
6+
7+
The realtime `BulkDataEvent` payload (`@objectstack/spec/api`, the body of every
8+
`data.records.updated` / `data.records.deleted` event) gains an optional
9+
`organizationId`: the organization every record the predicate write affected
10+
belongs to — one organization for the whole batch, never per-row and never a
11+
list. It takes the same spelling, the same position beside the match term
12+
`object`, and the same refusal of the empty string as `DataEvent`'s
13+
`organizationId`, so a tenant-scoped consumer discriminates both event families
14+
on one key with one comparison — never a partition of the batch.
15+
16+
Why one organization can be honest on a batch that names no rows: a predicate
17+
write reaches the driver with the security layer's tenant wall AND-composed
18+
onto the caller's filter first (under `isolated` an equality on the caller's
19+
active organization; under `group` membership in the caller's organization
20+
set), and nothing in business RLS or sharing can widen it. When that wall names
21+
exactly one organization, every affected row belongs to it, and the producer
22+
can state so from what it already holds.
23+
24+
What a consumer may assume — and where this deliberately diverges from
25+
`DataEvent`:
26+
27+
- **Present** — every record the write affected belongs to exactly that
28+
organization. Never fabricated, and never the caller's active organization
29+
standing in for the rows'.
30+
- **Absent** — the producer did not assert one organization for the batch: every
31+
event on a `single`-posture deployment; a system, environment-wide or
32+
cross-membership predicate write; any write whose affected rows are not known
33+
to belong to one organization. A bulk event names no rows, so absence is a
34+
statement about the producer's knowledge, not about the rows. It is NOT
35+
`DataEvent`'s reading "belongs to no organization, not behind any wall". A
36+
tenant-scoped consumer (a per-organization webhook subscription, a
37+
per-organization realtime subscriber) must treat an absent key as not
38+
attributable to its organization and must not deliver the event inside an
39+
organization wall; a deployment-wide consumer may use it.
40+
41+
Declared = enforced: the key is optional and nothing else. No default
42+
fabricates a tenant; `null` and the empty string are refused with a located
43+
issue, so "not asserted" has exactly one spelling — the key is absent.
44+
45+
Additive and shape-preserving: every bulk event that parsed before parses
46+
identically, and no producer emits the key yet — the ObjectQL engine's bulk
47+
publish site is a separate change that follows this contract. `DataEvent` and
48+
`MetadataEvent` are unchanged.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): a published `DataEvent` now names the organization the RECORD belongs to
6+
7+
`DataEventSchema.organizationId` has been declared and published since the spec
8+
half landed, and its TSDoc states the obligation on the producer's side: *"a
9+
producer that omits the key on an organization-stamped row publishes a
10+
cross-tenant event, which is fixed at the publish site — never by a
11+
consumer-side lookup."* The engine populated it on no event at all. Every
12+
`data.record.created` / `updated` / `deleted` went out with the key absent,
13+
which a consumer is required to read as *"this record is behind no organization
14+
wall"* — so an organization-stamped row was published as an unwalled one, and a
15+
tenant-scoped fan-out had nothing to discriminate on.
16+
17+
`publishDataEvent` now resolves the organization from the row itself and spreads
18+
the key in when there is one. The row is already in hand at all three call
19+
sites — the written record on `created`, the post-state on `updated`, and the
20+
pre-image on `deleted` (the by-id branch reads it unconditionally for its
21+
existence gate) — so this buys **no** per-event read: the key exists precisely
22+
to keep a per-event lookup off the fan-out path.
23+
24+
Three properties are deliberate:
25+
26+
- **The RECORD's organization, never the caller's.** The row's own tenant column
27+
is the only source consulted. `ExecutionContext.tenantId` is the caller's
28+
*active* organization; the two coincide on an ordinary tenant write and
29+
diverge on a system or unscoped one, where substituting it would mislabel an
30+
administrator's write into another organization as belonging to the
31+
administrator's.
32+
- **Absence has exactly one spelling: the key is omitted.** An object that is
33+
not tenant-scoped, a row whose column is empty, and a value no id can be read
34+
off all publish the key absent rather than `null`, `''` or an explicit
35+
`undefined`. The schema refuses the empty string outright, so producing one
36+
would have thrown at the publish site and dropped the event entirely.
37+
- **The column is resolved the way the write path resolves it** — the
38+
`tenancy.enabled: false` opt-out, then a declared `tenancy.tenantField`, then
39+
the injected `organization_id` — so the event cannot name an organization for
40+
a column the engine does not actually scope by. Note the two spellings differ:
41+
the column is `organization_id`, the published key is `organizationId`.
42+
43+
No schema, no accepted shape and no public export moves: the key was already
44+
declared, already validated and already part of what consumers parse. Only the
45+
implementation changed, from omitting a declared key to populating it.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): the packages domain reaches the `protocol` service through a typed handle (#13598)
6+
7+
`deps.resolveService(context, 'protocol')` answers `any``protocol` is
8+
deliberately left unmapped in `ServiceSlotContracts` — so every request literal
9+
downstream of that seam compiled against nothing. Twelve sites in
10+
`domains/packages.ts` held that `any` (two of them on the variable declaration
11+
rather than the call), and an undeclared or misspelt key in the ADR-0045
12+
publish-visibility flip's `getMetaItems` / `saveMetaItem` literals compiled
13+
silently. Measured on the base tree: injecting `bogusUndeclaredKey: true` into
14+
the `saveMetaItem` literal gave `tsc --noEmit` exit 0 and zero diagnostics.
15+
16+
The slot is now narrowed once, at one helper, to a handle `Pick`ed from the
17+
DECLARED contracts — `MetadataProtocol` / `PackageProtocol` from
18+
`@objectstack/spec`, plus the producer's own exported `DeletePackageRequest`
19+
so the same injection is now `error TS2353`. Every member is OPTIONAL and every
20+
`typeof protocol.<verb> === 'function'` capability probe is unchanged: a host
21+
may occupy the slot with a partial object, and the type answers "is this key
22+
declared?" while the probe still answers "did this host bring the verb?".
23+
24+
Compile-layer signal only — no request is newly accepted or refused, no
25+
response shape moves, and the eight verbs no contract declares keep an explicit
26+
`any` request rather than a private restatement nothing verifies.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
fix(service-automation): put the test layer in front of tsc, and repair the TS2341 x3 it was hiding (#15048)
6+
7+
`packages/services/service-automation` had **no `typecheck` script at all**
8+
its scripts were `build` and `test` — so no tsc program anywhere read this
9+
package (`turbo run typecheck` selects only packages that declare the task, so
10+
it skipped this one silently). `tsup` transpiles with esbuild and `vitest`
11+
runs through esbuild type-**stripping**; neither type-checks. The package's
12+
own `tsconfig.json` does include the tests and always did, so the program that
13+
would have read them already existed and was simply never invoked. This is
14+
the `packages/services/**` sibling of `@objectstack/service-cluster`'s same
15+
graduation (#14181 / PR #15032), reached by the same road in.
16+
17+
What that hid was three `TS2341`s, all in
18+
`src/nested-region-parity.test.ts` (lines 95/151/180):
19+
20+
```
21+
error TS2341: Property 'flows' is private and only accessible within class 'AutomationEngine'.
22+
```
23+
24+
Three tests dot-read the private `AutomationEngine#flows` map directly
25+
instead of going through the class's own public accessor,
26+
`await engine.getFlow(name)` — already the idiom every other test file in
27+
this package uses. The fix replaces the three private reads with that
28+
existing public call (making the two synchronous test bodies `async` where
29+
they were not already); no source signature was widened, no cast was added.
30+
31+
Wired by the route the `packages/plugins/**` family settled on in #14062 and
32+
`service-cluster` carried into `packages/services/**` in #14181: a sibling
33+
`tsconfig.test.json` that changes **module semantics only** (`esnext` /
34+
`bundler` / `lib: ES2022`, matching how vitest actually executes these files)
35+
with **strictness inherited and untouched**, named by a new `typecheck`
36+
script through the shared `check:test-typecheck` gate. Measured before the
37+
repair: 3 errors under build semantics (`tsc -p tsconfig.json`, which already
38+
included the tests), 3 under the new config — the two readings agree, so this
39+
package carried no config-tier pile, and all 3 were genuinely code-tier from
40+
the start. After: 0 and 0, across a 555-file program covering all 103 of its
41+
`src/**/*.test.ts`.
42+
43+
No `test-typecheck-debt.json` is added, and its **absence is the zero**: the
44+
gate reads a missing ledger as `{ entries: {} }`, under which any error in any
45+
file here is red immediately. The package's `DEBT` entry in
46+
`scripts/check-type-check-coverage.mjs` (`errors: 3`) is deleted in this PR
47+
rather than lowered — that is the graduation the ratchet's own invariant
48+
requires, and it is why the errors were fixed rather than ledgered.
49+
50+
`scripts/check-type-source-resolution.mjs` also gains a registry entry for
51+
this package: onboarding `tsconfig.test.json` moved the package's tsc program
52+
set (per that gate's documented onboarding-limb terms), exposing 9 workspace
53+
deps whose types resolve through `dist/` with no pre-existing program for them
54+
to have been laundered through. `paths` was measured and rejected as the
55+
alternative — it takes this package's test layer from 0 errors to 648, nearly
56+
all billed to other packages' source.
57+
58+
No runtime code changes: `src/**` (excluding the one edited test file, whose
59+
own assertions are unchanged — only how it reaches the flow moved) is
60+
otherwise byte-identical, so no shipped behaviour moves. The `patch` level
61+
reflects the published `package.json` gaining `typecheck` /
62+
`check:test-typecheck` scripts and a `tsx` devDependency.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/runtime": patch
3+
"@objectstack/metadata": patch
4+
---
5+
6+
fix(runtime,metadata): the default local environment id is `env_local`, not `proj_local` (#13366)
7+
8+
The v5.0 `project` to `environment` rename changed the default local environment
9+
id and shipped that change on the surfaces most people meet: `packages/cli`'s
10+
`CHANGELOG.md` records "Default local env id: `proj_local` -> `env_local`", the
11+
`os dev` / `os start` / `os serve` commands emit `env_local`, and
12+
`content/docs/deployment/cli.mdx` documents `env_local` as the default. Two
13+
sites never received it and kept stamping `proj_local`.
14+
15+
FROM: `createStandaloneStack()` — with no `environmentId` in its config and no
16+
`OS_ENVIRONMENT_ID` in the environment — stamped `proj_local` on the kernel it
17+
composed, and `MetadataPlugin` used `proj_local` to fill the environment-artifact
18+
validation envelope for a bare definition.
19+
20+
TO: both stamp `env_local`.
21+
22+
WHO SEES IT. Two audiences, both on the DEFAULT path — no `environmentId` in
23+
the config and no `OS_ENVIRONMENT_ID` in the environment:
24+
25+
1. a host that calls `createStandaloneStack` / `createDefaultHostConfig`
26+
**directly**;
27+
2. a **bare `os serve`** — one not spawned by `os dev` / `os start`. Those two
28+
commands export `OS_ENVIRONMENT_ID=env_local` into the child process, which
29+
the fallback yields to, so a boot they start never reached the changed line.
30+
`os serve` sets no such variable for its own boot: it only READS one to name
31+
the runtime state file. So a bare `os serve` used to run a kernel stamped
32+
`proj_local` while publishing `runtime.env_local.json` beside it; the two now
33+
agree.
34+
35+
Where the id is observable — row scoping in `ObjectQLPlugin`, the
36+
`X-Environment-Id` header, `sys_metadata.environment_id` — such an embedder now
37+
sees `env_local` where it saw `proj_local`, so an install with rows already
38+
written under the old id should set `environmentId: 'proj_local'` (or
39+
`OS_ENVIRONMENT_ID=proj_local`) explicitly to keep them addressed. That escape
40+
hatch is unchanged and still wins over the default.
41+
42+
NOT CHANGED, deliberately: `@objectstack/cloud-connection` still treats BOTH
43+
spellings as the local sentinel, so a persisted `OS_ENVIRONMENT_ID=proj_local`
44+
config keeps being recognised as local rather than presented to the control
45+
plane as a cloud environment id; and `package-state-store`'s separate `'default'`
46+
fallback keeps its own spelling, because renaming it would re-key persisted
47+
package-disable state files.

0 commit comments

Comments
 (0)