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
11 changes: 11 additions & 0 deletions .changeset/30020-objectql-identity-port-bodies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@objectstack/objectql': patch
---

refactor(objectql): eight `ObjectQL` bodies that hand the engine on move into their facets

Clause-②: no
Metadata-protocol: none

- The bodies of `bindHooks`, `createContext`, `buildHookApi`, `resolveReadableIdsSecurity`, `ownershipQueueService`, `readMigrationFlagRowMemoized` and `recomputeSummaries` move out of `engine.ts` into the facet module of their section, and the `formulaReadsReference` slot of `serveReferenceNames`' host moves into `engine-reference-name-visible.ts`. Each method stays on the class as a one-call stub with its signature, and a public method keeps its docblock there. A body that handed the engine itself on (`bindHooksToEngine(this, …)`, `new ScopedContext(ctx, this)`, a symbol slot read off the engine) reads the same engine through its host's `facade` entry.
- No behaviour changes and nothing to do on upgrade. The built declaration files keep every member and every public or protected docblock; the five private methods' docblocks moved with their bodies, so they no longer appear in the shared declaration chunk, and the source maps move.
21 changes: 19 additions & 2 deletions packages/objectql/src/engine-compatibility-section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* [#29986 codemod] The "Compatibility / convenience API" section of `ObjectQL`:
* the bodies of `unregisterObject`, `getObject`, `getDriverByName`,
* `getDriverForObject`, `dropObjectSchema`, `datasource`, `on`, `close`,
* `create` moved out of `engine.ts` whole by
* `createContext`, `create` moved out of `engine.ts` whole by
* `scripts/codemod-split-engine.mjs`.
*
* A MOVE, not a redesign. {@link CompatibilitySectionFacet} is a stateless facet of `ObjectQL`
Expand Down Expand Up @@ -34,8 +34,11 @@

import type { HookContext, ServiceObject } from '@objectstack/spec/data';
import { StorageNameMapping } from '@objectstack/spec/system';
import { ExecutionContextSchema } from '@objectstack/spec/kernel';
import type { ExecutionContext } from '@objectstack/spec/kernel';
import { carryAppAutomationElevation } from '@objectstack/core';
import type { IDataDriver } from '@objectstack/core';
import { ObjectQL } from './engine.js';
import { ObjectQL, ScopedContext } from './engine.js';

/**
* The `ObjectQL` members the bodies of {@link CompatibilitySectionFacet} read — every one of
Expand All @@ -48,6 +51,8 @@ export interface CompatibilitySectionHost {
readonly _registry: ObjectQL['_registry'];
destroy: ObjectQL['destroy'];
readonly drivers: ObjectQL['drivers'];
/** The engine itself: a moved body reads `this.facade` where the method read a bare `this` (#30020). */
readonly facade: ObjectQL;
getDriver: ObjectQL['getDriver'];
getDriverForObject: ObjectQL['getDriverForObject'];
getSchema: ObjectQL['getSchema'];
Expand Down Expand Up @@ -113,6 +118,18 @@ export class CompatibilitySectionFacet {
return this.destroy();
}

/** The body of `ObjectQL#createContext`; the method's docblock there is its contract. */
static createContext(this: Pick<CompatibilitySectionHost, 'facade'>, ctx: Partial<ExecutionContext>): ScopedContext {
// [#25653] The parse keeps declared keys only, so the app-automation
// elevation mark an action body's envelope carries is carried across it:
// dropped here, the body's `ctx.api` writes would reach the
// identity-assignment refusal unmarked.
return new ScopedContext(
carryAppAutomationElevation(ctx, ExecutionContextSchema.parse(ctx)),
this.facade
);
}

/** The body of `ObjectQL#create`; the method's docblock there is its contract. */
static async create(config: {
datasources?: Record<string, IDataDriver>;
Expand Down
31 changes: 24 additions & 7 deletions packages/objectql/src/engine-defaults-and-autonumbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
/**
* [#29986 codemod] The "Field defaults and autonumbers" section of `ObjectQL`:
* the bodies of `resolveNowDefault`, `normalizeExpressionDefault`,
* `resolveOptionDefault`, `applyFieldDefaults`, `defaultFieldEntries`,
* `applyAutonumbers`, `adoptExplicitAutonumber`, `createWithAutonumberResync`,
* `isIssuedAutonumberCollision`, `warnAutonumberReadersUnserved`,
* `seedAutonumber` moved out of `engine.ts` whole by
* `scripts/codemod-split-engine.mjs`.
* `buildHookApi`, `resolveOptionDefault`, `applyFieldDefaults`,
* `defaultFieldEntries`, `applyAutonumbers`, `adoptExplicitAutonumber`,
* `createWithAutonumberResync`, `isIssuedAutonumberCollision`,
* `warnAutonumberReadersUnserved`, `seedAutonumber` moved out of `engine.ts`
* whole by `scripts/codemod-split-engine.mjs`.
*
* A MOVE, not a redesign. {@link DefaultsAndAutonumbersFacet} is a stateless facet of `ObjectQL`
* (ruling 6624345766): each static method of it is the body of the `ObjectQL`
Expand All @@ -28,12 +28,15 @@
* class, and a field a body assigns goes with a SETTER that assigns it on the
* engine.
*
* The module imports `engine.ts` for types only, so the two modules form no
* evaluation cycle. `index.ts` re-exports nothing from this module.
* Values that stayed in `engine.ts` are imported from it (lever (a) of
* ruling 6591753301); every use is inside a function body, so the import
* cycle is never read at evaluation time. `index.ts` re-exports nothing from
* this module.
*/

import { isCurrentUserDefaultToken, isNowDefaultToken, missingFieldValues, parseAutonumberFormat, renderAutonumber, resolveAutonumberFormat } from '@objectstack/spec/data';
import type { EngineQueryOptions } from '@objectstack/spec/data';
import type { IDataEngine } from '@objectstack/core';
import { isUniqueViolationError, keysetWalk, uniqueViolationColumn } from '@objectstack/types';
import type { KeysetPageQuery } from '@objectstack/types';
import type { ExecutionContext } from '@objectstack/spec/kernel';
Expand All @@ -46,6 +49,7 @@ import {
readStoredAutonumberCounter, recomputeAutonumberReaders,
} from './engine-top-level.js';
import type { AutonumberReaders, FormulaPlanEntry, IssuedAutonumber } from './engine-top-level.js';
import { ScopedContext } from './engine.js';
import type { ObjectQL } from './engine.js';

/**
Expand All @@ -61,6 +65,8 @@ export interface DefaultsAndAutonumbersHost {
readonly autonumberCounters: ObjectQL['autonumberCounters'];
readonly autonumberReadersUnservedReported: ObjectQL['autonumberReadersUnservedReported'];
defaultFieldEntries: ObjectQL['defaultFieldEntries'];
/** The engine itself: a moved body reads `this.facade` where the method read a bare `this` (#30020). */
readonly facade: ObjectQL;
find: ObjectQL['find'];
getSchema: ObjectQL['getSchema'];
isIssuedAutonumberCollision: ObjectQL['isIssuedAutonumberCollision'];
Expand Down Expand Up @@ -189,6 +195,17 @@ export class DefaultsAndAutonumbersFacet {
return this.resolveNowDefault(fieldType, value);
}

/**
* Build a HookContext.api: a ScopedContext that hooks can use to
* read/write other objects within the same execution context.
* Falls back to a system-elevated empty context when no execCtx
* is supplied (e.g. system-triggered hooks).
*/
static buildHookApi(this: Pick<DefaultsAndAutonumbersHost, 'facade'>, execCtx?: ExecutionContext): ScopedContext {
const safeCtx: ExecutionContext = execCtx ?? ({ isSystem: true } as any);
return new ScopedContext(safeCtx, this.facade as unknown as IDataEngine);
}

/**
* The initial value a field's OPTION LIST declares — the option marked
* `default: true` — or `undefined` when it declares none (#7246).
Expand Down
39 changes: 35 additions & 4 deletions packages/objectql/src/engine-hooks-and-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
* `ObjectQL`: the bodies of `recordResidualOwner`, `residualOwnerSource`,
* `getStatus`, `registerHook`, `unregisterHooksByPackage`, `registerFunction`,
* `resolveFunction`, `resolveFunctionEntry`, `unregisterFunctionsByPackage`,
* `setDefaultBodyRunner`, `getDefaultBodyRunner`, `setDefaultActionRunner`,
* `getDefaultActionRunner`, `triggerHooks`, `hasHooksFor`,
* `opensAfterAbortUnit`, `dispatchUnscopedMultiWriteHooks` moved out of
* `engine.ts` whole by `scripts/codemod-split-engine.mjs`.
* `bindHooks`, `setDefaultBodyRunner`, `getDefaultBodyRunner`,
* `setDefaultActionRunner`, `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`
Expand Down Expand Up @@ -42,6 +42,8 @@ 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 { bindHooksToEngine } from './hook-binder.js';
import type { BindHooksOptions } from './hook-binder.js';
import {
DISPATCHABLE_HOOK_EVENTS, assertDispatchableHookEvent, assertHookScopeNotSelfCancelling,
assertValidHookExcludeObjects, assertValidHookObject, assertValidUnscopedMultiWriteFlag, hookMatchesObject,
Expand All @@ -61,6 +63,8 @@ export interface HooksAndFunctionsHost {
_defaultBodyRunner: ObjectQL['_defaultBodyRunner'];
readonly defaultDriver: ObjectQL['defaultDriver'];
readonly drivers: ObjectQL['drivers'];
/** The engine itself: a moved body reads `this.facade` where the method read a bare `this` (#30020). */
readonly facade: ObjectQL;
readonly functions: ObjectQL['functions'];
getDriver: ObjectQL['getDriver'];
readonly hooks: ObjectQL['hooks'];
Expand Down Expand Up @@ -229,6 +233,33 @@ export class HooksAndFunctionsFacet {
return removed;
}

/** The body of `ObjectQL#bindHooks`; the method's docblock there is its contract. */
static bindHooks(this: Pick<HooksAndFunctionsHost, '_defaultBodyRunner' | 'facade' | 'logger'>, hooks: any[] | undefined, opts?: {
packageId?: string;
functions?: Record<string, HookHandler>;
bodyRunner?: any;
strict?: boolean;
warnLegacyHandler?: boolean;
metrics?: any;
/** Wraps each in-process handler after its name resolves — see {@link BindHooksOptions.wrapHandler}. */
wrapHandler?: BindHooksOptions['wrapHandler'];
}): void {
const merged = { ...(opts ?? {}), logger: this.logger } as any;
if (!merged.bodyRunner && this._defaultBodyRunner) {
merged.bodyRunner = this._defaultBodyRunner;
}
if (merged.strict === undefined && (this.facade as any)._strictHookBinding) {
merged.strict = true;
}
if (merged.warnLegacyHandler === undefined && (this.facade as any)._warnLegacyHandler) {
merged.warnLegacyHandler = true;
}
if (!merged.metrics && (this.facade as any)._hookMetricsRecorder) {
merged.metrics = (this.facade as any)._hookMetricsRecorder;
}
bindHooksToEngine(this.facade, hooks, merged);
}

/** The body of `ObjectQL#setDefaultBodyRunner`; the method's docblock there is its contract. */
static setDefaultBodyRunner(this: Pick<HooksAndFunctionsHost, '_defaultBodyRunner' | 'logger'>, runner: any): boolean {
if (this._defaultBodyRunner) {
Expand Down
24 changes: 21 additions & 3 deletions packages/objectql/src/engine-insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
/**
* [#29986 codemod] The "Write preparation and insert" section of `ObjectQL`:
* the bodies of `staticReadonlyCreateStrip`, `updateByIdExpecting`,
* `assignment`, `readUpdatePriorRow`, `insertMany` moved out of `engine.ts`
* whole by `scripts/codemod-split-engine.mjs`.
* `ownershipQueueService`, `assignment`, `readUpdatePriorRow`, `insertMany`
* moved out of `engine.ts` whole by `scripts/codemod-split-engine.mjs`.
*
* A MOVE, not a redesign. {@link InsertFacet} is a stateless facet of `ObjectQL`
* (ruling 6624345766): each static method of it is the body of the `ObjectQL`
Expand Down Expand Up @@ -33,7 +33,8 @@ import type { WriteObservabilityOptions } from '@objectstack/spec/contracts';
import type { ExecutionContext } from '@objectstack/spec/kernel';
import type { IDataDriver } from '@objectstack/core';
import { ConcurrentUpdateError } from './concurrent-update-error.js';
import { ASSIGNMENT_RULE_TYPE, AssignmentRuleEvaluator } from './assignment-rules.js';
import { ASSIGNMENT_RULE_TYPE, AssignmentRuleEvaluator, OWNERSHIP_QUEUE_SOURCE_SLOT } from './assignment-rules.js';
import type { AssignmentQueueService } from './assignment-rules.js';
import type { EvalPermissions } from '@objectstack/formula';
import { stripReadonlyFields } from './validation/rule-validator.js';
import type { staticReadonlyInsertSubject } from './validation/rule-validator.js';
Expand All @@ -55,6 +56,8 @@ export interface InsertHost {
assignmentEvaluator: ObjectQL['assignmentEvaluator'];
buildDriverOptions: ObjectQL['buildDriverOptions'];
count: ObjectQL['count'];
/** The engine itself: a moved body reads `this.facade` where the method read a bare `this` (#30020). */
readonly facade: ObjectQL;
find: ObjectQL['find'];
getDriver: ObjectQL['getDriver'];
insert: ObjectQL['insert'];
Expand Down Expand Up @@ -180,6 +183,21 @@ export class InsertFacet {
return driver.findOne(object, { object, where: { id }, limit: 1 } as QueryAST, options);
}

/** The registered ownership-queue service, through the plugin's slot — `null` on every other state. */
static ownershipQueueService(this: Pick<InsertHost, 'facade'>): AssignmentQueueService | null {
const source = (this.facade as unknown as Record<symbol, unknown>)[OWNERSHIP_QUEUE_SOURCE_SLOT];
if (typeof source !== 'function') return null;
try {
const svc = (source as () => unknown)();
return svc && typeof (svc as AssignmentQueueService).resolveQueue === 'function'
&& typeof (svc as AssignmentQueueService).expandMembers === 'function'
? (svc as AssignmentQueueService)
: null;
} catch {
return null;
}
}

/** The body of `ObjectQL#assignment`; the method's docblock there is its contract. */
static assignment(this: Pick<InsertHost, '_registry' | 'assignmentEvaluator' | 'count' | 'find' | 'insert' | 'logger' | 'ownershipQueueService' | 'update'>): AssignmentRuleEvaluator {
if (!this.assignmentEvaluator) {
Expand Down
35 changes: 30 additions & 5 deletions packages/objectql/src/engine-migration-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
* `mediaValueShapeStrictFor`, `objectHasCoveredValueField`,
* `valueShapeStrictFor`, `isFileReferencesMigrationVerified`,
* `haveFileColumnsMoved`, `readMigrationFlagMemoized`,
* `valueShapeViolationsAdmitted`, `admittedViolationSink`,
* `recordObservedDeviation`, `retractCreationAttestation`,
* `invalidateDataMigrationFlags`, `destroy` moved out of `engine.ts` whole by
* `scripts/codemod-split-engine.mjs`.
* `readMigrationFlagRowMemoized`, `valueShapeViolationsAdmitted`,
* `admittedViolationSink`, `recordObservedDeviation`,
* `retractCreationAttestation`, `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`
* (ruling 6624345766): each static method of it is the body of the `ObjectQL`
Expand Down Expand Up @@ -46,7 +46,7 @@ import { DATA_MIGRATION_FLAG_OBJECT, FILE_REFERENCES_MIGRATION_ID, isDataMigrati
import type { ExecutionContext } from '@objectstack/spec/kernel';
import { isScannableValueShapeField } from './validation/record-validator.js';
import type { AdmittedValueShapeViolationSink } from './validation/record-validator.js';
import type { AdmittedValueShapeViolationTally } from './engine-top-level.js';
import type { AdmittedValueShapeViolationTally, MigrationFlagRead } from './engine-top-level.js';
import { ObjectQLStatics as ObjectQL } from './engine.js';
import type { ObjectQL as ObjectQLClass } from './engine.js';

Expand Down Expand Up @@ -75,6 +75,7 @@ export interface MigrationStateHost {
objectHasMediaField: ObjectQLClass['objectHasMediaField'];
readFileReferencesFlagRow: ObjectQLClass['readFileReferencesFlagRow'];
readMigrationFlagRowMemoized: ObjectQLClass['readMigrationFlagRowMemoized'];
readMigrationFlagVerified: ObjectQLClass['readMigrationFlagVerified'];
readonly recordedDeviations: ObjectQLClass['recordedDeviations'];
readonly retractedCreationAttestations: ObjectQLClass['retractedCreationAttestations'];
unregisterDriver: ObjectQLClass['unregisterDriver'];
Expand Down Expand Up @@ -195,6 +196,30 @@ export class MigrationStateFacet {
return (await this.readMigrationFlagRowMemoized(slot, migrationId, verifiedLog)).verified;
}

/**
* The same memoized read, handed back WHOLE (#15989).
*
* The row carries two facts a consumer can want — is the migration verified,
* and have the columns moved — and they must come from one read: two reads
* can straddle a `--apply` and answer out of one another's date, which for
* these two facts means "verified, columns not moved" (the JSON arm) and
* "not verified, columns moved" (an unreachable state) both become
* observable. {@link readMigrationFlagMemoized} is this function projected
* onto its first field, so the two can never disagree.
*/
static async readMigrationFlagRowMemoized(this: Pick<MigrationStateHost, 'fileReferencesMigrationVerified' | 'readMigrationFlagVerified' | 'valueShapesMigrationVerified'>, slot: 'fileReferencesMigrationVerified' | 'valueShapesMigrationVerified', migrationId: string, verifiedLog: string): Promise<MigrationFlagRead> {
const cached = this[slot];
if (cached) return await cached;
const pending = this.readMigrationFlagVerified(migrationId, verifiedLog);
this[slot] = pending;
const result = await pending;
// Only a read that actually consulted the ledger may be remembered. Clear
// by identity so a concurrent `invalidateDataMigrationFlags()` (or a
// re-read that already replaced this slot) is not undone here.
if (!result.conclusive && this[slot] === pending) this[slot] = null;
return result;
}

/** The body of `ObjectQL#valueShapeViolationsAdmitted`; the method's docblock there is its contract. */
static valueShapeViolationsAdmitted(this: Pick<MigrationStateHost, 'admittedValueShapeViolations'>): Record<string, AdmittedValueShapeViolationTally> {
const out: Record<string, AdmittedValueShapeViolationTally> = {};
Expand Down
17 changes: 17 additions & 0 deletions packages/objectql/src/engine-reference-name-visible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import type { ExecutionContext } from '@objectstack/spec/kernel';
import { withoutOperationPrivateKeys } from '@objectstack/core';
import type { ObjectQL } from './engine.js';
import { isReadRefusal } from './engine-file-reference-hydration.js';
import { formulaPlanEntry } from './engine-top-level.js';

/**
* The `ObjectQL` members {@link serveReferenceNames} reads, every one of them
Expand Down Expand Up @@ -263,3 +264,19 @@ async function servableNameField(
return undefined;
}
}

/**
* [#30020 codemod] The `formulaReadsReference` slot of the host
* `ObjectQL#serveReferenceNames` hands {@link serveReferenceNames}: moved here
* whole by `--hand-slots` of `scripts/codemod-split-engine.mjs`, and the slot
* names it.
*/
export const formulaReadsReference: ReferenceNameVisibleHost['formulaReadsReference'] = (name, def, fields) => {
try {
const entry = formulaPlanEntry(name, def, fields);
return entry.hops !== undefined || entry.hopConflict !== undefined;
} catch {
// An expression the planner cannot compile is not served by name.
return true;
}
};
Loading
Loading