Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/adr-0062-default-driver-construction.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
---
"@objectstack/runtime": patch
---

refactor(runtime): build the standalone default driver via the shared datasource factory (ADR-0062 follow-up)

`createStandaloneStack` now constructs its `default` driver for the user-facing
kinds (memory / better-sqlite3 / postgres / mongodb) through the **same**
`createDefaultDatasourceDriverFactory` used for declared and runtime-admin
datasources — one "driver kind → instance" construction path instead of two
hand-mirrored ones. Adding a dialect or changing connection/pool defaults now
happens in a single place. URL→config translation, filesystem prep (`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
in the stack — it's the standalone-specific, CI-safe default and not a
user-creatable datasource type, so it has a single construction site already.

No behavior change: the same driver instances are built for the same inputs
(verified by a per-kind connect + CRUD round-trip test and a real `os dev` boot).
Adds `@objectstack/service-datasource` as a runtime dependency (no cycle — that
package depends only on core/spec).
1 change: 1 addition & 0 deletions packages/runtime/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@
"@objectstack/plugin-security": "workspace:*",
"@objectstack/rest": "workspace:*",
"@objectstack/service-cluster": "workspace:*",
"@objectstack/service-datasource": "workspace:*",
"@objectstack/service-i18n": "workspace:*",
"@objectstack/spec": "workspace:*",
"@objectstack/types": "workspace:*",
Expand Down
59 changes: 59 additions & 0 deletions packages/runtime/src/standalone-stack.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,3 +121,62 @@ describe('createStandaloneStack — surfaces app RBAC from the artifact (ADR-005
expect(r.roles!.map((x: any) => x.name).sort()).toEqual(['contributor', 'manager']);
}, BOOT_TIMEOUT);
});

// ADR-0062 (Variant A) — the standalone `default` driver's CONSTRUCTION is
// unified: the user-facing kinds (memory / better-sqlite3 / postgres / mongodb)
// go through the SAME `createDefaultDatasourceDriverFactory` used for
// declared/runtime datasources, so there is one "driver kind → instance" path.
// The pure-JS WASM sqlite driver stays bespoke (it's the standalone-specific
// CI-safe default, not a user-creatable datasource type — its only construction
// site). These tests extract the constructed driver from the stack's
// `DriverPlugin` and exercise it directly (connect → syncSchema → create →
// find), proving the right driver is built per kind AND that it actually
// connects + does I/O — without booting the full kernel (the MetadataPlugin
// file-artifact boot doesn't play well with vitest's module runner, and isn't
// what this test is about). postgres/mongodb need a live server, so they're
// covered by the factory's own usage + the runtime-admin path.
describe('createStandaloneStack — default driver construction unified via the factory (ADR-0062)', () => {
let dir: string;
beforeAll(() => { dir = mkdtempSync(join(tmpdir(), 'os-standalone-driver-')); });
afterAll(() => { try { rmSync(dir, { recursive: true, force: true }); } catch { /* noop */ } });

const NOTE = { name: 'note', fields: { id: { type: 'text' }, title: { type: 'text' } } };

async function driverRoundTrip(
cfg: Parameters<typeof createStandaloneStack>[0],
): Promise<{ kind: string | undefined; titles: string[] }> {
const stack = await createStandaloneStack(cfg);
const plugin = stack.plugins.find(
(p: any) => p?.driver && typeof p.driver.find === 'function',
) as { driver: any } | undefined;
const driver = plugin!.driver;
const kind = driver?.constructor?.name as string | undefined;
await driver.connect?.();
try {
await driver.syncSchema('note', NOTE);
await driver.create('note', { id: 'n1', title: 'hello-driver' });
const rows = (await driver.find('note', {})) as Array<{ title?: string }>;
return { kind, titles: rows.map((r) => r.title as string) };
} finally {
try { await driver.disconnect?.(); } catch { /* noop */ }
}
}

it('memory:// → InMemoryDriver (factory), connects + round-trips', async () => {
const r = await driverRoundTrip({ databaseUrl: 'memory://default-driver' });
expect(r.kind).toMatch(/InMemoryDriver$/);
expect(r.titles).toContain('hello-driver');
}, BOOT_TIMEOUT);

it('file: → better-sqlite3 SqlDriver (factory), connects + round-trips', async () => {
const r = await driverRoundTrip({ databaseUrl: `file:${join(dir, 'better.db')}` });
expect(r.kind).toMatch(/SqlDriver$/);
expect(r.titles).toContain('hello-driver');
}, BOOT_TIMEOUT);

it('databaseDriver:sqlite-wasm → SqliteWasmDriver (bespoke), connects + round-trips', async () => {
const r = await driverRoundTrip({ databaseDriver: 'sqlite-wasm', databaseUrl: `file:${join(dir, 'wasm.db')}` });
expect(r.kind).toMatch(/SqliteWasmDriver$/);
expect(r.titles).toContain('hello-driver');
}, BOOT_TIMEOUT);
});
103 changes: 59 additions & 44 deletions packages/runtime/src/standalone-stack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -155,63 +155,78 @@ export async function createStandaloneStack(config?: StandaloneStackConfig): Pro
?? (process.env.OS_DATABASE_DRIVER?.trim() as ResolvedDriverKind | undefined);
const dbDriver: ResolvedDriverKind = explicitDriver ?? detectDriverFromUrl(dbUrl);

// Build the default driver. The user-facing kinds (memory / postgres /
// better-sqlite3 / mongodb) go through the SHARED datasource driver factory
// (ADR-0062) — the SAME `create({driver,config})` used for declared/runtime
// datasources — so adding a dialect or changing connection/pool defaults
// happens in ONE place instead of being mirrored here by hand. This stack
// still owns what's standalone-specific: URL→config translation, filesystem
// prep (`mkdir`), and `DriverPlugin` registration (pre-engine — unchanged).
let driverPlugin: any;
if (dbDriver === 'memory') {
const { InMemoryDriver } = await import('@objectstack/driver-memory');
driverPlugin = new DriverPlugin(new InMemoryDriver());
} else if (dbDriver === 'postgres') {
const { SqlDriver } = await import('@objectstack/driver-sql');
driverPlugin = new DriverPlugin(
new SqlDriver({
client: 'pg',
connection: dbUrl,
pool: { min: 0, max: 5 },
}) as any,
);
} else if (dbDriver === 'mongodb') {
// MongoDB driver is an optional peer dependency. Importing it lazily
// avoids forcing every standalone consumer to install the mongo SDK.
let MongoDBDriver: any;
try {
({ MongoDBDriver } = await import('@objectstack/driver-mongodb' as any));
} catch (err: any) {
throw new Error(
`[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. ` +
`Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
);
}
driverPlugin = new DriverPlugin(new MongoDBDriver({ url: dbUrl }) as any);
} else if (dbDriver === 'sqlite-wasm') {
if (dbDriver === 'sqlite-wasm') {
// The pure-JS WASM sqlite driver is the standalone-specific, CI-safe
// (no native build) default — NOT a user-creatable runtime datasource
// type, so it isn't part of the shared factory's surface. Construct it
// directly here (this is its only construction site, so no duplication).
const { SqliteWasmDriver } = await import('@objectstack/driver-sqlite-wasm' as any);
const filename = dbUrl
.replace(/^wasm-sqlite:(\/\/)?/i, '')
.replace(/^file:(\/\/)?/i, '');
if (filename && filename !== ':memory:') {
.replace(/^file:(\/\/)?/i, '') || ':memory:';
if (filename !== ':memory:') {
mkdirSync(resolvePath(filename, '..'), { recursive: true });
}
driverPlugin = new DriverPlugin(
new SqliteWasmDriver({
filename: filename || ':memory:',
persist: filename && filename !== ':memory:' ? 'on-write' : undefined,
filename,
persist: filename !== ':memory:' ? 'on-write' : undefined,
}) as any,
);
} else {
// sqlite
const { SqlDriver } = await import('@objectstack/driver-sql');
const filename = dbUrl.replace(/^file:(\/\/)?/, '');
if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
throw new Error(
`[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". ` +
`Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
);
const { createDefaultDatasourceDriverFactory } = await import('@objectstack/service-datasource');
let driverId: string;
let driverConfig: Record<string, unknown>;
if (dbDriver === 'memory') {
driverId = 'memory';
driverConfig = {};
} else if (dbDriver === 'postgres') {
// Factory applies the pg pool default ({ min: 0, max: 5 }) internally.
driverId = 'postgres';
driverConfig = { url: dbUrl };
} else if (dbDriver === 'mongodb') {
driverId = 'mongodb';
driverConfig = { url: dbUrl };
} else {
// sqlite (better-sqlite3)
driverId = 'sqlite';
const filename = dbUrl.replace(/^file:(\/\/)?/, '');
if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
throw new Error(
`[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". ` +
`Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
);
}
mkdirSync(resolvePath(filename, '..'), { recursive: true });
driverConfig = { filename };
}

let driverHandle: { driver?: unknown } | unknown;
try {
driverHandle = await createDefaultDatasourceDriverFactory().create({ driver: driverId, config: driverConfig });
} catch (err: any) {
// Preserve the actionable hint the bespoke path gave for the optional
// mongo peer dep (the factory throws a generic "not installed" message).
if (dbDriver === 'mongodb') {
throw new Error(
`[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. ` +
`Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
);
}
throw err;
}
mkdirSync(resolvePath(filename, '..'), { recursive: true });
// The factory returns a handle whose `.driver` is the concrete engine
// driver (falls back to the handle itself for structural drivers).
driverPlugin = new DriverPlugin(
new SqlDriver({
client: 'better-sqlite3',
connection: { filename },
useNullAsDefault: true,
}),
((driverHandle as { driver?: unknown })?.driver ?? driverHandle) as any,
);
}

Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.