Skip to content

Commit b0c16a5

Browse files
os-helpclaude
andauthored
fix(service-datasource): the open-core libSQL arm tells you how to install the driver it is missing (#7314) (#7384)
`@objectstack/driver-turso` is an OPTIONAL install, so both loaders that can build a libSQL datasource have to answer "the package is not here". They answered it very differently. The HOST loader (`@objectstack/runtime`'s `loadTursoDriverFactory`, single owner since #6268) raises `MissingDriverPackageError` carrying the install command as data, plus a message naming the command, the consequence, and why the boot refuses instead of quietly opening a SQLite file. The shared open-core factory's `turso` arm — the one serving every OTHER door: a datasource added in Setup, `testConnection`, a declared non-default datasource — said only turso driver requested but @objectstack/driver-turso is not installed (…). the fault and nothing else. Same missing package; whether you were told how to fix it depended on whether your datasource happened to be named `default`. That arm now answers with the same quality of remedy, in `missingTursoDriverMessage()`, pinned by CONTENT rather than by `toThrow()` — the defect is what the message OMITS, and a throw-only assertion stayed green throughout the years it omitted it. Two deliberate differences from the host loader's wording, because this arm serves different doors. It NAMES THE DATASOURCE (here there may be several and only one is libSQL). And it names no `OS_DATABASE_URL` / `--database` / `OS_ALLOW_DRIVER_CONNECT_FAILURE`: those select or bypass the HOST's `default` and can do nothing for the datasource that actually failed — pointing a stuck reader at a knob that cannot affect their problem is the failure `connect-failure-remedy.ts` was written to end (#5794). One fix, stated once. The original import error is still interpolated in full, which is load-bearing: this re-throw drops the error's `code`, so `isUnbuiltWorkspaceFailure` can only recognise a half-built checkout from the `Cannot find package` TEXT the message carries. A test pins that classification, not just the wording. Deliberately NOT done here (they are `domain:cli` territory, #7314 points 2/3): the typed `MissingDriverPackageError` is not mirrored — that class lives in `@objectstack/runtime`, which DEPENDS on this package, so importing it inverts the dependency and declaring a second same-named class is precisely the identity hazard #6268 closed. And the host loader's narrow `url`/`authToken`-only config read is untouched; this arm already reads the full `TursoConfigSchema` surface. Claude-Session: https://claude.ai/code/session_015fkdTyGmMD5s8ZtEifvuGy Co-authored-by: Claude <noreply@anthropic.com>
1 parent bbee302 commit b0c16a5

4 files changed

Lines changed: 249 additions & 4 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
"@objectstack/service-datasource": patch
3+
---
4+
5+
fix(service-datasource): the open-core libSQL arm tells you how to install the driver it is missing (#7314)
6+
7+
`@objectstack/driver-turso` is an OPTIONAL install — it drags `@libsql/client`
8+
and its native bindings — so both loaders that can build a libSQL datasource
9+
have to answer "the package is not here". Until now they answered it very
10+
differently.
11+
12+
The HOST loader (`@objectstack/runtime`'s `loadTursoDriverFactory`, the single
13+
owner since #6268) raises `MissingDriverPackageError` carrying the install
14+
command as data, plus a message naming the command, the consequence, and why the
15+
boot refuses instead of quietly opening a SQLite file. The shared open-core
16+
factory's `turso` arm — the one that serves **every other door**: a datasource
17+
added in Setup, `testConnection`, a declared non-default datasource — said only:
18+
19+
```text
20+
turso driver requested but @objectstack/driver-turso is not installed (…).
21+
```
22+
23+
The fault and nothing else. Same missing package, and whether you were told how
24+
to fix it depended on whether your datasource happened to be named `default`.
25+
26+
That arm now answers with the same quality of remedy:
27+
28+
```text
29+
datasource 'warehouse': a libSQL/Turso datasource was requested, but the driver
30+
package @objectstack/driver-turso is not installed. Install it next to the
31+
server that opens this datasource:
32+
33+
npm install @objectstack/driver-turso
34+
35+
(pnpm add … / yarn add ….) It is an OPTIONAL package, so a default install stays
36+
free of @libsql/client and its native bindings. This refuses rather than falling
37+
back to another engine: a silent fallback would open an empty local database
38+
that accepts writes while your libSQL data stays untouched, and every write
39+
would land in the wrong database. Import error: …
40+
```
41+
42+
Two deliberate differences from the host loader's wording, because this arm
43+
serves different doors. It **names the datasource** — here there may be several
44+
and only one of them is libSQL. And it names **no** `OS_DATABASE_URL` /
45+
`--database` / `OS_ALLOW_DRIVER_CONNECT_FAILURE`: those select or bypass the
46+
HOST's `default` datasource and can do nothing for the datasource that actually
47+
failed, and pointing a stuck reader at a knob that cannot affect their problem
48+
is the failure `connect-failure-remedy.ts` was written to end (#5794). One fix,
49+
stated once, no escape hatch named.
50+
51+
The original import error is still interpolated in full, which is load-bearing
52+
rather than context: this re-throw drops the error's `code`, so the
53+
unbuilt-workspace classifier can only recognise a half-built checkout from the
54+
`Cannot find package` text the message carries.
55+
56+
`TURSO_DRIVER_PACKAGE`, `TURSO_DRIVER_INSTALL_COMMAND` and
57+
`missingTursoDriverMessage` are exported, so a host that renders the remedy
58+
itself reads one declaration instead of re-typing a sentence.
59+
60+
Behaviour is otherwise unchanged: the same failure at the same moment, still a
61+
refusal and never a fallback to a different engine. Only the message differs.

packages/services/service-datasource/src/__tests__/default-datasource-driver-factory.test.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
99
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
1010
import { tmpdir } from 'node:os';
1111
import { join } from 'node:path';
12-
import { createDefaultDatasourceDriverFactory } from '../default-datasource-driver-factory.js';
12+
import {
13+
createDefaultDatasourceDriverFactory,
14+
missingTursoDriverMessage,
15+
TURSO_DRIVER_INSTALL_COMMAND,
16+
TURSO_DRIVER_PACKAGE,
17+
} from '../default-datasource-driver-factory.js';
18+
import { isUnbuiltWorkspaceFailure } from '../connect-failure-remedy.js';
1319

1420
const factory = () => createDefaultDatasourceDriverFactory({ dev: false });
1521

@@ -354,3 +360,98 @@ describe('createDefaultDatasourceDriverFactory — legacy config spellings are n
354360
expect(driver.config.url).toBe('mongodb://svc:pw@mongo.internal:27017/events');
355361
});
356362
});
363+
364+
// #7314 — the OPTIONAL libSQL driver is missing, and until now this arm said so
365+
// and stopped: `turso driver requested but @objectstack/driver-turso is not
366+
// installed (…)`. The HOST loader (`@objectstack/runtime`'s
367+
// `loadTursoDriverFactory`, single owner since #6268) has answered the same
368+
// missing package with the install command, the consequence and the reason for
369+
// refusing since #5602 — so the SAME fault got two qualities of answer depending
370+
// on whether the datasource happened to be the host's `default` (told how to fix
371+
// it) or one added in Setup / probed by `testConnection` / declared as a
372+
// non-default (told only that it was broken).
373+
//
374+
// Pinned by CONTENT rather than by `toThrow()`: the defect is what the message
375+
// omits, and a throw-only assertion was green throughout the years this arm
376+
// omitted it.
377+
describe('createDefaultDatasourceDriverFactory — the missing libSQL package is answered with a remedy (#7314)', () => {
378+
const message = (cause: unknown, datasource?: string) =>
379+
missingTursoDriverMessage({ cause, ...(datasource ? { datasource } : {}) });
380+
381+
const notInstalled = Object.assign(
382+
new Error(`Cannot find package '${TURSO_DRIVER_PACKAGE}' imported from /app/node_modules/x.mjs`),
383+
{ code: 'ERR_MODULE_NOT_FOUND' },
384+
);
385+
386+
it('states the exact install command, on its own copy-pasteable line', () => {
387+
expect(TURSO_DRIVER_INSTALL_COMMAND).toBe('npm install @objectstack/driver-turso');
388+
expect(message(notInstalled)).toContain(`\n\n ${TURSO_DRIVER_INSTALL_COMMAND}\n\n`);
389+
});
390+
391+
it('names the package that is missing', () => {
392+
expect(TURSO_DRIVER_PACKAGE).toBe('@objectstack/driver-turso');
393+
expect(message(notInstalled)).toContain(TURSO_DRIVER_PACKAGE);
394+
});
395+
396+
it('states the consequence and that the refusal is deliberate', () => {
397+
const text = message(notInstalled);
398+
// Not decoration: without it the reader's next move is to look for the
399+
// fallback, and a libSQL selection quietly served by another engine is the
400+
// #3276 class — writes accepted into the wrong database.
401+
expect(text).toContain('refuses rather than falling back');
402+
expect(text).toContain('wrong database');
403+
});
404+
405+
it('names the datasource that failed, and falls back to `default`', () => {
406+
expect(message(notInstalled, 'warehouse')).toContain("datasource 'warehouse'");
407+
expect(message(notInstalled)).toContain("datasource 'default'");
408+
});
409+
410+
it('keeps the import error verbatim, so the unbuilt-workspace classifier still fires', () => {
411+
// Load-bearing: this arm re-throws a NEW Error and therefore drops the
412+
// original `code`, so `isUnbuiltWorkspaceFailure` can only recognise an
413+
// unbuilt/uninstalled workspace from the `Cannot find package` TEXT the
414+
// message carries. Drop the interpolation and a half-built worktree silently
415+
// goes back to being told "Fix the datasource configuration" (#5794).
416+
const text = message(notInstalled);
417+
expect(text).toContain(notInstalled.message);
418+
expect(isUnbuiltWorkspaceFailure(new Error(text))).toBe(true);
419+
});
420+
421+
it('names no escape hatch and no host-boot knob — one fix, stated once', () => {
422+
const text = message(notInstalled);
423+
// `OS_ALLOW_DRIVER_CONNECT_FAILURE` would only hide a package that does not
424+
// exist (#5794), and `OS_DATABASE_URL` / `--database` select the HOST's
425+
// `default` datasource — neither can affect the datasource that failed here.
426+
expect(text).not.toContain('OS_ALLOW_DRIVER_CONNECT_FAILURE');
427+
expect(text).not.toContain('OS_DATABASE_URL');
428+
expect(text).not.toContain('--database');
429+
expect(text.match(new RegExp(TURSO_DRIVER_INSTALL_COMMAND.replace(/\//g, '\\/'), 'g'))).toHaveLength(1);
430+
});
431+
432+
it('is what the turso arm actually raises when the optional package is absent', async () => {
433+
// `@objectstack/driver-turso` is deliberately not a dependency of this
434+
// package — that is what "optional" means — so the missing-package path is
435+
// reachable here for a real reason and needs no stub.
436+
let raised: unknown;
437+
try {
438+
await factory().create({
439+
driver: 'turso',
440+
name: 'warehouse',
441+
config: { url: 'libsql://my-db.turso.io', authToken: 'tok' },
442+
});
443+
} catch (err) {
444+
raised = err;
445+
}
446+
if (raised === undefined) {
447+
throw new Error(
448+
`${TURSO_DRIVER_PACKAGE} resolved from @objectstack/service-datasource, so this case no `
449+
+ 'longer exercises the missing-package arm. If the package was made a dependency, this '
450+
+ 'assertion is the notice that the pin above needs a stubbed import instead.',
451+
);
452+
}
453+
expect((raised as Error).message).toContain(TURSO_DRIVER_INSTALL_COMMAND);
454+
expect((raised as Error).message).toContain(TURSO_DRIVER_PACKAGE);
455+
expect((raised as Error).message).toContain("datasource 'warehouse'");
456+
});
457+
});

packages/services/service-datasource/src/default-datasource-driver-factory.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,71 @@ function resolveKind(driverId: string): ResolvedKind | undefined {
7272
return resolveDriverId(driverId);
7373
}
7474

75+
/**
76+
* The optional package that provides the libSQL/Turso driver, and the exact
77+
* command an operator runs to install it.
78+
*
79+
* Declared as constants rather than left inline so the pin test asserts the
80+
* COMMAND rather than a sentence shape, and so a host that wants to render the
81+
* remedy itself has one place to read it from. `@objectstack/runtime` currently
82+
* declares its own equal pair (`TURSO_DRIVER_PACKAGE` /
83+
* `TURSO_DRIVER_INSTALL_COMMAND` in `turso-driver-factory.ts`); converging the
84+
* two onto these is a runtime-lane change — runtime already depends on this
85+
* package, so that import direction is the legal one, while the reverse is not
86+
* (#7314).
87+
*/
88+
export const TURSO_DRIVER_PACKAGE = '@objectstack/driver-turso';
89+
90+
/** @see {@link TURSO_DRIVER_PACKAGE} */
91+
export const TURSO_DRIVER_INSTALL_COMMAND = `npm install ${TURSO_DRIVER_PACKAGE}`;
92+
93+
/**
94+
* What this factory says when the OPTIONAL libSQL driver package is absent
95+
* (#7314).
96+
*
97+
* Until #7314 this arm said only *"turso driver requested but
98+
* @objectstack/driver-turso is not installed (…)"* — the fault and nothing
99+
* else. The host loader (`@objectstack/runtime`'s `loadTursoDriverFactory`,
100+
* single owner since #6268) has answered the SAME missing package with the
101+
* install command, the consequence and the reason for refusing since #5602, so
102+
* an operator who booted with a libSQL url was told how to fix it while an
103+
* admin who added the identical datasource in Setup was not. One missing
104+
* package, two qualities of answer, decided by which door the request came
105+
* through.
106+
*
107+
* Two deliberate differences from the host loader's wording, because this arm
108+
* serves different doors — a datasource created in Setup, `testConnection`, a
109+
* declared NON-default datasource — rather than a `default` that a host boots:
110+
*
111+
* - It names the datasource, like the url-less refusal in the same arm, since
112+
* here there may be several and only one of them is libSQL.
113+
* - It does NOT mention `OS_DATABASE_URL` / `--database`. Those select the
114+
* HOST's `default` datasource and would do nothing for the datasource that
115+
* actually failed — advice that sends the reader to a knob which cannot
116+
* affect their problem is the `connect-failure-remedy.ts` failure (#5794) in
117+
* a new spelling. One fix, stated once, and no escape hatch named.
118+
*
119+
* The underlying import error is interpolated at the END and in full. That is
120+
* load-bearing beyond context: this re-throw drops the original `code`, so
121+
* `isUnbuiltWorkspaceFailure` (via `isModuleNotFoundError`) can only recognise
122+
* an unbuilt/uninstalled workspace from the `Cannot find package` /
123+
* `Cannot find module` TEXT it carries — the same reason the `sqlite-wasm` and
124+
* `mongodb` arms interpolate theirs.
125+
*/
126+
export function missingTursoDriverMessage(args: { datasource?: string; cause: unknown }): string {
127+
const cause = args.cause instanceof Error ? args.cause.message : String(args.cause);
128+
return (
129+
`datasource '${args.datasource ?? 'default'}': a libSQL/Turso datasource was requested, but the `
130+
+ `driver package ${TURSO_DRIVER_PACKAGE} is not installed. Install it next to the server that `
131+
+ `opens this datasource:\n\n ${TURSO_DRIVER_INSTALL_COMMAND}\n\n`
132+
+ `(pnpm add ${TURSO_DRIVER_PACKAGE} / yarn add ${TURSO_DRIVER_PACKAGE}.) It is an OPTIONAL `
133+
+ 'package, so a default install stays free of @libsql/client and its native bindings. This '
134+
+ 'refuses rather than falling back to another engine: a silent fallback would open an empty '
135+
+ 'local database that accepts writes while your libSQL data stays untouched, and every write '
136+
+ `would land in the wrong database. Import error: ${cause}`
137+
);
138+
}
139+
75140
/**
76141
* Wrap a concrete engine driver in a probe handle. `ping`/`checkHealth` reuse
77142
* the driver's own health check; `driver` is the escape hatch the admin service
@@ -500,13 +565,20 @@ export function createDefaultDatasourceDriverFactory(
500565
// seam), which wins over this one; this arm is what serves every OTHER
501566
// door — a runtime datasource created in Setup, `testConnection`, a
502567
// declared non-default datasource.
568+
//
569+
// The missing-package message states the install command, the
570+
// consequence and the refusal — the same quality of answer the host
571+
// loader has given since #5602, which this arm did not (#7314). The
572+
// typed `MissingDriverPackageError` the host raises is deliberately NOT
573+
// mirrored here: that class lives in `@objectstack/runtime`, which
574+
// DEPENDS on this package, so importing it would invert the dependency,
575+
// and declaring a second same-named class is precisely the identity
576+
// hazard #6268 closed (`serve.ts` decides fatality with `instanceof`).
503577
let TursoDriver: any;
504578
try {
505579
({ TursoDriver } = await import('@objectstack/driver-turso' as any));
506580
} catch (err: any) {
507-
throw new Error(
508-
`turso driver requested but @objectstack/driver-turso is not installed (${err?.message ?? err}).`,
509-
);
581+
throw new Error(missingTursoDriverMessage({ datasource: spec.name, cause: err }));
510582
}
511583
const url = typeof cfg.url === 'string' ? cfg.url.trim() : '';
512584
if (!url) {

packages/services/service-datasource/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ export type { PoolUnsupportedDriverId } from './datasource-pool-support.js';
8989

9090
// Host glue: dev driver factory + fail-closed secret binder.
9191
export { createDefaultDatasourceDriverFactory } from './default-datasource-driver-factory.js';
92+
// The OPTIONAL libSQL/Turso package and its install command, plus the
93+
// missing-package message this factory raises (#7314) — exported so the answer
94+
// to "how do I install it" has one declaration a host can read rather than a
95+
// sentence to re-type. `@objectstack/runtime`'s host loader keeps its own equal
96+
// pair today; it depends on this package, so converging onto these is a legal
97+
// import direction whenever that lane takes it up.
98+
export {
99+
TURSO_DRIVER_PACKAGE,
100+
TURSO_DRIVER_INSTALL_COMMAND,
101+
missingTursoDriverMessage,
102+
} from './default-datasource-driver-factory.js';
92103
// The "adopt a host-built driver instance" seam (ADR-0062 D1, #3826) — for
93104
// driver kinds outside open-core (cloud turso) and pooled instances whose
94105
// lifecycle outlives one kernel; keeps the connect + failure verdict on the

0 commit comments

Comments
 (0)