Skip to content

Commit 728757e

Browse files
os-zhuangclaude
andcommitted
fix(cli): fail loudly when turso/libSQL is selected in the open-core CLI (#3276 follow-up)
Same "declared ≠ enforced" bug class as the `memory` fix (#3276): `os dev` / `os start` / `os serve` advertise a `turso` driver (`--database-driver turso`, `OS_DATABASE_DRIVER=turso`, `libsql://` URLs), but the dispatch had no `turso` branch — so selecting it silently fell through to the SQLite default and ignored the requested remote libSQL connection. turso/libSQL is a cloud/EE driver (`@objectstack/driver-turso`, an extension of SqlDriver over `@libsql/client`), composed by the cloud runtime's own kernel factory; the open-core standalone stack deliberately stopped consuming it and its config schema lives in the cloud package to keep it out of `@objectstack/spec`. Rather than pull the EE driver into open-core: - `createStorageDriver` throws a typed `UnsupportedDriverError` for `turso` / `libsql`, with an actionable message (names @objectstack/driver-turso + the open-core alternatives). - `serve.ts` re-throws it from the driver block's catch so run()'s fatal handler prints the message and exits 1 (dev AND prod) — never a silent SQLite boot. - `inferDriverTypeFromUrl` keeps classifying `libsql://` / `*.turso.*` as `turso` (not '') so those URLs hit the same loud failure instead of the SQLite default. Verified end-to-end: `OS_DATABASE_DRIVER=turso` and `OS_DATABASE_URL=libsql://...` both exit 1 with the fatal message; `OS_DATABASE_DRIVER=memory` still boots. Unit tests prove the branch goes red (dev→SqlDriver, prod→null) without it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d8cfa0b commit 728757e

4 files changed

Lines changed: 120 additions & 1 deletion

File tree

.changeset/cli-turso-loud-error.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): fail loudly when `turso`/libSQL is selected in the open-core CLI (#3276 follow-up)
6+
7+
Same "declared ≠ enforced" class as the `memory` fix: the CLI advertised `turso`
8+
(`--database-driver turso`, `OS_DATABASE_DRIVER=turso`, `libsql://` URLs) but the
9+
driver dispatch had no `turso` branch, so it silently fell through to the SQLite
10+
default and ignored the requested engine.
11+
12+
`turso`/libSQL ships in the cloud / enterprise distribution
13+
(`@objectstack/driver-turso`, composed by the cloud runtime's own kernel factory —
14+
open-core's standalone stack deliberately does not consume it). Rather than pull an
15+
EE driver into open-core, `createStorageDriver` now throws a typed
16+
`UnsupportedDriverError` for `turso`/`libsql`, and `serve.ts` surfaces it as a
17+
fatal, actionable boot error (naming the cloud/EE package and the open-core
18+
alternatives) instead of silently degrading to SQLite. `libsql://` / `*.turso.*`
19+
URLs stay classified as `turso` so they hit the same loud failure.

packages/cli/src/commands/serve.ts

Lines changed: 9 additions & 1 deletion
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 } from '../utils/storage-driver.js';
11+
import { resolveDriverType, createStorageDriver, UnsupportedDriverError } from '../utils/storage-driver.js';
1212
import { readEnvWithDeprecation, resolveMultiOrgEnabled, resolveAllowDegradedTenancy, isMcpServerEnabled, resolveSearchPinyinEnabled, isModuleNotFoundError } from '@objectstack/types';
1313
import { PLATFORM_CAPABILITY_TOKENS, canonicalizePlatformCapability } from '@objectstack/spec/kernel';
1414
import { resolveObjectStackHome } from '@objectstack/runtime';
@@ -937,6 +937,14 @@ export default class Serve extends Command {
937937
}
938938
}
939939
} catch (e: any) {
940+
// "declared ≠ enforced" guard (#3276-class): a driver that is
941+
// RECOGNIZED but the open-core CLI cannot construct — currently
942+
// `turso`/libSQL, a cloud/EE driver — must fail LOUDLY, never silently
943+
// fall through to the SQLite default and ignore the selected engine.
944+
// Re-throw so run()'s fatal handler restores output, prints the
945+
// actionable message, and exits 1 (in dev AND prod). All OTHER driver
946+
// construction errors keep the prior best-effort silent behavior.
947+
if (e instanceof UnsupportedDriverError) throw e;
940948
// silent
941949
}
942950
}

packages/cli/src/utils/storage-driver.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
inferDriverTypeFromUrl,
77
resolveDriverType,
88
createStorageDriver,
9+
UnsupportedDriverError,
910
} from './storage-driver.js';
1011

1112
describe('inferDriverTypeFromUrl', () => {
@@ -131,3 +132,42 @@ describe('createStorageDriver', () => {
131132
expect(await createStorageDriver('nonsense', { isDev: false })).toBeNull();
132133
});
133134
});
135+
136+
describe('createStorageDriver: turso / libSQL is recognized but fails loud', () => {
137+
// turso is a cloud/EE driver (@objectstack/driver-turso) the open-core CLI
138+
// cannot construct. Selecting it must THROW a typed error — NOT fall through
139+
// to the SQLite default. Remove the `turso` branch in storage-driver.ts and
140+
// these go red: in dev it resolves to a SqlDriver, in prod it returns null —
141+
// both silently ignoring the requested turso engine (the reported bug).
142+
it('throws UnsupportedDriverError for `turso` in DEV and PROD', async () => {
143+
await expect(createStorageDriver('turso', { isDev: true })).rejects.toBeInstanceOf(UnsupportedDriverError);
144+
await expect(createStorageDriver('turso', { isDev: false })).rejects.toBeInstanceOf(UnsupportedDriverError);
145+
});
146+
147+
it('throws for the `libsql` alias too', async () => {
148+
await expect(createStorageDriver('libsql', { isDev: true })).rejects.toBeInstanceOf(UnsupportedDriverError);
149+
});
150+
151+
// The message must be actionable: name the cloud/EE package and the open-core
152+
// alternatives, so an operator knows exactly how to proceed.
153+
it('carries an actionable message (cloud/EE package + open-core alternatives)', async () => {
154+
await expect(createStorageDriver('turso', { isDev: false })).rejects.toThrow(/@objectstack\/driver-turso/);
155+
await expect(createStorageDriver('turso', { isDev: false })).rejects.toThrow(/cloud|enterprise/i);
156+
const err = await createStorageDriver('turso', { isDev: false }).catch((e) => e);
157+
expect(err).toBeInstanceOf(UnsupportedDriverError);
158+
expect((err as UnsupportedDriverError).driverType).toBe('turso');
159+
});
160+
161+
// A `libsql://` / Turso URL routes to the same loud failure — it is NOT left
162+
// unrecognized (which would silently fall through to SQLite).
163+
it('routes libsql:// and *.turso.* URLs to the turso failure, never SQLite', async () => {
164+
expect(resolveDriverType(undefined, 'libsql://my-db.turso.io')).toBe('turso');
165+
expect(resolveDriverType(undefined, 'https://my-db.turso.io')).toBe('turso');
166+
await expect(
167+
createStorageDriver(resolveDriverType(undefined, 'libsql://my-db.turso.io'), {
168+
databaseUrl: 'libsql://my-db.turso.io',
169+
isDev: true,
170+
}),
171+
).rejects.toBeInstanceOf(UnsupportedDriverError);
172+
});
173+
});

packages/cli/src/utils/storage-driver.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,28 @@
3232
/** Engines the shared sqlite step-down (`resolveSqliteDriver`) can produce. */
3333
export type SqliteFamilyEngine = 'better-sqlite3' | 'sqlite-wasm' | 'memory';
3434

35+
/**
36+
* Thrown by {@link createStorageDriver} when a driver kind is *recognized* but the
37+
* open-core CLI cannot construct it — currently `turso`/libSQL, which ships in the
38+
* ObjectStack cloud / enterprise distribution (`@objectstack/driver-turso`, an
39+
* extension of SqlDriver over `@libsql/client`), composed by the cloud runtime's
40+
* own kernel factory, not by open-core's auto driver-registration.
41+
*
42+
* The whole point of surfacing this as a *typed* error is so `serve.ts` can fail
43+
* LOUDLY (fatal) instead of letting the selection fall through to the SQLite
44+
* default. That silent fall-through is the same "declared ≠ enforced" bug as
45+
* #3276 (the CLI advertised `memory`/`turso` but had no dispatch branch, so both
46+
* silently became SQLite-in-memory).
47+
*/
48+
export class UnsupportedDriverError extends Error {
49+
readonly driverType: string;
50+
constructor(driverType: string, message: string) {
51+
super(message);
52+
this.name = 'UnsupportedDriverError';
53+
this.driverType = driverType;
54+
}
55+
}
56+
3557
/**
3658
* Infer a canonical driver kind from an `OS_DATABASE_URL` scheme.
3759
* Returns `''` when the URL is absent or its scheme is unrecognized (the caller
@@ -43,6 +65,11 @@ export function inferDriverTypeFromUrl(url: string | undefined): string {
4365
if (/^mongodb(\+srv)?:\/\//i.test(u)) return 'mongodb';
4466
if (/^postgres(ql)?:\/\//i.test(u)) return 'postgres';
4567
if (/^mysql2?:\/\//i.test(u)) return 'mysql';
68+
// libSQL / Turso URLs are DELIBERATELY still classified as `turso` (not left
69+
// unrecognized). Open-core can't construct that driver, but classifying it
70+
// lets createStorageDriver fail LOUDLY with a clear cloud/EE message — if we
71+
// returned '' here instead, a `libsql://` URL would fall through to the SQLite
72+
// default and silently ignore the remote connection (the very bug we're fixing).
4673
if (/^libsql:\/\//i.test(u)) return 'turso';
4774
if (/^https?:\/\//i.test(u) && /\.turso\./i.test(u)) return 'turso';
4875
if (/^wasm-sqlite:\/\//i.test(u) || /\.wasm\.db$/i.test(u)) return 'sqlite-wasm';
@@ -101,6 +128,10 @@ export interface StorageDriverResolution {
101128
* nothing matches and we are NOT in dev (production with an unknown/absent
102129
* driver registers no driver, matching the prior inline behavior).
103130
*
131+
* Throws {@link UnsupportedDriverError} for `turso`/libSQL — a cloud/EE driver the
132+
* open-core CLI cannot construct. serve.ts surfaces that as a fatal, actionable
133+
* boot error so the selection never silently degrades to SQLite.
134+
*
104135
* @see {@link resolveDriverType}
105136
*/
106137
export async function createStorageDriver(
@@ -207,6 +238,27 @@ export async function createStorageDriver(
207238
};
208239
}
209240

241+
// turso / libSQL: recognized but NOT constructible by the open-core CLI. The
242+
// driver (`@objectstack/driver-turso`) ships in the cloud / enterprise
243+
// distribution and is composed by the cloud runtime's own kernel factory —
244+
// runtime/standalone-stack.ts explicitly stopped consuming its auth token, and
245+
// its config schema lives in the cloud package so it never pollutes open-core
246+
// `@objectstack/spec`. Fail LOUDLY here rather than let the selection fall
247+
// through to the SQLite default (the reported "declared ≠ enforced" bug):
248+
// serve.ts turns this typed error into a fatal, actionable boot message.
249+
if (driverType === 'turso' || driverType === 'libsql') {
250+
throw new UnsupportedDriverError(
251+
'turso',
252+
'The `turso`/libSQL driver ships with the ObjectStack cloud / enterprise '
253+
+ 'distribution (@objectstack/driver-turso), not the open-core CLI. To use '
254+
+ "it, register it explicitly in your stack config (a datasource with driver: "
255+
+ "'turso' and config { url, authToken }, with @objectstack/driver-turso "
256+
+ 'installed), or run under the cloud distribution. Otherwise select an '
257+
+ 'open-core driver via OS_DATABASE_DRIVER / OS_DATABASE_URL: '
258+
+ 'sqlite | postgres | mysql | mongodb | memory.',
259+
);
260+
}
261+
210262
// #3276: explicit in-memory (mingo) driver. Honored in dev AND production — an
211263
// operator asking for `memory` gets the mingo InMemoryDriver (ephemeral, not
212264
// real SQL), never the SQLite `:memory:` default. This is the branch whose

0 commit comments

Comments
 (0)