Skip to content

Commit 7309c81

Browse files
authored
fix(driver-memory,spec): persistence is opt-in again, and the remaining suites run on in-memory SQLite (#4065) (#4100)
`InMemoryDriverConfig.persistence` defaulted to `'auto'`, which on Node means a file adapter — so a bare `new InMemoryDriver()` silently wrote `.objectstack/data/memory-driver.json` into the CWD and reloaded it next boot. `pnpm test` could only ever go green once in one working tree. The default is now `false`, restoring what #815 requirement #1 specified ("默认情况下不启用持久化") and the `'auto'` default had drifted from. Complements #4083, which fixed the same hazard from the factory side and landed first; its per-datasource scoping and regression test are kept as-is. Nine test files move off mingo to in-memory WASM SQLite, matching the dogfood gate. Every failure that migration produced was a fixture defect the memory driver had been absorbing — objects registered after bootstrap were never given tables (surfacing as a 404 OBJECT_NOT_FOUND, a routing-shaped symptom for a DDL-shaped cause), and `sys_notification` was written by a plugin that never declared it (filed as #4154). Also: scaffolds no longer name a driver; the driver's "production-ready" docstring is trimmed to say what it does not enforce; `projectRoot` now reaches the metadata repository instead of only the database; and the CLI stops compiling its own tests into `dist`, where vitest had been running them as stale duplicates of their own sources (81 files/849 tests vs 58/581).
1 parent d9194f0 commit 7309c81

36 files changed

Lines changed: 589 additions & 110 deletions
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/runtime": patch
3+
"@objectstack/cli": patch
4+
---
5+
6+
fix(runtime,cli): `projectRoot` reaches the metadata repository; stop compiling tests into the CLI's dist (#4065)
7+
8+
Two defects behind the last of #4065's stray `.objectstack/` directories — the
9+
one under `packages/cli/`. Neither is cosmetic.
10+
11+
**1. `projectRoot` only got half the stack.** `createStandaloneStack`'s
12+
`projectRoot` is documented as scoping a boot's on-disk state to the project
13+
folder "so different examples / apps don't share a single database by accident",
14+
and it did redirect the default sqlite database. But it was never passed to
15+
`MetadataPlugin`, whose `FileSystemRepository` kept rooting at `process.cwd()`.
16+
So one "project root" meant two different directories: a boot pointed at project
17+
A wrote `A/.objectstack/data/` and `<cwd>/.objectstack/metadata/`. It now
18+
forwards `rootDir`, and `bootSchemaStack` accepts a `projectRoot` to pass down
19+
(defaulting to `process.cwd()`, which is right for every real `os migrate` — the
20+
CLI runs from the project directory). The two migrate integration suites, which
21+
build a fixture project in a tempdir, now scope their boots to it.
22+
23+
**2. The CLI compiled its own tests into `dist/` — and vitest ran them.**
24+
`tsconfig.build.json` included all of `src` with no exclude, so every
25+
`src/**/*.test.ts` was emitted as `dist/**/*.test.js`. Two consequences:
26+
27+
- `files: ["dist"]` **published** them.
28+
- This package has no vitest config, so `vitest run` collected the compiled
29+
copies alongside the sources: **81 test files and 849 tests where the sources
30+
hold 58 and 581**. Every `src/` test also ran as a stale `dist/` twin built
31+
from whatever the source said at the last build.
32+
33+
That is not just noise — it silently defeats edits. A fix to a source test
34+
appeared not to work, because the run was still executing the pre-fix compiled
35+
duplicate; that is exactly how the `.objectstack` residue survived a correct
36+
fix long enough to look like a different bug. It also means a source test could
37+
be edited to pass while its stale twin kept asserting the old behaviour, and
38+
neither would be obviously wrong. Test files are now excluded from the build.
39+
40+
No other package is affected: the rest build with `tsup`, which emits only
41+
declared entry points. Verified by scanning every `packages/*/dist` for
42+
`*.test.js` — the CLI was the only hit.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
"@objectstack/driver-memory": major
3+
"@objectstack/spec": major
4+
"@objectstack/plugin-dev": patch
5+
---
6+
7+
fix(driver-memory,spec): persistence is opt-in again — `new InMemoryDriver()` is pure in-memory (#4065)
8+
9+
`InMemoryDriverConfig.persistence` defaulted to `'auto'`, and in Node.js `'auto'`
10+
means **file**. So a bare `new InMemoryDriver()` — the shape every caller in this
11+
repo used — silently wrote `.objectstack/data/memory-driver.json` into the process
12+
CWD and reloaded it on the next boot. The default is now `false`.
13+
14+
**This restores the accepted design rather than replacing it.** #815, the issue
15+
that introduced the persistence capability, specified it as opt-in in requirement
16+
\#1 — "默认情况下不启用持久化(纯内存,行为不变)" — and listed
17+
`new InMemoryDriver()` under "纯内存" in its own config examples. The `'auto'`
18+
default was a drift from that spec.
19+
20+
What let the drift survive is worth naming, because it is not "there was no
21+
test". `MemoryConfigSchema` *did* pin the default, and asserted `'auto'`; the
22+
driver honoured `'auto'`; so spec and implementation agreed, and the pair looked
23+
verified. What nothing checked was whether the value they agreed on was the one
24+
#815 accepted. The driver's own `persistence.test.ts` could not have caught it
25+
either — every case there passes `persistence` explicitly, so the omitted-value
26+
path was untested on the implementation side. Both sides are now covered: three
27+
behavioural tests in `persistence.test.ts` (no CWD write, no cross-instance row
28+
carry-over, opt-in still persists) and the flipped schema assertion.
29+
30+
**The symptom this fixes.** `packages/runtime/src/datasource-autoconnect.test.ts`
31+
seeds two rows with fixed ids and asserts the exact set. Run 1 passed and wrote
32+
the rows to disk; run 2 loaded them back, appended two more, and failed with four
33+
rows; run N had 2N. CI never saw it — every job is a fresh clone, so every CI run
34+
is run 1 — but `pnpm test` twice in one working tree could only ever go green
35+
once. The persisted file's `created_at` values, one pair per run, were the proof.
36+
37+
(#4083 fixed that particular suite from the factory side, and its regression
38+
test is kept as-is. The blast radius was wider than one suite, though: **every**
39+
bare `new InMemoryDriver()` inherited the default, so any code path constructing
40+
one directly wrote to its working directory. Unit tests should not have write
41+
side effects on the CWD at all.)
42+
43+
**Migrating.** Callers that want durability now ask for it:
44+
45+
```ts
46+
new InMemoryDriver() // pure in-memory (new default)
47+
new InMemoryDriver({ persistence: 'file' }) // Node.js, durable across restarts
48+
new InMemoryDriver({ persistence: 'local' }) // browser, durable across reloads
49+
new InMemoryDriver({ persistence: 'auto' }) // previous default behaviour
50+
```
51+
52+
The `'auto'` / `'file'` / `'local'` / custom-adapter paths are unchanged; only
53+
the value used when `persistence` is omitted moved.
54+
55+
**Relationship to #4083.** That issue fixed the same hazard one consumer at a
56+
time, and landed first: `createDefaultDatasourceDriverFactory` now passes
57+
`persistence: false` for a declared `{ driver: 'memory' }` datasource and scopes
58+
an opted-in destination *per datasource*, and the dev sqlite step-down's
59+
last-resort rung passes `false` too. Both are kept exactly as #4083 wrote them.
60+
This change closes the half they deliberately left open — a directly-constructed
61+
`new InMemoryDriver()` — which is the path that still wrote into the working
62+
directory of whatever process happened to build one.
63+
64+
The two are complementary, not redundant. #4083's per-datasource scoping is
65+
still the only thing that expands `'auto'`/`'file'`/`'local'` into a destination
66+
carrying the datasource name, so two pools that DO opt in never alias one file;
67+
its explicit `false` becomes belt-and-braces, which is the right posture for a
68+
path that must never persist.
69+
70+
`DevPlugin`'s driver is now explicitly `persistence: false`, matching the cache,
71+
queue, job, i18n, storage and search stubs it ships beside — it was the one piece
72+
of that stack that quietly outlived the process.
73+
74+
**One claim trimmed, no behaviour attached.** The class docstring called this a
75+
"production-ready implementation of the ObjectStack Driver Protocol". It stores
76+
no constraints at all — `create()` is a `table.push()` and `syncSchema()` only
77+
allocates an array — so there is no primary key, uniqueness, `NOT NULL`, foreign
78+
key or column typing, and `bulkCreate` lands duplicate ids where a SQL driver
79+
raises a violation (the second finding in #4065). The docstring now says so, and
80+
points test authors at in-memory SQLite. Per Prime Directive #10 the fix for
81+
`declared ≠ enforced` is to implement it, trim the claim, or file it; with this
82+
driver moving to maintenance-only the claim is what goes.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/cli": patch
3+
"create-objectstack": patch
4+
---
5+
6+
chore(cli,create-objectstack): scaffolds no longer name a driver (#4065)
7+
8+
`os init` and the `create-objectstack` blank template both listed
9+
`@objectstack/driver-memory` in the generated `dependencies`. It was the only
10+
driver named, which read as an endorsement — "this is the driver your app runs
11+
on" — when it is in fact the **last-resort rung** of the dev step-down (native
12+
`better-sqlite3` → WASM SQLite → mingo). A new project's first impression of the
13+
data layer should not be the engine that enforces no primary keys, no
14+
uniqueness, no `NOT NULL` and no column types.
15+
16+
It was also redundant: `@objectstack/runtime` already depends on `driver-sql`,
17+
`driver-sqlite-wasm` and `driver-memory`, and every script in both scaffolds runs
18+
through the CLI, which carries all four. Removing the line changes nothing a
19+
generated project can do — `objectstack dev` still resolves SQLite by default,
20+
and `OS_DATABASE_URL` still selects Postgres / MySQL / MongoDB.
21+
22+
Docs updated to match: the "packages you depend on" table in *Your first project*
23+
no longer lists a driver row (it now says where drivers come from), and the
24+
Memory Driver section of *Database Drivers* documents the opt-in persistence
25+
default, carries a migration callout for the old `'auto'` behaviour, and points
26+
test authors at in-memory SQLite. That section also claimed "Data is lost when
27+
the process exits", which was simply false while `'auto'` was the default — it
28+
wrote a file into the working directory.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/runtime": patch
3+
"@objectstack/client": patch
4+
"@objectstack/metadata": patch
5+
---
6+
7+
test(runtime,client,metadata): back the remaining suites with in-memory SQLite instead of the mingo driver (#4065)
8+
9+
Ten test files used `InMemoryDriver` as a convenience backing store — somewhere
10+
for rows to go while the suite proved something else (REST routing, datasource
11+
auto-connect, the batch `$ref` contract, metadata history). They now run on
12+
`SqliteWasmDriver` at `:memory:`, the same engine `@objectstack/verify`'s
13+
`bootStack` already gives the dogfood gate: pure JS (no native build, CI-safe on
14+
any runner) and real SQL semantics.
15+
16+
The point is fidelity, not tidiness. Production runs SQL, and mingo differs from
17+
it in ways that let a suite pass while the behaviour it stands for is broken.
18+
Every failure this migration produced was a fixture defect the memory driver had
19+
been absorbing:
20+
21+
- **Tables were never created.** `driver.create()` on the memory driver is a
22+
bare `table.push()` onto an auto-vivified array, so an object registered
23+
*after* `kernel.bootstrap()` — which misses the boot-time schema sync — looked
24+
fine. On SQL the first write fails with `no such table`, which the REST error
25+
mapper turns into a **404 `OBJECT_NOT_FOUND`**: a routing-shaped symptom for a
26+
DDL-shaped cause. Four suites needed an explicit `syncObjectSchema`.
27+
- **A missing object declaration read as working.** `notifications.hono.integration`
28+
writes `sys_notification`, which `MessagingServicePlugin` does not declare —
29+
it is a platform object, and that lean kernel never booted `platform-objects`.
30+
Auto-vivification hid the omission entirely. The suite now registers the real
31+
`SysNotification` rather than a hand-copied stand-in, so there is still exactly
32+
one schema for it (Prime Directive #12).
33+
- **`connect()` was optional.** The memory driver needs none; a SQL driver does.
34+
35+
What deliberately did NOT move: `read-coercion-conformance` keeps its two-driver
36+
matrix (proving a stored value reads back as its declared type on *both* engines
37+
is the entire point of that gate), and the suites whose subject IS the memory
38+
driver or its wiring — `standalone-stack` (`memory://` scheme),
39+
`sqlite-driver-fallback` (the dev step-down), the CLI's driver-label tests, and
40+
driver-memory's own suite.
41+
42+
`datasource-autoconnect` is in that second group as of #4083, which landed a
43+
regression test there for exactly the memory-pool property this PR originally
44+
proposed to migrate away from. Moving that file to SQLite would have left the
45+
new test passing vacuously — a wasm-SQLite pool never writes `.objectstack/` at
46+
all — so it stays on the memory driver and keeps guarding what it was written
47+
to guard.
48+
49+
No new coverage is claimed here: each suite asserts exactly what it asserted
50+
before, against a more faithful store.

content/docs/data-modeling/drivers.mdx

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -353,34 +353,62 @@ equivalent age-based reap.
353353
## Memory Driver
354354

355355
The in-memory driver keeps records in plain in-process objects (queried via
356-
[`mingo`](https://github.com/kofrasa/mingo)).
356+
[`mingo`](https://github.com/kofrasa/mingo)). Data is lost when the process
357+
exits unless persistence is explicitly requested.
357358
It is the **last-resort fallback** in dev mode: `objectstack dev` prefers native
358359
SQLite (`better-sqlite3`), falls back to the pure-JS WASM SQLite driver if the
359360
native binary is unavailable, and only drops to the in-memory driver if WASM also
360361
fails to load. Set `OS_DATABASE_DRIVER=memory` to select it explicitly.
361362

362-
**Whether data survives the process depends on how the driver is built** (#4083):
363+
**Both ways of building the driver are ephemeral by default** (#4083, #4065):
363364

364365
| How it is built | Persistence |
365366
| :--- | :--- |
366367
| 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. |
367-
| `new InMemoryDriver()` **constructed directly** | `persistence: 'auto'`under Node that means a JSON file at `.objectstack/data/memory-driver.json`, relative to the process's working directory, reloaded on the next boot. |
368-
369-
Pass `persistence: false` for a driver you construct yourself and want purely in
370-
memorya test, for instance, which should neither depend on nor leave behind
371-
state in the working directory. Two directly-constructed `'auto'` drivers in one
372-
process still share that single default path.
368+
| `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. |
373369

374370
```typescript
375371
import { InMemoryDriver } from '@objectstack/driver-memory';
376372
377-
new InMemoryDriver({ persistence: false }); // pure memory
378-
new InMemoryDriver(); // 'auto' — file-backed under Node
373+
new InMemoryDriver(); // pure memory — the default
374+
new InMemoryDriver({ persistence: 'file' }); // opt in to durability
375+
```
376+
377+
### Persistence is opt-in
378+
379+
A bare `new InMemoryDriver()` persists nothing. Durability is requested
380+
explicitly:
381+
382+
```typescript
383+
new InMemoryDriver({ persistence: 'file' }) // Node.js — .objectstack/data/memory-driver.json
384+
new InMemoryDriver({ persistence: 'local' }) // browser — localStorage
385+
new InMemoryDriver({ persistence: 'auto' }) // pick per environment (file / localStorage / off)
379386
```
380387

388+
`'auto'` chooses localStorage in a browser, a file under Node.js, and **disables**
389+
persistence in serverless/edge runtimes (Vercel, Lambda, Netlify, Cloud Run, Deno
390+
Deploy) where a local write would be silently discardedsupply a custom adapter
391+
there instead.
392+
393+
<Callout type="warn">
394+
Before v17 the default was `'auto'`, which on Node.js meant **file** — so a bare
395+
`new InMemoryDriver()` silently wrote `.objectstack/data/memory-driver.json` into
396+
the working directory and reloaded it on the next boot. If you relied on that,
397+
pass `persistence: 'auto'` (or `'file'`) explicitly. See
398+
[#4065](https://github.com/objectstack-ai/objectstack/issues/4065).
399+
</Callout>
400+
381401
<Callout type="tip">
382-
Use the memory driver for unit tests. It requires no setup and runs instantly
383-
with `persistence: false`, so one run cannot see what an earlier run left behind.
402+
For tests, prefer in-memory **SQLite** — `SqlDriver` with
403+
`connection: { filename: ':memory:' }`, or `SqliteWasmDriver({ filename: ':memory:' })`
404+
when you want no native build. Both give the SQL semantics production runs on;
405+
mingo does not enforce primary keys, uniqueness, `NOT NULL` or column types, so a
406+
green run against the memory driver is weaker evidence than it looks. The
407+
framework's own dogfood gate boots on WASM SQLite at `:memory:` for this reason.
408+
409+
The memory driver remains fine where you want no setup at all and the assertions
410+
do not depend on storage semanticsit is ephemeral by default, so one run
411+
cannot see what an earlier run left behind.
384412
</Callout>
385413

386414
## Local Environment Runtime

content/docs/getting-started/your-first-project.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,16 @@ framework surface you install:
164164
| Package | What it does |
165165
|:---|:---|
166166
| `@objectstack/spec` | The protocol: `defineStack`, `ObjectSchema`, `Field.*` builders, all Zod schemas. Your metadata imports only this. |
167-
| `@objectstack/runtime` | The kernel that interprets metadata at runtime. |
168-
| `@objectstack/driver-memory` | In-memory database driver for development. Swap for SQLite / Postgres / MongoDB in production — [no code changes](/docs/data-modeling/drivers). |
167+
| `@objectstack/runtime` | The kernel that interprets metadata at runtime. Brings the database drivers with it — you do not install one separately. |
169168
| `@objectstack/plugin-hono-server` | The HTTP server that mounts the generated REST API. |
170169
| `@objectstack/connector-rest` · `-openapi` · `-mcp` | The three generic connector executors — register the `rest` / `openapi` / `mcp` provider factories so declarative `connectors:` entries materialize ([ADR-0097](/docs/automation/flows)). |
171170
| `@objectstack/cli` *(dev)* | The `os` / `objectstack` CLI: `dev`, `validate`, `build`, `start`. |
172171

172+
No driver package is listed because none is installed directly: `@objectstack/runtime`
173+
depends on `driver-sql`, `driver-sqlite-wasm` and `driver-memory`, and the CLI carries
174+
them too. `objectstack dev` resolves **SQLite** by default; point `OS_DATABASE_URL` at
175+
Postgres / MySQL / MongoDB for production — [no code changes](/docs/data-modeling/drivers).
176+
173177
## 3. Run it
174178

175179
```bash

packages/cli/src/commands/init.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,17 @@ export const TEMPLATES: Record<string, {
103103
description: 'Full application with objects, views, and actions',
104104
get dependencies() {
105105
const v = pkgVersion();
106+
// No driver is listed on purpose. `@objectstack/runtime` already depends
107+
// on driver-sql / driver-sqlite-wasm / driver-memory, and every script
108+
// here runs through the CLI, which carries them too — so naming one was
109+
// redundant. Naming `driver-memory` specifically also read as an
110+
// endorsement: it is the LAST-RESORT rung of the dev step-down (native
111+
// better-sqlite3 → wasm SQLite → mingo), not the driver a new app should
112+
// start on. `objectstack dev` resolves sqlite by default.
106113
return {
107114
'@objectstack/spec': v,
108115
'@objectstack/runtime': v,
109116
'@objectstack/objectql': v,
110-
'@objectstack/driver-memory': v,
111117
};
112118
},
113119
get devDependencies() {

0 commit comments

Comments
 (0)