|
| 1 | +--- |
| 2 | +"@objectstack/service-datasource": minor |
| 3 | +"@objectstack/types": patch |
| 4 | +"@objectstack/objectql": patch |
| 5 | +--- |
| 6 | + |
| 7 | +fix(datasource)!: a declared datasource that objects bind to must connect, or the boot fails (#3758) |
| 8 | + |
| 9 | +`DatasourceConnectionService.handleFailure()` fail-fasted only for an `external` |
| 10 | +datasource with `validation.onMismatch: 'fail'`. Everything else degraded to one |
| 11 | +`warn` line — including the case the D2 auto-connect gate itself flags as having |
| 12 | +**no fallback path**: a datasource that objects bind to explicitly via |
| 13 | +`object.datasource`. Those objects never fall through to the `default` driver; |
| 14 | +`engine.getDriver` throws `Datasource 'x' is not registered` for them. |
| 15 | + |
| 16 | +So an app declaring `datasource: 'analytics'` with 20 objects bound to it, booted |
| 17 | +against a wrong `ANALYTICS_URL`, started clean and exited zero — and then failed |
| 18 | +every read and write of those 20 objects with an error that reads nothing like |
| 19 | +*the analytics database is unreachable*. The rest of the app worked, which made it |
| 20 | +**harder** to locate than a total outage: it looks like "some pages are broken", |
| 21 | +not like a misconfigured datasource. This is the same decision #3741/#3751 fixed |
| 22 | +one layer up in `ObjectQLEngine.init()`; the boundary here was still drawn in the |
| 23 | +old place. |
| 24 | + |
| 25 | +- **Fail-fast is now keyed on "no fallback path", not on `onMismatch` alone.** At |
| 26 | + the `declared-auto` (boot) trigger, a connect failure aborts the boot when the |
| 27 | + datasource is `external` + `onMismatch: 'fail'` **or** when ≥1 object binds to |
| 28 | + it explicitly. `autoConnect: true` with nothing bound stays lenient — that is |
| 29 | + "connect it if you can", and nothing declares a dependency on it. The |
| 30 | + runtime-admin create/update and boot-rehydration triggers are unchanged and |
| 31 | + still always degrade: a UI action must never brick a running server. |
| 32 | +- **Every failure mode counts**, not just an unreachable socket: an unresolvable |
| 33 | + `external.credentialsRef` (D3) and an unsupported `driver` leave the bound |
| 34 | + objects exactly as dead, so they take the same verdict. |
| 35 | +- **The error names the bound objects** (up to 10, then `+N more`) alongside the |
| 36 | + underlying cause, so the message points at the real problem instead of just the |
| 37 | + datasource name. The service already receives the list for post-connect |
| 38 | + `syncObjectSchema`. |
| 39 | +- **`connectDeclared()` attempts every gated datasource before throwing**, and |
| 40 | + aggregates, so one failed boot reports all the misconfigured ones rather than |
| 41 | + one per restart — the same shape as `ObjectQLEngine.init()`'s |
| 42 | + `DriverConnectError`. |
| 43 | +- **The escape hatch is shared with the engine guard**: |
| 44 | + `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` now also covers this path (and covers |
| 45 | + `onMismatch: 'fail'`, which previously had no opt-out). The operator intent is |
| 46 | + identical — "I know the database is unreachable, boot anyway" — and two flags |
| 47 | + would only guarantee one of them gets missed. When set, boot continues and a |
| 48 | + `DEGRADED BOOT` banner goes to stderr as well as the logger, because `os serve` |
| 49 | + swallows stdout during boot. `emitDegradedBootBanner` moved to |
| 50 | + `@objectstack/types` so both call sites share one implementation; |
| 51 | + `@objectstack/objectql` re-exports it unchanged. |
| 52 | + |
| 53 | +ADR-0062 D5 is amended with the new criterion and the shared flag. |
| 54 | + |
| 55 | +**Migration.** No change for a correctly configured deployment — a datasource that |
| 56 | +connected before still connects. A deployment that was *silently* booting with a |
| 57 | +dead, explicitly-bound datasource now fails the boot instead, naming the |
| 58 | +datasource, the cause, and the objects that depend on it; fix the datasource |
| 59 | +configuration. To keep booting without it — deliberately, knowing every request |
| 60 | +touching those objects will fail — set `OS_ALLOW_DRIVER_CONNECT_FAILURE=1`. |
0 commit comments