Skip to content

Commit 5cfd4d5

Browse files
authored
feat(cli): the serve storage fallback declares the default datasource instead of constructing a driver (#3826) (#3886)
The last open-core second site of "definition → live driver": a host objectstack.config.ts with objects but no driver plugin had serve build a driver via createStorageDriver + DriverPlugin. Now resolveStorageDefinition translates kind + URL into { driverId, config } and serve hands it to DefaultDatasourcePlugin — shared factory, bootCritical verdict, shared OS_ALLOW_DRIVER_CONNECT_FAILURE, real status in Setup → Datasources. - mysql/mysql2 joined the shared driver factory (SqlDriver over mysql2). - Factory honours config.autoMigrate (#2186 dev self-heal) and config.persist (CLI wasm on-disconnect) as host-composition passthroughs. - turso/libSQL keeps the loud typed UnsupportedDriverError at resolution. - The telemetry sibling stays a pre-built DriverPlugin (documented escape hatch); gating moves to the statically-known sqlite file path. Verified: host-composed config boots through the declared fallback; artifact path dev:crm --fresh is table-for-table unchanged. Remaining second site: cloud-stack (tracked in #3826). Pre-existing "Service 'manifest' is async" failure ruled unrelated by stashed-baseline control → #3887.
1 parent 259af21 commit 5cfd4d5

7 files changed

Lines changed: 303 additions & 211 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"@objectstack/cli": minor
3+
"@objectstack/service-datasource": minor
4+
---
5+
6+
feat(cli): the serve storage fallback declares the default datasource instead of constructing a driver (#3826)
7+
8+
The last open-core second site of "definition → live driver": when a host
9+
`objectstack.config.ts` supplies objects but no driver plugin, `serve` built a
10+
driver via `createStorageDriver` and registered it through `DriverPlugin`, with
11+
its connect and failure verdict landing in `ObjectQLEngine.init()` — the same
12+
split #3869 removed from the standalone stack.
13+
14+
- **`createStorageDriver` is gone.** `resolveStorageDefinition` translates the
15+
driver kind + URL into `{ driverId, config }` (a pure host-side translation,
16+
like `standalone-stack`'s), and serve hands it to the runtime's
17+
`DefaultDatasourcePlugin` — same shared factory, same `bootCritical` failure
18+
verdict, same `OS_ALLOW_DRIVER_CONNECT_FAILURE` escape hatch, and the primary
19+
DB's real status in Setup → Datasources.
20+
- **`mysql`/`mysql2` joined the shared driver factory** (SqlDriver over
21+
`mysql2`; DSN or discrete fields, secret as password).
22+
- **Host-composition passthroughs**: the factory honours `config.autoMigrate`
23+
(the #2186 dev loosen-only self-heal, for the SQL kinds) and `config.persist`
24+
(the CLI's wasm `on-disconnect` mode). Connection builders ignore both keys.
25+
- **`turso`/libSQL fails loud at resolution**, same typed
26+
`UnsupportedDriverError`, same actionable message — nothing is constructed to
27+
fail later.
28+
- **The `telemetry` sibling datasource stays a pre-built `DriverPlugin`** — the
29+
documented escape hatch for named auxiliary drivers. Its provisioning now
30+
gates on the statically-known sqlite file path; the old coupling to the
31+
primary's *resolved* engine is replaced by the telemetry provision's own
32+
step-down check, which already guarded the ABI-broken case.
33+
34+
Verified end to end: a host-composed config (plugins + objects, no driver)
35+
boots through the declared fallback with the same banner labels; the artifact
36+
path (`dev:crm --fresh`) is table-for-table unchanged (71 tables, zero
37+
`no such table`).
38+
39+
**Migration.** None for CLI users — same URLs, same env vars, same banner. The
40+
removed `createStorageDriver` was CLI-internal; `resolveDriverType`,
41+
`inferDriverTypeFromUrl` and `UnsupportedDriverError` are unchanged.

docs/adr/0062-external-datasource-runtime.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ Introduce a single service that, given a datasource definition, builds a driver
6666
>
6767
> **Resolution (#3826, second pass) — the standalone `default` is now a declared definition.** The input-shape mismatch was resolved by making the definition the input: `createStandaloneStack` translates the database URL into a `{ driver, config }` definition (URL→config translation and `mkdir` stay host concerns) and the runtime's **`DefaultDatasourcePlugin`** — registered before `ObjectQLPlugin`, so the driver exists before boot schema-sync — connects it through `DatasourceConnectionService.connect(record, { asDefault: true })`. The definition is marked **`bootCritical`**, which adds a third fail-fast cause to D5 (the platform cannot run without it; every unbound object routes to it), sharing `OS_ALLOW_DRIVER_CONNECT_FAILURE` and the `DEGRADED BOOT` banner with the engine guard. `asDefault` keeps the driver's **natural name** (routing to `default` uses the engine's default-driver fallback, never `drivers.get('default')`) and registers with `isDefault: true`. The presumed layering inversion did not materialize: the *runtime host* orchestrates (runtime already depends on `service-datasource`); `ObjectQLPlugin` learned nothing. When the datasource-admin plugin is present its shared connection service is used (so `default` shows a real `status` in Setup → Datasources, #3827); a lite kernel instantiates the same class locally — one implementation either way. `sqlite-wasm` joined the shared factory (the last bespoke construction site), `default` became a host-reserved name (rejected in app bundles at load and in runtime-admin create), and `ObjectQLEngine.init()` keeps its #3741 fail-fast unchanged — it re-connects the already-connected default (all open-core drivers' `connect()` is idempotent), which is precisely the boot *verification* role D1 leaves it.
6868
>
69-
> **Remaining second sites, tracked in #3826:** the CLI serve **config-load fallback** (`createStorageDriver` + `DriverPlugin`, used when a host `objectstack.config.ts` supplies no driver — it also carries mysql/turso kinds the shared factory does not build, and the `telemetry` sibling-datasource provisioning is coupled to its resolution result), and the cloud stack's own composition. Until those converge, `packages/runtime/src/degraded-boot-parity.test.ts` remains load-bearing: it pins both connect paths to the same operator-visible contract (fail-fast by default, identical `OS_ALLOW_DRIVER_CONNECT_FAILURE` parsing, `DEGRADED BOOT` on stderr), so a change to one that forgets the other fails CI instead of shipping. #3741#3758 was exactly that miss.
69+
> **Config-load fallback converged too (#3826, third pass).** `createStorageDriver` is gone: the CLI's serve fallback (a host `objectstack.config.ts` with objects but no driver plugin) now emits a definition via `resolveStorageDefinition` and hands it to the same `DefaultDatasourcePlugin`. `mysql` joined the shared factory for it; the dev loosen-only self-heal (#2186) rides as `config.autoMigrate` and the CLI's wasm persistence mode as `config.persist` — host-composition passthroughs the factory honours, never part of the app-facing datasource spec. `turso`/libSQL keeps its loud typed failure at *resolution* (nothing is constructed to fail later). The `telemetry` sibling datasource deliberately stays a pre-built `DriverPlugin` (the documented escape hatch for named auxiliary drivers): it is best-effort, dev-oriented, and its own `resolveSqliteDriver` step-down check replaces the old primary-resolution coupling.
70+
>
71+
> **Remaining second site: the cloud stack's own composition** (cloud repo, `cloud-stack.ts`), which also owns the `turso` driver. Until it converges, `packages/runtime/src/degraded-boot-parity.test.ts` remains load-bearing: it pins both connect paths to the same operator-visible contract (fail-fast by default, identical `OS_ALLOW_DRIVER_CONNECT_FAILURE` parsing, `DEGRADED BOOT` on stderr). #3741#3758 was exactly the miss it exists to catch.
7072
7173
### D2 — Connect is opt-in-safe: existing managed apps are byte-for-byte unchanged
7274

packages/cli/src/commands/serve.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import chalk from 'chalk';
88
import { bundleRequire } from 'bundle-require';
99
import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
1010
import { isHostConfig, shouldBootWithLibrary } from '../utils/plugin-detection.js';
11-
import { resolveDriverType, createStorageDriver, UnsupportedDriverError } from '../utils/storage-driver.js';
11+
import { resolveDriverType, resolveStorageDefinition, UnsupportedDriverError } from '../utils/storage-driver.js';
1212
import { readEnvWithDeprecation, resolveMultiOrgEnabled, resolveTenancyPosture, resolveAllowDegradedTenancy, isMcpServerEnabled, resolveSearchPinyinEnabled, isModuleNotFoundError } from '@objectstack/types';
1313
import { PLATFORM_CAPABILITY_TOKENS } from '@objectstack/spec/kernel';
1414
import { missingProviderMessage } from '../utils/capability-preflight.js';
@@ -887,14 +887,22 @@ export default class Serve extends Command {
887887
const driverType = resolveDriverType(process.env.OS_DATABASE_DRIVER, databaseUrl);
888888

889889
try {
890-
const { DriverPlugin } = await import('@objectstack/runtime');
891-
const resolution = await createStorageDriver(driverType, {
892-
databaseUrl,
893-
isDev,
894-
warn: (m) => console.warn(chalk.yellow(m)),
895-
});
890+
// #3826: the fallback no longer constructs a driver — it declares
891+
// the `default` datasource and lets the runtime's
892+
// DefaultDatasourcePlugin connect it at boot through the shared
893+
// DatasourceConnectionService (one connect path, one failure
894+
// verdict incl. OS_ALLOW_DRIVER_CONNECT_FAILURE, retained status in
895+
// Setup → Datasources). URL→config translation stays host-side in
896+
// resolveStorageDefinition. The dev sqlite step-down (#2229) and
897+
// the loosen-only self-heal (#2186, via config.autoMigrate) now run
898+
// inside the factory at connect.
899+
const { DriverPlugin, DefaultDatasourcePlugin } = await import('@objectstack/runtime');
900+
const resolution = resolveStorageDefinition(driverType, { databaseUrl, isDev });
896901
if (resolution) {
897-
await kernel.use(new DriverPlugin(resolution.driver as any));
902+
await kernel.use(new DefaultDatasourcePlugin(
903+
{ driver: resolution.driverId, config: resolution.config },
904+
{ dev: isDev },
905+
));
898906
trackPlugin(resolution.trackName);
899907
resolvedDriverLabel = resolution.label;
900908
resolvedDatabaseUrl = resolution.displayUrl;
@@ -906,9 +914,14 @@ export default class Serve extends Command {
906914
// for file-backed primaries; `OS_TELEMETRY_DB=0` opts out,
907915
// `OS_TELEMETRY_DB=<path>` opts in anywhere (incl. serve). Gated on
908916
// an explicit SQLite primary (`sqliteFilePath`, unset for the mingo
909-
// memory driver AND the dev-default `:memory:` store) whose resolved
910-
// engine is real SQLite — never mingo in-memory.
911-
if (resolution.sqliteFilePath && resolution.engine !== 'memory') {
917+
// memory driver AND the dev-default `:memory:` store). The old
918+
// `resolution.engine !== 'memory'` refinement is unknowable now
919+
// that the primary connects later (#3826); the telemetry
920+
// provision's own `telemetry.engine !== 'memory'` check below
921+
// still guards the ABI-broken step-down case. The telemetry
922+
// driver itself stays a pre-built DriverPlugin — the documented
923+
// escape hatch for named auxiliary drivers.
924+
if (resolution.sqliteFilePath) {
912925
const { resolveTelemetryDbPath } = await import('../utils/telemetry-datasource.js');
913926
const telemetryPath = resolveTelemetryDbPath({ primaryPath: resolution.sqliteFilePath, env: process.env, dev: isDev });
914927
if (telemetryPath) {

0 commit comments

Comments
 (0)