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
36 changes: 36 additions & 0 deletions .changeset/optional-driver-peers-declared.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
"@objectstack/service-datasource": patch
"@objectstack/runtime": patch
---

fix(datasource,runtime): declare the guarded optional-driver loads as optional peers, so a consumer is told at install time (#12943)

Five guarded `await import(...)` loads of workspace driver packages sat in
published `src/**` with no manifest declaration a consumer could see:
`@objectstack/service-datasource` reaches `driver-turso`, `driver-sqlite-wasm`
(twice) and `driver-mongodb`, and `@objectstack/runtime` reaches `driver-turso`.
`driver-mongodb` and `driver-sqlite-wasm` were `devDependencies` of
`service-datasource`, which tells an installing consumer nothing at all;
`driver-turso` was in no section of either manifest.

Each is now an optional `peerDependencies` entry with
`peerDependenciesMeta: { optional: true }` — the form `@objectstack/cli` already
uses for `driver-turso`. **Nothing is installed and no code path changes**: an
optional peer declares a relationship that already existed at runtime, so
`npm ls`, a lockfile, an audit tool and a reader of the manifest can all see the
driver a datasource may ask for, instead of learning about it only by hitting
the failure arm. The runtime errors were already good — each carries its install
command as data — but they arrive at the moment of failure rather than at
install time.

The `rest` to `objectql` occurrence of the same shape is deliberately left
alone: `@objectstack/rest`'s non-coupling to the data engine is a stated
architectural position, not a hygiene gap.

Three test pins had reached their missing-package arm with no stub, because the
undeclared package genuinely did not resolve from the importing package. pnpm
links an optional workspace peer, so that is no longer true, and each pin's own
comment had said in advance what to do about it. All three now stage the absence
(`vi.doMock` with the resolver's own `ERR_MODULE_NOT_FOUND`) and keep every
assertion, including the typed-identity ones that make `serve.ts`'s
`e instanceof MissingDriverPackageError` boot-fatality branch meaningful.
23 changes: 14 additions & 9 deletions packages/cli/src/utils/storage-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,17 +563,22 @@ describe('#6268 — one loader, one class identity across cli and runtime', () =

// The one thing the convergence deliberately did NOT move: the dynamic
// import's specifier, whose RESOLUTION ROOT is the module that evaluates it.
// `@objectstack/driver-turso` is an optional PEER of `@objectstack/cli` and is
// not declared by `@objectstack/runtime` at all, so under pnpm's strict layout
// it is linked into the CLI's node_modules and not the runtime's. Had the CLI
// taken the runtime's default thunk, an operator who ran the exact install
// command this error prints would still be told the package was missing.
// `@objectstack/driver-turso` is an optional PEER of `@objectstack/cli` and,
// since #12943, of `@objectstack/runtime` too — an optional peer names the
// relationship and installs nothing, so the package still sits in whichever
// tree the operator installed it into. Had the CLI taken the runtime's default
// thunk, an operator who ran the exact install command this error prints would
// still be told the package was missing.
//
// This case pins the CLI half — the default thunk finds the package that is
// installed next to the CLI. The runtime half is pinned from the other side by
// This case pins the CLI half — the default thunk finds the package installed
// next to the CLI, through the CLI's own `devDependencies` entry. ⚠️ The
// runtime half USED to be pinned from the other side by
// `standalone-stack.libsql.test.ts`, where a `libsql://` boot with no injected
// thunk takes the missing-package arm precisely because the runtime does not
// declare it. Together they assert that the two roots are still distinct.
// thunk took the missing-package arm because nothing linked the package under
// the runtime. #12943's optional peer makes pnpm link it there too, so that
// case now STAGES the absence rather than relying on the layout to supply it.
// The pair still asserts that the two roots are distinct; what neither can
// assert any more is that one of them is empty.
it('resolves the optional package from the CLI’s own node_modules by default', async () => {
const factory = await loadTursoDriverFactory();
expect(factory.supports('turso')).toBe(true);
Expand Down
8 changes: 8 additions & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@
"zod": "^4.4.3",
"@objectstack/metadata-core": "workspace:*"
},
"peerDependencies": {
"@objectstack/driver-turso": "workspace:^"
},
"peerDependenciesMeta": {
"@objectstack/driver-turso": {
"optional": true
}
},
"optionalDependencies": {
"@objectstack/driver-mongodb": "workspace:*"
},
Expand Down
60 changes: 47 additions & 13 deletions packages/runtime/src/standalone-stack.libsql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
// 3. the whole boot — `createStandaloneStack({ databaseUrl: 'libsql://…' })`
// no longer produces the "unsupported scheme" refusal.
//
// No test here touches a real Turso endpoint: the package is substituted through
// `importDriverPackage`, which is what makes the "package missing" arm testable
// even in a workspace where the package happens to be installed.

import { describe, it, expect, afterEach } from 'vitest';
// No test here touches a real Turso endpoint. Every loader-level case
// substitutes the package through `importDriverPackage`; the whole-boot case in
// ③ has no such seam and stages absence with `vi.doMock` instead (#12943).
// Both make the "package missing" arm testable in a workspace where the package
// IS installed — which, since `@objectstack/driver-turso` became a declared
// optional peer of this package, is now every workspace.

import { describe, it, expect, afterEach, vi } from 'vitest';
import {
resolveStandaloneDatabase,
resolveDatabaseAuthToken,
Expand Down Expand Up @@ -287,18 +290,49 @@ describe('loadTursoDriverFactory — the OPTIONAL driver package, both ways (#58
});
});

// ③ The whole boot, on the URL the issue is about. `@objectstack/driver-turso`
// is deliberately NOT a dependency of `@objectstack/runtime` — that is what
// "optional" means here — so in this workspace the boot takes the missing-package
// arm. What matters either way is the FIRST assertion: the refusal is no longer
// "unsupported scheme". (Should the package ever become a dependency of this one,
// this case turns red and names exactly why in this comment.)
// ③ The whole boot, on the URL the issue is about.
//
// ⭐ This case's old comment predicted its own future and was right: "Should the
// package ever become a dependency of this one, this case turns red and names
// exactly why in this comment." #12943 declared `@objectstack/driver-turso` an
// OPTIONAL PEER of `@objectstack/runtime` — install-time honesty for a
// relationship the source already had, installing nothing for a consumer — and
// pnpm LINKS an optional workspace peer. Measured on that change: the boot
// stopped taking the missing-package arm and SUCCEEDED, building a real driver
// against `libsql://my-db.turso.io`. Red, and in the worse of the two
// directions: a green-looking boot pointed at a remote endpoint.
//
// So absence is STAGED now. `createStandaloneStack` has no `importDriverPackage`
// seam of its own — it calls `loadTursoDriverFactory()` bare, which is exactly
// the standalone default this case exists to cover — so the specifier is mocked
// with a factory that throws the resolver's own error. ⛔ No `vi.resetModules()`:
// the assertion below is an `instanceof` against the binding this file imported,
// and a reset would hand the arm a different class object and make that false
// for a correct error.
//
// What matters either way is still the FIRST assertion: the refusal is no longer
// "unsupported scheme".
describe('createStandaloneStack — a libsql:// boot is dispatched, not refused as unknown (#5820)', () => {
it('fails with the install command instead of "Unsupported database URL scheme"', async () => {
clearUrlEnv();
vi.doMock('@objectstack/driver-turso', () => {
throw Object.assign(
new Error("Cannot find module '@objectstack/driver-turso' imported from /app/node_modules/x.mjs"),
{ code: 'ERR_MODULE_NOT_FOUND' },
);
});
const err = await createStandaloneStack({ databaseUrl: 'libsql://my-db.turso.io' })
.then(() => null, (e: unknown) => e);

.then(() => null, (e: unknown) => e)
.finally(() => { vi.doUnmock('@objectstack/driver-turso'); });

if (err === null) {
throw new Error(
'staging @objectstack/driver-turso as absent no longer makes the standalone libsql boot '
+ 'refuse, so this case has stopped exercising the missing-package arm — it booted a real '
+ 'driver against a remote endpoint instead. ⛔ Do not delete it and do not weaken it: '
+ 'find out why the stub stops short of the arm.',
);
}
expect(err).not.toBeNull();
expect(String((err as Error).message)).not.toMatch(/Unsupported database URL scheme/);
expect(err).toBeInstanceOf(MissingDriverPackageError);
Expand Down
38 changes: 31 additions & 7 deletions packages/runtime/src/turso-driver-factory.convergence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
// No test here touches a real libSQL endpoint: the optional package is
// substituted through `importDriverPackage`.

import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import {
buildTursoDriverConfig,
createDefaultDatasourceDriverFactory,
Expand Down Expand Up @@ -167,22 +167,46 @@ describe('#7314 point 2 — one MissingDriverPackageError class across the seam'
// Until #7314 that arm raised a plain `Error` and this assertion could not
// have been written.
it('the open-core arm raises an error the runtime binding matches', async () => {
// `@objectstack/driver-turso` is deliberately not a dependency of
// `@objectstack/service-datasource` — that is what "optional" means — so its
// missing-package arm is reachable here without a stub.
// ⭐ STAGED absence since #12943. `@objectstack/driver-turso` is now an
// OPTIONAL PEER of `@objectstack/service-datasource` and of this package —
// the honest install-time declaration of a relationship the source already
// had. It installs nothing for a consumer, but pnpm LINKS an optional
// workspace peer, so the package resolves here and the bare form stopped
// entering the missing-package arm. That is precisely the transition this
// pin's old notice named, and this is it carried out.
//
// ⛔ Mocked WITHOUT `vi.resetModules()`, and that is load-bearing rather
// than a shortcut: this pin is about CLASS IDENTITY ACROSS THE SEAM, and a
// reset re-evaluates `missing-driver-package-error.js` inside
// `@objectstack/service-datasource`. The arm would then raise a fresh class
// object, `instanceof` against the runtime binding would be FALSE for a
// perfectly correct error, and the reset would have destroyed the very fact
// under test. No reset is needed here: nothing in this file imports the
// driver package before this point, so the factory's lazy
// `await import(...)` is the first one and the mock is what it finds.
vi.doMock('@objectstack/driver-turso', () => {
throw Object.assign(
new Error("Cannot find package '@objectstack/driver-turso' imported from /app/node_modules/x.mjs"),
{ code: 'ERR_MODULE_NOT_FOUND' },
);
});
let err: unknown = null;
try {
await createDefaultDatasourceDriverFactory()
.create({ name: 'warehouse', driver: 'turso', config: { url: 'libsql://my-db.turso.io' } });
} catch (e) {
err = e;
} finally {
vi.doUnmock('@objectstack/driver-turso');
}

if (err === null) {
throw new Error(
'@objectstack/driver-turso resolved from @objectstack/service-datasource, so this case no '
+ 'longer exercises the missing-package arm. If the package was made a dependency, this '
+ 'assertion is the notice that the pin needs a stubbed import instead.',
'staging @objectstack/driver-turso as absent no longer makes the open-core turso arm '
+ 'raise, so this case has stopped exercising the missing-package arm. ⛔ Do not delete '
+ 'it and do not weaken it: find out why the stub stops short of the arm. An arm no test '
+ 'can enter is a decoration, and this one is the whole reason serve.ts can decide boot '
+ 'fatality on a failure the OPEN-CORE factory raised.',
);
}
expect(err).toBeInstanceOf(MissingDriverPackageError);
Expand Down
48 changes: 29 additions & 19 deletions packages/runtime/src/turso-driver-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,21 @@
* duplicate:
*
* - **{@link LoadTursoDriverFactoryOptions.importDriverPackage} — the module
* resolution root.** `@objectstack/driver-turso` is an optional PEER of
* `@objectstack/cli` and is not declared by `@objectstack/runtime` at all.
* A bare `import('@objectstack/driver-turso')` resolves from the node_modules
* tree of the module that *evaluates* it, so moving the CLI's import into
* this file would look for the package under `@objectstack/runtime` — which
* under pnpm's strict layout does not link it. An operator who ran the
* install command the error tells them to run would still be told the package
* is missing. The CLI therefore keeps passing its own thunk; the default
* below is this package's own root, for the standalone stack.
* resolution root.** `@objectstack/driver-turso` is an OPTIONAL PEER of
* `@objectstack/cli` and, since #12943, of `@objectstack/runtime` too. An
* optional peer NAMES the relationship and installs nothing, so the package
* still sits in whichever tree the operator installed it into — and a bare
* `import('@objectstack/driver-turso')` resolves from the node_modules tree
* of the module that *evaluates* it. That is what keeps this a host-supplied
* input rather than a duplicate: an operator who installed the package next
* to the CLI put it in the CLI's tree, so moving the CLI's import into this
* file would look for it under `@objectstack/runtime` and tell them it is
* missing right after they ran the exact command the error printed. The CLI
* therefore keeps passing its own thunk; the default below is this package's
* own root, for the standalone stack.
* ⚠️ Inside THIS workspace pnpm links an optional peer, so the default thunk
* resolves here. Every test covering the missing-package arm therefore stages
* the absence rather than relying on the layout to supply it (#12943).
* - **{@link LoadTursoDriverFactoryOptions.missingUrlError} — the error TYPE
* for a config with no url.** The CLI raises its own `UnsupportedDriverError`
* (a CLI-only semantic: `serve.ts` re-throws it as a fatal boot error), which
Expand Down Expand Up @@ -149,15 +155,17 @@ export interface LoadTursoDriverFactoryOptions {
*
* NOT merely a test seam: the specifier resolves from the node_modules tree of
* whichever module evaluates the `import()`, and the package is an optional
* peer of `@objectstack/cli` while `@objectstack/runtime` does not declare it.
* The CLI passes its own thunk so its operators keep resolving the package
* they installed next to the CLI (see the module docstring). The default is
* this package's own root, which is what the standalone stack wants.
* peer of BOTH `@objectstack/cli` and `@objectstack/runtime` (#12943) — a
* declaration that installs nothing, so which tree actually holds the package
* is still decided by where the operator installed it. The CLI passes its own
* thunk so its operators keep resolving the package they installed next to the
* CLI (see the module docstring). The default is this package's own root,
* which is what the standalone stack wants.
*
* Tests pass a stub module (dispatch WITH the package) or a rejecting thunk
* (dispatch WITHOUT it) — neither needs a real Turso endpoint, and the
* missing-package path must stay testable in a workspace where the package
* happens to be installed.
* missing-package path must stay testable in a workspace where the package IS
* installed, which since the optional peer landed is every workspace.
*/
importDriverPackage?: () => Promise<unknown>;
/**
Expand Down Expand Up @@ -193,10 +201,12 @@ export interface LoadTursoDriverFactoryOptions {
export async function loadTursoDriverFactory(
opts: LoadTursoDriverFactoryOptions = {},
): Promise<IDatasourceDriverFactory> {
// `as any` on the specifier: the package is deliberately NOT a dependency of
// `@objectstack/runtime` (that is what "optional" means here), so the literal
// must not be type-resolved. Same shape the shared factory uses for the other
// optional drivers (`default-datasource-driver-factory.ts`).
// `as any` on the specifier: the package is an OPTIONAL PEER of
// `@objectstack/runtime`, never a dependency (that is what "optional" means
// here — #12943 declared the relationship, and an optional peer installs
// nothing), so the literal must not be type-resolved: a consumer who did not
// install it must still compile. Same shape the shared factory uses for the
// other optional drivers (`default-datasource-driver-factory.ts`).
const load = opts.importDriverPackage ?? (() => import('@objectstack/driver-turso' as any));
const missingUrlError =
opts.missingUrlError ?? ((message: string) => new Error(`[StandaloneStack] ${message}`));
Expand Down
16 changes: 16 additions & 0 deletions packages/services/service-datasource/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@
"@objectstack/types": "workspace:*",
"pg-connection-string": "^2.14.0"
},
"peerDependencies": {
"@objectstack/driver-mongodb": "workspace:^",
"@objectstack/driver-sqlite-wasm": "workspace:^",
"@objectstack/driver-turso": "workspace:^"
},
"peerDependenciesMeta": {
"@objectstack/driver-mongodb": {
"optional": true
},
"@objectstack/driver-sqlite-wasm": {
"optional": true
},
"@objectstack/driver-turso": {
"optional": true
}
},
"devDependencies": {
"@objectstack/driver-mongodb": "workspace:*",
"@objectstack/driver-sqlite-wasm": "workspace:*",
Expand Down
Loading
Loading