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
19 changes: 19 additions & 0 deletions .changeset/cli-turso-loud-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@objectstack/cli': patch
---

fix(cli): fail loudly when `turso`/libSQL is selected in the open-core CLI (#3276 follow-up)

Same "declared ≠ enforced" class as the `memory` fix: the CLI advertised `turso`
(`--database-driver turso`, `OS_DATABASE_DRIVER=turso`, `libsql://` URLs) but the
driver dispatch had no `turso` branch, so it silently fell through to the SQLite
default and ignored the requested engine.

`turso`/libSQL ships in the cloud / enterprise distribution
(`@objectstack/driver-turso`, composed by the cloud runtime's own kernel factory —
open-core's standalone stack deliberately does not consume it). Rather than pull an
EE driver into open-core, `createStorageDriver` now throws a typed
`UnsupportedDriverError` for `turso`/`libsql`, and `serve.ts` surfaces it as a
fatal, actionable boot error (naming the cloud/EE package and the open-core
alternatives) instead of silently degrading to SQLite. `libsql://` / `*.turso.*`
URLs stay classified as `turso` so they hit the same loud failure.
10 changes: 9 additions & 1 deletion packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import chalk from 'chalk';
import { bundleRequire } from 'bundle-require';
import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
import { isHostConfig, shouldBootWithLibrary } from '../utils/plugin-detection.js';
import { resolveDriverType, createStorageDriver } from '../utils/storage-driver.js';
import { resolveDriverType, createStorageDriver, UnsupportedDriverError } from '../utils/storage-driver.js';
import { readEnvWithDeprecation, resolveMultiOrgEnabled, resolveAllowDegradedTenancy, isMcpServerEnabled, resolveSearchPinyinEnabled, isModuleNotFoundError } from '@objectstack/types';
import { PLATFORM_CAPABILITY_TOKENS, canonicalizePlatformCapability } from '@objectstack/spec/kernel';
import { resolveObjectStackHome } from '@objectstack/runtime';
Expand Down Expand Up @@ -937,6 +937,14 @@ export default class Serve extends Command {
}
}
} catch (e: any) {
// "declared ≠ enforced" guard (#3276-class): a driver that is
// RECOGNIZED but the open-core CLI cannot construct — currently
// `turso`/libSQL, a cloud/EE driver — must fail LOUDLY, never silently
// fall through to the SQLite default and ignore the selected engine.
// Re-throw so run()'s fatal handler restores output, prints the
// actionable message, and exits 1 (in dev AND prod). All OTHER driver
// construction errors keep the prior best-effort silent behavior.
if (e instanceof UnsupportedDriverError) throw e;
// silent
}
}
Expand Down
40 changes: 40 additions & 0 deletions packages/cli/src/utils/storage-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
inferDriverTypeFromUrl,
resolveDriverType,
createStorageDriver,
UnsupportedDriverError,
} from './storage-driver.js';

describe('inferDriverTypeFromUrl', () => {
Expand Down Expand Up @@ -131,3 +132,42 @@ describe('createStorageDriver', () => {
expect(await createStorageDriver('nonsense', { isDev: false })).toBeNull();
});
});

describe('createStorageDriver: turso / libSQL is recognized but fails loud', () => {
// turso is a cloud/EE driver (@objectstack/driver-turso) the open-core CLI
// cannot construct. Selecting it must THROW a typed error — NOT fall through
// to the SQLite default. Remove the `turso` branch in storage-driver.ts and
// these go red: in dev it resolves to a SqlDriver, in prod it returns null —
// both silently ignoring the requested turso engine (the reported bug).
it('throws UnsupportedDriverError for `turso` in DEV and PROD', async () => {
await expect(createStorageDriver('turso', { isDev: true })).rejects.toBeInstanceOf(UnsupportedDriverError);
await expect(createStorageDriver('turso', { isDev: false })).rejects.toBeInstanceOf(UnsupportedDriverError);
});

it('throws for the `libsql` alias too', async () => {
await expect(createStorageDriver('libsql', { isDev: true })).rejects.toBeInstanceOf(UnsupportedDriverError);
});

// The message must be actionable: name the cloud/EE package and the open-core
// alternatives, so an operator knows exactly how to proceed.
it('carries an actionable message (cloud/EE package + open-core alternatives)', async () => {
await expect(createStorageDriver('turso', { isDev: false })).rejects.toThrow(/@objectstack\/driver-turso/);
await expect(createStorageDriver('turso', { isDev: false })).rejects.toThrow(/cloud|enterprise/i);
const err = await createStorageDriver('turso', { isDev: false }).catch((e) => e);
expect(err).toBeInstanceOf(UnsupportedDriverError);
expect((err as UnsupportedDriverError).driverType).toBe('turso');
});

// A `libsql://` / Turso URL routes to the same loud failure — it is NOT left
// unrecognized (which would silently fall through to SQLite).
it('routes libsql:// and *.turso.* URLs to the turso failure, never SQLite', async () => {
expect(resolveDriverType(undefined, 'libsql://my-db.turso.io')).toBe('turso');
expect(resolveDriverType(undefined, 'https://my-db.turso.io')).toBe('turso');
await expect(
createStorageDriver(resolveDriverType(undefined, 'libsql://my-db.turso.io'), {
databaseUrl: 'libsql://my-db.turso.io',
isDev: true,
}),
).rejects.toBeInstanceOf(UnsupportedDriverError);
});
});
52 changes: 52 additions & 0 deletions packages/cli/src/utils/storage-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@
/** Engines the shared sqlite step-down (`resolveSqliteDriver`) can produce. */
export type SqliteFamilyEngine = 'better-sqlite3' | 'sqlite-wasm' | 'memory';

/**
* Thrown by {@link createStorageDriver} when a driver kind is *recognized* but the
* open-core CLI cannot construct it — currently `turso`/libSQL, which ships in the
* ObjectStack cloud / enterprise distribution (`@objectstack/driver-turso`, an
* extension of SqlDriver over `@libsql/client`), composed by the cloud runtime's
* own kernel factory, not by open-core's auto driver-registration.
*
* The whole point of surfacing this as a *typed* error is so `serve.ts` can fail
* LOUDLY (fatal) instead of letting the selection fall through to the SQLite
* default. That silent fall-through is the same "declared ≠ enforced" bug as
* #3276 (the CLI advertised `memory`/`turso` but had no dispatch branch, so both
* silently became SQLite-in-memory).
*/
export class UnsupportedDriverError extends Error {
readonly driverType: string;
constructor(driverType: string, message: string) {
super(message);
this.name = 'UnsupportedDriverError';
this.driverType = driverType;
}
}

/**
* Infer a canonical driver kind from an `OS_DATABASE_URL` scheme.
* Returns `''` when the URL is absent or its scheme is unrecognized (the caller
Expand All @@ -43,6 +65,11 @@ export function inferDriverTypeFromUrl(url: string | undefined): string {
if (/^mongodb(\+srv)?:\/\//i.test(u)) return 'mongodb';
if (/^postgres(ql)?:\/\//i.test(u)) return 'postgres';
if (/^mysql2?:\/\//i.test(u)) return 'mysql';
// libSQL / Turso URLs are DELIBERATELY still classified as `turso` (not left
// unrecognized). Open-core can't construct that driver, but classifying it
// lets createStorageDriver fail LOUDLY with a clear cloud/EE message — if we
// returned '' here instead, a `libsql://` URL would fall through to the SQLite
// default and silently ignore the remote connection (the very bug we're fixing).
if (/^libsql:\/\//i.test(u)) return 'turso';
if (/^https?:\/\//i.test(u) && /\.turso\./i.test(u)) return 'turso';
if (/^wasm-sqlite:\/\//i.test(u) || /\.wasm\.db$/i.test(u)) return 'sqlite-wasm';
Expand Down Expand Up @@ -101,6 +128,10 @@ export interface StorageDriverResolution {
* nothing matches and we are NOT in dev (production with an unknown/absent
* driver registers no driver, matching the prior inline behavior).
*
* Throws {@link UnsupportedDriverError} for `turso`/libSQL — a cloud/EE driver the
* open-core CLI cannot construct. serve.ts surfaces that as a fatal, actionable
* boot error so the selection never silently degrades to SQLite.
*
* @see {@link resolveDriverType}
*/
export async function createStorageDriver(
Expand Down Expand Up @@ -207,6 +238,27 @@ export async function createStorageDriver(
};
}

// turso / libSQL: recognized but NOT constructible by the open-core CLI. The
// driver (`@objectstack/driver-turso`) ships in the cloud / enterprise
// distribution and is composed by the cloud runtime's own kernel factory —
// runtime/standalone-stack.ts explicitly stopped consuming its auth token, and
// its config schema lives in the cloud package so it never pollutes open-core
// `@objectstack/spec`. Fail LOUDLY here rather than let the selection fall
// through to the SQLite default (the reported "declared ≠ enforced" bug):
// serve.ts turns this typed error into a fatal, actionable boot message.
if (driverType === 'turso' || driverType === 'libsql') {
throw new UnsupportedDriverError(
'turso',
'The `turso`/libSQL driver ships with the ObjectStack cloud / enterprise '
+ 'distribution (@objectstack/driver-turso), not the open-core CLI. To use '
+ "it, register it explicitly in your stack config (a datasource with driver: "
+ "'turso' and config { url, authToken }, with @objectstack/driver-turso "
+ 'installed), or run under the cloud distribution. Otherwise select an '
+ 'open-core driver via OS_DATABASE_DRIVER / OS_DATABASE_URL: '
+ 'sqlite | postgres | mysql | mongodb | memory.',
);
}

// #3276: explicit in-memory (mingo) driver. Honored in dev AND production — an
// operator asking for `memory` gets the mingo InMemoryDriver (ephemeral, not
// real SQL), never the SQLite `:memory:` default. This is the branch whose
Expand Down