Skip to content

Commit 2db3281

Browse files
fix(objectql): the 503 text for a failed datasource no longer blames a startup connect under OS_ALLOW_DRIVER_CONNECT_FAILURE (#28361)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoXFoG5pmoNTrXPd7hYMN4JA)_ Fixes #28135 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:数据源连接失败时的 503(`ERR_DATASOURCE_UNAVAILABLE`)提示不再一律说「启动时连接失败、且以 OS_ALLOW_DRIVER_CONNECT_FAILURE 启动、必须重启」,改为「最近一次连接失败」并指向 Setup → Datasources 或服务器日志;只有该开关确实开着时,才附带一句有条件的「代码声明的数据源需重启」。 - 为什么改:运行中安装的包(install-local)和在 Setup 里新建的数据源连接失败时,用户被告知去重启一台从未带该开关启动的服务器,原因也是错的;连接层对所有来源记录的都是同一个 `failed`,所以文案不再声称它不知道的来源。 - 风险与回滚:只改错误文案与文档,错误码、503 状态、`reason: failed` 均不变;回滚即 revert 本 PR。 - 先看:`packages/objectql/src/driver-connect-errors.ts`(`failed` 分支与 `degradedBootNote`)、`packages/objectql/src/datasource-unavailable.test.ts`(开关开/关两条 pin)。 - 两分钟验证:`pnpm --filter @objectstack/objectql exec vitest run src/datasource-unavailable.test.ts`,预期 8 条全绿。 - 需要你做的:无,席位落地。 ## What changes `DatasourceUnavailableError`'s `failed` arm said every such datasource "failed to connect at startup and the server was started with `OS_ALLOW_DRIVER_CONNECT_FAILURE` … must be restarted". The connection layer cannot back that claim: it records every failed connect as the same `{ kind: 'failed' }`. | # | Origin | Caller → service | Trigger | Flag needed to reach a running server | Engine record | What re-runs the connect | |---|---|---|---|---|---|---| | 1 | Boot, code-declared, bound / mapped / bootCritical / `onMismatch: 'fail'` | `AppPlugin.start` → `connectPackageDatasources(installed: false)` → `connectDeclared`; `DefaultDatasourcePlugin` → `connect` | `declared-auto` | yes (otherwise the boot is refused) | `{ kind: 'failed' }` | restart | | 2 | Boot, code-declared, `autoConnect: true`, nothing bound | same | `declared-auto` | no (degrades with a warning) | `{ kind: 'failed' }` | restart | | 3 | install-local, hot install | `MarketplaceInstallLocal` → `connectPackageDatasources(installed: true)` | `declared-auto` | no (the fatal throw is caught, verdict returned) | `{ kind: 'failed' }` | reinstall, restart | | 4 | install-local, rehydrate at boot | same | `declared-auto` | no | `{ kind: 'failed' }` | restart | | 5 | Setup create / update | `registerPool` → `connect`, `reregisterPool` → `reconnect` | `runtime-admin` | no (never fatal) | `{ kind: 'failed' }` | save in Setup, restart | | – | Policy refusal, any origin | any | any | – | `{ kind: 'blocked', publicDetail? }` | – | `DatasourceConnectionService.recordState` maps availability to the kind and drops trigger, origin and flag; `publicDetail` is set only by a policy denial (measured on `main` 69c39b1). So the `failed` arm now names no origin: "its last connect attempt failed, so no driver serves it", ending "See Setup → Datasources or the server log for the cause." The one fact that can make a restart sentence true is the process flag: without `OS_ALLOW_DRIVER_CONNECT_FAILURE` origin 1 cannot exist, because that boot is refused. While the flag is set, the message adds, conditionally, that a datasource declared in code whose connect failed at startup is re-connected only by a restart. The flag is read through `resolveAllowDriverConnectFailure` from `@objectstack/types`, the same parse the boot paths use; it reads `process` through `globalThis` and answers `false` without one, so the constructor cannot throw. Unchanged: the `blocked` and `remote` arms, the code, the 503, the `reason` class, `publicDetail`, the "See … for the cause" tail, and the rule that the message never carries the underlying cause. Also re-worded, because they quoted the old sentence: `content/docs/api/wire-format.mdx` (the 503 section), `content/docs/data-modeling/external-datasources.mdx` (what a query against an unusable datasource says), and the restated error in two `plugin-security` fixtures (`controlled-by-parent-sharing.test.ts`, `store-fault-fail-closed.test.ts`). Patch changeset `.changeset/28135-datasource-unavailable-failed-cause.md`. ## Tests - `packages/objectql/src/datasource-unavailable.test.ts`: the old degraded-boot pin is split in two. With the flag set (the boot-under-flag origin) the message says the connect failed and, conditionally, that a restart re-runs it. With the flag unset (the install-local and Setup origins, which record the same kind) it names neither the flag, a restart nor "at startup", and ends with the new tail. The "never carries the underlying cause" pin now asserts `Setup → Datasources` instead of `startup logs`. - Ablation, on committed head ea7bffd, through `scripts/ablation-replace.mjs` (anchor `if (!resolveAllowDriverConnectFailure()) return '';`, 1 → 0 hits each leg): - guard replaced by `if (false) return '';` (note always said): the flag-unset pin goes red (1 failed); - guard replaced by `return '';` (note never said): the flag-set pin goes red (1 failed); - restored: `git diff HEAD` empty, blob 82ec394 equals `HEAD:packages/objectql/src/driver-connect-errors.ts`. ## Acceptance notes - **Follow-up for the seat to decide (not filed):** the engine cannot tell the five origins apart. Naming the recorded origin needs `IDataEngine.markDatasourceUnavailable`'s info shape widened in `packages/spec` (its literal union is pinned by `packages/spec/src/contracts/data-engine.test.ts`), and `DatasourceUnavailableKind` is served as the 503's wire `reason`, so a new kind member is not an option either. Two docblocks state the old narrow meaning of `failed` and belong to that follow-up: `IDataEngine.markDatasourceUnavailable` in `packages/spec/src/contracts/data-engine.ts` ("the connect failed while the operator opted into a degraded boot") and `ObjectQL.markDatasourceUnavailable` in `packages/objectql/src/engine.ts`. Two code comments also describe `failed` that way and were left alone as outside this card's file surface: `packages/objectql/src/engine-datasource-routing.ts` (above the unavailable lookup) and `packages/rest/src/rest.test.ts` (above the 503 mapping pin). - The degraded-boot banner printed by `DatasourceConnectionService.handleFailure` says "DEGRADED BOOT" also for a package install into a running server while the flag is set (origin 3 under the flag). Not touched here; noted only. - `packages/objectql/src/datasource-mapping.test.ts` keeps its comment calling its `failed` mark "a failed boot connect under OS_ALLOW_DRIVER_CONNECT_FAILURE"; that is still one true origin of the record. ## Verification All readings on head `ea7bffd6` (the PR head), objectstack `main` merge base 69c39b1. <details><summary>Affected-package tests and typecheck</summary> - `pnpm --filter @objectstack/objectql test` (under `os-verify-lock`): `Test Files 502 passed (502)`, `Tests 9767 passed (9767)`, VERDICT command-exit 0. - `pnpm --filter @objectstack/plugin-security test`: `Test Files 237 passed (237)`, `Tests 5119 passed | 45 skipped (5164)`, VERDICT command-exit 0. - `pnpm --filter @objectstack/objectql typecheck`: exit 0 (`check:test-typecheck: OK`). </details> <details><summary>Derived gates: 94 derived, 90 run green, 4 NOT MEASURED</summary> `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran F`: "94 derived famil(ies) accounted for — 90 run, 4 NOT-MEASURED". The four refused their own prerequisite (exit 3) and are left to CI's `Lint & Repo Gates`: - `check-plugin-teardown-shape.mjs --self-test`: needs unshallowed history (51-commit clone). - `spec check:skill-examples`: needs the client SDK's built `.d.ts`. - `check:dual-build-cjs-loads`: needs a full `pnpm build`. - `check:i18n`: needs the built CLI and its extract closure. ``` node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-adr-links.mjs :: exit 0 node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 node scripts/check-docs-section-name.mjs :: exit 0 node scripts/check-docs-section-name.mjs --self-test :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-engine-split-ratio.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 3 node scripts/check-section-landing-index.mjs :: exit 0 node scripts/check-section-landing-index.mjs --self-test :: exit 0 node scripts/check-tenant-audit-census.mjs :: exit 0 node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0 pnpm --filter @objectstack/spec run check:docs :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm --filter @objectstack/spec run check:empty-state :: exit 0 pnpm --filter @objectstack/spec run check:liveness :: exit 0 pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:corpus-claim-drift :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:doc-anchors :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:doc-frontmatter :: exit 0 pnpm check:docs-audit-scope :: exit 0 pnpm check:docs-redirects :: exit 0 pnpm check:docs-single-h1 :: exit 0 pnpm check:docs-spec-enumerations :: exit 0 pnpm check:docs-transcript-drift :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 3 pnpm check:durability-log-level :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:i18n :: exit 3 pnpm check:i18n-stale-fill :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:published-readme-links :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:react-page-adapter-contract :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:role-word :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:vendor-version-stamps :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibwanr5nGX9vQKHm1Cu)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L9qibwanr5nGX9vQKHm1Cu Co-authored-by: Claude <noreply@anthropic.com>
1 parent 71de060 commit 2db3281

7 files changed

Lines changed: 106 additions & 24 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql): the 503 `ERR_DATASOURCE_UNAVAILABLE` text for a failed datasource no longer blames a startup connect under `OS_ALLOW_DRIVER_CONNECT_FAILURE`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A datasource whose connect failed answers a read of its bound objects with `503 ERR_DATASOURCE_UNAVAILABLE`. Its message used to say the datasource "failed to connect at startup and the server was started with `OS_ALLOW_DRIVER_CONNECT_FAILURE`" and that the server "must be restarted". That was false for a datasource declared by a package installed into a running server (`POST /api/v1/marketplace/install-local`) and for one created in Setup: no flag was involved, and saving it again or reinstalling the package re-runs the connect.
11+
12+
The connection layer records every failed connect the same way, so the message now names no origin: "its last connect attempt failed, so no driver serves it", ending "See Setup → Datasources or the server log for the cause." Only while `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set does it add that a datasource declared in code whose connect failed at startup is re-connected only by a restart. The code, the 503 status and the `reason: failed` class are unchanged, and the message still never carries the underlying cause.

‎content/docs/api/wire-format.mdx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,9 +400,10 @@ no stored version can ever equal "nothing".
400400
### Datasource Unavailable — `503 Service Unavailable`
401401

402402
Returned when the object's declared `datasource` has no live driver: the host's
403-
connect policy refused it, or it failed to connect at startup and the server was
404-
started with `OS_ALLOW_DRIVER_CONNECT_FAILURE`. Nothing about the request is
405-
wrong, and the state may clear — so this is a `503`, not a `400` or a `500`.
403+
connect policy refused it, or its last connect attempt failed — at startup under
404+
`OS_ALLOW_DRIVER_CONNECT_FAILURE`, when a package declaring it was installed, or
405+
when it was created in Setup. Nothing about the request is wrong, and the state
406+
may clear — so this is a `503`, not a `400` or a `500`.
406407

407408
`reason` is the class (`blocked` | `failed`). The underlying cause is
408409
deliberately **not** included — it routinely contains hosts, ports and DSNs; it

‎content/docs/data-modeling/external-datasources.mdx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,11 @@ pull an otherwise-working replica out of the load balancer.
227227
An object bound to a datasource with no live driver fails with
228228
`ERR_DATASOURCE_UNAVAILABLE` (HTTP **503**), and the message says which situation
229229
it is ([#3828](https://github.com/objectstack-ai/objectstack/issues/3828)):
230-
refused by policy, or a connect that failed under
231-
`OS_ALLOW_DRIVER_CONNECT_FAILURE`. A datasource name that was never declared
230+
refused by policy, or a connect that failed. A failed connect is recorded the
231+
same way whether it failed at startup under `OS_ALLOW_DRIVER_CONNECT_FAILURE`,
232+
at a package install or in Setup, so the message names no origin; it says a
233+
restart re-runs the connect only while that flag is set, and then only for a
234+
datasource declared in code. A datasource name that was never declared
232235
still gets the original `is not registered` — that one really is an authoring
233236
bug, and there is nothing to add.
234237

‎packages/objectql/src/datasource-unavailable.test.ts‎

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// typo that isn't there. The connection layer knows which one it is; the engine
88
// just had no way to be told.
99

10-
import { describe, it, expect } from 'vitest';
10+
import { describe, it, expect, afterEach, vi } from 'vitest';
1111
import { ObjectQL } from './engine.js';
1212
import { DatasourceUnavailableError } from './driver-connect-errors.js';
1313

@@ -67,16 +67,48 @@ describe('ObjectQL.getDriver — a declared datasource with no driver (framework
6767
expect((await queryError(engine)).message).toContain('require the Scale plan');
6868
});
6969

70-
it('says the connect failed and a restart is needed, for the degraded-boot case', async () => {
71-
const engine = engineWithBoundObject();
72-
engine.markDatasourceUnavailable({ name: 'analytics', kind: 'failed' });
70+
// A `failed` record is the same `{ kind: 'failed' }` for a boot connect the
71+
// flag waved through, a package installed into the running server and a
72+
// Setup-created datasource: the connection layer records no origin. The
73+
// flag is the one fact that separates them — without it the first cannot
74+
// exist, because that boot is refused.
75+
describe('a failed connect', () => {
76+
afterEach(() => {
77+
vi.unstubAllEnvs();
78+
});
7379

74-
const err = await queryError(engine);
75-
expect(err.kind).toBe('failed');
76-
expect(err.message).toContain('OS_ALLOW_DRIVER_CONNECT_FAILURE');
77-
// The durable fact: the client may reconnect, but nothing re-runs the
78-
// connect, so waiting it out does not help (#3759).
79-
expect(err.message).toContain('restarted');
80+
it('says the connect failed, and a restart only conditionally, while OS_ALLOW_DRIVER_CONNECT_FAILURE is set', async () => {
81+
// The boot-under-flag origin: the client may reconnect, but nothing
82+
// re-runs the connect, so waiting it out does not help (#3759).
83+
vi.stubEnv('OS_ALLOW_DRIVER_CONNECT_FAILURE', '1');
84+
const engine = engineWithBoundObject();
85+
engine.markDatasourceUnavailable({ name: 'analytics', kind: 'failed' });
86+
87+
const err = await queryError(engine);
88+
expect(err.kind).toBe('failed');
89+
expect(err.message).toContain('its last connect attempt failed');
90+
expect(err.message).toContain('OS_ALLOW_DRIVER_CONNECT_FAILURE is set on this server');
91+
expect(err.message).toContain('if this datasource is declared in code');
92+
expect(err.message).toContain('until the server is restarted');
93+
});
94+
95+
it('names neither the flag nor a restart without it — the install-local and Setup origins', async () => {
96+
// A datasource whose connect failed when its package was installed into
97+
// the running server, or when it was created in Setup: no flag was
98+
// involved, and a restart is not what re-runs its connect.
99+
vi.stubEnv('OS_ALLOW_DRIVER_CONNECT_FAILURE', '');
100+
const engine = engineWithBoundObject();
101+
engine.markDatasourceUnavailable({ name: 'analytics', kind: 'failed' });
102+
103+
const err = await queryError(engine);
104+
expect(err.kind).toBe('failed');
105+
expect(err.code).toBe('ERR_DATASOURCE_UNAVAILABLE');
106+
expect(err.message).toContain('its last connect attempt failed, so no driver serves it.');
107+
expect(err.message).not.toContain('OS_ALLOW_DRIVER_CONNECT_FAILURE');
108+
expect(err.message).not.toMatch(/restart/i);
109+
expect(err.message).not.toContain('at startup');
110+
expect(err.message).toContain('See Setup → Datasources or the server log for the cause.');
111+
});
80112
});
81113

82114
it('never carries the underlying cause — that is what leaks hosts and DSNs', async () => {
@@ -86,7 +118,7 @@ describe('ObjectQL.getDriver — a declared datasource with no driver (framework
86118
engine.markDatasourceUnavailable({ name: 'analytics', kind: 'failed' });
87119
const err = await queryError(engine);
88120
expect(err.message).not.toMatch(/ECONNREFUSED|\d+\.\d+\.\d+\.\d+|postgres:\/\//);
89-
expect(err.message).toContain('startup logs');
121+
expect(err.message).toContain('Setup → Datasources');
90122
});
91123

92124
it('clears the explanation once the datasource connects', async () => {

‎packages/objectql/src/driver-connect-errors.ts‎

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3+
import { resolveAllowDriverConnectFailure } from '@objectstack/types';
4+
35
/** One boot-registered driver whose `connect()` rejected during engine init. */
46
export interface DriverConnectFailure {
57
driverName: string;
@@ -231,8 +233,10 @@ export const DATASOURCE_UNAVAILABLE_CODE = 'ERR_DATASOURCE_UNAVAILABLE' as const
231233
* is not registered.`:
232234
*
233235
* 1. the host's connect policy refused it (plan / egress isolation),
234-
* 2. it failed to connect at boot and `OS_ALLOW_DRIVER_CONNECT_FAILURE` let
235-
* the server start anyway,
236+
* 2. its connect failed — at boot under `OS_ALLOW_DRIVER_CONNECT_FAILURE`, when
237+
* a package declaring it was installed into the running server, or when it
238+
* was created in Setup (the record does not say which; see
239+
* `degradedBootNote`),
236240
* 3. the app misspelled the datasource name, and
237241
* 4. it is declared `active: false`.
238242
*
@@ -266,16 +270,46 @@ export class DatasourceUnavailableError extends Error {
266270
`error. It may clear; retry later.`
267271
: kind === 'blocked'
268272
? `: the host's datasource connect policy refused it.`
269-
: `: it failed to connect at startup and the server was started with ` +
270-
`OS_ALLOW_DRIVER_CONNECT_FAILURE. Nothing re-runs the connect — the server must be ` +
271-
`restarted once the datasource is reachable.`;
273+
: `: its last connect attempt failed, so no driver serves it.` + degradedBootNote();
272274
const detail = publicDetail ? ` ${publicDetail}` : '';
273-
const where = kind === 'remote' ? 'the server log' : 'the startup logs or Setup → Datasources';
275+
const where =
276+
kind === 'remote'
277+
? 'the server log'
278+
: kind === 'blocked'
279+
? 'the startup logs or Setup → Datasources'
280+
: 'Setup → Datasources or the server log';
274281
super(`${head}${why}${detail} See ${where} for the cause.`);
275282
this.name = 'DatasourceUnavailableError';
276283
}
277284
}
278285

286+
/**
287+
* The `failed` arm's restart sentence, or nothing.
288+
*
289+
* A `failed` record is the same `{ kind: 'failed' }` whatever produced it: a
290+
* code datasource whose boot connect failed under
291+
* `OS_ALLOW_DRIVER_CONNECT_FAILURE`, one declared by a package installed into
292+
* the running server, and one created in Setup. A restart re-runs each of
293+
* them, but only the first has no other door — saving re-runs a Setup
294+
* datasource's connect, reinstalling re-runs a package's — and only the first
295+
* can exist only while the flag is set, because without it that boot is
296+
* refused. So the flag is the one fact that can make the sentence true, and it
297+
* is said conditionally: with the flag set, the record may still come from
298+
* either of the other two.
299+
*
300+
* The flag is read through `resolveAllowDriverConnectFailure`, the one parse
301+
* the boot paths use, which answers `false` where there is no `process`, so
302+
* this never throws inside the constructor.
303+
*/
304+
function degradedBootNote(): string {
305+
if (!resolveAllowDriverConnectFailure()) return '';
306+
return (
307+
` OS_ALLOW_DRIVER_CONNECT_FAILURE is set on this server: if this datasource is declared in code ` +
308+
`and its connect failed at startup, the flag let the server start without it, and nothing re-runs ` +
309+
`that connect until the server is restarted.`
310+
);
311+
}
312+
279313
/**
280314
* The degraded-boot banner now lives in `@objectstack/types` because
281315
* `DatasourceConnectionService` owes the operator the same banner for the same

‎packages/plugins/plugin-security/src/controlled-by-parent-sharing.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ function makeStore(rows: Record<string, Row[]>, detail: DetailVariant = 'valid',
280280
function datasourceOutage(object: string): Error {
281281
const e = new Error(
282282
`[ObjectQL] Datasource 'primary' configured for object '${object}' is declared but not connected: ` +
283-
`it failed to connect at startup and the server was started with OS_ALLOW_DRIVER_CONNECT_FAILURE.`,
283+
`its last connect attempt failed, so no driver serves it. See Setup → Datasources or the server log for the cause.`,
284284
) as Error & { code: string; datasource: string };
285285
e.name = 'DatasourceUnavailableError';
286286
e.code = 'ERR_DATASOURCE_UNAVAILABLE';

‎packages/plugins/plugin-security/src/store-fault-fail-closed.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ type Row = Record<string, unknown>;
6060
function datasourceOutage(object: string): Error {
6161
const e = new Error(
6262
`[ObjectQL] Datasource 'primary' configured for object '${object}' is declared but not connected: ` +
63-
`it failed to connect at startup and the server was started with OS_ALLOW_DRIVER_CONNECT_FAILURE.`,
63+
`its last connect attempt failed, so no driver serves it. See Setup → Datasources or the server log for the cause.`,
6464
) as Error & { code: string; datasource: string };
6565
e.name = 'DatasourceUnavailableError';
6666
e.code = 'ERR_DATASOURCE_UNAVAILABLE';

0 commit comments

Comments
 (0)