Skip to content

Commit 94d2161

Browse files
xuyushun441-sysos-zhuangclaude
authored
refactor(runtime): build standalone default driver via the shared datasource factory (ADR-0062) (#2207)
Unifies driver CONSTRUCTION: createStandaloneStack builds its `default` driver for the user-facing kinds (memory / better-sqlite3 / postgres / mongodb) through the same createDefaultDatasourceDriverFactory used for declared + runtime-admin datasources — one "driver kind → instance" path instead of two hand-mirrored ones. URL→config translation, mkdir, and pre-engine DriverPlugin registration stay in the stack (unchanged); the factory only constructs the driver. The pure-JS WASM sqlite driver stays bespoke (standalone-specific CI-safe default, not a user-creatable datasource type — single construction site already). No behavior change — same driver instances for the same inputs. Verified by a per-kind connect + CRUD round-trip test (memory/better-sqlite3/sqlite-wasm) and a real `os dev` boot (factory-built default SqlDriver serves the live app; external auto-connect still works). Adds @objectstack/service-datasource as a runtime dep (no cycle: it depends only on core/spec). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cf97765 commit 94d2161

5 files changed

Lines changed: 143 additions & 47 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
refactor(runtime): build the standalone default driver via the shared datasource factory (ADR-0062 follow-up)
6+
7+
`createStandaloneStack` now constructs its `default` driver for the user-facing
8+
kinds (memory / better-sqlite3 / postgres / mongodb) through the **same**
9+
`createDefaultDatasourceDriverFactory` used for declared and runtime-admin
10+
datasources — one "driver kind → instance" construction path instead of two
11+
hand-mirrored ones. Adding a dialect or changing connection/pool defaults now
12+
happens in a single place. URL→config translation, filesystem prep (`mkdir`),
13+
and pre-engine `DriverPlugin` registration stay in the stack (unchanged); the
14+
factory only constructs the driver. The pure-JS WASM sqlite driver stays bespoke
15+
in the stack — it's the standalone-specific, CI-safe default and not a
16+
user-creatable datasource type, so it has a single construction site already.
17+
18+
No behavior change: the same driver instances are built for the same inputs
19+
(verified by a per-kind connect + CRUD round-trip test and a real `os dev` boot).
20+
Adds `@objectstack/service-datasource` as a runtime dependency (no cycle — that
21+
package depends only on core/spec).

packages/runtime/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@objectstack/plugin-security": "workspace:*",
3333
"@objectstack/rest": "workspace:*",
3434
"@objectstack/service-cluster": "workspace:*",
35+
"@objectstack/service-datasource": "workspace:*",
3536
"@objectstack/service-i18n": "workspace:*",
3637
"@objectstack/spec": "workspace:*",
3738
"@objectstack/types": "workspace:*",

packages/runtime/src/standalone-stack.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,62 @@ describe('createStandaloneStack — surfaces app RBAC from the artifact (ADR-005
121121
expect(r.roles!.map((x: any) => x.name).sort()).toEqual(['contributor', 'manager']);
122122
}, BOOT_TIMEOUT);
123123
});
124+
125+
// ADR-0062 (Variant A) — the standalone `default` driver's CONSTRUCTION is
126+
// unified: the user-facing kinds (memory / better-sqlite3 / postgres / mongodb)
127+
// go through the SAME `createDefaultDatasourceDriverFactory` used for
128+
// declared/runtime datasources, so there is one "driver kind → instance" path.
129+
// The pure-JS WASM sqlite driver stays bespoke (it's the standalone-specific
130+
// CI-safe default, not a user-creatable datasource type — its only construction
131+
// site). These tests extract the constructed driver from the stack's
132+
// `DriverPlugin` and exercise it directly (connect → syncSchema → create →
133+
// find), proving the right driver is built per kind AND that it actually
134+
// connects + does I/O — without booting the full kernel (the MetadataPlugin
135+
// file-artifact boot doesn't play well with vitest's module runner, and isn't
136+
// what this test is about). postgres/mongodb need a live server, so they're
137+
// covered by the factory's own usage + the runtime-admin path.
138+
describe('createStandaloneStack — default driver construction unified via the factory (ADR-0062)', () => {
139+
let dir: string;
140+
beforeAll(() => { dir = mkdtempSync(join(tmpdir(), 'os-standalone-driver-')); });
141+
afterAll(() => { try { rmSync(dir, { recursive: true, force: true }); } catch { /* noop */ } });
142+
143+
const NOTE = { name: 'note', fields: { id: { type: 'text' }, title: { type: 'text' } } };
144+
145+
async function driverRoundTrip(
146+
cfg: Parameters<typeof createStandaloneStack>[0],
147+
): Promise<{ kind: string | undefined; titles: string[] }> {
148+
const stack = await createStandaloneStack(cfg);
149+
const plugin = stack.plugins.find(
150+
(p: any) => p?.driver && typeof p.driver.find === 'function',
151+
) as { driver: any } | undefined;
152+
const driver = plugin!.driver;
153+
const kind = driver?.constructor?.name as string | undefined;
154+
await driver.connect?.();
155+
try {
156+
await driver.syncSchema('note', NOTE);
157+
await driver.create('note', { id: 'n1', title: 'hello-driver' });
158+
const rows = (await driver.find('note', {})) as Array<{ title?: string }>;
159+
return { kind, titles: rows.map((r) => r.title as string) };
160+
} finally {
161+
try { await driver.disconnect?.(); } catch { /* noop */ }
162+
}
163+
}
164+
165+
it('memory:// → InMemoryDriver (factory), connects + round-trips', async () => {
166+
const r = await driverRoundTrip({ databaseUrl: 'memory://default-driver' });
167+
expect(r.kind).toMatch(/InMemoryDriver$/);
168+
expect(r.titles).toContain('hello-driver');
169+
}, BOOT_TIMEOUT);
170+
171+
it('file: → better-sqlite3 SqlDriver (factory), connects + round-trips', async () => {
172+
const r = await driverRoundTrip({ databaseUrl: `file:${join(dir, 'better.db')}` });
173+
expect(r.kind).toMatch(/SqlDriver$/);
174+
expect(r.titles).toContain('hello-driver');
175+
}, BOOT_TIMEOUT);
176+
177+
it('databaseDriver:sqlite-wasm → SqliteWasmDriver (bespoke), connects + round-trips', async () => {
178+
const r = await driverRoundTrip({ databaseDriver: 'sqlite-wasm', databaseUrl: `file:${join(dir, 'wasm.db')}` });
179+
expect(r.kind).toMatch(/SqliteWasmDriver$/);
180+
expect(r.titles).toContain('hello-driver');
181+
}, BOOT_TIMEOUT);
182+
});

packages/runtime/src/standalone-stack.ts

Lines changed: 59 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -155,63 +155,78 @@ export async function createStandaloneStack(config?: StandaloneStackConfig): Pro
155155
?? (process.env.OS_DATABASE_DRIVER?.trim() as ResolvedDriverKind | undefined);
156156
const dbDriver: ResolvedDriverKind = explicitDriver ?? detectDriverFromUrl(dbUrl);
157157

158+
// Build the default driver. The user-facing kinds (memory / postgres /
159+
// better-sqlite3 / mongodb) go through the SHARED datasource driver factory
160+
// (ADR-0062) — the SAME `create({driver,config})` used for declared/runtime
161+
// datasources — so adding a dialect or changing connection/pool defaults
162+
// happens in ONE place instead of being mirrored here by hand. This stack
163+
// still owns what's standalone-specific: URL→config translation, filesystem
164+
// prep (`mkdir`), and `DriverPlugin` registration (pre-engine — unchanged).
158165
let driverPlugin: any;
159-
if (dbDriver === 'memory') {
160-
const { InMemoryDriver } = await import('@objectstack/driver-memory');
161-
driverPlugin = new DriverPlugin(new InMemoryDriver());
162-
} else if (dbDriver === 'postgres') {
163-
const { SqlDriver } = await import('@objectstack/driver-sql');
164-
driverPlugin = new DriverPlugin(
165-
new SqlDriver({
166-
client: 'pg',
167-
connection: dbUrl,
168-
pool: { min: 0, max: 5 },
169-
}) as any,
170-
);
171-
} else if (dbDriver === 'mongodb') {
172-
// MongoDB driver is an optional peer dependency. Importing it lazily
173-
// avoids forcing every standalone consumer to install the mongo SDK.
174-
let MongoDBDriver: any;
175-
try {
176-
({ MongoDBDriver } = await import('@objectstack/driver-mongodb' as any));
177-
} catch (err: any) {
178-
throw new Error(
179-
`[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. ` +
180-
`Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
181-
);
182-
}
183-
driverPlugin = new DriverPlugin(new MongoDBDriver({ url: dbUrl }) as any);
184-
} else if (dbDriver === 'sqlite-wasm') {
166+
if (dbDriver === 'sqlite-wasm') {
167+
// The pure-JS WASM sqlite driver is the standalone-specific, CI-safe
168+
// (no native build) default — NOT a user-creatable runtime datasource
169+
// type, so it isn't part of the shared factory's surface. Construct it
170+
// directly here (this is its only construction site, so no duplication).
185171
const { SqliteWasmDriver } = await import('@objectstack/driver-sqlite-wasm' as any);
186172
const filename = dbUrl
187173
.replace(/^wasm-sqlite:(\/\/)?/i, '')
188-
.replace(/^file:(\/\/)?/i, '');
189-
if (filename && filename !== ':memory:') {
174+
.replace(/^file:(\/\/)?/i, '') || ':memory:';
175+
if (filename !== ':memory:') {
190176
mkdirSync(resolvePath(filename, '..'), { recursive: true });
191177
}
192178
driverPlugin = new DriverPlugin(
193179
new SqliteWasmDriver({
194-
filename: filename || ':memory:',
195-
persist: filename && filename !== ':memory:' ? 'on-write' : undefined,
180+
filename,
181+
persist: filename !== ':memory:' ? 'on-write' : undefined,
196182
}) as any,
197183
);
198184
} else {
199-
// sqlite
200-
const { SqlDriver } = await import('@objectstack/driver-sql');
201-
const filename = dbUrl.replace(/^file:(\/\/)?/, '');
202-
if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
203-
throw new Error(
204-
`[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". ` +
205-
`Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
206-
);
185+
const { createDefaultDatasourceDriverFactory } = await import('@objectstack/service-datasource');
186+
let driverId: string;
187+
let driverConfig: Record<string, unknown>;
188+
if (dbDriver === 'memory') {
189+
driverId = 'memory';
190+
driverConfig = {};
191+
} else if (dbDriver === 'postgres') {
192+
// Factory applies the pg pool default ({ min: 0, max: 5 }) internally.
193+
driverId = 'postgres';
194+
driverConfig = { url: dbUrl };
195+
} else if (dbDriver === 'mongodb') {
196+
driverId = 'mongodb';
197+
driverConfig = { url: dbUrl };
198+
} else {
199+
// sqlite (better-sqlite3)
200+
driverId = 'sqlite';
201+
const filename = dbUrl.replace(/^file:(\/\/)?/, '');
202+
if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
203+
throw new Error(
204+
`[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". ` +
205+
`Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
206+
);
207+
}
208+
mkdirSync(resolvePath(filename, '..'), { recursive: true });
209+
driverConfig = { filename };
210+
}
211+
212+
let driverHandle: { driver?: unknown } | unknown;
213+
try {
214+
driverHandle = await createDefaultDatasourceDriverFactory().create({ driver: driverId, config: driverConfig });
215+
} catch (err: any) {
216+
// Preserve the actionable hint the bespoke path gave for the optional
217+
// mongo peer dep (the factory throws a generic "not installed" message).
218+
if (dbDriver === 'mongodb') {
219+
throw new Error(
220+
`[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. ` +
221+
`Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
222+
);
223+
}
224+
throw err;
207225
}
208-
mkdirSync(resolvePath(filename, '..'), { recursive: true });
226+
// The factory returns a handle whose `.driver` is the concrete engine
227+
// driver (falls back to the handle itself for structural drivers).
209228
driverPlugin = new DriverPlugin(
210-
new SqlDriver({
211-
client: 'better-sqlite3',
212-
connection: { filename },
213-
useNullAsDefault: true,
214-
}),
229+
((driverHandle as { driver?: unknown })?.driver ?? driverHandle) as any,
215230
);
216231
}
217232

pnpm-lock.yaml

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)