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

fix(cli): a production `os serve` of a host config that resolves no storage says so before any plugin starts, and refuses to boot when its auth service needs a driver

Clause-②: no

A host config (one whose `plugins` hold plugin instances) served in production with no recognized `OS_DATABASE_URL`, no `--dev` and no plugin named like a driver declares no default datasource. Nothing said so.

- **Before.** The boot went on and died when `AuthPlugin` started: `✗ Plugin com.objectstack.auth failed to start - rollback complete: [ObjectQL] No driver available for object 'sys_organization'`. Before that line, objectql logged `ERROR sys_metadata could NOT be read at boot`, whose fix read `check the datasource behind sys_metadata — connection, credentials, and that the table exists`, for a datasource the operator never declared. No line named `OS_DATABASE_URL` or `--dev`. `examples/app-showcase` under a bare `os serve` took this path.
- **After, when the auth service runs under the `single` tenancy posture (the default) and no plugin registered a driver by the end of plugin init.** The boot is refused before any plugin starts, exit 1, with `✖ This production serve has no storage: …` and three fixes: set `OS_DATABASE_URL` (for example `file:./data/app.db`, or a `postgres://` or `mysql://` URL), mount a storage driver in the stack's `plugins` (for example `new DriverPlugin(driver)` from `@objectstack/runtime`), or run with `--dev`. Neither of the two lines above prints. Every configuration refused here failed to boot before this change.
- **After, when no driver is registered and the auth service does not need one at start** (no auth composed, as under `--no-server --preset minimal`, or a walled tenancy posture): one `⚠ No storage: …` line names the same fixes, and the boot goes on as before. A plugin that registers a driver in its `start()`, such as a module's `onEnable`, still works.
- **When a plugin registers a driver during init**, under any name, nothing is printed and nothing changes.

`os start`, a standalone (non-host) config, `--dev` and any recognized `OS_DATABASE_URL` take other paths and are unchanged.
71 changes: 71 additions & 0 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3089,6 +3089,13 @@ export default class Serve extends Command {
// discards as already-registered.
// [#15006] Same seam, same reason — see the ObjectQL gate above. The
// driver-provider duck-typing moved into it with the read it guards.
//
// [#23573] Whether this step declared NOTHING (production, no URL, no
// driver plugin by name). Only recorded here: this step cannot tell a
// boot that fails anyway from one whose own plugin registers a driver
// the name test misses, so the verdict waits for the no-storage boot
// guard at the end of Phase 1 (registered last, before runtime.start()).
let productionResolvedNoStorage = false;
if (shouldAutoRegisterStorageDriver(config, plugins)) {
const databaseUrl = process.env.OS_DATABASE_URL;
const driverType = resolveDriverType(process.env.OS_DATABASE_DRIVER, databaseUrl);
Expand Down Expand Up @@ -3136,6 +3143,8 @@ export default class Serve extends Command {
// every other kind AND the dev-default `:memory:` store). The ONE
// provision, shared with the standalone boot below.
await provisionTelemetry(resolution.sqliteFilePath);
} else {
productionResolvedNoStorage = true;
}
} catch (e: any) {
// "declared ≠ enforced" guard (#3276-class): a selection the CLI
Expand Down Expand Up @@ -5108,6 +5117,68 @@ export default class Serve extends Command {
// published banner surface on every boot to report a thing an operator
// cannot act on.

// ── No-storage boot guard (#23573) ─────────────────────────────
// Step 2 recorded that this production boot declared no storage. Whether
// that is fatal is only known once every plugin's init() has run, since a
// stack plugin the name test missed may register a driver there. This is
// the LAST plugin registered, so its init() runs after every other init()
// and before any start() (resolvePluginOrder keeps insertion order; no
// plugin names this one as a dependency). It reads the one map ObjectQL
// adopts `driver.*` services from at its start(), plus any driver the
// engine already holds.
// • a driver is there ⇒ nothing to say.
// • none, and the auth service runs under the 'single' posture ⇒ refuse
// before any start(). AuthPlugin.start() creates the Default
// Organization there, which needs a driver, and no driver can arrive
// before it: AppPlugin orders ObjectQL and auth ahead of every stack
// plugin, so a driver registered in any start() comes too late.
// Without this, the boot died there with a driver error, after an
// objectql ERROR naming a datasource nobody declared.
// • none otherwise ⇒ ONE line, and the boot goes on.
// Worded as of the end of init, because a start() (a module's
// onEnable) may still register a driver and some such stacks boot.
// ⛔ Not `trackPlugin`ed, for the announcer's reason above.
if (productionResolvedNoStorage) {
// ONE list, rendered as the refusal's bullets and as the line's sentence.
const noStorageRemedies = [
'set OS_DATABASE_URL (e.g. file:./data/app.db, or a postgres:// or mysql:// URL)',
"mount a storage driver in the stack's plugins (e.g. new DriverPlugin(driver) from @objectstack/runtime)",
'run with --dev (an in-memory SQLite store, for development)',
];
await kernel.use({
name: 'com.objectstack.cli.no-storage-boot-guard',
version: '1.0.0',
init: async (ctx: any) => {
const services: Map<string, any> = ctx.getServices();
const engine = services.get('objectql');
const hasDriver = [...services.keys()].some((name) => name.startsWith('driver.'))
|| (typeof engine?.getDefaultDriverName === 'function'
&& engine.getDefaultDriverName() !== undefined);
if (hasDriver) return;
if (services.has('auth') && services.get('tenancy')?.posture === 'single') {
restoreOutput();
console.error(chalk.red(
'\n ✖ This production serve has no storage: no recognized OS_DATABASE_URL declared a\n'
+ ' default datasource, and no plugin registered a storage driver by the end of plugin\n'
+ " init. The auth service runs under the 'single' tenancy posture, and its start()\n"
+ ' creates the Default Organization, which needs a driver.\n\n'
+ ' Fix one of:\n'
+ noStorageRemedies.map((remedy) => ` • ${remedy}\n`).join(''),
));
throw new Error(
'Refusing to boot: this production serve has no storage, and its auth service needs a '
+ 'driver when it starts. Set OS_DATABASE_URL, mount a storage driver plugin, or run with --dev.',
);
}
console.warn(chalk.yellow(
' ⚠ No storage: no recognized OS_DATABASE_URL, and no plugin registered a storage driver by '
+ 'the end of plugin init, so objects have no store unless a plugin registers one when it '
+ `starts. Fix: ${noStorageRemedies[0]}; ${noStorageRemedies[1]}; or ${noStorageRemedies[2]}.`,
));
},
} as any);
}

// Boot the runtime
await runtime.start();

Expand Down
Loading
Loading