diff --git a/.changeset/optional-driver-package-remedy.md b/.changeset/optional-driver-package-remedy.md new file mode 100644 index 0000000000..ce97786673 --- /dev/null +++ b/.changeset/optional-driver-package-remedy.md @@ -0,0 +1,20 @@ +--- +'@objectstack/service-datasource': patch +--- + +The `sqlite-wasm` and `mongodb` arms of the shared datasource driver factory now tell you how to install the optional driver package they are missing (#7385) + +All three of `sqlite-wasm`, `mongodb` and `turso` are built from OPTIONAL packages, so all three have to answer "the package is not here". After #7314 fixed the libSQL arm, the other two still answered with the fault and nothing else: + +```text +sqlite-wasm driver requested but @objectstack/driver-sqlite-wasm is not installed (…). +mongodb driver requested but @objectstack/driver-mongodb is not installed (…). +``` + +No install command, no statement of what happens next, and not even the name of the datasource that failed — while the `turso` arm beside them stated all three. An admin who added a mongo datasource in Setup and one who added a libSQL datasource hit the same class of problem and got two different qualities of answer, decided by nothing but which driver they picked. + +Both arms now answer through a shared builder, keeping the two discipline points #7384 landed under: the message NAMES THE DATASOURCE (several may be declared and only one of them is this engine), and it names exactly one fix with no escape hatch — no `OS_ALLOW_DRIVER_CONNECT_FAILURE` (it would only hide a package that does not exist) and no `OS_DATABASE_URL` / `--database` (they select the HOST's `default` datasource and can do nothing for the one that failed). The underlying import error is still interpolated in full, which is what keeps `isUnbuiltWorkspaceFailure` able to recognise a half-built checkout from these arms and re-route the remedy to `pnpm install && pnpm build`. + +The consequence sentence is per-engine rather than copied. Mongo, like libSQL, is a server this process connects to, so a silent fallback would open a local database while the real server stayed untouched. `sqlite-wasm` has no remote to shadow, so it states its own truth instead: stepping down to the in-process memory driver would accept every write and drop it at shutdown, leaving the configured file empty, and stepping down to native `better-sqlite3` would need exactly the native addon a WASM datasource is chosen to avoid. + +New exports, mirroring the libSQL pair, so a host that renders its own remedy reads one declaration instead of re-typing a command: `SQLITE_WASM_DRIVER_PACKAGE`, `SQLITE_WASM_DRIVER_INSTALL_COMMAND`, `missingSqliteWasmDriverMessage`, `MONGODB_DRIVER_PACKAGE`, `MONGODB_DRIVER_INSTALL_COMMAND`, `missingMongodbDriverMessage`. diff --git a/packages/services/service-datasource/src/__tests__/datasource-connection-service.test.ts b/packages/services/service-datasource/src/__tests__/datasource-connection-service.test.ts index 6ea80e534b..c45b8b2b6e 100644 --- a/packages/services/service-datasource/src/__tests__/datasource-connection-service.test.ts +++ b/packages/services/service-datasource/src/__tests__/datasource-connection-service.test.ts @@ -13,6 +13,7 @@ import { GENERIC_CONNECT_FAILURE_REMEDY, isUnbuiltWorkspaceFailure, } from '../connect-failure-remedy.js'; +import { missingSqliteWasmDriverMessage } from '../default-datasource-driver-factory.js'; /** One `markDatasourceUnavailable` call, as the engine would receive it. */ type UnavailableCall = { name: string; kind: 'blocked' | 'failed'; publicDetail?: string }; @@ -484,11 +485,21 @@ describe('fail-fast remedy is chosen by CAUSE (#5794)', () => { }); it('by message alone: the factory-wrapped optional-driver form, no code', async () => { + // Built from the factory's OWN message rather than a copy of its wording + // (#7385): this fixture used to spell the pre-#7385 sentence by hand, so + // it would have gone on asserting a shape the factory no longer emits. + // The property under test is unchanged — the wrapper drops the `code`, so + // the classifier has only the interpolated `Cannot find module` text to + // work with — and it is now pinned against the real wrapper. const err = await failFast( factoryThrowing( new Error( - 'sqlite-wasm driver requested but @objectstack/driver-sqlite-wasm is not installed ' + - "(Cannot find module '/w/node_modules/@objectstack/driver-sqlite-wasm/dist/index.mjs').", + missingSqliteWasmDriverMessage({ + datasource: 'default', + cause: new Error( + "Cannot find module '/w/node_modules/@objectstack/driver-sqlite-wasm/dist/index.mjs'", + ), + }), ), ), ); diff --git a/packages/services/service-datasource/src/__tests__/default-datasource-driver-factory.test.ts b/packages/services/service-datasource/src/__tests__/default-datasource-driver-factory.test.ts index d95f901351..52ea88e3ac 100644 --- a/packages/services/service-datasource/src/__tests__/default-datasource-driver-factory.test.ts +++ b/packages/services/service-datasource/src/__tests__/default-datasource-driver-factory.test.ts @@ -5,7 +5,7 @@ // driver — builds through the same `create({driver,config})` as every other // kind. These are the first direct tests of the factory's id → driver mapping. -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; import { existsSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -14,6 +14,12 @@ import { missingTursoDriverMessage, TURSO_DRIVER_INSTALL_COMMAND, TURSO_DRIVER_PACKAGE, + missingSqliteWasmDriverMessage, + SQLITE_WASM_DRIVER_INSTALL_COMMAND, + SQLITE_WASM_DRIVER_PACKAGE, + missingMongodbDriverMessage, + MONGODB_DRIVER_INSTALL_COMMAND, + MONGODB_DRIVER_PACKAGE, } from '../default-datasource-driver-factory.js'; import { isUnbuiltWorkspaceFailure } from '../connect-failure-remedy.js'; @@ -455,3 +461,212 @@ describe('createDefaultDatasourceDriverFactory — the missing libSQL package is expect((raised as Error).message).toContain("datasource 'warehouse'"); }); }); + +// #7385 — the same generalisation over the two SIBLING optional arms. +// +// `sqlite-wasm` and `mongodb` ride in optional packages exactly like `turso`, +// and after #7314 they were the two arms still answering an absent one with +// +// sqlite-wasm driver requested but @objectstack/driver-sqlite-wasm is not installed (…). +// mongodb driver requested but @objectstack/driver-mongodb is not installed (…). +// +// — the fault, no install command, no consequence, and not even the name of the +// datasource that failed. One class of problem, two qualities of answer, +// decided by which driver the admin picked. +// +// Pinned by CONTENT for the reason #7384 gave: the defect is what the message +// OMITS, so `toThrow()` was green throughout. +// +// Unlike the libSQL package, BOTH of these resolve inside this workspace (they +// are `devDependencies` of `@objectstack/service-datasource`, which is how the +// construction suites above build real drivers). So the arm-level cases cannot +// simply ask for a driver and watch it fail the way the turso one does — they +// make the optional import fail on purpose instead. +const notInstalled = (pkg: string) => + Object.assign(new Error(`Cannot find package '${pkg}' imported from /app/node_modules/x.mjs`), { + code: 'ERR_MODULE_NOT_FOUND', + }); + +/** + * Build a datasource whose OPTIONAL driver package is absent, and return what + * the arm raised. The package is a real dependency here, so absence is staged: + * the module registry is reset, the specifier is mocked with a factory that + * throws the resolver's own error, and the factory module is re-imported so its + * lazy `await import(...)` hits the mock. Both are undone in `finally`, so the + * construction suites in this file keep seeing the real drivers. + */ +async function raiseWithPackageAbsent( + pkg: string, + spec: { driver: string; name?: string; config?: Record }, +): Promise { + vi.resetModules(); + vi.doMock(pkg, () => { + throw notInstalled(pkg); + }); + try { + const mod = await import('../default-datasource-driver-factory.js'); + await mod.createDefaultDatasourceDriverFactory({ dev: false }).create(spec as never); + return undefined; + } catch (err) { + return err; + } finally { + vi.doUnmock(pkg); + vi.resetModules(); + } +} + +describe('createDefaultDatasourceDriverFactory — the missing WASM SQLite package is answered with a remedy (#7385)', () => { + const message = (cause: unknown, datasource?: string) => + missingSqliteWasmDriverMessage({ cause, ...(datasource ? { datasource } : {}) }); + const cause = notInstalled(SQLITE_WASM_DRIVER_PACKAGE); + + it('states the exact install command, on its own copy-pasteable line', () => { + expect(SQLITE_WASM_DRIVER_INSTALL_COMMAND).toBe('npm install @objectstack/driver-sqlite-wasm'); + expect(message(cause)).toContain(`\n\n ${SQLITE_WASM_DRIVER_INSTALL_COMMAND}\n\n`); + }); + + it('names the package that is missing', () => { + expect(SQLITE_WASM_DRIVER_PACKAGE).toBe('@objectstack/driver-sqlite-wasm'); + expect(message(cause)).toContain(SQLITE_WASM_DRIVER_PACKAGE); + }); + + it('states a consequence that is TRUE for this engine, not the libSQL one', () => { + const text = message(cause); + expect(text).toContain('refuses rather than falling back'); + // What a fallback would actually cost here: durability (the memory driver + // accepts writes and drops them at shutdown, #4083), or the native addon + // this driver id exists to avoid. + expect(text).toContain('drop it at shutdown'); + expect(text).toContain('better-sqlite3'); + // And what it must NOT claim: `sqlite-wasm` opens a local file, so there is + // no remote database for a fallback to shadow. #7384's "your libSQL data + // stays untouched … the wrong database" reads well here and would be a lie. + expect(text).not.toContain('wrong database'); + expect(text).not.toContain('stays untouched'); + }); + + it('names the datasource that failed, and falls back to `default`', () => { + expect(message(cause, 'wasm-store')).toContain("datasource 'wasm-store'"); + expect(message(cause)).toContain("datasource 'default'"); + }); + + it('keeps the import error verbatim, so the unbuilt-workspace classifier still fires', () => { + // Load-bearing exactly as in the turso arm: this re-throw drops the original + // `code`, so `isUnbuiltWorkspaceFailure` can only recognise a half-built + // checkout from the `Cannot find package` TEXT carried here. That case is + // the COMMON one for this package — `@objectstack/runtime` and the CLI both + // depend on it outright, so a reader who hits this is usually unbuilt rather + // than uninstalled, and must not be told to install what they already have. + const text = message(cause); + expect(text).toContain(cause.message); + expect(isUnbuiltWorkspaceFailure(new Error(text))).toBe(true); + }); + + it('names no escape hatch and no host-boot knob — one fix, stated once', () => { + const text = message(cause); + expect(text).not.toContain('OS_ALLOW_DRIVER_CONNECT_FAILURE'); + expect(text).not.toContain('OS_DATABASE_URL'); + expect(text).not.toContain('--database'); + expect(text.match(new RegExp(SQLITE_WASM_DRIVER_INSTALL_COMMAND.replace(/\//g, '\\/'), 'g'))) + .toHaveLength(1); + }); + + it('is what the sqlite-wasm arm actually raises when the optional package is absent', async () => { + const raised = await raiseWithPackageAbsent(SQLITE_WASM_DRIVER_PACKAGE, { + driver: 'sqlite-wasm', + name: 'wasm-store', + config: { filename: 'data/app.db' }, + }); + expect(raised).toBeInstanceOf(Error); + expect((raised as Error).message).toContain(SQLITE_WASM_DRIVER_INSTALL_COMMAND); + expect((raised as Error).message).toContain(SQLITE_WASM_DRIVER_PACKAGE); + expect((raised as Error).message).toContain("datasource 'wasm-store'"); + expect((raised as Error).message).toContain('refuses rather than falling back'); + }); +}); + +describe('createDefaultDatasourceDriverFactory — the missing MongoDB package is answered with a remedy (#7385)', () => { + const message = (cause: unknown, datasource?: string) => + missingMongodbDriverMessage({ cause, ...(datasource ? { datasource } : {}) }); + const cause = notInstalled(MONGODB_DRIVER_PACKAGE); + + it('states the exact install command, on its own copy-pasteable line', () => { + expect(MONGODB_DRIVER_INSTALL_COMMAND).toBe('npm install @objectstack/driver-mongodb'); + expect(message(cause)).toContain(`\n\n ${MONGODB_DRIVER_INSTALL_COMMAND}\n\n`); + }); + + it('names the package that is missing', () => { + expect(MONGODB_DRIVER_PACKAGE).toBe('@objectstack/driver-mongodb'); + expect(message(cause)).toContain(MONGODB_DRIVER_PACKAGE); + }); + + it('states the consequence and that the refusal is deliberate', () => { + const text = message(cause); + expect(text).toContain('refuses rather than falling back'); + // Mongo is a server this process CONNECTS to, so #7384's consequence is + // true here in substance: a local store shadowing a remote database is the + // #3276 class — writes accepted into the wrong place. + expect(text).toContain('stays untouched'); + expect(text).toContain('wrong database'); + }); + + it('names the datasource that failed, and falls back to `default`', () => { + expect(message(cause, 'events')).toContain("datasource 'events'"); + expect(message(cause)).toContain("datasource 'default'"); + }); + + it('keeps the import error verbatim, so the unbuilt-workspace classifier still fires', () => { + const text = message(cause); + expect(text).toContain(cause.message); + expect(isUnbuiltWorkspaceFailure(new Error(text))).toBe(true); + }); + + it('names no escape hatch and no host-boot knob — one fix, stated once', () => { + const text = message(cause); + expect(text).not.toContain('OS_ALLOW_DRIVER_CONNECT_FAILURE'); + expect(text).not.toContain('OS_DATABASE_URL'); + expect(text).not.toContain('--database'); + expect(text.match(new RegExp(MONGODB_DRIVER_INSTALL_COMMAND.replace(/\//g, '\\/'), 'g'))) + .toHaveLength(1); + }); + + it('is what the mongodb arm actually raises when the optional package is absent', async () => { + const raised = await raiseWithPackageAbsent(MONGODB_DRIVER_PACKAGE, { + driver: 'mongodb', + name: 'events', + config: { host: 'mongo.internal', database: 'events' }, + }); + expect(raised).toBeInstanceOf(Error); + expect((raised as Error).message).toContain(MONGODB_DRIVER_INSTALL_COMMAND); + expect((raised as Error).message).toContain(MONGODB_DRIVER_PACKAGE); + expect((raised as Error).message).toContain("datasource 'events'"); + expect((raised as Error).message).toContain('refuses rather than falling back'); + }); +}); + +// The point of the card, stated as one assertion: the three optional arms now +// give ONE quality of answer. Skeleton parity rather than byte equality — +// `missingTursoDriverMessage` is deliberately left as its own function (it +// merged hours earlier and #7384's tests pin its wording), and this is what +// makes converging it onto the shared builder a provably inert change later. +describe('createDefaultDatasourceDriverFactory — all three optional-driver arms answer in the same shape (#7385)', () => { + const messages = [ + missingTursoDriverMessage({ datasource: 'd', cause: notInstalled(TURSO_DRIVER_PACKAGE) }), + missingSqliteWasmDriverMessage({ datasource: 'd', cause: notInstalled(SQLITE_WASM_DRIVER_PACKAGE) }), + missingMongodbDriverMessage({ datasource: 'd', cause: notInstalled(MONGODB_DRIVER_PACKAGE) }), + ]; + + it.each([ + ["datasource 'd'", 'names the datasource'], + ['is not installed. Install it next to the server that opens this datasource:', 'states the fault + where to install'], + ['It is an OPTIONAL package,', 'says the package is optional'], + ['This refuses rather than falling back to another engine:', 'says the refusal is deliberate'], + ['Import error: ', 'ends on the verbatim import error'], + ])('every arm carries %j (%s)', (fragment) => { + for (const text of messages) expect(text).toContain(fragment); + }); + + it('every arm is classified as an unbuilt workspace when that is the real cause', () => { + for (const text of messages) expect(isUnbuiltWorkspaceFailure(new Error(text))).toBe(true); + }); +}); diff --git a/packages/services/service-datasource/src/default-datasource-driver-factory.ts b/packages/services/service-datasource/src/default-datasource-driver-factory.ts index 89cf12234b..62b2e1a4c7 100644 --- a/packages/services/service-datasource/src/default-datasource-driver-factory.ts +++ b/packages/services/service-datasource/src/default-datasource-driver-factory.ts @@ -137,6 +137,172 @@ export function missingTursoDriverMessage(args: { datasource?: string; cause: un ); } +/** + * One optional driver package, as much of it as an operator needs to be told + * when it turns out to be absent (#7385). + * + * The two prose fields are per-engine and are NOT decoration. #7384 wrote the + * libSQL wording around a REMOTE database being shadowed by a local one — true + * for libSQL, true for mongo, and false for `sqlite-wasm`, which has no remote + * to shadow. Copying that sentence onto the other arms would have produced a + * remedy that reads well and lies, so each arm states its own consequence and + * its own reason for being an optional install. + */ +interface OptionalDriverPackage { + /** Noun phrase for what was asked for: *"a MongoDB datasource"*. */ + readonly requested: string; + /** @see {@link TURSO_DRIVER_PACKAGE} */ + readonly packageName: string; + /** @see {@link TURSO_DRIVER_INSTALL_COMMAND} */ + readonly installCommand: string; + /** Completes *"It is an OPTIONAL package, …"* — what a default install is spared. */ + readonly optionalBecause: string; + /** + * Completes *"This refuses rather than falling back to another engine: …"* — + * what would actually happen to this engine's data if it did fall back. + */ + readonly consequence: string; +} + +/** + * The shared shape of "the optional driver package for this arm is not here" + * (#7385), generalised from the libSQL wording #7314 landed. + * + * All three of `sqlite-wasm`, `mongodb` and `turso` ride in optional packages, + * and until #7314 all three answered an absent one with the fault and nothing + * else. #7314 fixed the libSQL arm; an admin who added a mongo datasource in + * Setup still got the strictly worse answer, decided by nothing but which + * driver they picked. This builder is what makes "same class of problem, same + * quality of answer" a property of the file rather than of whoever edits an arm + * next. + * + * Every discipline point #7384 landed under is kept, because each was a fix for + * a measured failure rather than a style choice: + * + * - **Name the datasource.** Several may be declared and only one of them is + * this engine; the url-less refusal in the `turso` arm names it too. + * - **Name exactly one fix, and no escape hatch.** No `OS_DATABASE_URL` / + * `--database` (they select the HOST's `default` datasource and can do + * nothing for the one that actually failed) and no + * `OS_ALLOW_DRIVER_CONNECT_FAILURE` (it would only hide a package that does + * not exist). Naming an escape hatch is how it gets used — the + * `connect-failure-remedy.ts` failure (#5794). + * - **Interpolate the import error at the END and in full.** Load-bearing + * beyond context: these arms re-throw a NEW Error and so drop the original + * `code`, leaving `isUnbuiltWorkspaceFailure` (via `isModuleNotFoundError`) + * able to recognise a half-built checkout only from the `Cannot find + * package` / `Cannot find module` TEXT this message carries. Drop it and a + * contributor with an unbuilt worktree is told to install a package they + * already have. + * + * `missingTursoDriverMessage` is deliberately left as its own function rather + * than re-expressed through this builder: it merged hours before this change + * and its wording is pinned by #7384's own tests, so converging it is a + * behaviour-preserving edit with nothing to gain today. The parity test asserts + * all three answers share this skeleton, which is what makes that convergence a + * one-line change whenever the lane wants it. + */ +function missingDriverPackageMessage( + driver: OptionalDriverPackage, + args: { datasource?: string; cause: unknown }, +): string { + const cause = args.cause instanceof Error ? args.cause.message : String(args.cause); + return ( + `datasource '${args.datasource ?? 'default'}': ${driver.requested} was requested, but the ` + + `driver package ${driver.packageName} is not installed. Install it next to the server that ` + + `opens this datasource:\n\n ${driver.installCommand}\n\n` + + `(pnpm add ${driver.packageName} / yarn add ${driver.packageName}.) It is an OPTIONAL ` + + `package, ${driver.optionalBecause}. This refuses rather than falling back to another ` + + `engine: ${driver.consequence}. Import error: ${cause}` + ); +} + +/** + * The optional package that provides the WASM SQLite driver, and the exact + * command an operator runs to install it. + * + * Optional from THIS package's side, which is the side that matters here: + * `@objectstack/service-datasource` declares it in `devDependencies` only, so a + * host that installs this service on its own does not get it. Hosts that go + * through `@objectstack/runtime` or `@objectstack/cli` DO get it as a hard + * dependency — for them the reachable case is a half-built workspace, whose + * `Cannot find module` text `isUnbuiltWorkspaceFailure` re-routes to + * `pnpm install && pnpm build` further down the stack. + * + * @see {@link TURSO_DRIVER_PACKAGE} for why these are constants and not inline. + */ +export const SQLITE_WASM_DRIVER_PACKAGE = '@objectstack/driver-sqlite-wasm'; + +/** @see {@link SQLITE_WASM_DRIVER_PACKAGE} */ +export const SQLITE_WASM_DRIVER_INSTALL_COMMAND = `npm install ${SQLITE_WASM_DRIVER_PACKAGE}`; + +/** + * The optional package that provides the MongoDB driver, and the exact command + * an operator runs to install it. + * + * `@objectstack/service-datasource` declares it in `devDependencies` only and + * `@objectstack/runtime` carries it as an `optionalDependencies` entry, so + * `--omit=optional` and a direct install of this service both reach the missing + * package path. (`@objectstack/cli` depends on it outright.) + * + * @see {@link TURSO_DRIVER_PACKAGE} for why these are constants and not inline. + */ +export const MONGODB_DRIVER_PACKAGE = '@objectstack/driver-mongodb'; + +/** @see {@link MONGODB_DRIVER_PACKAGE} */ +export const MONGODB_DRIVER_INSTALL_COMMAND = `npm install ${MONGODB_DRIVER_PACKAGE}`; + +/** + * What this factory says when the OPTIONAL WASM SQLite package is absent + * (#7385). + * + * The consequence clause is this arm's own. `sqlite-wasm` has no remote + * database for a local one to shadow, so #7384's "your libSQL data stays + * untouched" would be false here; what a fallback would actually cost is either + * the durability the datasource asked for (the memory driver accepts writes and + * drops them at shutdown, #4083) or the whole point of picking WASM in the + * first place (the native `better-sqlite3` addon this id exists to avoid). + */ +export function missingSqliteWasmDriverMessage(args: { datasource?: string; cause: unknown }): string { + return missingDriverPackageMessage( + { + requested: 'a WASM SQLite datasource', + packageName: SQLITE_WASM_DRIVER_PACKAGE, + installCommand: SQLITE_WASM_DRIVER_INSTALL_COMMAND, + optionalBecause: 'so an install that pulls in only this service stays free of the sql.js WASM build', + consequence: + 'stepping down to the in-process memory driver would accept every write and drop it at ' + + 'shutdown, leaving the file this datasource names empty, and stepping down to the native ' + + 'better-sqlite3 build would need exactly the native addon a WASM datasource is chosen to ' + + 'avoid', + }, + args, + ); +} + +/** + * What this factory says when the OPTIONAL MongoDB package is absent (#7385). + * + * This arm's consequence IS the libSQL one in substance — a remote server + * shadowed by something local — because mongo, like libSQL, is a database this + * process connects to rather than one it opens. + */ +export function missingMongodbDriverMessage(args: { datasource?: string; cause: unknown }): string { + return missingDriverPackageMessage( + { + requested: 'a MongoDB datasource', + packageName: MONGODB_DRIVER_PACKAGE, + installCommand: MONGODB_DRIVER_INSTALL_COMMAND, + optionalBecause: 'so a default install stays free of the mongodb Node.js client', + consequence: + 'a silent fallback would open a local database that accepts writes while the MongoDB server ' + + 'this datasource points at stays untouched, and every write would land in the wrong ' + + 'database', + }, + args, + ); +} + /** * Wrap a concrete engine driver in a probe handle. `ping`/`checkHealth` reuse * the driver's own health check; `driver` is the escape hatch the admin service @@ -501,9 +667,11 @@ export function createDefaultDatasourceDriverFactory( try { ({ SqliteWasmDriver } = await import('@objectstack/driver-sqlite-wasm' as any)); } catch (err: any) { - throw new Error( - `sqlite-wasm driver requested but @objectstack/driver-sqlite-wasm is not installed (${err?.message ?? err}).`, - ); + // Until #7385 this said only "sqlite-wasm driver requested but + // @objectstack/driver-sqlite-wasm is not installed (…)" — the fault + // and no next step, while the `turso` arm below has stated the + // command, the consequence and the refusal since #7314. + throw new Error(missingSqliteWasmDriverMessage({ datasource: spec.name, cause: err })); } const conn = buildSqlConnection(spec, 'better-sqlite3') as { filename?: string }; const filename = conn.filename ?? ':memory:'; @@ -532,9 +700,11 @@ export function createDefaultDatasourceDriverFactory( try { ({ MongoDBDriver } = await import('@objectstack/driver-mongodb' as any)); } catch (err: any) { - throw new Error( - `mongodb driver requested but @objectstack/driver-mongodb is not installed (${err?.message ?? err}).`, - ); + // Same generalisation as the `sqlite-wasm` arm above (#7385): this + // said "mongodb driver requested but @objectstack/driver-mongodb is + // not installed (…)" and stopped, so an admin who added a mongo + // datasource in Setup was told less than one who added a libSQL one. + throw new Error(missingMongodbDriverMessage({ datasource: spec.name, cause: err })); } // `options` (the MongoClient passthrough) and the datasource's `pool` // block reach the client since #4410 — the driver has always read diff --git a/packages/services/service-datasource/src/index.ts b/packages/services/service-datasource/src/index.ts index 6f976afde4..dd9aca15d4 100644 --- a/packages/services/service-datasource/src/index.ts +++ b/packages/services/service-datasource/src/index.ts @@ -100,6 +100,21 @@ export { TURSO_DRIVER_INSTALL_COMMAND, missingTursoDriverMessage, } from './default-datasource-driver-factory.js'; +// The other two OPTIONAL driver packages this factory can be asked for, and the +// messages it raises when they are absent (#7385) — same seam as the libSQL pair +// above, because the three arms answer one class of problem and had answered it +// at two different qualities: `turso` stated the install command, the +// consequence and the refusal, while `sqlite-wasm` and `mongodb` stated only the +// fault. Exported for the same reason: a host rendering its own remedy reads one +// declaration instead of re-typing a command. +export { + SQLITE_WASM_DRIVER_PACKAGE, + SQLITE_WASM_DRIVER_INSTALL_COMMAND, + missingSqliteWasmDriverMessage, + MONGODB_DRIVER_PACKAGE, + MONGODB_DRIVER_INSTALL_COMMAND, + missingMongodbDriverMessage, +} from './default-datasource-driver-factory.js'; // The "adopt a host-built driver instance" seam (ADR-0062 D1, #3826) — for // driver kinds outside open-core (cloud turso) and pooled instances whose // lifecycle outlives one kernel; keeps the connect + failure verdict on the