Skip to content

Commit 4bc22fe

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21571-unprojected-read-declared-fields
2 parents 99033a9 + 9a4182a commit 4bc22fe

43 files changed

Lines changed: 1751 additions & 407 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/runtime': minor
4+
'@objectstack/cli': minor
5+
---
6+
7+
fix(spec,runtime,cli)!: the in-memory (mingo) engine is no longer a boot store — every boot door refuses it and names SQLite instead (#21492, #21572)
8+
9+
Clause-②: yes (narrowing)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) no metadata body, authorable key or stored shape moves: the driver table keeps `memory`, `mingo`, `in-memory` and `inmemory` on its config-contract face, so `resolveDriverId` answers them exactly as before and a stored `datasource.driver: memory` still parses against `MemoryConfigSchema`; what narrows is the boot selection (`--database-driver`, `OS_DATABASE_DRIVER`, `databaseDriver`, a `memory://` or `mingo://` database URL, and a project's default datasource declared on the engine), which is host configuration that `objectstack migrate meta` does not rewrite — and no rewrite would be truthful, since the only replacement is a different engine the operator has to choose. The other categories are closed on facts: all three packages publish (not `unpublished`); no ADR-0087 id covers a boot selection (not `registered` / `already-registered`); and the change is runtime behaviour plus exported constant values, not an interface or a type alone (not `runtime-interface-only` / `type-surface-only`). -->
12+
13+
**BREAKING**: the in-memory (mingo) engine can no longer be selected as the store a server, a migration or an embedded stack boots on. It refuses every tenant-scoped read by design, so a boot on it signed a user in and then answered `503` to every data request; there was nothing working to keep. The retirement is made at the declaration: `@objectstack/spec`'s driver table withdrew `memory`, `mingo` and `in-memory` from its selection face (they stay on the config-contract face beside `inmemory`), and every boot door refuses the engine with one sentence that names the replacement.
14+
15+
- **`@objectstack/spec`** — `DATABASE_DRIVER_SELECTION_ALIASES` no longer lists `memory`, `mingo` or `in-memory`; `DATABASE_DRIVER_SELECTION_IDS` no longer lists `memory`; `resolveDatabaseDriverId` answers `undefined` for all four spellings. `resolveDriverId`, `DRIVER_ID_ALIASES`, `BUILTIN_DRIVER_IDS` and the `memory` config contract are unchanged.
16+
- **`@objectstack/cli`** — `--database-driver memory` is refused while the flags parse (`os dev`, `os start`); `OS_DATABASE_DRIVER=memory` / `mingo` / `in-memory` is refused before `os dev` or `os start` prints its Database row; `os serve`'s legacy path refuses the spellings and the `memory://` / `mingo://` schemes as a fatal boot error. The help no longer offers `memory://`.
17+
- **`@objectstack/runtime`** — `createStandaloneStack`, `createDefaultHostConfig` and `resolveStandaloneDatabase` (every ordinary `os dev` / `os start` / `os serve` boot and every `os migrate` subcommand) refuse the spellings, the `memory://` and `mingo://` schemes, and a project whose default datasource is declared with `driver: 'memory'`. `resolveProjectDatabaseUrl` refuses a retired driver selection ahead of every rung, and its `ProjectDatabaseUrlSource` type no longer has the `'memory-driver'` member. `ResolvedStandaloneDatabase.driver` never names `memory`. Two exports are added for hosts that refuse the engine themselves: `namesRetiredMemoryEngine` and `retiredMemoryEngineMessage`.
18+
- **Unchanged:** the `@objectstack/driver-memory` package; a declared non-default datasource with `driver: 'memory'` and a directly constructed `InMemoryDriver`, both still built; SQLite's dev step-down, whose last rung is still this driver.
19+
20+
Migration — one flag change:
21+
22+
- FROM `os dev --database-driver memory` (or `OS_DATABASE_DRIVER=memory`) TO `os dev --fresh` for a throwaway database deleted on exit.
23+
- FROM `OS_DATABASE_URL=memory://…` / `--database memory://…` / `databaseUrl: 'memory://…'` TO `:memory:` (SQLite's own in-memory database), e.g. `OS_DATABASE_URL=:memory:`.
24+
- FROM a default datasource declared `{ driver: 'memory' }` TO a SQLite one, e.g. `{ driver: 'sqlite', config: { filename: ':memory:' } }`.
25+
26+
No shipped example selects the engine. It ships as `minor` under the launch-window convention for accept-set narrowings.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
A hook whose `body` targets a table of stored metadata, `sys_metadata` or `sys_metadata_history`, is refused at parse, with the runtime's prescription: change metadata through the metadata API.
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered hook-body-stored-metadata-target-refused -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
**Why.** An app-authored body may not touch the two stored-metadata tables: for a body, the metadata protocol is their only writer, where a change is validated and its provenance is recorded. The runtime already enforces that where a body hook becomes a handler: such a hook is refused at registration and never runs. But `HookSchema` still accepted it, so the metadata save door answered 200 for a hook that would never fire, and the author learned otherwise only from a server log.
14+
15+
**What is refused.** A hook carrying a `body`, in any form, whose `object` names `sys_metadata` or `sys_metadata_history`, as the string or as any member of the list. One such member refuses the whole hook, as the runtime does. The issue's `code` is `custom`, at `object` (or `object.N` for a list member), and its message names the table and ends with the runtime's prescription. The membership test is the kernel's own `isStoredMetadataBodyObject`, the predicate the runtime judges by. That covers `HookSchema`, `defineHook()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `hooks.N.object`), `os validate`, which runs the same stack parse, an artifact's parse, and the metadata save door (`422 INVALID_METADATA`).
16+
17+
**What stays accepted, byte for byte.** A hook with no `body` on those tables (a code `handler`, which is how the platform writes its own hooks), a wildcard (`object: '*'`) hook with a `body` (it names neither table: the runtime binds it and never runs its body for those tables' events), and every hook on any other object.
18+
19+
## FROM → TO
20+
21+
| you wrote | write instead |
22+
|:--|:--|
23+
| a hook with a `body` and `object: 'sys_metadata'` or `object: 'sys_metadata_history'` | change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) instead, and delete the hook |
24+
| a hook with a `body` whose `object` list includes either table | drop those tables from the list; change metadata through the metadata API instead |
25+
| a hook with a `body` on `'*'` or on any other object | unchanged |
26+
27+
**The one-line fix: delete the hook, or remove `sys_metadata` and `sys_metadata_history` from its `object`, and make the change through the metadata API.** The runtime never ran such a hook, so removing it changes nothing an app does.
28+
29+
**Who is affected, measured.** No authored hook targets either table in this repository's `packages/**` and `examples/**` at `44072fc2b9` (317 hook-shaped declarations, 24 of them outside tests; the only hits are the runtime's own tests of its registration refusal) or in hotcrm at `94668373f2` (44 declarations, 40 outside tests, no hit). Deployed metadata was not measured. A stored hook row of this shape still loads, now with a `[metadata_spec_invalid]` warning and a `_diagnostics` badge, and is still never bound.
30+
31+
### The kit
32+
33+
- **The refusal.** An object-level check attached to `HookSchema` with `.superRefine(...)`. A schema derived from `HookSchema` by overriding a key must use `.safeExtend()`, which keeps the check; zod refuses `.extend()` over a refined object. The artifact-stage hook in `@objectstack/spec` now derives that way.
34+
- **The ledger.** The D3 semantic entry `hook-body-stored-metadata-target-refused` (protocol 18). No key is removed, so there is no tombstone, and there is no D2 conversion: a refused hook carries no intent a rewrite could keep.

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ libSQL data stayed untouched. Pass the token with `--database-auth-token`
7979
| **SQLite (WASM)** | `@objectstack/driver-sqlite-wasm` | `SqliteWasmDriver` | `sqlite-wasm` \| `wasm-sqlite` \| `wasm` |
8080
| **MongoDB** | `@objectstack/driver-mongodb` | `MongoDBDriver` | `mongodb` \| `mongo` (single-tenant only — see [below](#multi-tenancy-not-supported)) |
8181
| **Turso / libSQL** | `@objectstack/driver-turso` (optional install — see the callout above) | `TursoDriver` | `turso` \| `libsql` |
82-
| **Memory** | `@objectstack/driver-memory` | `InMemoryDriver` | `memory` |
82+
| **Memory** | `@objectstack/driver-memory` | `InMemoryDriver` | none — not a boot store (see [Memory Driver](#memory-driver)) |
8383

8484
> All SQL flavours (PostgreSQL / MySQL / SQLite) are served by a single
8585
> `@objectstack/driver-sql` package — there is no separate
@@ -101,7 +101,7 @@ driver ships that contract as a zod schema, exported from `@objectstack/spec/dat
101101
| `sqlite` \| `sqlite3` | `SqliteConfigSchema` | `filename`, `autoMigrate` |
102102
| `sqlite-wasm` \| `wasm-sqlite` | `SqliteWasmConfigSchema` | `filename`, `persist` |
103103
| `mongo` \| `mongodb` | `MongoConfigSchema` | `url`, `host`, `port`, `database`, `username`, `password`, `authSource`, `options` |
104-
| `memory` \| `in-memory` | `MemoryConfigSchema` | `initialData`, `strictMode`, `persistence` |
104+
| `memory` \| `in-memory` \| `mingo` | `MemoryConfigSchema` | `initialData`, `strictMode`, `persistence` |
105105

106106
An unrecognised key is rejected with its correction, at authoring time and in the
107107
Setup → Datasources wizard alike:
@@ -798,13 +798,25 @@ exits unless persistence is explicitly requested.
798798
It is the **last-resort fallback** in dev mode: `objectstack dev` prefers native
799799
SQLite (`better-sqlite3`), falls back to the pure-JS WASM SQLite driver if the
800800
native binary is unavailable, and only drops to the in-memory driver if WASM also
801-
fails to load. Set `OS_DATABASE_DRIVER=memory` to select it explicitly.
801+
fails to load.
802+
803+
<Callout type="warn">
804+
**It cannot be selected as a boot store.** The driver has no row-level tenant
805+
isolation and refuses every tenant-scoped call, so a server booted on it signs a
806+
user in and then answers `503` to every data request. Every boot door refuses it,
807+
naming the replacement: `OS_DATABASE_DRIVER=memory` / `mingo` / `in-memory`,
808+
`--database-driver memory`, a `memory://` or `mingo://` database URL, and a
809+
project whose **default** datasource is declared with `driver: 'memory'`. For a
810+
throwaway database use `os dev --fresh`; for SQLite's own in-memory database use
811+
the URL `:memory:` (`OS_DATABASE_URL=:memory:`). The driver package itself is
812+
unchanged: the declared datasources and direct construction below still build it.
813+
</Callout>
802814

803815
**Both ways of building the driver are ephemeral by default** (#4083, #4065):
804816

805817
| How it is built | Persistence |
806818
| :--- | :--- |
807-
| A **declared datasource** — `{ driver: 'memory' }` in a stack/app config | **Ephemeral.** Nothing is written to disk unless the declaration sets `config.persistence` — and when it does, the destination is scoped per datasource, so two memory datasources never share one file. |
819+
| A **declared datasource** — `{ driver: 'memory' }` in a stack/app config (not as the project's default datasource, which the boot refuses) | **Ephemeral.** Nothing is written to disk unless the declaration sets `config.persistence` — and when it does, the destination is scoped per datasource, so two memory datasources never share one file. |
808820
| `new InMemoryDriver()` **constructed directly** | **Ephemeral.** Pass `persistence` to opt in (see below). Note the scoping above is the *factory's* job: two directly-constructed `'auto'` drivers in one process still share the single default path. |
809821

810822
```typescript

‎content/docs/deployment/cli.mdx‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ predicate/schema/binding mistakes that fail silently at runtime), and `os dev --
6969
</Callout>
7070

7171
<Callout type="tip">
72-
`os dev --ui` starts a dev server with the bundled Console UI, auto-loads ObjectQL, an in-memory driver when appropriate, and the Hono HTTP server.
72+
`os dev --ui` starts a dev server with the bundled Console UI, auto-loads ObjectQL, a SQLite database (a persistent project file by default, a throwaway one with `--fresh`), and the Hono HTTP server.
7373
</Callout>
7474

7575
## Commands
@@ -182,8 +182,8 @@ os dev --database file:./data/test.db --auth-secret $(openssl rand -hex 32)
182182
| Flag | Env equivalent | Purpose |
183183
|---|---|---|
184184
| `-a, --artifact <path\|url>` | `OS_ARTIFACT_URL` / `OS_ARTIFACT_PATH` | Boot a pre-built artifact directly; **skips auto-compile** |
185-
| `-d, --database <url>` | `OS_DATABASE_URL` | `file:…` / `libsql://` / `postgres://` / `mongodb://` / `memory://` |
186-
| `--database-driver <kind>` | `OS_DATABASE_DRIVER` | Force `sqlite` \| `sqlite-wasm` \| `turso` \| `postgres` \| `mysql` \| `mongodb` \| `memory` |
185+
| `-d, --database <url>` | `OS_DATABASE_URL` | `file:…` / `:memory:` / `libsql://` / `postgres://` / `mongodb://` |
186+
| `--database-driver <kind>` | `OS_DATABASE_DRIVER` | Force `sqlite` \| `sqlite-wasm` \| `turso` \| `postgres` \| `mysql` \| `mongodb` |
187187
| `--database-auth-token <t>` | `OS_DATABASE_AUTH_TOKEN` | libsql/Turso token |
188188
| `--auth-secret <s>` | `OS_AUTH_SECRET` | Override the dev-fallback secret |
189189
| `--environment-id <id>` | `OS_ENVIRONMENT_ID` | Environment identifier (default `env_local`) |
@@ -472,8 +472,8 @@ os start
472472
|---|---|---|
473473
| `-a, --artifact <path\|url>` | `OS_ARTIFACT_PATH` | File path or `http(s)://` URL to the compiled artifact |
474474
| — | `OS_ARTIFACT_URL` | Boot a published artifact **by reference**, optionally content-hash pinned via a `#sha256=` fragment. See [Artifact-pinned boot](/docs/deployment/self-hosting#artifact-pinned-boot-os_artifact_url) |
475-
| `-d, --database <url>` | `OS_DATABASE_URL` | `file:…` / `libsql://` / `postgres://` / `mongodb://` / `memory://` |
476-
| `--database-driver <kind>` | `OS_DATABASE_DRIVER` | Force `sqlite` \| `sqlite-wasm` \| `turso` \| `postgres` \| `mysql` \| `mongodb` \| `memory` when the URL is ambiguous |
475+
| `-d, --database <url>` | `OS_DATABASE_URL` | `file:…` / `:memory:` / `libsql://` / `postgres://` / `mongodb://` |
476+
| `--database-driver <kind>` | `OS_DATABASE_DRIVER` | Force `sqlite` \| `sqlite-wasm` \| `turso` \| `postgres` \| `mysql` \| `mongodb` when the URL is ambiguous |
477477
| `--database-auth-token <token>` | `OS_DATABASE_AUTH_TOKEN` | Auth token for libsql/Turso |
478478
| `--auth-secret <secret>` | `OS_AUTH_SECRET` / `AUTH_SECRET` | Secret for `@objectstack/plugin-auth`. If neither the flag nor the env var is set, `os start` **auto-generates one** and persists it at `<home>/auth-secret` |
479479
| `--home <dir>` | `OS_HOME` | Home directory for persistent state (default `<cwd>/.objectstack` when an `objectstack.config.ts` is present, otherwise `~/.objectstack`) |
@@ -502,10 +502,24 @@ The fall-through applies to the **conventional** locations only. Remote
502502
(`http(s)://`) sources cannot be checked up front and are validated when fetched.
503503
</Callout>
504504
505+
<Callout type="warn">
506+
**The in-memory (mingo) engine is not a boot store.** It refuses every
507+
tenant-scoped read, so a server booted on it signed you in and then answered
508+
`503` to every data request. Every boot command — `os dev`, `os start`,
509+
`os serve` and every `os migrate` subcommand — refuses it: the
510+
`--database-driver memory` flag value (rejected while the flags parse),
511+
`OS_DATABASE_DRIVER=memory` / `mingo` / `in-memory`, and a `memory://` or
512+
`mingo://` database URL, each with a message naming the replacement. Use SQLite
513+
instead: `os dev --fresh` for a throwaway database that is deleted on exit, or
514+
`--database :memory:` (`OS_DATABASE_URL=:memory:`) for SQLite's own in-memory
515+
database. A datasource you declare with `driver: 'memory'` still validates;
516+
as the project's **default** datasource it is refused at boot the same way.
517+
</Callout>
518+
505519
**What it boots:**
506520
- Reads the artifact's `manifest`, `objects`, `views`, `flows`, …
507521
- Auto-registers the platform services declared in `requires: [...]` (e.g. `ai`, `automation`, `analytics`, `auth`, `ui`). Declaring a **service** capability (`automation`, `analytics`, `ai`, `audit`, …) is a *requirement*: if its provider package isn't installed, boot **fails fast** with a clear error instead of silently starting without a capability you asked for. (`auth` and `ui` are tier-gated with their own opt-in rules — `auth`'s secret-gated skip is described below.)
508-
- Auto-detects the driver from the database URL scheme (`memory://` → in-memory, `libsql://`/`https://*.turso.*` → Turso — via the optional `@objectstack/driver-turso` package, and a loud failure with the install command when it is missing rather than a fallback to sqlite —, `postgres[ql]://`/`pg://` → pg, `mongodb[+srv]://` → MongoDB, otherwise sqlite)
522+
- Auto-detects the driver from the database URL scheme (`libsql://`/`https://*.turso.*` → Turso — via the optional `@objectstack/driver-turso` package, and a loud failure with the install command when it is missing rather than a fallback to sqlite —, `postgres[ql]://`/`pg://` → pg, `mongodb[+srv]://` → MongoDB, otherwise sqlite; `:memory:` is SQLite's own in-memory database). `memory://` and `mingo://` are refused — see the callout below.
509523
- Runs standalone boot mode with one active environment.
510524
511525
**Authentication:**

0 commit comments

Comments
 (0)