diff --git a/.changeset/30054-objectql-return-types.md b/.changeset/30054-objectql-return-types.md new file mode 100644 index 00000000000..ec3baff2024 --- /dev/null +++ b/.changeset/30054-objectql-return-types.md @@ -0,0 +1,10 @@ +--- +'@objectstack/objectql': patch +--- + +refactor(objectql): the eight `ObjectQL` methods that kept an inferred return type spell it, and five of them move into their facets + +Clause-②: no +Metadata-protocol: none + +- `registerApp`, `registerHook`, `registerDriver`, `init`, `triggerHooks`, `destroy`, `setDatasourceMapping` and `getStatus` now declare their return type, copied from the published declaration file it was inferred into. The bodies of `getStatus`, `triggerHooks`, `setDatasourceMapping`, `init` and `destroy` move out of `engine.ts` into their facet modules (the methods stay on the class as one-call stubs). The built declaration files are byte-equal before and after, and no behaviour changes; only the source maps move. Nothing to do on upgrade. diff --git a/packages/objectql/src/engine-hooks-and-functions.ts b/packages/objectql/src/engine-hooks-and-functions.ts index 8e43f2c8737..26198f35ef9 100644 --- a/packages/objectql/src/engine-hooks-and-functions.ts +++ b/packages/objectql/src/engine-hooks-and-functions.ts @@ -4,12 +4,12 @@ * [#29986 codemod] The * "Class head: units, hooks, functions, body runners, hook dispatch" section of * `ObjectQL`: the bodies of `recordResidualOwner`, `residualOwnerSource`, - * `unregisterHooksByPackage`, `registerFunction`, `resolveFunction`, - * `resolveFunctionEntry`, `unregisterFunctionsByPackage`, + * `getStatus`, `unregisterHooksByPackage`, `registerFunction`, + * `resolveFunction`, `resolveFunctionEntry`, `unregisterFunctionsByPackage`, * `setDefaultBodyRunner`, `getDefaultBodyRunner`, `setDefaultActionRunner`, - * `getDefaultActionRunner`, `hasHooksFor`, `opensAfterAbortUnit`, - * `dispatchUnscopedMultiWriteHooks` moved out of `engine.ts` whole by - * `scripts/codemod-split-engine.mjs`. + * `getDefaultActionRunner`, `triggerHooks`, `hasHooksFor`, + * `opensAfterAbortUnit`, `dispatchUnscopedMultiWriteHooks` moved out of + * `engine.ts` whole by `scripts/codemod-split-engine.mjs`. * * A MOVE, not a redesign. {@link HooksAndFunctionsFacet} is a stateless facet of `ObjectQL` * (ruling 6624345766): each static method of it is the body of the `ObjectQL` @@ -40,6 +40,8 @@ import type { ExecutionContext } from '@objectstack/spec/kernel'; import { hookStoppedRefusal } from './summary-errors.js'; import { HookTargetRebindError } from './hook-target-rebind-errors.js'; import { declaredHookCondition } from './hook-wrappers.js'; +import { CoreServiceName } from '@objectstack/spec/system'; +import { bindHookNotice } from './hook-notice.js'; import { hookMatchesObject } from './engine-top-level.js'; import type { FunctionEntry, FunctionRegistrationOptions, HookHandler } from './engine-top-level.js'; import type { ObjectQL } from './engine.js'; @@ -78,6 +80,21 @@ export class HooksAndFunctionsFacet { return this.residualOwnerSources.get(ownerId); } + /** The body of `ObjectQL#getStatus`; the method's docblock there is its contract. */ + static getStatus(): { + name: "data"; + status: string; + version: string; + features: string[]; + } { + return { + name: CoreServiceName.enum.data, + status: 'running', + version: '0.9.0', + features: ['crud', 'query', 'aggregate', 'transactions', 'metadata'] + }; + } + /** The body of `ObjectQL#unregisterHooksByPackage`; the method's docblock there is its contract. */ static unregisterHooksByPackage(this: Pick, packageId: string): number { if (!packageId) return 0; @@ -162,6 +179,49 @@ export class HooksAndFunctionsFacet { return this._defaultActionRunner; } + /** The body of `ObjectQL#triggerHooks`; the method's docblock there is its contract. */ + static async triggerHooks(this: Pick, event: string, context: HookContext): Promise { + const entries = this.hooks.get(event) || []; + // [#28251] `ctx.notice` — the write's own `onNotice` listener, bound here + // so every insert / update dispatch site carries it (see hook-notice.ts). + bindHookNotice(event, context); + + if (entries.length === 0) { + this.logger.debug('No hooks registered for event', { event }); + return; + } + + this.logger.debug('Triggering hooks', { event, count: entries.length }); + + // `session.skipAutomations` (set from ExecutionContext.skipAutomations — + // import with "run automations & triggers" unchecked, import undo) + // suppresses hooks bound FROM METADATA (`bindHooksToEngine` stamps + // `entry.meta`). Hooks registered in code by plugins — audit, capability + // gates, sharing projection — have no `meta` and always run: the opt-out + // must never bypass security or audit (#2922). + const skipAutomations = + (context.session as { skipAutomations?: boolean } | undefined)?.skipAutomations === true; + + for (const entry of entries) { + // Per-object matching — allow face minus exclusion face, the one shared + // rule `hasHooksFor` also reads (#5928). + if (!hookMatchesObject(entry, context.object)) { + continue; // Skip non-matching hooks + } + if (skipAutomations && entry.meta) { + this.logger.debug('Skipping metadata-bound hook (skipAutomations)', { event, hook: entry.hookName }); + continue; + } + // [#29422] A stale-roll-up refusal a hook's own `ctx.api` write raised + // describes THAT write, not this one: `hookStoppedRefusal`. + try { + await entry.handler(context); + } catch (e) { + throw hookStoppedRefusal(e, event, context.object, entry.hookName); + } + } + } + /** * [#5038] Would `triggerHooks(event, ctx)` reach ANY handler for `object`? * diff --git a/packages/objectql/src/engine-migration-state.ts b/packages/objectql/src/engine-migration-state.ts index 9be9ac49a82..9816ee29802 100644 --- a/packages/objectql/src/engine-migration-state.ts +++ b/packages/objectql/src/engine-migration-state.ts @@ -9,7 +9,7 @@ * `haveFileColumnsMoved`, `readMigrationFlagMemoized`, * `valueShapeViolationsAdmitted`, `admittedViolationSink`, * `recordObservedDeviation`, `retractCreationAttestation`, - * `invalidateDataMigrationFlags` moved out of `engine.ts` whole by + * `invalidateDataMigrationFlags`, `destroy` moved out of `engine.ts` whole by * `scripts/codemod-split-engine.mjs`. * * A MOVE, not a redesign. {@link MigrationStateFacet} is a stateless facet of `ObjectQL` @@ -62,6 +62,7 @@ export interface MigrationStateHost { readonly admittedValueShapeViolations: ObjectQLClass['admittedValueShapeViolations']; creationAttestationRetraction: ObjectQLClass['creationAttestationRetraction']; deviationRecording: ObjectQLClass['deviationRecording']; + readonly drivers: ObjectQLClass['drivers']; fileReferencesMigrationVerified: ObjectQLClass['fileReferencesMigrationVerified']; findOne: ObjectQLClass['findOne']; invalidateDataMigrationFlags: ObjectQLClass['invalidateDataMigrationFlags']; @@ -76,6 +77,7 @@ export interface MigrationStateHost { readMigrationFlagRowMemoized: ObjectQLClass['readMigrationFlagRowMemoized']; readonly recordedDeviations: ObjectQLClass['recordedDeviations']; readonly retractedCreationAttestations: ObjectQLClass['retractedCreationAttestations']; + unregisterDriver: ObjectQLClass['unregisterDriver']; update: ObjectQLClass['update']; valueShapesMigrationVerified: ObjectQLClass['valueShapesMigrationVerified']; wasDatastoreCreatedFromEmpty: ObjectQLClass['wasDatastoreCreatedFromEmpty']; @@ -427,4 +429,28 @@ export class MigrationStateFacet { this.migrationGatesAnnounced = false; this.recordedDeviations.clear(); } + + /** The body of `ObjectQL#destroy`; the method's docblock there is its contract. */ + static async destroy(this: Pick): Promise { + this.logger.info('Destroying ObjectQL engine', { driverCount: this.drivers.size }); + + for (const [name, driver] of this.drivers.entries()) { + try { + await driver.disconnect(); + } catch (e) { + this.logger.error('Error disconnecting driver', e as Error, { driverName: name }); + } + } + + // #13578 — teardown is an eviction path too, and it used to disconnect + // every driver while leaving all of them REGISTERED. A destroyed engine + // therefore still answered `checkDriversHealth()` by pinging pools it had + // just closed, so a readiness probe racing shutdown read a list of drivers + // that were deliberately dead. Disconnect-then-evict, through the one + // primitive, so `defaultDriver` and the datasource defs go with them. + // Names are snapshotted first: `unregisterDriver` mutates the Map. + for (const name of [...this.drivers.keys()]) this.unregisterDriver(name); + + this.logger.info('ObjectQL engine destroyed'); + } } diff --git a/packages/objectql/src/engine-secrets-and-routing.ts b/packages/objectql/src/engine-secrets-and-routing.ts index 95e539662db..d56b9931bff 100644 --- a/packages/objectql/src/engine-secrets-and-routing.ts +++ b/packages/objectql/src/engine-secrets-and-routing.ts @@ -7,8 +7,8 @@ * `onCryptoProviderChange`, `setTenancyPostureProvider`, `getSchema`, * `resolveObjectName`, `judgeFilter`, `isSystemLedgerObject`, * `resolveDatasourceBinding`, `resolveEffectiveDatasource`, - * `resolveMappedDatasource` moved out of `engine.ts` whole by - * `scripts/codemod-split-engine.mjs`. + * `resolveMappedDatasource`, `setDatasourceMapping`, `init` moved out of + * `engine.ts` whole by `scripts/codemod-split-engine.mjs`. * * A MOVE, not a redesign. {@link SecretsAndRoutingFacet} is a stateless facet of `ObjectQL` * (ruling 6624345766): each static method of it is the body of the `ObjectQL` @@ -37,6 +37,9 @@ import type { EngineQueryOptions, ServiceObject } from '@objectstack/spec/data'; import type { EngineFilterJudgement, EngineFilterJudgementOptions, ICryptoProvider } from '@objectstack/spec/contracts'; +import { DriverConnectError, emitDegradedBootBanner } from './driver-connect-errors.js'; +import type { DriverConnectFailure } from './driver-connect-errors.js'; +import { resolveAllowDriverConnectFailure } from '@objectstack/types'; import { StorageNameMapping } from '@objectstack/spec/system'; import { objectNotFoundError } from '@objectstack/core'; import { judgeWhereAdmission } from './engine-top-level.js'; @@ -53,6 +56,7 @@ export interface SecretsAndRoutingHost { readonly _registry: ObjectQL['_registry']; cryptoProvider: ObjectQL['cryptoProvider']; readonly cryptoProviderListeners: ObjectQL['cryptoProviderListeners']; + datasourceMapping: ObjectQL['datasourceMapping']; readonly defaultDriver: ObjectQL['defaultDriver']; readonly drivers: ObjectQL['drivers']; readonly logger: ObjectQL['logger']; @@ -264,4 +268,61 @@ export class SecretsAndRoutingFacet { static resolveMappedDatasource(this: Pick, objectName: string): string | null { return this.resolveDatasourceFromMapping(objectName, this._registry.getObject(objectName)); } + + /** The body of `ObjectQL#setDatasourceMapping`; the method's docblock there is its contract. */ + static setDatasourceMapping(this: Pick, rules: Array<{ + namespace?: string; + package?: string; + objectPattern?: string; + default?: boolean; + datasource: string; + priority?: number; + }>): void { + this.datasourceMapping = rules; + this.logger.info('Datasource mapping rules configured', { + ruleCount: rules.length + }); + } + + /** The body of `ObjectQL#init`; the method's docblock there is its contract. */ + static async init(this: Pick): Promise { + this.logger.info('Initializing ObjectQL engine', { + driverCount: this.drivers.size, + drivers: Array.from(this.drivers.keys()) + }); + + const failures: DriverConnectFailure[] = []; + for (const [name, driver] of this.drivers) { + try { + await driver.connect(); + this.logger.info('Driver connected successfully', { driverName: name }); + } catch (e) { + failures.push({ driverName: name, error: e }); + this.logger.error('Failed to connect driver', e as Error, { driverName: name }); + } + } + + if (failures.length > 0) { + if (!resolveAllowDriverConnectFailure()) { + throw new DriverConnectError(failures, this.drivers.size); + } + const failedDrivers = failures.map(f => f.driverName); + const banner = + `⚠️ DEGRADED BOOT: ${failures.length} of ${this.drivers.size} driver(s) failed to connect ` + + `(${failedDrivers.join(', ')}), but OS_ALLOW_DRIVER_CONNECT_FAILURE is set — starting anyway. ` + + `Every query routed to them fails until the datasource becomes reachable, and the boot-time ` + + `schema sync is SKIPPED FOR GOOD — the client will reconnect on its own, but nothing re-runs ` + + `the DDL, so those objects may have no tables even after the database comes back. Unset ` + + `OS_ALLOW_DRIVER_CONNECT_FAILURE to restore fail-fast boot.`; + this.logger.warn(banner, { failedDrivers }); + // …and again on a channel the operator's log level cannot filter away: the + // line above is dropped outright at `error`/`fatal`/`silent`, which is a + // normal production setting and precisely where this flag gets used. See + // the helper's note — the duplication is deliberate, and it is NOT about + // `os serve`'s boot-quiet window (that swallowed it too until #4012). + emitDegradedBootBanner(banner); + } + + this.logger.info('ObjectQL engine initialization complete'); + } } diff --git a/packages/objectql/src/engine.facade-return-types.pin.test.ts b/packages/objectql/src/engine.facade-return-types.pin.test.ts new file mode 100644 index 00000000000..9c653af72ec --- /dev/null +++ b/packages/objectql/src/engine.facade-return-types.pin.test.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #30054 — the eight `ObjectQL` methods that stayed whole on the engine.ts + * facade because their return type was inferred now spell it. + * + * The engine codemod keeps a public method with an inferred return type on + * the class: the published `.d.ts` prints that type, and once the body moved + * the compiler would infer it through the facet instead. Each method below + * carries its type on the signature, copied from the `.d.ts` it was inferred + * into (the PR proves the built `.d.ts` byte-equal before and after). Five of + * them have since moved to their facets with the codemod's `--members` pass; + * `registerApp`, `registerDriver` and `registerHook` keep their bodies and + * their rows in `scripts/facade-delegation-registry.json`. An edit that drops + * one of these annotations turns this red: on a stub, the published type would + * then be re-inferred through the facet. + */ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; +import { describe, expect, it } from 'vitest'; + +const FILE = fileURLToPath(new URL('./engine.ts', import.meta.url)); +const CLASS_NAME = 'ObjectQL'; + +/** Each method with the type it spells (the text of its `.d.ts` line). */ +const SPELLED: Readonly> = { + registerApp: 'void', + registerHook: 'void', + registerDriver: 'void', + init: 'Promise', + triggerHooks: 'Promise', + destroy: 'Promise', + setDatasourceMapping: 'void', + getStatus: '{\n name: "data";\n status: string;\n version: string;\n features: string[];\n }', +}; + +function classMethods(): Map { + const text = readFileSync(FILE, 'utf8'); + const sf = ts.createSourceFile(FILE, text, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS); + const cls = sf.statements.find((s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === CLASS_NAME); + const out = new Map(); + for (const m of cls?.members ?? []) { + if (!ts.isMethodDeclaration(m) || !m.body || !m.name || !ts.isIdentifier(m.name)) continue; + out.set(m.name.text, [...(out.get(m.name.text) ?? []), m]); + } + return out; +} + +describe('engine.ts ObjectQL methods spell their return type (#30054)', () => { + const methods = classMethods(); + + it('finds the class and every method, once each', () => { + expect(methods.size).toBeGreaterThan(0); + for (const name of Object.keys(SPELLED)) expect(methods.get(name)?.length, name).toBe(1); + }); + + for (const [name, spelled] of Object.entries(SPELLED)) { + it(`${name} declares its return type on the signature`, () => { + const type = methods.get(name)?.[0]?.type; + expect(type, `${name} has an inferred return type`).toBeDefined(); + expect(type!.getText()).toBe(spelled); + }); + } +}); diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index baa2912e80d..2fa8f5600de 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -143,7 +143,7 @@ import { appAutomationElevationOf, } from '@objectstack/core'; import { WriteEpoch } from './write-epoch.js'; -import { SummaryRecomputeError, cascadeStoppedRefusal, hookStoppedRefusal, rolledBackRefusal, type SummaryRecomputeFailure } from './summary-errors.js'; +import { SummaryRecomputeError, cascadeStoppedRefusal, rolledBackRefusal, type SummaryRecomputeFailure } from './summary-errors.js'; import { TransactionUnsupportedError } from './transaction-errors.js'; import { aggregateSummaryValue, @@ -164,14 +164,11 @@ import { import { DriverConnectError, DatasourceUnavailableError, - emitDegradedBootBanner, - type DriverConnectFailure, type DriverHealth, type DatasourceUnavailableInfo, type DatasourceUnavailableKind, type PrimaryDatasourceVerdict, } from './driver-connect-errors.js'; -import { resolveAllowDriverConnectFailure } from '@objectstack/types'; // [#5979] The ONE shared "which read failure is benign?" predicate (#4825 // family). Imported from the leaf `/errors` subpath — which exists precisely // so a cross-package consumer gets the 40-line predicate without the manager, @@ -231,7 +228,6 @@ import { export type InsertManyRowOutcome = | { ok: true; record: any; droppedFields?: DroppedFieldsEvent[] } | { ok: false; error: unknown }; -import { CoreServiceName } from '@objectstack/spec/system'; import { IRealtimeService } from '@objectstack/spec/contracts'; import type { ICryptoProvider } from '@objectstack/spec/contracts'; // [#25522 cut 18] `count` and `aggregate()`'s credential guard @@ -349,7 +345,6 @@ import { // members it reads (`ValidateHost`). Imported after every module that body // imports, so the engine's module evaluation order is what it was. import { validate } from './engine-validate.js'; -import { bindHookNotice } from './hook-notice.js'; // [#25522 cut 20] The `where`-lowering bodies (`QueryLoweringHost`), imported // after every module they import, so module evaluation order is unchanged. import { lowerRelationConditions, resolveSearchQueryableFields, resolveWhereTokens } from './engine-query-lowering.js'; @@ -366,7 +361,7 @@ import { assertHookScopeNotSelfCancelling, assertOrderByIsMaterializable, assertProjectionHasNoDottedPaths, assertValidHookExcludeObjects, assertValidHookObject, assertValidUnscopedMultiWriteFlag, autonumberFieldNames, callerKeySets, callerSuppliedRow, computeStoredExpressions, declaredDatetimeLowering, droppedFieldEvents, - foldEngineOptionAliases, formulaPlanEntry, handBackWithheldInsertReadonly, hookMatchesObject, hydrateWriteFormulas, + foldEngineOptionAliases, formulaPlanEntry, handBackWithheldInsertReadonly, hydrateWriteFormulas, insertStaticReadonlySubject, lowerWhereFilterArray, masterIdOf, mergeReadContext, organizationPredicateDeleteRefusal, packageBodyCollectionRefusal, packageBodyObjectAndAppOwnerStampRefusal, packageBodySeedDataRefusal, planFormulaProjection, planStoredExpressions, recomputeAutonumberReaders, @@ -755,13 +750,14 @@ export class ObjectQL implements IObjectQLEngine { * Service Status Report * Used by Kernel to verify health and capabilities. */ - getStatus() { - return { - name: CoreServiceName.enum.data, - status: 'running', - version: '0.9.0', - features: ['crud', 'query', 'aggregate', 'transactions', 'metadata'] - }; + getStatus(): { + name: "data"; + status: string; + version: string; + features: string[]; + } { + // [#29986 codemod] The body moved to `engine-hooks-and-functions.ts`. + return HooksAndFunctionsFacet.getStatus(); } /** @@ -810,7 +806,7 @@ export class ObjectQL implements IObjectQLEngine { meta?: any; /** Stable name from metadata (set by `bindHooksToEngine`). */ hookName?: string; - }) { + }): void { // Refuse a scope that is statically decidable as meaningless before // anything is registered or reported. Each face is checked on its own // first, so the combined check below can assume well-formed names. @@ -1055,46 +1051,9 @@ export class ObjectQL implements IObjectQLEngine { * added here that defers a dispatch past the enclosing unit of work would be * changing that ruling, not implementing it. */ - public async triggerHooks(event: string, context: HookContext) { - const entries = this.hooks.get(event) || []; - // [#28251] `ctx.notice` — the write's own `onNotice` listener, bound here - // so every insert / update dispatch site carries it (see hook-notice.ts). - bindHookNotice(event, context); - - if (entries.length === 0) { - this.logger.debug('No hooks registered for event', { event }); - return; - } - - this.logger.debug('Triggering hooks', { event, count: entries.length }); - - // `session.skipAutomations` (set from ExecutionContext.skipAutomations — - // import with "run automations & triggers" unchecked, import undo) - // suppresses hooks bound FROM METADATA (`bindHooksToEngine` stamps - // `entry.meta`). Hooks registered in code by plugins — audit, capability - // gates, sharing projection — have no `meta` and always run: the opt-out - // must never bypass security or audit (#2922). - const skipAutomations = - (context.session as { skipAutomations?: boolean } | undefined)?.skipAutomations === true; - - for (const entry of entries) { - // Per-object matching — allow face minus exclusion face, the one shared - // rule `hasHooksFor` also reads (#5928). - if (!hookMatchesObject(entry, context.object)) { - continue; // Skip non-matching hooks - } - if (skipAutomations && entry.meta) { - this.logger.debug('Skipping metadata-bound hook (skipAutomations)', { event, hook: entry.hookName }); - continue; - } - // [#29422] A stale-roll-up refusal a hook's own `ctx.api` write raised - // describes THAT write, not this one: `hookStoppedRefusal`. - try { - await entry.handler(context); - } catch (e) { - throw hookStoppedRefusal(e, event, context.object, entry.hookName); - } - } + public triggerHooks(event: string, context: HookContext): Promise { + // [#29986 codemod] The body moved to `engine-hooks-and-functions.ts` (see `HooksAndFunctionsHost`). + return HooksAndFunctionsFacet.triggerHooks.call(hooksAndFunctionsHost.of(this), event, context); } private hasHooksFor(event: string, object: string): boolean { @@ -2303,7 +2262,7 @@ export class ObjectQL implements IObjectQLEngine { * comes through here (it is installed by `installPackage` in * `@objectstack/metadata-protocol`), so `'authored'` is not accepted. */ - registerApp(registeredBody: any, scope?: ArtifactInstallScope, source: Exclude = 'code') { + registerApp(registeredBody: any, scope?: ArtifactInstallScope, source: Exclude = 'code'): void { // The CONVERTED body, for everything below — the package record, the // live manifest, every item it registers (see {@link convertPackageBodyForward}). // [#23972] Refused before anything registers — see the docblock: an @@ -2504,7 +2463,7 @@ export class ObjectQL implements IObjectQLEngine { * else already is the default** → the caller's intent is being dropped, * so it is loud too rather than folded into the quiet path. */ - registerDriver(driver: IDataDriver, isDefault: boolean = false) { + registerDriver(driver: IDataDriver, isDefault: boolean = false): void { const existing = this.drivers.get(driver.name); if (existing) { // [#27277] The entry may be the floored instance of this very driver. @@ -3749,11 +3708,9 @@ export class ObjectQL implements IObjectQLEngine { default?: boolean; datasource: string; priority?: number; - }>) { - this.datasourceMapping = rules; - this.logger.info('Datasource mapping rules configured', { - ruleCount: rules.length - }); + }>): void { + // [#29986 codemod] The body moved to `engine-secrets-and-routing.ts` (see `SecretsAndRoutingHost`). + return SecretsAndRoutingFacet.setDatasourceMapping.call(secretsAndRoutingHost.of(this), rules); } /** @@ -3784,45 +3741,9 @@ export class ObjectQL implements IObjectQLEngine { * `OS_ALLOW_DRIVER_CONNECT_FAILURE=1`, which boots in a state that is warned * about loudly rather than assumed. */ - async init() { - this.logger.info('Initializing ObjectQL engine', { - driverCount: this.drivers.size, - drivers: Array.from(this.drivers.keys()) - }); - - const failures: DriverConnectFailure[] = []; - for (const [name, driver] of this.drivers) { - try { - await driver.connect(); - this.logger.info('Driver connected successfully', { driverName: name }); - } catch (e) { - failures.push({ driverName: name, error: e }); - this.logger.error('Failed to connect driver', e as Error, { driverName: name }); - } - } - - if (failures.length > 0) { - if (!resolveAllowDriverConnectFailure()) { - throw new DriverConnectError(failures, this.drivers.size); - } - const failedDrivers = failures.map(f => f.driverName); - const banner = - `⚠️ DEGRADED BOOT: ${failures.length} of ${this.drivers.size} driver(s) failed to connect ` + - `(${failedDrivers.join(', ')}), but OS_ALLOW_DRIVER_CONNECT_FAILURE is set — starting anyway. ` + - `Every query routed to them fails until the datasource becomes reachable, and the boot-time ` + - `schema sync is SKIPPED FOR GOOD — the client will reconnect on its own, but nothing re-runs ` + - `the DDL, so those objects may have no tables even after the database comes back. Unset ` + - `OS_ALLOW_DRIVER_CONNECT_FAILURE to restore fail-fast boot.`; - this.logger.warn(banner, { failedDrivers }); - // …and again on a channel the operator's log level cannot filter away: the - // line above is dropped outright at `error`/`fatal`/`silent`, which is a - // normal production setting and precisely where this flag gets used. See - // the helper's note — the duplication is deliberate, and it is NOT about - // `os serve`'s boot-quiet window (that swallowed it too until #4012). - emitDegradedBootBanner(banner); - } - - this.logger.info('ObjectQL engine initialization complete'); + init(): Promise { + // [#29986 codemod] The body moved to `engine-secrets-and-routing.ts` (see `SecretsAndRoutingHost`). + return SecretsAndRoutingFacet.init.call(secretsAndRoutingHost.of(this)); } /** @@ -4275,27 +4196,9 @@ export class ObjectQL implements IObjectQLEngine { return wasDatastoreCreatedFromEmpty.call(host); } - async destroy() { - this.logger.info('Destroying ObjectQL engine', { driverCount: this.drivers.size }); - - for (const [name, driver] of this.drivers.entries()) { - try { - await driver.disconnect(); - } catch (e) { - this.logger.error('Error disconnecting driver', e as Error, { driverName: name }); - } - } - - // #13578 — teardown is an eviction path too, and it used to disconnect - // every driver while leaving all of them REGISTERED. A destroyed engine - // therefore still answered `checkDriversHealth()` by pinging pools it had - // just closed, so a readiness probe racing shutdown read a list of drivers - // that were deliberately dead. Disconnect-then-evict, through the one - // primitive, so `defaultDriver` and the datasource defs go with them. - // Names are snapshotted first: `unregisterDriver` mutates the Map. - for (const name of [...this.drivers.keys()]) this.unregisterDriver(name); - - this.logger.info('ObjectQL engine destroyed'); + destroy(): Promise { + // [#29986 codemod] The body moved to `engine-migration-state.ts` (see `MigrationStateHost`). + return MigrationStateFacet.destroy.call(migrationStateHost.of(this)); } // ============================================ @@ -10535,6 +10438,7 @@ export class ObjectQL implements IObjectQLEngine { set creationAttestationRetraction(v) { engine.creationAttestationRetraction = v; }, get deviationRecording() { return engine.deviationRecording; }, set deviationRecording(v) { engine.deviationRecording = v; }, + get drivers() { return engine.drivers; }, get fileReferencesMigrationVerified() { return engine.fileReferencesMigrationVerified; }, set fileReferencesMigrationVerified(v) { engine.fileReferencesMigrationVerified = v; }, findOne: (...a) => engine.findOne(...a), @@ -10551,6 +10455,7 @@ export class ObjectQL implements IObjectQLEngine { readMigrationFlagRowMemoized: (...a) => engine.readMigrationFlagRowMemoized(...a), get recordedDeviations() { return engine.recordedDeviations; }, get retractedCreationAttestations() { return engine.retractedCreationAttestations; }, + unregisterDriver: (...a) => engine.unregisterDriver(...a), update: (...a) => engine.update(...a), get valueShapesMigrationVerified() { return engine.valueShapesMigrationVerified; }, set valueShapesMigrationVerified(v) { engine.valueShapesMigrationVerified = v; }, @@ -10596,6 +10501,8 @@ export class ObjectQL implements IObjectQLEngine { get cryptoProvider() { return engine.cryptoProvider; }, set cryptoProvider(v) { engine.cryptoProvider = v; }, get cryptoProviderListeners() { return engine.cryptoProviderListeners; }, + get datasourceMapping() { return engine.datasourceMapping; }, + set datasourceMapping(v) { engine.datasourceMapping = v; }, get defaultDriver() { return engine.defaultDriver; }, get drivers() { return engine.drivers; }, get logger() { return engine.logger; }, diff --git a/scripts/facade-delegation-registry.json b/scripts/facade-delegation-registry.json index 764fd289333..22b1dd10d74 100644 --- a/scripts/facade-delegation-registry.json +++ b/scripts/facade-delegation-registry.json @@ -296,56 +296,21 @@ "class": "ObjectQL", "member": "registerApp", "card": "#30054", - "reason": "inferred-return-type (#29986 report: stays on the class until the return type is spelled)" + "reason": "return type spelled on the signature (#30054, public .d.ts byte-equal); the codemod --members pass refuses to move it: engine-registration.ts's header is no longer as the first pass wrote it (a hand-written member was added there)" }, { "file": "packages/objectql/src/engine.ts", "class": "ObjectQL", "member": "registerHook", "card": "#30054", - "reason": "inferred-return-type (#29986 report: stays on the class until the return type is spelled)" + "reason": "return type spelled on the signature (#30054, public .d.ts byte-equal); the codemod --members pass cannot prove its move: undoing it does not rebuild engine-hooks-and-functions.ts byte for byte (first difference at line 44, in its import block)" }, { "file": "packages/objectql/src/engine.ts", "class": "ObjectQL", "member": "registerDriver", "card": "#30054", - "reason": "inferred-return-type (#29986 report: stays on the class until the return type is spelled)" - }, - { - "file": "packages/objectql/src/engine.ts", - "class": "ObjectQL", - "member": "init", - "card": "#30054", - "reason": "inferred-return-type (#29986 report: stays on the class until the return type is spelled)" - }, - { - "file": "packages/objectql/src/engine.ts", - "class": "ObjectQL", - "member": "triggerHooks", - "card": "#30054", - "reason": "inferred-return-type (#29986 report: stays on the class until the return type is spelled)" - }, - { - "file": "packages/objectql/src/engine.ts", - "class": "ObjectQL", - "member": "destroy", - "card": "#30054", - "reason": "inferred-return-type (#29986 report: stays on the class until the return type is spelled)" - }, - { - "file": "packages/objectql/src/engine.ts", - "class": "ObjectQL", - "member": "setDatasourceMapping", - "card": "#30054", - "reason": "inferred-return-type (#29986 report: stays on the class until the return type is spelled)" - }, - { - "file": "packages/objectql/src/engine.ts", - "class": "ObjectQL", - "member": "getStatus", - "card": "#30054", - "reason": "inferred-return-type (#29986 report: stays on the class until the return type is spelled)" + "reason": "return type spelled on the signature (#30054, public .d.ts byte-equal); the codemod --members pass refuses to move it: engine-registration.ts's header is no longer as the first pass wrote it (a hand-written member was added there)" }, { "file": "packages/objectql/src/engine.ts",