From 26e21f8b531127360d73e71c64eac0acf9c27923 Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 23 Oct 2026 13:28:37 +0200 Subject: [PATCH 01/19] TML-3690: RuntimeMode is declared once, in framework-components The SQL runtime, the MongoDB runtime, the MongoDB client options and the activity context import it from there. @internal/sql-runtime still exports it, from its exports entry. Signed-off-by: willbot Signed-off-by: Will Madden --- .../src/execution/runtime-middleware.ts | 5 ++++- .../1-core/framework-components/src/exports/runtime.ts | 1 + packages/2-mongo-family/7-runtime/src/mongo-runtime.ts | 3 ++- packages/2-sql-family/5-runtime/src/exports/index.ts | 2 +- packages/2-sql-family/5-runtime/src/sql-runtime.ts | 4 +--- .../5-runtime/test/transaction-activity-fixtures.ts | 8 ++++++-- packages/3-targets/mongo/9-facade/src/runtime/mongo.ts | 4 ++-- test/integration/test/utils.ts | 3 ++- 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/1-framework/1-core/framework-components/src/execution/runtime-middleware.ts b/packages/1-framework/1-core/framework-components/src/execution/runtime-middleware.ts index e1a03dfd590f..944a234e05a0 100644 --- a/packages/1-framework/1-core/framework-components/src/execution/runtime-middleware.ts +++ b/packages/1-framework/1-core/framework-components/src/execution/runtime-middleware.ts @@ -72,12 +72,15 @@ export interface StartActivity>; } +/** How middleware treats a guardrail violation. `'strict'`, the default, makes `budgets` throw on every violation; `'permissive'` lets a violation whose severity is `'warn'` go to `log.warn`. */ +export type RuntimeMode = 'strict' | 'permissive'; + /** * The context of one activity. Every hook of the activity receives the same object. */ export interface ActivityContext { readonly contract: unknown; - readonly mode: 'strict' | 'permissive'; + readonly mode: RuntimeMode; readonly now: () => number; readonly log: RuntimeLog; /** diff --git a/packages/1-framework/1-core/framework-components/src/exports/runtime.ts b/packages/1-framework/1-core/framework-components/src/exports/runtime.ts index 3d6d961a11be..9354e6d5c500 100644 --- a/packages/1-framework/1-core/framework-components/src/exports/runtime.ts +++ b/packages/1-framework/1-core/framework-components/src/exports/runtime.ts @@ -70,6 +70,7 @@ export type { RuntimeExecutor, RuntimeLog, RuntimeMiddleware, + RuntimeMode, RuntimeStatementStats, StartActivity, StatementActivity, diff --git a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts index 79d9d6c56696..e87979bb139f 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts @@ -7,6 +7,7 @@ import { executeStatement, RuntimeCore, type RuntimeExecuteOptions, + type RuntimeMode, type RuntimeStatementStats, runtimeError, type StatementActivityRun, @@ -75,7 +76,7 @@ export interface MongoRuntimeOptions { readonly context: MongoExecutionContext; readonly driver: MongoDriver; readonly middleware?: readonly MongoMiddleware[]; - readonly mode?: 'strict' | 'permissive'; + readonly mode?: RuntimeMode; } export interface MongoRuntime { diff --git a/packages/2-sql-family/5-runtime/src/exports/index.ts b/packages/2-sql-family/5-runtime/src/exports/index.ts index 547ee679d659..798daa53081d 100644 --- a/packages/2-sql-family/5-runtime/src/exports/index.ts +++ b/packages/2-sql-family/5-runtime/src/exports/index.ts @@ -12,6 +12,7 @@ export type { ExecuteInterceptResult, QueryInterceptResult, RuntimeLog as Log, + RuntimeMode, StartActivity, StatementActivity, StatementEnded, @@ -94,7 +95,6 @@ export type { ConnectionProvider, Runtime, RuntimeConnection, - RuntimeMode, RuntimeOptions, RuntimeQueryable, RuntimeTransaction, diff --git a/packages/2-sql-family/5-runtime/src/sql-runtime.ts b/packages/2-sql-family/5-runtime/src/sql-runtime.ts index 89d0583f7071..2974db3c0f8f 100644 --- a/packages/2-sql-family/5-runtime/src/sql-runtime.ts +++ b/packages/2-sql-family/5-runtime/src/sql-runtime.ts @@ -14,6 +14,7 @@ import { RuntimeCore, type RuntimeExecuteOptions, type RuntimeLog, + type RuntimeMode, runtimeError, type StartActivity, type StatementActivityRun, @@ -127,9 +128,6 @@ export type Log = RuntimeLog; */ export type CloseRefusal = 'when-idle' | 'at-once'; -/** How middleware treats a guardrail violation. `'strict'`, the default, makes `budgets` throw on every violation; `'permissive'` lets a violation whose severity is `'warn'` go to `log.warn`. */ -export type RuntimeMode = 'strict' | 'permissive'; - export interface RuntimeOptions = Contract> { readonly context: ExecutionContext; readonly adapter: Adapter, LoweredStatement>; diff --git a/packages/2-sql-family/5-runtime/test/transaction-activity-fixtures.ts b/packages/2-sql-family/5-runtime/test/transaction-activity-fixtures.ts index 3ebc8adb1123..2284d1b9c1ae 100644 --- a/packages/2-sql-family/5-runtime/test/transaction-activity-fixtures.ts +++ b/packages/2-sql-family/5-runtime/test/transaction-activity-fixtures.ts @@ -1,5 +1,9 @@ import { instantiateExecutionStack } from '@internal/framework-components/execution'; -import type { Activity, ActivityFinalizedResult } from '@internal/framework-components/runtime'; +import type { + Activity, + ActivityFinalizedResult, + RuntimeMode, +} from '@internal/framework-components/runtime'; import { type MarkerReadResult, RawQueryAst, @@ -144,7 +148,7 @@ export interface SetupOptions { readonly beforeHookFailure?: Error; readonly readMarker?: () => Promise; readonly log?: Log; - readonly mode?: 'strict' | 'permissive'; + readonly mode?: RuntimeMode; /** Middleware registered after the recorder. */ readonly middleware?: readonly SqlMiddleware[]; /** When false, the recorder implements no `activityFinalized`. */ diff --git a/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts b/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts index 1ad7f255a5c1..44cb1343e851 100644 --- a/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts +++ b/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts @@ -1,5 +1,5 @@ import { MongoDriverImpl } from '@internal/driver-mongo'; -import { AsyncIterableResult } from '@internal/framework-components/runtime'; +import { AsyncIterableResult, type RuntimeMode } from '@internal/framework-components/runtime'; import type { AnyMongoTypeMaps, MongoContract, @@ -41,7 +41,7 @@ export interface MongoClient< } export interface MongoOptionsBase { - readonly mode?: 'strict' | 'permissive'; + readonly mode?: RuntimeMode; /** * Optional middleware chain applied to every Mongo execution. */ diff --git a/test/integration/test/utils.ts b/test/integration/test/utils.ts index 1977f442ca11..2bc0349137fd 100644 --- a/test/integration/test/utils.ts +++ b/test/integration/test/utils.ts @@ -11,6 +11,7 @@ import type { SqlStorage } from '@internal/sql-contract/types'; import type { Log, Runtime, + RuntimeMode, SqlMiddleware, SqlRuntimeExtensionDescriptor, VerifyMarkerOption, @@ -25,7 +26,7 @@ export interface CreateTestRuntimeOptions { readonly verifyMarker?: VerifyMarkerOption; readonly extensions?: readonly SqlRuntimeExtensionDescriptor<'postgres'>[]; readonly middleware?: readonly SqlMiddleware[]; - readonly mode?: 'strict' | 'permissive'; + readonly mode?: RuntimeMode; readonly log?: Log; } From 2a8447a0903bc4b4f6d23f2d8770485a96e1f33c Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 23 Oct 2026 13:36:35 +0200 Subject: [PATCH 02/19] TML-3689: A stack with a driver always has one SqlExecutionStackWithDriver now requires its driver. createSqlExecutionStack returns it when given a driver, and SqlExecutionStack, which has no driver, when not. toSql and the static entries take SqlExecutionStack. The clients that connect no longer check for a missing driver. The framework ExecutionStack declares its driver optional, so a stack with no driver still instantiates. Signed-off-by: willbot Signed-off-by: Will Madden --- .../4. Runtime & Middleware Framework.md | 2 +- .../src/execution/execution-stack.ts | 2 +- packages/2-sql-family/5-runtime/README.md | 6 +- .../2-sql-family/5-runtime/src/sql-context.ts | 60 ++++++++++++------- packages/2-sql-family/5-runtime/src/to-sql.ts | 4 +- .../test/execution-stack.types.test-d.ts | 37 ++++++++++++ packages/2-sql-family/5-runtime/test/utils.ts | 12 ++-- .../supabase/src/runtime/supabase.ts | 4 -- .../src/runtime/postgres-serverless.ts | 3 - .../postgres/9-facade/src/runtime/postgres.ts | 3 - .../9-facade/src/static/postgres-static.ts | 4 +- .../sqlite/9-facade/src/runtime/sqlite.ts | 3 - .../9-facade/src/static/sqlite-static.ts | 4 +- 13 files changed, 93 insertions(+), 51 deletions(-) create mode 100644 packages/2-sql-family/5-runtime/test/execution-stack.types.test-d.ts diff --git a/docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md b/docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md index 88172dbf2399..0b0f1d02b450 100644 --- a/docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md +++ b/docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md @@ -635,7 +635,7 @@ The runtime does not own pooling or sockets; it delegates to a target driver. Th **Lifecycle:** instantiate stack with driver → connect at boundary → create runtime ([ADR 159](../adrs/ADR%20159%20-%20Driver%20Terminology%20and%20Lifecycle.md)). 1. Create - - `createSqlExecutionStack({ target, adapter, driver, extensions })` - Creates SQL descriptor stack. + - `createSqlExecutionStack({ target, adapter, driver, extensions })` - Creates SQL descriptor stack: a `SqlExecutionStackWithDriver` when given a driver, for clients that connect, and a `SqlExecutionStack` without one, for the query surface and `toSql`. - `createExecutionContext({ contract, stack })` - Creates context from contract and descriptors-only stack. Context creation uses descriptor `SqlStaticContributions` (codecs, operation signatures, parameterized codecs) — no instantiation required. - `instantiateExecutionStack(stack)` - Instantiates all components including driver (unbound). - Caller resolves binding from options and calls `driver.connect(binding)` at the boundary where env is available. diff --git a/packages/1-framework/1-core/framework-components/src/execution/execution-stack.ts b/packages/1-framework/1-core/framework-components/src/execution/execution-stack.ts index 3c9237566ba6..54ba58752d88 100644 --- a/packages/1-framework/1-core/framework-components/src/execution/execution-stack.ts +++ b/packages/1-framework/1-core/framework-components/src/execution/execution-stack.ts @@ -29,7 +29,7 @@ export interface ExecutionStack< > { readonly target: RuntimeTargetDescriptor; readonly adapter: RuntimeAdapterDescriptor; - readonly driver: + readonly driver?: | RuntimeDriverDescriptor | undefined; readonly extensions: readonly RuntimeExtensionDescriptor< diff --git a/packages/2-sql-family/5-runtime/README.md b/packages/2-sql-family/5-runtime/README.md index 53655c261b95..71610922b80f 100644 --- a/packages/2-sql-family/5-runtime/README.md +++ b/packages/2-sql-family/5-runtime/README.md @@ -82,7 +82,7 @@ const db = postgres({ ### Context - `createExecutionContext` - Create an execution context from contract + descriptors-only stack -- `createSqlExecutionStack` - SQL-specific stack factory that preserves descriptor types +- `createSqlExecutionStack` - SQL-specific stack factory that preserves descriptor types. Given a driver it returns `SqlExecutionStackWithDriver`; without one it returns `SqlExecutionStack` - `ExecutionContext` - Context type for SQL operations ### Descriptors & Stack @@ -90,8 +90,8 @@ const db = postgres({ - `SqlStaticContributions` - Interface for descriptor-level static contributions (codecs, operations, parameterized codecs, mutation default generators typed by the framework's `RuntimeMutationDefaultGenerator` from `@internal/framework-components/runtime`) - `SqlRuntimeTargetDescriptor`, `SqlRuntimeAdapterDescriptor`, `SqlRuntimeExtensionDescriptor` - Structural descriptor types requiring `SqlStaticContributions` - `SqlRuntimeAdapterInstance`, `SqlRuntimeDriverInstance`, `SqlRuntimeExtensionInstance` - Instance types -- `SqlExecutionStack` - Descriptors-only stack type for static context creation -- `SqlExecutionStackWithDriver` - Descriptor stack including driver for runtime instantiation +- `SqlExecutionStack` - Stack with no driver, for the query surface, static entries and `toSql` +- `SqlExecutionStackWithDriver` - Stack whose driver is required, for clients that connect - `RuntimeParameterizedCodecDescriptor` - Parameterized codec descriptor type ### Codecs diff --git a/packages/2-sql-family/5-runtime/src/sql-context.ts b/packages/2-sql-family/5-runtime/src/sql-context.ts index 36df3cfd8607..349ccf29d132 100644 --- a/packages/2-sql-family/5-runtime/src/sql-context.ts +++ b/packages/2-sql-family/5-runtime/src/sql-context.ts @@ -12,17 +12,16 @@ import { checkContractComponentRequirements, mergeCapabilityMatrices, } from '@internal/framework-components/components'; -import { - createExecutionStack, - type ExecutionStack, - type RuntimeAdapterDescriptor, - type RuntimeAdapterInstance, - type RuntimeDriverDescriptor, - type RuntimeDriverInstance, - type RuntimeExtensionDescriptor, - type RuntimeExtensionInstance, - type RuntimeTargetDescriptor, - type RuntimeTargetInstance, +import type { + ExecutionStack, + RuntimeAdapterDescriptor, + RuntimeAdapterInstance, + RuntimeDriverDescriptor, + RuntimeDriverInstance, + RuntimeExtensionDescriptor, + RuntimeExtensionInstance, + RuntimeTargetDescriptor, + RuntimeTargetInstance, } from '@internal/framework-components/execution'; import { applyMutationDefaults, @@ -121,9 +120,12 @@ export type SqlExecutionStackWithDriver = Omi > & { readonly target: SqlRuntimeTargetDescriptor; readonly adapter: SqlRuntimeAdapterDescriptor>; - readonly driver: - | RuntimeDriverDescriptor<'sql', TTargetId, unknown, SqlRuntimeDriverInstance> - | undefined; + readonly driver: RuntimeDriverDescriptor< + 'sql', + TTargetId, + unknown, + SqlRuntimeDriverInstance + >; readonly extensions: readonly SqlRuntimeExtensionDescriptor[]; }; @@ -145,20 +147,32 @@ export type SqlRuntimeDriverInstance = Runtim > & SqlDriver; -export function createSqlExecutionStack(options: { +interface SqlExecutionStackOptions { readonly target: SqlRuntimeTargetDescriptor; readonly adapter: SqlRuntimeAdapterDescriptor; - readonly driver?: - | RuntimeDriverDescriptor<'sql', TTargetId, unknown, SqlRuntimeDriverInstance> - | undefined; readonly extensions?: readonly SqlRuntimeExtensionDescriptor[] | undefined; -}): SqlExecutionStackWithDriver { - return createExecutionStack({ +} + +/** Builds the stack a client connects with when given a driver, and the stack that builds the query surface and `toSql` without one. */ +export function createSqlExecutionStack( + options: SqlExecutionStackOptions & { + readonly driver: SqlExecutionStackWithDriver['driver']; + }, +): SqlExecutionStackWithDriver; +export function createSqlExecutionStack( + options: SqlExecutionStackOptions, +): SqlExecutionStack; +export function createSqlExecutionStack( + options: SqlExecutionStackOptions & { + readonly driver?: SqlExecutionStackWithDriver['driver']; + }, +): SqlExecutionStack | SqlExecutionStackWithDriver { + const stack = { target: options.target, adapter: options.adapter, - driver: options.driver, - extensions: options.extensions, - }); + extensions: options.extensions ?? [], + }; + return options.driver === undefined ? stack : { ...stack, driver: options.driver }; } export type { ExecutionContext }; diff --git a/packages/2-sql-family/5-runtime/src/to-sql.ts b/packages/2-sql-family/5-runtime/src/to-sql.ts index 643ba172831c..95b694bc7a40 100644 --- a/packages/2-sql-family/5-runtime/src/to-sql.ts +++ b/packages/2-sql-family/5-runtime/src/to-sql.ts @@ -2,7 +2,7 @@ import type { Contract } from '@internal/contract/types'; import type { SqlStorage } from '@internal/sql-contract/types'; import type { SqlQueryPlan } from '@internal/sql-relational-core/plan'; import { lowerSqlPlan } from './lower-sql-plan'; -import type { SqlExecutionStackWithDriver, SqlRuntimeAdapterInstance } from './sql-context'; +import type { SqlExecutionStack, SqlRuntimeAdapterInstance } from './sql-context'; import { validateSqlPlan } from './sql-family-adapter'; export interface RenderedSql { @@ -20,7 +20,7 @@ export interface ToSqlMember { } export function createToSql( - stack: SqlExecutionStackWithDriver, + stack: SqlExecutionStack, contract: Contract, ): ToSqlMember['toSql'] { let adapter: SqlRuntimeAdapterInstance | undefined; diff --git a/packages/2-sql-family/5-runtime/test/execution-stack.types.test-d.ts b/packages/2-sql-family/5-runtime/test/execution-stack.types.test-d.ts new file mode 100644 index 000000000000..6c56ccd6c702 --- /dev/null +++ b/packages/2-sql-family/5-runtime/test/execution-stack.types.test-d.ts @@ -0,0 +1,37 @@ +import type { RuntimeDriverDescriptor } from '@internal/framework-components/execution'; +import { expectTypeOf, test } from 'vitest'; +import type { + SqlExecutionStack, + SqlExecutionStackWithDriver, + SqlRuntimeDriverInstance, +} from '../src/sql-context'; +import { createSqlExecutionStack } from '../src/sql-context'; +import { + createStubAdapter, + createTestAdapterDescriptor, + createTestTargetDescriptor, +} from './utils'; + +declare const driver: RuntimeDriverDescriptor< + 'sql', + 'postgres', + unknown, + SqlRuntimeDriverInstance<'postgres'> +>; + +const target = createTestTargetDescriptor(); +const adapter = createTestAdapterDescriptor(createStubAdapter()); + +test('a stack with a driver always has one', () => { + expectTypeOf().not.toBeNullable(); + expectTypeOf(createSqlExecutionStack({ target, adapter, driver })).toEqualTypeOf< + SqlExecutionStackWithDriver<'postgres'> + >(); +}); + +test('a stack built without a driver has no driver member', () => { + expectTypeOf(createSqlExecutionStack({ target, adapter })).toEqualTypeOf< + SqlExecutionStack<'postgres'> + >(); + expectTypeOf().not.toHaveProperty('driver'); +}); diff --git a/packages/2-sql-family/5-runtime/test/utils.ts b/packages/2-sql-family/5-runtime/test/utils.ts index 73203e93c231..041f925a4aa0 100644 --- a/packages/2-sql-family/5-runtime/test/utils.ts +++ b/packages/2-sql-family/5-runtime/test/utils.ts @@ -394,14 +394,18 @@ export function createTestStackInstance(options?: { SqlRuntimeDriverInstance<'postgres'> >; }) { - const stack = createSqlExecutionStack({ + const parts = { target: createTestTargetDescriptor(), adapter: createTestAdapterDescriptor(createStubAdapter()), - driver: options?.driver, extensions: options?.extensions ?? [], - }); + }; + const driver = options?.driver; - return instantiateExecutionStack(stack); + return instantiateExecutionStack( + driver === undefined + ? createSqlExecutionStack(parts) + : createSqlExecutionStack({ ...parts, driver }), + ); } /** diff --git a/packages/3-extensions/supabase/src/runtime/supabase.ts b/packages/3-extensions/supabase/src/runtime/supabase.ts index 4abf563f425a..9ed3376e1981 100644 --- a/packages/3-extensions/supabase/src/runtime/supabase.ts +++ b/packages/3-extensions/supabase/src/runtime/supabase.ts @@ -53,7 +53,6 @@ import { import postgresTarget, { PostgresContractSerializer } from '@internal/target-postgres/runtime'; import { blindCast } from '@internal/utils/casts'; import { ifDefined } from '@internal/utils/defined'; -import { InternalError } from '@internal/utils/internal-error'; import { structuredError } from '@internal/utils/structured-error'; import { createRemoteJWKSet, decodeProtectedHeader, type JWTVerifyResult, jwtVerify } from 'jose'; import { Pool } from 'pg'; @@ -394,9 +393,6 @@ export default async function supabase< } const driverDescriptor = stack.driver; - if (!driverDescriptor) { - throw new InternalError('Driver descriptor missing from execution stack'); - } const adapter = stack.adapter.create(stack); const driver = driverDescriptor.create({ cursor: { disabled: true } }); const sharedOptions = { diff --git a/packages/3-targets/postgres/9-facade/src/runtime/postgres-serverless.ts b/packages/3-targets/postgres/9-facade/src/runtime/postgres-serverless.ts index f7d13b061a76..bd67180ec80c 100644 --- a/packages/3-targets/postgres/9-facade/src/runtime/postgres-serverless.ts +++ b/packages/3-targets/postgres/9-facade/src/runtime/postgres-serverless.ts @@ -193,9 +193,6 @@ export default function postgresServerless< const url = validatePostgresUrl(binding.url); const driverDescriptor = stack.driver; - if (!driverDescriptor) { - throw new InternalError('Driver descriptor missing from execution stack'); - } const adapter = stack.adapter.create(stack); const driver = driverDescriptor.create({ diff --git a/packages/3-targets/postgres/9-facade/src/runtime/postgres.ts b/packages/3-targets/postgres/9-facade/src/runtime/postgres.ts index cf2edcf16f20..f926fc405c5a 100644 --- a/packages/3-targets/postgres/9-facade/src/runtime/postgres.ts +++ b/packages/3-targets/postgres/9-facade/src/runtime/postgres.ts @@ -185,9 +185,6 @@ export default function postgres< const adapter = stack.adapter.create(stack); const driverDescriptor = stack.driver; - if (!driverDescriptor) { - throw new InternalError('Driver descriptor missing from execution stack'); - } const driver = driverDescriptor.create({ cursor: toDriverCursorOptions(cursor), diff --git a/packages/3-targets/postgres/9-facade/src/static/postgres-static.ts b/packages/3-targets/postgres/9-facade/src/static/postgres-static.ts index d8bd09b8b114..b7475c22f3e7 100644 --- a/packages/3-targets/postgres/9-facade/src/static/postgres-static.ts +++ b/packages/3-targets/postgres/9-facade/src/static/postgres-static.ts @@ -6,7 +6,7 @@ import type { Db, RawLane } from '@internal/sql-builder/types'; import type { SqlStorage } from '@internal/sql-contract/types'; import type { ExecutionContext, - SqlExecutionStackWithDriver, + SqlExecutionStack, SqlRuntimeExtensionDescriptor, ToSqlMember, } from '@internal/sql-runtime'; @@ -33,7 +33,7 @@ export interface PostgresStaticContext> e export function buildPostgresStaticContext>( contract: TContract, context: ExecutionContext, - stack: SqlExecutionStackWithDriver, + stack: SqlExecutionStack, ): PostgresStaticContext { const { rawCodecInferer } = stack.adapter; const sqlDb: Db = sql({ context, rawCodecInferer }); diff --git a/packages/3-targets/sqlite/9-facade/src/runtime/sqlite.ts b/packages/3-targets/sqlite/9-facade/src/runtime/sqlite.ts index 60f4abd74604..78f941971f17 100644 --- a/packages/3-targets/sqlite/9-facade/src/runtime/sqlite.ts +++ b/packages/3-targets/sqlite/9-facade/src/runtime/sqlite.ts @@ -285,9 +285,6 @@ export default function sqlite< const adapter = stack.adapter.create(stack); const driverDescriptor = stack.driver; - if (!driverDescriptor) { - throw new InternalError('Driver descriptor missing from execution stack'); - } const driver = driverDescriptor.create(); ownedDispose = () => driver.close(); diff --git a/packages/3-targets/sqlite/9-facade/src/static/sqlite-static.ts b/packages/3-targets/sqlite/9-facade/src/static/sqlite-static.ts index 673b3e333109..36805751ef80 100644 --- a/packages/3-targets/sqlite/9-facade/src/static/sqlite-static.ts +++ b/packages/3-targets/sqlite/9-facade/src/static/sqlite-static.ts @@ -7,7 +7,7 @@ import type { Db, RawLane } from '@internal/sql-builder/types'; import type { SqlStorage } from '@internal/sql-contract/types'; import type { ExecutionContext, - SqlExecutionStackWithDriver, + SqlExecutionStack, SqlRuntimeExtensionDescriptor, ToSqlMember, } from '@internal/sql-runtime'; @@ -38,7 +38,7 @@ export interface SqliteStaticContext> ext export function buildSqliteStaticContext>( contract: TContract, context: ExecutionContext, - stack: SqlExecutionStackWithDriver, + stack: SqlExecutionStack, ): SqliteStaticContext { const { rawCodecInferer } = stack.adapter; const sqlNamespace = sql({ context, rawCodecInferer })[UNBOUND_NAMESPACE_ID]; From dd0b29f0c42105ba5d74f33e20f7de53336574ce Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 23 Oct 2026 13:39:40 +0200 Subject: [PATCH 03/19] TML-3689: Record upgrade instructions for the required stack driver Signed-off-by: willbot Signed-off-by: Will Madden --- .../stack-driver-required/detection.test.ts | 36 +++++++++++++++++++ .../extension/instructions.md | 27 ++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts create mode 100644 upgrade-instructions/pending/stack-driver-required/extension/instructions.md diff --git a/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts b/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts new file mode 100644 index 000000000000..bebbd33d45e9 --- /dev/null +++ b/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts @@ -0,0 +1,36 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'pathe'; +import { describe, expect, it } from 'vitest'; + +const GUIDE_PATH = 'upgrade-instructions/pending/stack-driver-required/extension/instructions.md'; +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../../..'); +const id = 'sql-stack-with-driver-requires-driver'; + +function detects(content: string): boolean { + const guide = readFileSync(join(repoRoot, GUIDE_PATH), 'utf-8'); + const entry = guide.split(/\n {2}- id: /).find((block) => block.startsWith(`${id}\n`)); + if (entry === undefined) throw new Error(`The guide has no change ${id}`); + const patterns = [...entry.matchAll(/^ {8}- '(.*)'$/gm)].map( + ([, pattern]) => new RegExp((pattern ?? '').replaceAll("''", "'")), + ); + if (patterns.length === 0) throw new Error(`The change ${id} has no detection`); + return patterns.some((pattern) => pattern.test(content)); +} + +describe('the extension detection for a required stack driver', () => { + it.each([ + ['the type name', 'function build(stack: SqlExecutionStackWithDriver) {}'], + ['a missing-driver check', 'if (!stack.driver) throw new Error();'], + ])('finds %s', (_name, content) => { + expect(detects(content)).toBe(true); + }); + + it.each([ + ['the stack with no driver', 'function build(stack: SqlExecutionStack) {}'], + ['a longer name', 'type MySqlExecutionStackWithDriverFactory = () => void;'], + ['another object', 'const d = runtimeStack.driverName;'], + ])('skips %s', (_name, content) => { + expect(detects(content)).toBe(false); + }); +}); diff --git a/upgrade-instructions/pending/stack-driver-required/extension/instructions.md b/upgrade-instructions/pending/stack-driver-required/extension/instructions.md new file mode 100644 index 000000000000..0c6e859c0024 --- /dev/null +++ b/upgrade-instructions/pending/stack-driver-required/extension/instructions.md @@ -0,0 +1,27 @@ +--- +changes: + - id: sql-stack-with-driver-requires-driver + summary: | + `SqlExecutionStackWithDriver` now requires its `driver`, and `createSqlExecutionStack(...)` returns it only when given a driver. Without a driver it returns `SqlExecutionStack`, which has no `driver` member. `createToSql(stack, contract)` takes `SqlExecutionStack`. Type a stack built without a driver as `SqlExecutionStack`, and remove checks such as `if (!stack.driver) throw ...` on a stack typed `SqlExecutionStackWithDriver`, which can no longer fail. The framework `ExecutionStack` now declares `driver` optional. The detection matches files that name `SqlExecutionStackWithDriver` or read `stack.driver`. + detection: + glob: "**/*.{ts,mts,cts,tsx}" + matches: + - '(? = createSqlExecutionStack({ target, adapter }); +if (!stack.driver) throw new Error('Driver descriptor missing from execution stack'); + +// After +const staticStack: SqlExecutionStack<'postgres'> = createSqlExecutionStack({ target, adapter }); +const clientStack = createSqlExecutionStack({ target, adapter, driver }); +const driverInstance = clientStack.driver.create(); +``` From 9449d14eb34de4ea6b591b938466b14245adfc54 Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 23 Oct 2026 13:49:38 +0200 Subject: [PATCH 04/19] TML-3686: The published mongoQuery validates its contract once @prisma/orm-mongo/query-builder now serves the MongoDB facade mongoQuery, which validates with validateMongoContract, the validator and error of mongo({ contract }), and skips a value already validated. The internal query builder is unchanged, and every other export of the subpath still reaches users. Signed-off-by: willbot Signed-off-by: Will Madden --- .../subsystems/10. MongoDB Family.md | 2 + .../0-shared/publish-surface/src/shells.ts | 2 +- .../3-targets/mongo/9-facade/package.json | 1 + .../9-facade/src/exports/query-builder.ts | 2 + .../mongo/9-facade/src/static/mongo-query.ts | 19 +++++++ .../mongo/9-facade/test/mongo-query.test.ts | 53 +++++++++++++++++++ .../3-targets/mongo/9-facade/tsdown.config.ts | 1 + .../detection.test.ts | 37 +++++++++++++ .../app/instructions.md | 20 +++++++ 9 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 packages/3-targets/mongo/9-facade/src/exports/query-builder.ts create mode 100644 packages/3-targets/mongo/9-facade/src/static/mongo-query.ts create mode 100644 packages/3-targets/mongo/9-facade/test/mongo-query.test.ts create mode 100644 test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts create mode 100644 upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md diff --git a/docs/architecture docs/subsystems/10. MongoDB Family.md b/docs/architecture docs/subsystems/10. MongoDB Family.md index be183b947b72..30006a535555 100644 --- a/docs/architecture docs/subsystems/10. MongoDB Family.md +++ b/docs/architecture docs/subsystems/10. MongoDB Family.md @@ -354,6 +354,8 @@ Row type inference, custom collection subclasses, and the `include` interface al Below the ORM and above the raw command AST sits `@internal/mongo-query-builder`: a contract-bound, typed CRUD surface that produces the same `MongoQueryPlan` values the ORM emits. The ORM is model-centric and multi-collection (roots, includes, repositories); the query builder is collection-centric and single-command (reads, writes, find-and-modify, `$merge` / `$out`). Migrations, raw tooling, and other non-ORM callers author plans through the query builder rather than hand-building AST nodes. +The internal `mongoQuery` trusts the contract it is given. The `mongoQuery` that `@prisma/orm-mongo/query-builder` publishes validates the contract first, with the same validator and error as `mongo({ contract })`, and skips a value that was already validated, such as one from `contractFromJson`. + ```typescript import { acc, mongoQuery } from '@internal/mongo-query-builder'; diff --git a/packages/0-shared/publish-surface/src/shells.ts b/packages/0-shared/publish-surface/src/shells.ts index 720ccae0437c..2b998125dd55 100644 --- a/packages/0-shared/publish-surface/src/shells.ts +++ b/packages/0-shared/publish-surface/src/shells.ts @@ -276,7 +276,7 @@ const SQL_QUERY_REEXPORTS: readonly ShellReexportMapping[] = [ */ const MONGO_QUERY_REEXPORTS: readonly ShellReexportMapping[] = [ { package: '@internal/mongo-orm', entry: 'orm' }, - { package: '@internal/mongo-query-builder', entry: 'query-builder' }, + { package: '@internal/mongo-query-builder', entry: 'query-builder', root: false }, { package: '@internal/mongo-query-ast', entry: 'query-ast' }, { package: '@internal/mongo-value', entry: 'value' }, ]; diff --git a/packages/3-targets/mongo/9-facade/package.json b/packages/3-targets/mongo/9-facade/package.json index 2a49456ca522..0c3746aa7cbc 100644 --- a/packages/3-targets/mongo/9-facade/package.json +++ b/packages/3-targets/mongo/9-facade/package.json @@ -69,6 +69,7 @@ "./contract-builder": "./dist/contract-builder.mjs", "./control": "./dist/control.mjs", "./family": "./dist/family.mjs", + "./query-builder": "./dist/query-builder.mjs", "./runtime": "./dist/runtime.mjs", "./static": "./dist/static.mjs", "./target": "./dist/target.mjs", diff --git a/packages/3-targets/mongo/9-facade/src/exports/query-builder.ts b/packages/3-targets/mongo/9-facade/src/exports/query-builder.ts new file mode 100644 index 000000000000..beb712d27e2d --- /dev/null +++ b/packages/3-targets/mongo/9-facade/src/exports/query-builder.ts @@ -0,0 +1,2 @@ +export * from '@internal/mongo-query-builder'; +export { mongoQuery } from '../static/mongo-query'; diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-query.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-query.ts new file mode 100644 index 000000000000..fece596184e4 --- /dev/null +++ b/packages/3-targets/mongo/9-facade/src/static/mongo-query.ts @@ -0,0 +1,19 @@ +import type { + AnyMongoTypeMaps, + MongoContract, + MongoContractWithTypeMaps, +} from '@internal/mongo-contract'; +import { + type QueryRoot, + mongoQuery as queryOnValidatedContract, +} from '@internal/mongo-query-builder'; +import { validateMongoContract } from './contract-from-json'; + +/** + * The typed query builder for `contract`. The contract is validated as `mongo({ contract })` validates it, once per contract value. + */ +export function mongoQuery< + TContract extends MongoContractWithTypeMaps, +>(options: { readonly contract: TContract }): QueryRoot { + return queryOnValidatedContract({ contract: validateMongoContract(options.contract) }); +} diff --git a/packages/3-targets/mongo/9-facade/test/mongo-query.test.ts b/packages/3-targets/mongo/9-facade/test/mongo-query.test.ts new file mode 100644 index 000000000000..aeae9ff405c3 --- /dev/null +++ b/packages/3-targets/mongo/9-facade/test/mongo-query.test.ts @@ -0,0 +1,53 @@ +import { MongoContractSerializer } from '@internal/family-mongo/ir'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { Contract } from '../../../../2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract'; +import fixtureContractJson from '../../../../2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.json' with { + type: 'json', +}; +import { mongoQuery, PipelineChain } from '../src/exports/query-builder'; +import mongo from '../src/exports/runtime'; +import { contractFromJson } from '../src/exports/static'; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +function thrownBy(run: () => unknown): unknown { + try { + run(); + } catch (error) { + return error; + } + throw new Error('nothing was thrown'); +} + +describe('mongoQuery({ contract })', () => { + it('rejects a malformed contract with the error mongo({ contract }) throws', () => { + const malformed = { target: 'mongo' } as unknown as Contract; + + const fromQuery = thrownBy(() => mongoQuery({ contract: malformed })); + const fromClient = thrownBy(() => mongo({ contract: malformed })); + + expect(fromQuery).toMatchObject({ code: 'CONTRACT.VALIDATION_FAILED' }); + expect({ + code: (fromQuery as { code: unknown }).code, + message: (fromQuery as Error).message, + }).toEqual({ + code: (fromClient as { code: unknown }).code, + message: (fromClient as Error).message, + }); + }); + + it('does not validate a contractFromJson value again', () => { + const contract = contractFromJson(structuredClone(fixtureContractJson)); + const deserialize = vi.spyOn(MongoContractSerializer.prototype, 'deserializeContract'); + + mongoQuery({ contract }); + + expect(deserialize).not.toHaveBeenCalled(); + }); + + it('still offers the rest of the query builder', () => { + expect(PipelineChain).toBeTypeOf('function'); + }); +}); diff --git a/packages/3-targets/mongo/9-facade/tsdown.config.ts b/packages/3-targets/mongo/9-facade/tsdown.config.ts index 3638dc0cbf61..eecbd9c1d392 100644 --- a/packages/3-targets/mongo/9-facade/tsdown.config.ts +++ b/packages/3-targets/mongo/9-facade/tsdown.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ 'src/exports/contract-builder.ts', 'src/exports/control.ts', 'src/exports/family.ts', + 'src/exports/query-builder.ts', 'src/exports/runtime.ts', 'src/exports/static.ts', 'src/exports/target.ts', diff --git a/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts b/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts new file mode 100644 index 000000000000..45523b32b97f --- /dev/null +++ b/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'pathe'; +import { describe, expect, it } from 'vitest'; + +const GUIDE_PATH = + 'upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md'; +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../../..'); +const id = 'mongo-query-validates-contract'; + +function detects(content: string): boolean { + const guide = readFileSync(join(repoRoot, GUIDE_PATH), 'utf-8'); + const entry = guide.split(/\n {2}- id: /).find((block) => block.startsWith(`${id}\n`)); + if (entry === undefined) throw new Error(`The guide has no change ${id}`); + const patterns = [...entry.matchAll(/^ {8}- '(.*)'$/gm)].map( + ([, pattern]) => new RegExp((pattern ?? '').replaceAll("''", "'")), + ); + if (patterns.length === 0) throw new Error(`The change ${id} has no detection`); + return patterns.some((pattern) => pattern.test(content)); +} + +describe('the mongoQuery detection', () => { + it.each([ + ['a call', 'const query = mongoQuery({ contract });'], + ['a call with a space', 'mongoQuery ({ contract })'], + ])('finds %s', (_name, content) => { + expect(detects(content)).toBe(true); + }); + + it.each([ + ['a method of the same name', 'db.mongoQuery({ contract })'], + ['a longer name', 'myMongoQuery({ contract })'], + ['an import only', "import { acc } from 'somewhere';"], + ])('skips %s', (_name, content) => { + expect(detects(content)).toBe(false); + }); +}); diff --git a/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md b/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md new file mode 100644 index 000000000000..d3e8602509d2 --- /dev/null +++ b/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md @@ -0,0 +1,20 @@ +--- +changes: + - id: mongo-query-validates-contract + summary: | + `mongoQuery({ contract })` from `@prisma/orm-mongo/query-builder` now validates the contract as `mongo({ contract })` does, and throws `CONTRACT.VALIDATION_FAILED` for a malformed one. A value from `contractFromJson(...)` or `defineContract(...)` is not validated again. If you pass raw `contract.json`, or an object you built by hand, pass `contractFromJson(json)` instead. The detection matches files that call `mongoQuery(`. + detection: + glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs}" + matches: + - '(?(contractJson) }); +``` From 2480add0723fa53142e6d1e5b0421a2003ec66f1 Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 23 Oct 2026 13:53:33 +0200 Subject: [PATCH 05/19] TML-3686: Register the MongoDB facade query-builder entry in the architecture map Signed-off-by: willbot Signed-off-by: Will Madden --- architecture.config.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/architecture.config.json b/architecture.config.json index 7e186154fa06..2c71c3a67dc9 100644 --- a/architecture.config.json +++ b/architecture.config.json @@ -1596,6 +1596,12 @@ "layer": "adapters", "plane": "runtime" }, + { + "glob": "packages/3-targets/mongo/9-facade/src/exports/query-builder.ts", + "domain": "extensions", + "layer": "adapters", + "plane": "runtime" + }, { "glob": "packages/3-targets/mongo/9-facade/src/exports/target.ts", "domain": "extensions", From d96b2ea45673a563212bb0eabf1793e0d80f8782 Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 23 Oct 2026 14:08:43 +0200 Subject: [PATCH 06/19] TML-3689: SqlExecutionStackWithDriver is SqlExecutionStack with a required driver Signed-off-by: willbot Signed-off-by: Will Madden --- .../2-sql-family/5-runtime/src/sql-context.ts | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/packages/2-sql-family/5-runtime/src/sql-context.ts b/packages/2-sql-family/5-runtime/src/sql-context.ts index 349ccf29d132..550d86b2ff48 100644 --- a/packages/2-sql-family/5-runtime/src/sql-context.ts +++ b/packages/2-sql-family/5-runtime/src/sql-context.ts @@ -13,7 +13,6 @@ import { mergeCapabilityMatrices, } from '@internal/framework-components/components'; import type { - ExecutionStack, RuntimeAdapterDescriptor, RuntimeAdapterInstance, RuntimeDriverDescriptor, @@ -108,26 +107,15 @@ export interface SqlExecutionStack { readonly extensions: readonly SqlRuntimeExtensionDescriptor[]; } -export type SqlExecutionStackWithDriver = Omit< - ExecutionStack< - 'sql', - TTargetId, - SqlRuntimeAdapterInstance, - SqlRuntimeDriverInstance, - SqlRuntimeExtensionInstance - >, - 'target' | 'adapter' | 'driver' | 'extensions' -> & { - readonly target: SqlRuntimeTargetDescriptor; - readonly adapter: SqlRuntimeAdapterDescriptor>; - readonly driver: RuntimeDriverDescriptor< - 'sql', - TTargetId, - unknown, - SqlRuntimeDriverInstance - >; - readonly extensions: readonly SqlRuntimeExtensionDescriptor[]; -}; +export type SqlExecutionStackWithDriver = + SqlExecutionStack & { + readonly driver: RuntimeDriverDescriptor< + 'sql', + TTargetId, + unknown, + SqlRuntimeDriverInstance + >; + }; export interface SqlRuntimeExtensionInstance extends RuntimeExtensionInstance<'sql', TTargetId> {} From 8fda81f4ef7b058a98a0e64000399fa3e7dbb397 Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 23 Oct 2026 14:08:52 +0200 Subject: [PATCH 07/19] TML-3689: Docs show the runtime path the clients use, not instantiateExecutionStack Signed-off-by: willbot Signed-off-by: Will Madden --- .../4. Runtime & Middleware Framework.md | 4 ++-- packages/2-sql-family/9-family/README.md | 2 +- packages/3-extensions/pgvector/README.md | 15 +++------------ packages/3-targets/postgres/9-facade/README.md | 4 ++-- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md b/docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md index 0b0f1d02b450..a91c8ed8fb2f 100644 --- a/docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md +++ b/docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md @@ -632,12 +632,12 @@ TPlan The runtime does not own pooling or sockets; it delegates to a target driver. This thin-core approach keeps the runtime simple and stable while adapters and packs carry dialect and capability logic. -**Lifecycle:** instantiate stack with driver → connect at boundary → create runtime ([ADR 159](../adrs/ADR%20159%20-%20Driver%20Terminology%20and%20Lifecycle.md)). +**Lifecycle:** create the adapter and driver from the stack's descriptors → connect at boundary → create runtime ([ADR 159](../adrs/ADR%20159%20-%20Driver%20Terminology%20and%20Lifecycle.md)). 1. Create - `createSqlExecutionStack({ target, adapter, driver, extensions })` - Creates SQL descriptor stack: a `SqlExecutionStackWithDriver` when given a driver, for clients that connect, and a `SqlExecutionStack` without one, for the query surface and `toSql`. - `createExecutionContext({ contract, stack })` - Creates context from contract and descriptors-only stack. Context creation uses descriptor `SqlStaticContributions` (codecs, operation signatures, parameterized codecs) — no instantiation required. - - `instantiateExecutionStack(stack)` - Instantiates all components including driver (unbound). + - `stack.adapter.create(stack)` and `stack.driver.create(options)` - The client creates the adapter and the unbound driver from the stack's descriptors. `instantiateExecutionStack(stack)` from the framework does the same for every component, and tests use it. - Caller resolves binding from options and calls `driver.connect(binding)` at the boundary where env is available. - Target factory (e.g. `postgres()`) constructs `new PostgresRuntimeImpl({ context, adapter, driver, verifyMarker, middleware })` — a concrete subclass of `SqlRuntimeBase` / `RuntimeCore`. There is no family-level `createRuntime` factory. - Validates `contract.json` and caches `storageHash` / `profileHash` (contract is already validated before being passed to context). diff --git a/packages/2-sql-family/9-family/README.md b/packages/2-sql-family/9-family/README.md index 7bd46ef5587a..cf56b35d9b96 100644 --- a/packages/2-sql-family/9-family/README.md +++ b/packages/2-sql-family/9-family/README.md @@ -139,7 +139,7 @@ The runner returns structured errors with the following codes: - **`./control-adapter`**: SQL control adapter interface (`SqlControlAdapter`, `SqlControlAdapterDescriptor`) for target-specific adapters - **`./psl-build`**: PSL building blocks both `contract infer` and `contract print` use, with no dialect knowledge: `mapDefault` (a stored default as the PSL attribute that reads back as it), `toEnumMemberName`, `inferredTypeConstructors` (the constructor marked `inferred` for each data type of a stack), `fewestArgumentsCall` and `pslTypeArgumentText` (the call infer and print write for a column's parameters), and `inferColumnTypes` (each introspected column's type, through the data type that claims its reported type, failing with `CONTRACT.INFER_COLUMN_TYPE_UNCLAIMED` on every column none writes) - **`./psl-infer`**: Database-to-PSL inference utilities for `contract infer`: name transforms, relation inference, and the printer-config types -- **`./runtime`**: Runtime plane identity exports only (family ID, types, descriptor identity). Does **not** export runtime creation helpers—use `instantiateExecutionStack` from `@internal/framework-components/execution` and `createExecutionContext`, `createRuntime`, `createSqlExecutionStack` from `@internal/sql-runtime`. See [ADR 152](../../../docs/architecture%20docs/adrs/ADR%20152%20-%20Execution%20Plane%20Descriptors%20and%20Instances.md). +- **`./runtime`**: Runtime plane identity exports only (family ID, types, descriptor identity). Does **not** export runtime creation helpers—use a target client such as `postgres()`, which builds its stack with `createSqlExecutionStack`, its context with `createExecutionContext` from `@internal/sql-runtime`, and its runtime from the stack's adapter and driver descriptors. See [ADR 152](../../../docs/architecture%20docs/adrs/ADR%20152%20-%20Execution%20Plane%20Descriptors%20and%20Instances.md). - **`./verify`**: Marker row parsing helper (`parseContractMarkerRow`). Marker reads are owned by each `SqlControlAdapter` (e.g. `PostgresControlAdapter.readMarker`) so dialect-specific SQL stays target-local. ## How to debug `db init` diff --git a/packages/3-extensions/pgvector/README.md b/packages/3-extensions/pgvector/README.md index 41d36ab2cc2e..7a0cf70f7b57 100644 --- a/packages/3-extensions/pgvector/README.md +++ b/packages/3-extensions/pgvector/README.md @@ -88,22 +88,13 @@ The `vector(N)` factory is registered through the unified `CodecDescriptor<{ len ### Runtime Setup -Register the extension when creating your execution stack: +Register the extension when creating the client: ```typescript -import { instantiateExecutionStack } from '@internal/framework-components/execution'; -import { createExecutionContext, createSqlExecutionStack } from '@internal/sql-runtime'; -import postgresAdapter from '@internal/adapter-postgres/runtime'; -import postgresTarget from '@internal/target-postgres/runtime'; +import postgres from '@internal/postgres/runtime'; import pgvector from '@internal/extension-pgvector/runtime'; -const stack = createSqlExecutionStack({ - target: postgresTarget, - adapter: postgresAdapter, - extensions: [pgvector], -}); -const context = createExecutionContext({ contract, stack }); -const stackInstance = instantiateExecutionStack(stack); +const db = postgres({ contract, url, extensions: [pgvector] }); ``` ### Query Usage diff --git a/packages/3-targets/postgres/9-facade/README.md b/packages/3-targets/postgres/9-facade/README.md index 0a36cf6e7d13..60b9cbf84dc1 100644 --- a/packages/3-targets/postgres/9-facade/README.md +++ b/packages/3-targets/postgres/9-facade/README.md @@ -326,11 +326,11 @@ flowchart TD Client --> Bound[Runtime-bound members: orm runtime() transaction prepare] Bound --> Lazy[runtime() on first use] - Lazy --> Instantiate[instantiateExecutionStack] + Lazy --> Instantiate[stack.adapter.create and stack.driver.create] Lazy --> Bind[Resolve binding: url or pg] Bind --> Pool[pg.Pool for url binding] Bind --> Reuse[Reuse Pool or Client for pg binding] - Lazy --> Runtime[createRuntime] + Lazy --> Runtime[new PostgresRuntimeImpl] Runtime --> Target[@internal/target-postgres] Runtime --> Adapter[@internal/adapter-postgres] From 5e05ed3dfdd835d6dcccedc5caf2b985fb830a90 Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 23 Oct 2026 14:08:53 +0200 Subject: [PATCH 08/19] TML-3686: The only mongoQuery a user can import is the validating one The family builder is renamed mongoQueryOnValidatedContract and moved to the internal entry @internal/mongo-query-builder/internal/on-validated-contract, published as @prisma/orm-family-mongo/query-builder/internal/on-validated-contract. The family query-builder root no longer exports any mongoQuery. The upgrade instructions add an extension fragment and find aliased imports and imports from the family package. Signed-off-by: willbot Signed-off-by: Will Madden --- .../subsystems/10. MongoDB Family.md | 5 +- docs/reference/Mongo Pipeline Builder.md | 2 +- .../0-shared/publish-surface/src/shells.ts | 8 +- .../query-builder/DEVELOPING.md | 2 +- .../5-query-builders/query-builder/README.md | 6 +- .../query-builder/package.json | 1 + .../query-builder/src/exports/index.ts | 1 - .../exports/internal-on-validated-contract.ts | 1 + .../query-builder/src/lookup-builder.ts | 2 +- .../query-builder/src/query.ts | 8 +- .../query-builder/src/state-classes.ts | 6 +- .../test/builder-new-stages.test.ts | 4 +- .../query-builder/test/builder.test-d.ts | 120 +++++++++++------- .../query-builder/test/builder.test.ts | 12 +- .../query-builder/test/exports.test.ts | 14 ++ .../test/field-accessor.test-d.ts | 12 +- .../test/find-and-modify.test.ts | 4 +- .../test/pipeline-updates.test.ts | 4 +- .../query-builder/test/raw-command.test.ts | 10 +- .../query-builder/test/result-shape.test.ts | 6 +- .../test/state-machine-surface.test-d.ts | 6 +- .../test/state-machine.test-d.ts | 24 ++-- .../query-builder/test/stored-names.test-d.ts | 8 +- .../query-builder/test/stored-names.test.ts | 4 +- .../test/structured-error-codes.test.ts | 12 +- .../query-builder/test/writes.test.ts | 4 +- .../query-builder/tsdown.config.ts | 1 + .../7-runtime/test/runtime-types.test-d.ts | 6 +- .../mongo/9-facade/src/static/mongo-query.ts | 8 +- .../mongo/9-facade/src/static/mongo-static.ts | 6 +- .../mongo/9-facade/test/mongo.test.ts | 10 +- .../9-facade/test/structured-errors.test.ts | 8 +- .../@prisma/orm-family-mongo/package.json | 1 + .../test/mongo-runtime/query-builder.test.ts | 9 +- .../test/mongo/query-builder.test.ts | 9 +- .../runtime/decode-via-query-builder.test.ts | 9 +- .../mongo/runtime/decode.integration.test.ts | 6 +- .../fixtures/decode-fixture-contract.ts | 4 +- .../detection.test.ts | 57 +++++++-- .../app/instructions.md | 4 +- .../extension/instructions.md | 20 +++ 41 files changed, 277 insertions(+), 167 deletions(-) create mode 100644 packages/2-mongo-family/5-query-builders/query-builder/src/exports/internal-on-validated-contract.ts create mode 100644 packages/2-mongo-family/5-query-builders/query-builder/test/exports.test.ts create mode 100644 upgrade-instructions/pending/mongo-query-validates-contract/extension/instructions.md diff --git a/docs/architecture docs/subsystems/10. MongoDB Family.md b/docs/architecture docs/subsystems/10. MongoDB Family.md index 30006a535555..adbd0173995e 100644 --- a/docs/architecture docs/subsystems/10. MongoDB Family.md +++ b/docs/architecture docs/subsystems/10. MongoDB Family.md @@ -354,10 +354,11 @@ Row type inference, custom collection subclasses, and the `include` interface al Below the ORM and above the raw command AST sits `@internal/mongo-query-builder`: a contract-bound, typed CRUD surface that produces the same `MongoQueryPlan` values the ORM emits. The ORM is model-centric and multi-collection (roots, includes, repositories); the query builder is collection-centric and single-command (reads, writes, find-and-modify, `$merge` / `$out`). Migrations, raw tooling, and other non-ORM callers author plans through the query builder rather than hand-building AST nodes. -The internal `mongoQuery` trusts the contract it is given. The `mongoQuery` that `@prisma/orm-mongo/query-builder` publishes validates the contract first, with the same validator and error as `mongo({ contract })`, and skips a value that was already validated, such as one from `contractFromJson`. +Applications use `mongoQuery` from `@prisma/orm-mongo/query-builder`. It validates the contract first, with the same validator and error as `mongo({ contract })`, and skips a value that was already validated, such as one from `contractFromJson`. The package's own builder, `mongoQueryOnValidatedContract`, is on the internal entry `@internal/mongo-query-builder/internal/on-validated-contract` (published as `@prisma/orm-family-mongo/query-builder/internal/on-validated-contract`) and does not validate. ```typescript -import { acc, mongoQuery } from '@internal/mongo-query-builder'; +import { acc } from '@internal/mongo-query-builder'; +import { mongoQuery } from '@internal/mongo/query-builder'; const q = mongoQuery({ contract }); // from contractFromJson(json) or defineContract diff --git a/docs/reference/Mongo Pipeline Builder.md b/docs/reference/Mongo Pipeline Builder.md index aab8c6c63964..f36a3a474dd2 100644 --- a/docs/reference/Mongo Pipeline Builder.md +++ b/docs/reference/Mongo Pipeline Builder.md @@ -10,7 +10,7 @@ Imagine an `orders` collection where each document has `customerId`, `amount`, a ```ts import { contractFromJson } from '@internal/mongo/static'; -import { mongoQuery } from '@internal/mongo-query-builder'; +import { mongoQuery } from '@internal/mongo/query-builder'; import type { Contract } from './contract'; import contractJson from './contract.json' with { type: 'json' }; diff --git a/packages/0-shared/publish-surface/src/shells.ts b/packages/0-shared/publish-surface/src/shells.ts index 2b998125dd55..018d870cb28f 100644 --- a/packages/0-shared/publish-surface/src/shells.ts +++ b/packages/0-shared/publish-surface/src/shells.ts @@ -276,7 +276,12 @@ const SQL_QUERY_REEXPORTS: readonly ShellReexportMapping[] = [ */ const MONGO_QUERY_REEXPORTS: readonly ShellReexportMapping[] = [ { package: '@internal/mongo-orm', entry: 'orm' }, - { package: '@internal/mongo-query-builder', entry: 'query-builder', root: false }, + { + package: '@internal/mongo-query-builder', + entry: 'query-builder', + root: false, + subpaths: ['contract-free'], + }, { package: '@internal/mongo-query-ast', entry: 'query-ast' }, { package: '@internal/mongo-value', entry: 'value' }, ]; @@ -548,6 +553,7 @@ export const publicShells: ReadonlyMap = new Map< dir: 'packages/2-mongo-family/5-query-builders/query-builder', name: '@internal/mongo-query-builder', entry: 'query-builder', + subpaths: ['.', 'contract-free', 'internal/on-validated-contract'], }, { dir: 'packages/2-mongo-family/6-transport/mongo-lowering', diff --git a/packages/2-mongo-family/5-query-builders/query-builder/DEVELOPING.md b/packages/2-mongo-family/5-query-builders/query-builder/DEVELOPING.md index 8646b77f8a80..98bc790d2d85 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/DEVELOPING.md +++ b/packages/2-mongo-family/5-query-builders/query-builder/DEVELOPING.md @@ -6,7 +6,7 @@ Internal architecture notes for contributors. | File | Responsibility | |------|---------------| -| `query.ts` | Entry point (`mongoQuery`) — validates root, creates initial builder | +| `query.ts` | Entry point (`mongoQueryOnValidatedContract`, internal entry; it trusts the contract it is given) — creates the initial builder | | `builder.ts` | `PipelineBuilder` class — immutable; each stage method returns a new instance with the appended stage | | `types.ts` | Core type machinery: `DocField`, `DocShape`, `ModelToDocShape`, `ResolveRow`, shape transformers (`ProjectedShape`, `GroupedDocShape`, `UnwoundShape`, etc.) | | `field-proxy.ts` | `Proxy`-based `FieldProxy` — intercepts property access to produce `TypedAggExpr` nodes | diff --git a/packages/2-mongo-family/5-query-builders/query-builder/README.md b/packages/2-mongo-family/5-query-builders/query-builder/README.md index c440c2527dae..b9ca466c9c18 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/README.md +++ b/packages/2-mongo-family/5-query-builders/query-builder/README.md @@ -4,8 +4,10 @@ A typed CRUD query builder for MongoDB contracts. Reads, writes, pipeline aggreg ## Quick start +Applications use `mongoQuery` from `@prisma/orm-mongo/query-builder`, which validates the contract first. This package's builder, `mongoQueryOnValidatedContract`, lives on the internal entry `@internal/mongo-query-builder/internal/on-validated-contract` (published as `@prisma/orm-family-mongo/query-builder/internal/on-validated-contract`). It does not validate: pass it only a contract that is already validated, such as one from `contractFromJson`. + ```typescript -import { mongoQuery } from '@internal/mongo-query-builder'; +import { mongoQuery } from '@prisma/orm-mongo/query-builder'; const q = mongoQuery({ contract }); @@ -109,7 +111,7 @@ The chain has three steps: The result-row encoding uses a `ModelArrayField` marker so `ResolveRow` produces `Array` — the field is **not** `unknown[]`. Marker behaviour is unchanged from the legacy shape: clears `UpdateEnabled` and `FindAndModifyEnabled`, sets `LeadingMatch` to `'past-leading'`, preserves the nested-shape parameter `N`. -**Limitation.** Typoed root names (`from('usexxxrs')`) are currently accepted at compile time and rejected at runtime. The constraint is a pre-existing limitation of `Contract.roots: Record` (shared with `mongoQuery.from('badname')`); compile-time rejection is tracked under [TML-2400](https://linear.app/prisma-company/issue/TML-2400). +**Limitation.** Typoed root names (`from('usexxxrs')`) are currently accepted at compile time and rejected at runtime. The constraint is a pre-existing limitation of `Contract.roots: Record` (shared with `mongoQueryOnValidatedContract.from('badname')`); compile-time rejection is tracked under [TML-2400](https://linear.app/prisma-company/issue/TML-2400). ## Field accessor diff --git a/packages/2-mongo-family/5-query-builders/query-builder/package.json b/packages/2-mongo-family/5-query-builders/query-builder/package.json index 35f26263055d..d3efbcc918f0 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/package.json +++ b/packages/2-mongo-family/5-query-builders/query-builder/package.json @@ -45,6 +45,7 @@ "exports": { ".": "./dist/index.mjs", "./contract-free": "./dist/exports/contract-free.mjs", + "./internal/on-validated-contract": "./dist/internal/on-validated-contract.mjs", "./package.json": "./package.json" }, "engines": { diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/exports/index.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/exports/index.ts index ac02cf05bcdd..e28e05e94e9c 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/src/exports/index.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/src/exports/index.ts @@ -24,7 +24,6 @@ export type { } from '../lookup-builder'; export type { FindAndModifyEnabled, UpdateEnabled } from '../markers'; export type { QueryRoot } from '../query'; -export { mongoQuery } from '../query'; export type { ModelArrayField, ModelNestedShape, diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/exports/internal-on-validated-contract.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/exports/internal-on-validated-contract.ts new file mode 100644 index 000000000000..7fcbada09dd1 --- /dev/null +++ b/packages/2-mongo-family/5-query-builders/query-builder/src/exports/internal-on-validated-contract.ts @@ -0,0 +1 @@ +export { mongoQueryOnValidatedContract } from '../query'; diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.ts index 59f32b04a9a0..24c7241dde93 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.ts @@ -112,7 +112,7 @@ export interface LookupBuilder< /** * Type of the `from` callable passed to `PipelineChain.lookup`'s outer * callback. The generic argument is inferred from a string-literal - * argument (the same pattern as `mongoQuery({ contract }).from('orders')`), + * argument (the same pattern as `mongoQueryOnValidatedContract({ contract }).from('orders')`), * which grounds `RootName` into the returned `LookupBuilder` *before* * the inner `on(...)` callback is type-checked. This sequential * inference is what makes `foreign` resolve narrowly to the foreign diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts index 00421d8322da..f4a312b5be6e 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts @@ -10,9 +10,7 @@ import { contractError } from './contract-errors'; import { asMongoContract, type CollectionHandle, createCollectionHandle } from './state-classes'; /** - * Public entry point of the query builder. `mongoQuery(...).from(rootName)` - * yields the root state of the three-state machine - * (`CollectionHandle` → `FilteredCollection` → `PipelineChain`). + * The query builder over a contract that is already validated; it does not validate. Applications use `mongoQuery` from `@prisma/orm-mongo/query-builder`, which validates first. `mongoQueryOnValidatedContract(...).from(rootName)` yields the root state of the three-state machine (`CollectionHandle` → `FilteredCollection` → `PipelineChain`). * * `rawCommand(cmd)` is the escape hatch for cases the typed surface does * not cover (yet) — it accepts any `AnyMongoCommand` (typed CRUD or a @@ -29,7 +27,7 @@ export interface QueryRoot< rawCommand(command: C): MongoQueryPlan; } -export function mongoQuery< +export function mongoQueryOnValidatedContract< TContract extends MongoContractWithTypeMaps, >(options: { readonly contract: TContract }): QueryRoot { const { contract } = options; @@ -43,7 +41,7 @@ export function mongoQuery< if (!storageHash) { throw contractError( 'CONTRACT.VALIDATION_FAILED', - 'Contract is missing storage.storageHash. Pass a validated contract to mongoQuery().', + 'Contract is missing storage.storageHash. Pass a validated contract to mongoQueryOnValidatedContract().', ); } const meta: PlanMeta = { diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/state-classes.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/state-classes.ts index c709642291f4..19bec88fd0cb 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/src/state-classes.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/src/state-classes.ts @@ -86,7 +86,7 @@ function writeMeta(storageHash: string): PlanMeta { /** * Root state of the query-builder state machine. Returned from - * `mongoQuery(...).from(name)` and bound to a single collection. + * `mongoQueryOnValidatedContract(...).from(name)` and bound to a single collection. * * Inherits the entire pipeline-stage surface from `PipelineChain` (since an * empty `CollectionHandle` is observably an empty pipeline). Adds: @@ -634,7 +634,7 @@ export interface BindingContext< /** * Construct a `CollectionHandle` from a validated contract + root name. - * Used by `mongoQuery(...).from(name)` to enter the state machine. + * Used by `mongoQueryOnValidatedContract(...).from(name)` to enter the state machine. */ export function createCollectionHandle< TContract extends MongoContractWithTypeMaps, @@ -662,7 +662,7 @@ export function createCollectionHandle< if (!c.storage?.storageHash) { throw contractError( 'CONTRACT.VALIDATION_FAILED', - 'Contract is missing storage.storageHash. Pass a validated contract to mongoQuery().', + 'Contract is missing storage.storageHash. Pass a validated contract to mongoQueryOnValidatedContract().', ); } return new CollectionHandle( diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts index 82dacde95c49..aac69506def6 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts @@ -23,11 +23,11 @@ import { MongoVectorSearchStage, } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import { testContract } from './fixtures/test-contract'; function createOrdersBuilder() { - return mongoQuery({ contract: testContract }).from('orders'); + return mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); } describe('new stage builder methods', () => { diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts index cf48983fa484..bbeebdc08c5f 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts @@ -3,7 +3,7 @@ import { MongoAggFieldRef, MongoLimitStage } from '@internal/mongo-query-ast/exe import { expectTypeOf } from 'vitest'; import { acc } from '../src/accumulator-helpers'; import { fn } from '../src/expression-helpers'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import type { TContract } from './fixtures/test-contract'; declare const contract: TContract; @@ -21,7 +21,7 @@ type OrderRow = { describe('builder shape tests', () => { it('sort() only accepts keys from current shape', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); const builder = p.from('orders'); builder.sort({ amount: -1 }); builder.sort({ status: 1 }); @@ -30,7 +30,7 @@ describe('builder shape tests', () => { }); it('group() replaces shape — previous fields inaccessible', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); const grouped = p.from('orders').group((f) => ({ _id: f.customerId, total: acc.sum(f.amount), @@ -47,7 +47,7 @@ describe('builder shape tests', () => { }); it('addFields() extends shape', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); const extended = p.from('orders').addFields((f) => ({ fullName: fn.concat(f.status, fn.literal(' ')), })); @@ -57,7 +57,7 @@ describe('builder shape tests', () => { }); it('project() inclusion narrows shape', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); const projected = p.from('orders').project('status', 'amount'); projected.sort({ status: 1 }); @@ -68,7 +68,7 @@ describe('builder shape tests', () => { }); it('count() replaces shape with single field', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); const counted = p.from('orders').count('total'); counted.sort({ total: 1 }); @@ -77,7 +77,7 @@ describe('builder shape tests', () => { }); it('lookup() adds array field and preserves existing fields', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); const withLookup = p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -96,7 +96,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects bad local field at type level (AC-1 TC-1)', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -109,7 +109,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects bad foreign field at type level (AC-1 TC-2)', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -122,7 +122,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects non-leaf returns from on() (AC-2 TC-3)', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -136,11 +136,11 @@ describe('builder shape tests', () => { // Bad-from(name) typo rejection is intentionally not tested here — it is // deferred to TML-2400 (Contract.roots: Record widens - // keyof under intersection, identical to today's mongoQuery.from('bad') + // keyof under intersection, identical to today's mongoQueryOnValidatedContract.from('bad') // behaviour). See spec § Open Questions / Resolved decisions. it('replaceRoot() replaces entire shape', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); type NewShape = { readonly x: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; }; @@ -155,12 +155,12 @@ describe('builder shape tests', () => { describe('resolved row types', () => { it('from() → build() resolves to concrete field types', () => { - const plan = mongoQuery({ contract }).from('orders').build(); + const plan = mongoQueryOnValidatedContract({ contract }).from('orders').build(); expectTypeOf>().toEqualTypeOf(); }); it('match() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .match((f) => f.status.eq('active') as MongoFilterExpr) .build(); @@ -168,17 +168,25 @@ describe('resolved row types', () => { }); it('sort() preserves row type', () => { - const plan = mongoQuery({ contract }).from('orders').sort({ amount: -1 }).build(); + const plan = mongoQueryOnValidatedContract({ contract }) + .from('orders') + .sort({ amount: -1 }) + .build(); expectTypeOf>().toEqualTypeOf(); }); it('limit() / skip() / sample() preserve row type', () => { - const plan = mongoQuery({ contract }).from('orders').limit(10).skip(5).sample(3).build(); + const plan = mongoQueryOnValidatedContract({ contract }) + .from('orders') + .limit(10) + .skip(5) + .sample(3) + .build(); expectTypeOf>().toEqualTypeOf(); }); it('addFields() extends row with new fields at correct types', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .addFields((f) => ({ fullName: fn.concat(f.status, fn.literal(' ')), @@ -198,7 +206,10 @@ describe('resolved row types', () => { }); it('project() inclusion narrows to selected fields at correct types', () => { - const plan = mongoQuery({ contract }).from('orders').project('status', 'amount').build(); + const plan = mongoQueryOnValidatedContract({ contract }) + .from('orders') + .project('status', 'amount') + .build(); expectTypeOf>().toEqualTypeOf<{ _id: string; status: string; @@ -207,7 +218,7 @@ describe('resolved row types', () => { }); it('project() computed includes expression fields at correct types', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .project((f) => ({ status: 1 as const, @@ -224,7 +235,7 @@ describe('resolved row types', () => { }); it('group() resolves _id at grouped-by field type, accumulators at correct types', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .group((f) => ({ _id: f.customerId, @@ -242,17 +253,17 @@ describe('resolved row types', () => { }); it('unwind() preserves row type', () => { - const plan = mongoQuery({ contract }).from('orders').unwind('tags').build(); + const plan = mongoQueryOnValidatedContract({ contract }).from('orders').unwind('tags').build(); expectTypeOf>().toEqualTypeOf(); }); it('count() row type is { [field]: number }', () => { - const plan = mongoQuery({ contract }).from('orders').count('total').build(); + const plan = mongoQueryOnValidatedContract({ contract }).from('orders').count('total').build(); expectTypeOf>().toEqualTypeOf<{ total: number }>(); }); it('sortByCount() row type is { _id: ; count: number }', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .sortByCount((f) => f.status) .build(); @@ -263,7 +274,7 @@ describe('resolved row types', () => { }); it('lookup() resolves the as-named field to Array with concrete leaf types (AC-3 TC-4)', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .lookup((from) => from('users') @@ -291,7 +302,7 @@ describe('resolved row types', () => { }); it('lookup() resolves nested value-object fields on the foreign model recursively', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .lookup((from) => from('customers') @@ -330,7 +341,7 @@ describe('resolved row types', () => { }); it('entry-point query resolves value-object fields recursively (model-origin brand)', () => { - const plan = mongoQuery({ contract }).from('customers').build(); + const plan = mongoQueryOnValidatedContract({ contract }).from('customers').build(); expectTypeOf>().toEqualTypeOf<{ readonly _id: string; readonly name: string; @@ -351,7 +362,7 @@ describe('resolved row types', () => { }); it('addFields() preserves model-origin resolution and adds the new fields on top', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('customers') .addFields((f) => ({ upperName: fn.concat(f.name, fn.literal('!')), @@ -378,7 +389,7 @@ describe('resolved row types', () => { }); it('group() drops model-origin resolution (legitimate shape replacement)', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('customers') .group((f) => ({ _id: f.name, @@ -398,7 +409,7 @@ describe('resolved row types', () => { readonly x: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; readonly y: { readonly codecId: 'mongo/double@1'; readonly nullable: true }; }; - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .replaceRoot((f) => f.status) .build(); @@ -409,7 +420,10 @@ describe('resolved row types', () => { }); it('pipe() preserves row type by default', () => { - const plan = mongoQuery({ contract }).from('orders').pipe(new MongoLimitStage(5)).build(); + const plan = mongoQueryOnValidatedContract({ contract }) + .from('orders') + .pipe(new MongoLimitStage(5)) + .build(); expectTypeOf>().toEqualTypeOf(); }); @@ -417,7 +431,7 @@ describe('resolved row types', () => { type NewShape = { readonly a: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; }; - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .pipe(new MongoLimitStage(5)) .build(); @@ -425,14 +439,14 @@ describe('resolved row types', () => { }); it('nullable fields resolve to T | null', () => { - const plan = mongoQuery({ contract }).from('orders').build(); + const plan = mongoQueryOnValidatedContract({ contract }).from('orders').build(); type Row = PlanRow; expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); it('chained pipeline produces correct cumulative row types', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .match((f) => f.status.eq('active') as MongoFilterExpr) .group((f) => ({ @@ -451,7 +465,7 @@ describe('resolved row types', () => { }); it('execute() infers Row from build() plan type', () => { - const plan = mongoQuery({ contract }).from('orders').build(); + const plan = mongoQueryOnValidatedContract({ contract }).from('orders').build(); const execute = {} as (p: MongoQueryPlan) => Promise; const result = execute(plan); expectTypeOf[0]>().toEqualTypeOf(); @@ -460,7 +474,7 @@ describe('resolved row types', () => { describe('resolved row types — new stages', () => { it('redact() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .redact((f) => f.status) .build(); @@ -468,22 +482,27 @@ describe('resolved row types — new stages', () => { }); it('out() returns a write-terminal plan with an unknown row type', () => { - const plan = mongoQuery({ contract }).from('orders').out('archive'); + const plan = mongoQueryOnValidatedContract({ contract }).from('orders').out('archive'); expectTypeOf>().toEqualTypeOf(); }); it('merge() returns a write-terminal plan with an unknown row type', () => { - const plan = mongoQuery({ contract }).from('orders').merge({ into: 'summary' }); + const plan = mongoQueryOnValidatedContract({ contract }) + .from('orders') + .merge({ into: 'summary' }); expectTypeOf>().toEqualTypeOf(); }); it('unionWith() preserves row type', () => { - const plan = mongoQuery({ contract }).from('orders').unionWith('archived_orders').build(); + const plan = mongoQueryOnValidatedContract({ contract }) + .from('orders') + .unionWith('archived_orders') + .build(); expectTypeOf>().toEqualTypeOf(); }); it('densify() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .densify({ field: 'amount', range: { step: 10, bounds: 'full' } }) .build(); @@ -491,7 +510,7 @@ describe('resolved row types — new stages', () => { }); it('fill() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .fill({ sortBy: { amount: 1 }, output: { notes: { method: 'linear' } } }) .build(); @@ -499,7 +518,7 @@ describe('resolved row types — new stages', () => { }); it('search() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .search({ text: { query: 'test', path: 'status' } }) .build(); @@ -507,7 +526,7 @@ describe('resolved row types — new stages', () => { }); it('vectorSearch() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .vectorSearch({ index: 'idx', @@ -521,7 +540,7 @@ describe('resolved row types — new stages', () => { }); it('bucket() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .bucket({ groupBy: MongoAggFieldRef.of('amount'), boundaries: [0, 100, 500] }) .build(); @@ -529,7 +548,7 @@ describe('resolved row types — new stages', () => { }); it('bucketAuto() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .bucketAuto({ groupBy: MongoAggFieldRef.of('amount'), buckets: 5 }) .build(); @@ -537,12 +556,15 @@ describe('resolved row types — new stages', () => { }); it('facet() resets to untyped row', () => { - const plan = mongoQuery({ contract }).from('orders').facet({ counts: [], top: [] }).build(); + const plan = mongoQueryOnValidatedContract({ contract }) + .from('orders') + .facet({ counts: [], top: [] }) + .build(); expectTypeOf>().toEqualTypeOf>(); }); it('geoNear() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .geoNear({ near: [0, 0], distanceField: 'dist' }) .build(); @@ -550,7 +572,7 @@ describe('resolved row types — new stages', () => { }); it('graphLookup() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .graphLookup({ from: 'categories', @@ -564,7 +586,7 @@ describe('resolved row types — new stages', () => { }); it('setWindowFields() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .setWindowFields({ sortBy: { amount: 1 }, output: {} }) .build(); @@ -572,7 +594,7 @@ describe('resolved row types — new stages', () => { }); it('searchMeta() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('orders') .searchMeta({ facet: { operator: {} } }) .build(); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts index 56264ed24422..c82cd1ae4fee 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts @@ -23,15 +23,15 @@ import { describe, expect, it } from 'vitest'; import { acc } from '../src/accumulator-helpers'; import { fn } from '../src/expression-helpers'; import type { LookupOnResult } from '../src/lookup-builder'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import { testContract } from './fixtures/test-contract'; function createOrdersBuilder() { - return mongoQuery({ contract: testContract }).from('orders'); + return mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); } function createCustomersBuilder() { - return mongoQuery({ contract: testContract }).from('customers'); + return mongoQueryOnValidatedContract({ contract: testContract }).from('customers'); } describe('PipelineChain', () => { @@ -385,16 +385,16 @@ describe('PipelineChain', () => { }); }); -describe('mongoQuery()', () => { +describe('mongoQueryOnValidatedContract()', () => { it('from() creates builder for known root', () => { - const p = mongoQuery({ contract: testContract }); + const p = mongoQueryOnValidatedContract({ contract: testContract }); const builder = p.from('orders'); const plan = builder.build(); expect(plan.collection).toBe('orders'); }); it('from() throws for unknown root', () => { - const p = mongoQuery({ contract: testContract }); + const p = mongoQueryOnValidatedContract({ contract: testContract }); expect(() => p.from('nonexistent' as 'orders')).toThrow('Unknown root'); }); }); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/exports.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/exports.test.ts new file mode 100644 index 000000000000..e681dc254308 --- /dev/null +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/exports.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import * as root from '../src/exports/index'; +import * as onValidatedContract from '../src/exports/internal-on-validated-contract'; + +describe('the query builder entries', () => { + it('keep the builder that trusts its contract off the root entry', () => { + expect(Object.keys(root)).not.toContain('mongoQuery'); + expect(Object.keys(root)).not.toContain('mongoQueryOnValidatedContract'); + }); + + it('offer it on the internal on-validated-contract entry only', () => { + expect(Object.keys(onValidatedContract)).toEqual(['mongoQueryOnValidatedContract']); + }); +}); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts index 300034407878..0824dfa2fb71 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts @@ -6,7 +6,7 @@ import type { ObjectExpression, } from '../src/field-accessor'; import { createFieldAccessor } from '../src/field-accessor'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import type { ModelNestedShape, NestedDocShape, ObjectField } from '../src/resolve-path'; import type { DocField, ModelToDocShape } from '../src/types'; import type { TContract, TestContract } from './fixtures/test-contract'; @@ -212,20 +212,20 @@ describe('FieldAccessor.rawPath escape hatch', () => { describe('Pipeline integration — N threading', () => { it('CollectionHandle.match callback allows callable dot-path access', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); p.from('customers').match((f) => f('address.city').eq('London')); p.from('customers').match((f) => f('address.geo.lat').gt(0)); }); it('FilteredCollection.updateMany callback allows callable dot-path access', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); p.from('customers') .match((f) => f('address.city').eq('London')) .updateMany((f) => [f('address.zip').set('SW1'), f('stats.visits').inc(1)]); }); it('rejects bogus paths in match callback', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); p.from('customers').match((f) => // @ts-expect-error -- 'address.bogus' is not a valid path f('address.bogus').eq('x'), @@ -233,7 +233,7 @@ describe('Pipeline integration — N threading', () => { }); it('additive stages (sort/addFields/redact) preserve callable dot-paths', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); p.from('customers') .sort({ name: 1 }) .match((f) => f('address.city').eq('London')); @@ -244,7 +244,7 @@ describe('Pipeline integration — N threading', () => { }); it('replacement stages (group/project/replaceRoot) disable callable dot-paths', () => { - const p = mongoQuery({ contract }); + const p = mongoQueryOnValidatedContract({ contract }); const grouped = p.from('customers').group((f) => ({ _id: f.name, count: f._id })); // After group, N has been reset. The flat Shape no longer contains // value objects, and the callable form rejects any string. diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts index 2423123bbc75..720079a5e08c 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts @@ -5,12 +5,12 @@ import { } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; import type { PipelineChain } from '../src/builder'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import type { ModelToDocShape } from '../src/types'; import type { TContract } from './fixtures/test-contract'; import { testContract } from './fixtures/test-contract'; -const orders = () => mongoQuery({ contract: testContract }).from('orders'); +const orders = () => mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); /** * After A25 (`CollectionHandle`/`FilteredCollection` start with diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts index ba8f3c08b733..c091cda124ff 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts @@ -7,13 +7,13 @@ import { } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; import type { PipelineChain } from '../src/builder'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import type { ModelToDocShape } from '../src/types'; import type { UpdaterResult } from '../src/update-ops'; import type { TContract } from './fixtures/test-contract'; import { testContract } from './fixtures/test-contract'; -const orders = () => mongoQuery({ contract: testContract }).from('orders'); +const orders = () => mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); /** * After A25 (`CollectionHandle`/`FilteredCollection` start with diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts index bf8e975fcadd..a541554071c4 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts @@ -1,10 +1,10 @@ import { InsertOneCommand, RawAggregateCommand } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import type { TContract } from './fixtures/test-contract'; import { testContract } from './fixtures/test-contract'; -const root = () => mongoQuery({ contract: testContract }); +const root = () => mongoQueryOnValidatedContract({ contract: testContract }); describe('M5 raw escape hatch', () => { it('packages a typed CRUD command into a plan with lane: mongo-query', () => { @@ -26,9 +26,9 @@ describe('M5 raw escape hatch', () => { it('throws when the contract is missing a storageHash (signals an unvalidated contract)', () => { const cmd = new InsertOneCommand('orders', { status: 'new' }); expect(() => - mongoQuery({ contract: { ...testContract, storage: {} } as unknown as TContract }).rawCommand( - cmd, - ), + mongoQueryOnValidatedContract({ + contract: { ...testContract, storage: {} } as unknown as TContract, + }).rawCommand(cmd), ).toThrow(/storageHash/); }); }); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts index e6bb51c324e6..e49677f052f2 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts @@ -1,6 +1,6 @@ import { MongoFieldFilter, MongoProjectStage } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import { contractFieldToMongoFieldShape, contractModelToMongoResultShape, @@ -197,12 +197,12 @@ describe('contractFieldToMongoFieldShape', () => { describe('PipelineChain build resultShape', () => { it('identity pipeline attaches document shape from bound model', () => { - const plan = mongoQuery({ contract: testContract }).from('orders').build(); + const plan = mongoQueryOnValidatedContract({ contract: testContract }).from('orders').build(); expect(plan.resultShape?.kind).toBe('document'); }); it('project stage reifies a document shape retaining _id and the projected field', () => { - const plan = mongoQuery({ contract: testContract }) + const plan = mongoQueryOnValidatedContract({ contract: testContract }) .from('orders') .match(MongoFieldFilter.eq('status', 'x')) .project('status') diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts index 3399dc27f0d0..2a79bf06fe89 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts @@ -1,17 +1,17 @@ import { MongoFieldFilter } from '@internal/mongo-query-ast/execution'; import { describe, it } from 'vitest'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import type { TContract } from './fixtures/test-contract'; declare const contract: TContract; -const handle = () => mongoQuery({ contract }).from('orders'); +const handle = () => mongoQueryOnValidatedContract({ contract }).from('orders'); const filtered = () => handle().match(MongoFieldFilter.eq('status', 'new')); /** * Negative-surface type tests for the M1 state machine + M2 write methods. * * The point is to lock down which methods *don't* exist on each state, so a - * misuse like `mongoQuery(...).from('x').updateMany(...)` (writing to every + * misuse like `mongoQueryOnValidatedContract(...).from('x').updateMany(...)` (writing to every * doc through the wrong terminal) doesn't typecheck. Each `@ts-expect-error` * here is load-bearing — its absence would silently re-open a footgun the * spec set out to close. diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts index bcb774871754..d6b9b15e6d8b 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts @@ -2,7 +2,7 @@ import { MongoFieldFilter } from '@internal/mongo-query-ast/execution'; import { describe, expectTypeOf, it } from 'vitest'; import type { PipelineChain } from '../src/builder'; import type { FindAndModifyEnabled, UpdateEnabled } from '../src/markers'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import type { CollectionHandle, FilteredCollection } from '../src/state-classes'; import type { TContract } from './fixtures/test-contract'; @@ -25,19 +25,19 @@ type GetF = describe('state machine', () => { it('from(name) returns CollectionHandle (root state) inheriting PipelineChain', () => { - const handle = mongoQuery({ contract }).from('orders'); + const handle = mongoQueryOnValidatedContract({ contract }).from('orders'); expectTypeOf(handle).toExtend>(); }); it('CollectionHandle.match(...) transitions to FilteredCollection', () => { - const filtered = mongoQuery({ contract }) + const filtered = mongoQueryOnValidatedContract({ contract }) .from('orders') .match(MongoFieldFilter.eq('status', 'active')); expectTypeOf(filtered).toExtend>(); }); it('FilteredCollection.match(...) stays in FilteredCollection (AND-folds)', () => { - const filtered = mongoQuery({ contract }) + const filtered = mongoQueryOnValidatedContract({ contract }) .from('orders') .match(MongoFieldFilter.eq('status', 'active')) .match(MongoFieldFilter.gt('amount', 100)); @@ -45,7 +45,7 @@ describe('state machine', () => { }); it('pipeline-stage methods drop out of the state-class subclasses', () => { - const sorted = mongoQuery({ contract }).from('orders').sort({ amount: -1 }); + const sorted = mongoQueryOnValidatedContract({ contract }).from('orders').sort({ amount: -1 }); // No longer a CollectionHandle/FilteredCollection — write/find-and-modify // surfaces have been left behind. expectTypeOf(sorted).not.toExtend>(); @@ -58,7 +58,7 @@ describe('state machine', () => { // find-and-modify terminals inherited from `PipelineChain` are gated off // by default, so a leading `.match(...)` is required before reaching them // via the `FilteredCollection` overrides (see ADR 201). - const handle = mongoQuery({ contract }).from('orders'); + const handle = mongoQueryOnValidatedContract({ contract }).from('orders'); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); }); @@ -70,7 +70,7 @@ describe('state machine', () => { // marker-gated PipelineChain versions), so the initial marker state // stays cleared to prevent accidental access to the PipelineChain // inheritance path. - const filtered = mongoQuery({ contract }) + const filtered = mongoQueryOnValidatedContract({ contract }) .from('orders') .match(MongoFieldFilter.eq('status', 'active')); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); @@ -78,23 +78,23 @@ describe('state machine', () => { }); it('marker table: limit() leaves both markers cleared', () => { - const limited = mongoQuery({ contract }).from('orders').limit(1); + const limited = mongoQueryOnValidatedContract({ contract }).from('orders').limit(1); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); }); it('marker table: sort / addFields / group all leave both markers cleared from .from()', () => { - const sorted = mongoQuery({ contract }).from('orders').sort({ amount: -1 }); + const sorted = mongoQueryOnValidatedContract({ contract }).from('orders').sort({ amount: -1 }); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); - const added = mongoQuery({ contract }) + const added = mongoQueryOnValidatedContract({ contract }) .from('orders') .addFields((f) => ({ doubled: f.amount })); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); - const grouped = mongoQuery({ contract }) + const grouped = mongoQueryOnValidatedContract({ contract }) .from('orders') .group((_f) => ({ _id: null })); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); @@ -106,7 +106,7 @@ describe('state machine', () => { // parameters here; the runtime / shape behaviour is asserted in // builder.test-d.ts. it('marker table: lookup() clears both markers', () => { - const looked = mongoQuery({ contract }) + const looked = mongoQueryOnValidatedContract({ contract }) .from('orders') .lookup((from) => from('users') diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts index 456a97dd2ea0..9baed8fe6844 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts @@ -1,6 +1,6 @@ import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; import { expectTypeOf } from 'vitest'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import type { ModelNestedShape } from '../src/resolve-path'; import type { ModelToDocShape } from '../src/types'; import { type TestContract, testContract } from './fixtures/test-contract'; @@ -38,12 +38,12 @@ describe('stored names', () => { }); it('exposes _id, not id, on a model whose id field is stored as _id', () => { - const plan = mongoQuery({ contract: testContract }).from('accounts').build(); + const plan = mongoQueryOnValidatedContract({ contract: testContract }).from('accounts').build(); expectTypeOf>().toEqualTypeOf(); }); it('accepts stored names and rejects domain field names in match', () => { - const accounts = mongoQuery({ contract: testContract }).from('accounts'); + const accounts = mongoQueryOnValidatedContract({ contract: testContract }).from('accounts'); accounts.match((f) => f._id.eq('a')); accounts.match((f) => f('home_address.city').eq('NYC')); // @ts-expect-error -- `id` is a domain field name, not a stored name @@ -53,7 +53,7 @@ describe('stored names', () => { }); it('keys a looked-up foreign row by stored name', () => { - const plan = mongoQuery({ contract: testContract }) + const plan = mongoQueryOnValidatedContract({ contract: testContract }) .from('orders') .lookup((from) => from('accounts') diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts index 472dda395a30..0134909f8ac5 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts @@ -4,11 +4,11 @@ import { MongoProjectStage, } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import { testContract } from './fixtures/test-contract'; describe('stored names', () => { - const accounts = () => mongoQuery({ contract: testContract }).from('accounts'); + const accounts = () => mongoQueryOnValidatedContract({ contract: testContract }).from('accounts'); it('keys the result shape of a model read by stored name', () => { expect(accounts().build().resultShape).toEqual({ diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts index 7520f3554432..127c116cccd5 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts @@ -2,12 +2,12 @@ import { InsertOneCommand } from '@internal/mongo-query-ast/execution'; import { isStructuredError } from '@internal/utils/structured-error'; import { describe, expect, it } from 'vitest'; import type { PipelineChain } from '../src/builder'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import type { ModelToDocShape } from '../src/types'; import type { TContract } from './fixtures/test-contract'; import { testContract, testContractJson } from './fixtures/test-contract'; -const orders = () => mongoQuery({ contract: testContract }).from('orders'); +const orders = () => mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); type FamReachableChain = PipelineChain< TContract, @@ -51,7 +51,7 @@ describe('structured error codes', () => { }); it('raises ORM.MODEL_UNKNOWN for an unknown root', () => { - const p = mongoQuery({ contract: testContract }); + const p = mongoQueryOnValidatedContract({ contract: testContract }); expect(codeOf(() => p.from('nonexistent' as 'orders'))).toBe('ORM.MODEL_UNKNOWN'); }); @@ -63,12 +63,14 @@ describe('structured error codes', () => { orders: { $ref: '#/domain/models/Ghost', model: 'Ghost' }, }, }; - const p = mongoQuery({ contract: patched as unknown as TContract }); + const p = mongoQueryOnValidatedContract({ contract: patched as unknown as TContract }); expect(codeOf(() => p.from('orders'))).toBe('CONTRACT.MODEL_UNKNOWN'); }); it('raises CONTRACT.VALIDATION_FAILED when the contract has no storageHash', () => { - const p = mongoQuery({ contract: { ...testContract, storage: {} } as unknown as TContract }); + const p = mongoQueryOnValidatedContract({ + contract: { ...testContract, storage: {} } as unknown as TContract, + }); expect(codeOf(() => p.rawCommand(new InsertOneCommand('orders', { status: 'new' })))).toBe( 'CONTRACT.VALIDATION_FAILED', ); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts index 29f606721e22..25c2d5d237c9 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts @@ -9,10 +9,10 @@ import { UpdateOneCommand, } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; -import { mongoQuery } from '../src/query'; +import { mongoQueryOnValidatedContract } from '../src/query'; import { testContract } from './fixtures/test-contract'; -const orders = () => mongoQuery({ contract: testContract }).from('orders'); +const orders = () => mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); describe('M2 write terminals', () => { describe('inserts (CollectionHandle)', () => { diff --git a/packages/2-mongo-family/5-query-builders/query-builder/tsdown.config.ts b/packages/2-mongo-family/5-query-builders/query-builder/tsdown.config.ts index 211121ea0e55..d54fb798a19d 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/tsdown.config.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/tsdown.config.ts @@ -4,5 +4,6 @@ export default defineConfig({ entry: { index: 'src/exports/index.ts', 'exports/contract-free': 'src/exports/contract-free.ts', + 'internal/on-validated-contract': 'src/exports/internal-on-validated-contract.ts', }, }); diff --git a/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts b/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts index 832a68b391ab..7b9207f9d8dc 100644 --- a/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts +++ b/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts @@ -6,7 +6,7 @@ import type { MongoTypeMaps, } from '@internal/mongo-contract'; import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; -import { mongoQuery } from '@internal/mongo-query-builder'; +import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; import { expectTypeOf } from 'vitest'; import type { MongoRuntime } from '../src/mongo-runtime'; @@ -93,7 +93,7 @@ declare const contract: TContract; describe('runtime type safety', () => { it('query() returns AsyncIterableResult where Row matches build() row type', () => { - const plan = mongoQuery({ contract }).from('orders').build(); + const plan = mongoQueryOnValidatedContract({ contract }).from('orders').build(); type Row = PlanRow; expectTypeOf().toEqualTypeOf(); @@ -103,7 +103,7 @@ describe('runtime type safety', () => { }); it('query() result awaits to Row[]', () => { - const plan = mongoQuery({ contract }).from('orders').build(); + const plan = mongoQueryOnValidatedContract({ contract }).from('orders').build(); type Row = PlanRow; const runtime = {} as MongoRuntime; diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-query.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-query.ts index fece596184e4..b3673fec2cb9 100644 --- a/packages/3-targets/mongo/9-facade/src/static/mongo-query.ts +++ b/packages/3-targets/mongo/9-facade/src/static/mongo-query.ts @@ -3,10 +3,8 @@ import type { MongoContract, MongoContractWithTypeMaps, } from '@internal/mongo-contract'; -import { - type QueryRoot, - mongoQuery as queryOnValidatedContract, -} from '@internal/mongo-query-builder'; +import type { QueryRoot } from '@internal/mongo-query-builder'; +import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; import { validateMongoContract } from './contract-from-json'; /** @@ -15,5 +13,5 @@ import { validateMongoContract } from './contract-from-json'; export function mongoQuery< TContract extends MongoContractWithTypeMaps, >(options: { readonly contract: TContract }): QueryRoot { - return queryOnValidatedContract({ contract: validateMongoContract(options.contract) }); + return mongoQueryOnValidatedContract({ contract: validateMongoContract(options.contract) }); } diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts index 3260ac48a32b..83f595df7a59 100644 --- a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts +++ b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts @@ -8,7 +8,7 @@ import type { } from '@internal/mongo-contract'; import type { MongoRawClient } from '@internal/mongo-orm'; import { mongoRaw } from '@internal/mongo-orm'; -import { mongoQuery } from '@internal/mongo-query-builder'; +import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; import { buildMongoEnums, createMongoExecutionContext, @@ -30,7 +30,7 @@ export interface MongoStaticContext< readonly context: MongoExecutionContext; readonly contract: TContract; readonly enums: UnboundEnums; - readonly query: ReturnType>; + readonly query: ReturnType>; readonly raw: MongoRawClient; } @@ -56,7 +56,7 @@ export function buildMongoStaticContext< UnboundEnums, 'built from this contract domain; the mapped-type shape cannot be proven statically' >(unbound); - const query = mongoQuery({ contract }); + const query = mongoQueryOnValidatedContract({ contract }); const raw = mongoRaw({ contract }); return { context, contract, enums, enumsByNamespace, query, raw }; } diff --git a/packages/3-targets/mongo/9-facade/test/mongo.test.ts b/packages/3-targets/mongo/9-facade/test/mongo.test.ts index c5f1cf85e0db..b86466c7a015 100644 --- a/packages/3-targets/mongo/9-facade/test/mongo.test.ts +++ b/packages/3-targets/mongo/9-facade/test/mongo.test.ts @@ -20,7 +20,7 @@ const mocks = vi.hoisted(() => ({ driverFromDb: vi.fn(), deserializeContract: vi.fn(), mongoOrm: vi.fn(), - mongoQuery: vi.fn(), + mongoQueryOnValidatedContract: vi.fn(), mongoRaw: vi.fn(), })); @@ -59,8 +59,8 @@ vi.mock('@internal/mongo-orm', () => ({ mongoRaw: mocks.mongoRaw, })); -vi.mock('@internal/mongo-query-builder', () => ({ - mongoQuery: mocks.mongoQuery, +vi.mock('@internal/mongo-query-builder/internal/on-validated-contract', () => ({ + mongoQueryOnValidatedContract: mocks.mongoQueryOnValidatedContract, })); import mongo from '../src/runtime/mongo'; @@ -93,7 +93,7 @@ describe('mongo() facade', () => { mocks.driverFromDb.mockReturnValue({ id: 'driver-from-db', close: fakeDriverFromDbClose }); mocks.createMongoRuntime.mockReturnValue(fakeRuntime); mocks.mongoOrm.mockReturnValue(fakeOrm); - mocks.mongoQuery.mockReturnValue(fakeQuery); + mocks.mongoQueryOnValidatedContract.mockReturnValue(fakeQuery); mocks.mongoRaw.mockReturnValue(fakeRaw); fakeRaw.collection.mockClear(); fakeRuntime.close.mockClear(); @@ -107,7 +107,7 @@ describe('mongo() facade', () => { expect(db.orm).toBe(fakeOrm); expect(db.query).toBe(fakeQuery); expect(mocks.mongoOrm).toHaveBeenCalledTimes(1); - expect(mocks.mongoQuery).toHaveBeenCalledTimes(1); + expect(mocks.mongoQueryOnValidatedContract).toHaveBeenCalledTimes(1); expect(mocks.driverFromConnection).not.toHaveBeenCalled(); expect(mocks.driverFromDb).not.toHaveBeenCalled(); diff --git a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts index 2a72216f5960..60ebf90bf274 100644 --- a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts +++ b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts @@ -19,7 +19,7 @@ const mocks = vi.hoisted(() => ({ deserializeContract: vi.fn(), mongoOrm: vi.fn(), mongoRaw: vi.fn(), - mongoQuery: vi.fn(), + mongoQueryOnValidatedContract: vi.fn(), })); vi.mock('@internal/adapter-mongo/runtime', () => ({ default: { id: 'adapter' } })); @@ -47,7 +47,9 @@ vi.mock('@internal/mongo-orm', () => ({ mongoOrm: mocks.mongoOrm, mongoRaw: mocks.mongoRaw, })); -vi.mock('@internal/mongo-query-builder', () => ({ mongoQuery: mocks.mongoQuery })); +vi.mock('@internal/mongo-query-builder/internal/on-validated-contract', () => ({ + mongoQueryOnValidatedContract: mocks.mongoQueryOnValidatedContract, +})); import { resolveMongoBinding, resolveOptionalMongoBinding } from '../src/runtime/binding'; import mongo from '../src/runtime/mongo'; @@ -188,7 +190,7 @@ describe('facade lifecycle structured codes', () => { }); mocks.createMongoRuntime.mockReturnValue({ id: 'runtime' }); mocks.mongoOrm.mockReturnValue({ id: 'orm' }); - mocks.mongoQuery.mockReturnValue({ id: 'query' }); + mocks.mongoQueryOnValidatedContract.mockReturnValue({ id: 'query' }); mocks.mongoRaw.mockReturnValue({ id: 'raw' }); }); diff --git a/packages/9-public/@prisma/orm-family-mongo/package.json b/packages/9-public/@prisma/orm-family-mongo/package.json index fd596b063aa1..bce5ea54ceb8 100644 --- a/packages/9-public/@prisma/orm-family-mongo/package.json +++ b/packages/9-public/@prisma/orm-family-mongo/package.json @@ -77,6 +77,7 @@ "./query-ast/execution": "./dist/query-ast__execution.mjs", "./query-builder": "./dist/query-builder.mjs", "./query-builder/contract-free": "./dist/query-builder__contract-free.mjs", + "./query-builder/internal/on-validated-contract": "./dist/query-builder__internal__on-validated-contract.mjs", "./runtime": "./dist/runtime.mjs", "./schema-ir": "./dist/schema-ir.mjs", "./value": "./dist/value.mjs", diff --git a/test/integration/test/mongo-runtime/query-builder.test.ts b/test/integration/test/mongo-runtime/query-builder.test.ts index dca351265af8..2c6ddd80940f 100644 --- a/test/integration/test/mongo-runtime/query-builder.test.ts +++ b/test/integration/test/mongo-runtime/query-builder.test.ts @@ -1,6 +1,6 @@ // Intentionally uses verbose mongo-contract-ts import: @internal/mongo/contract-builder's // defineContract wrap loses inline-model inference precision when consumers use -// mongoQuery chains (PlanRow row shapes collapse to _id: never / count: never). +// mongoQueryOnValidatedContract chains (PlanRow row shapes collapse to _id: never / count: never). // Tracked at https://linear.app/prisma-company/issue/TML-2633 — migrate to the facade form // once TML-2633 lands. @@ -14,7 +14,8 @@ import type { } from '@internal/mongo-contract'; import { defineContract, field, model, rel } from '@internal/mongo-contract-ts/contract-builder'; import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; -import { acc, fn, mongoQuery } from '@internal/mongo-query-builder'; +import { acc, fn } from '@internal/mongo-query-builder'; +import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; import mongoTargetPack from '@internal/target-mongo/pack'; import { timeouts } from '@repo/test-utils'; import { ObjectId } from 'mongodb'; @@ -180,7 +181,7 @@ const contract = defineContract({ }); describe('pipeline builder integration', { timeout: timeouts.spinUpMongoMemoryServer }, () => { - const p = mongoQuery({ contract: contractFromJson(contractJson) }); + const p = mongoQueryOnValidatedContract({ contract: contractFromJson(contractJson) }); it('match → group → sort → limit analytics query', async () => { await withMongod(async (ctx) => { @@ -358,7 +359,7 @@ describe('pipeline builder integration', { timeout: timeouts.spinUpMongoMemorySe }, ]); - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('tasks') .group((f) => ({ _id: f.type, diff --git a/test/integration/test/mongo/query-builder.test.ts b/test/integration/test/mongo/query-builder.test.ts index 9a6b4a218e9d..853dac339e1e 100644 --- a/test/integration/test/mongo/query-builder.test.ts +++ b/test/integration/test/mongo/query-builder.test.ts @@ -19,7 +19,8 @@ import { MongoRedactStage, MongoSortStage, } from '@internal/mongo-query-ast/execution'; -import { acc, fn, mongoQuery } from '@internal/mongo-query-builder'; +import { acc, fn } from '@internal/mongo-query-builder'; +import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; import { describe, expect, it } from 'vitest'; import { describeWithMongoDB } from './setup'; @@ -194,7 +195,9 @@ const contractJson = { // Helpers // --------------------------------------------------------------------------- -const query = mongoQuery({ contract: contractFromJson(contractJson) }); +const query = mongoQueryOnValidatedContract({ + contract: contractFromJson(contractJson), +}); function products() { return query.from('products'); @@ -256,7 +259,7 @@ const ORDERS = [ // Tests // --------------------------------------------------------------------------- -describeWithMongoDB('Pipeline builder integration (mongoQuery DSL)', (ctx) => { +describeWithMongoDB('Pipeline builder integration (mongoQueryOnValidatedContract DSL)', (ctx) => { async function seed() { const db = ctx.client.db(ctx.dbName); await db.collection('products').insertMany(PRODUCTS.map((p) => ({ ...p }))); diff --git a/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts b/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts index 94a1eac8bae8..e51beefbe054 100644 --- a/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts +++ b/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts @@ -1,6 +1,6 @@ /** * End-to-end test for the user-facing path: contract types → - * `mongoQuery(...).from(...)..build()` → `runtime.query(plan)` → + * `mongoQueryOnValidatedContract(...).from(...)..build()` → `runtime.query(plan)` → * decoded rows. * * This file is the single artifact tying contract type inference, lane @@ -13,7 +13,8 @@ import { MongoContractSerializer } from '@internal/family-mongo/ir'; import { contractFromJson } from '@internal/mongo/static'; -import { acc, mongoQuery } from '@internal/mongo-query-builder'; +import { acc } from '@internal/mongo-query-builder'; +import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; import { MongoParamRef } from '@internal/mongo-value'; import { timeouts } from '@repo/test-utils'; import { ObjectId } from 'mongodb'; @@ -24,7 +25,7 @@ import { } from './fixtures/decode-fixture-contract'; import { withMongod } from './setup'; -const q = mongoQuery({ +const q = mongoQueryOnValidatedContract({ contract: contractFromJson(decodeFixtureContractJson), }); @@ -45,7 +46,7 @@ describe('Mongo runtime decode integration via query-builder', { embeddings: vec, }); - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('users') .match((f) => f['_id']!.eq(MongoParamRef.of(insert.insertedId, { codecId: 'mongo/objectId@1' })), diff --git a/test/integration/test/mongo/runtime/decode.integration.test.ts b/test/integration/test/mongo/runtime/decode.integration.test.ts index fa3d0bbf4b4c..7647dff7c531 100644 --- a/test/integration/test/mongo/runtime/decode.integration.test.ts +++ b/test/integration/test/mongo/runtime/decode.integration.test.ts @@ -9,7 +9,7 @@ import { MongoMatchStage, RawAggregateCommand, } from '@internal/mongo-query-ast/execution'; -import { mongoQuery } from '@internal/mongo-query-builder'; +import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; import { MongoParamRef } from '@internal/mongo-value'; import { timeouts } from '@repo/test-utils'; import { ObjectId } from 'mongodb'; @@ -38,7 +38,7 @@ describe('Mongo runtime decode integration', { timeout: timeouts.spinUpMongoMemo }); // User-facing path: build the plan through the query-builder so the Row type is contract-derived (no explicit annotation on execute). - const plan = mongoQuery({ contract }) + const plan = mongoQueryOnValidatedContract({ contract }) .from('users') .match((f) => f['_id']!.eq(MongoParamRef.of(insert.insertedId, { codecId: 'mongo/objectId@1' })), @@ -59,7 +59,7 @@ describe('Mongo runtime decode integration', { timeout: timeouts.spinUpMongoMemo it('decode failure surfaces RUNTIME.DECODE_FAILED with details and cause', async () => { await withMongod(async (ctx) => { - // The synthetic codec id (`test/throws-on-decode@1`) is not a contract field codec, so this test legitimately needs a stub `resultShape` — the user-facing `mongoQuery(...)` path can't construct a shape referencing a codec the contract doesn't declare. The tradeoff is intentional: this test exercises the failure-envelope plumbing, not the lane-population path. The other two integration tests in this file go through the + // The synthetic codec id (`test/throws-on-decode@1`) is not a contract field codec, so this test legitimately needs a stub `resultShape` — the user-facing `mongoQueryOnValidatedContract(...)` path can't construct a shape referencing a codec the contract doesn't declare. The tradeoff is intentional: this test exercises the failure-envelope plumbing, not the lane-population path. The other two integration tests in this file go through the // query-builder. const failing = mongoCodec({ typeId: 'test/throws-on-decode@1', diff --git a/test/integration/test/mongo/runtime/fixtures/decode-fixture-contract.ts b/test/integration/test/mongo/runtime/fixtures/decode-fixture-contract.ts index b07e0afee258..f7f8f5ae13f8 100644 --- a/test/integration/test/mongo/runtime/fixtures/decode-fixture-contract.ts +++ b/test/integration/test/mongo/runtime/fixtures/decode-fixture-contract.ts @@ -98,10 +98,10 @@ type DecodeFixtureStorage = { /** * Shared fixture contract for the decode integration tests. * - * Lives in a typed `.ts` so it can drive `mongoQuery({ contract })` row-type + * Lives in a typed `.ts` so it can drive `mongoQueryOnValidatedContract({ contract })` row-type * inference end-to-end. Call sites validate the JSON via the Mongo contract * serializer SPI to keep the runtime structural shape honest, then thread - * `TContract` through `mongoQuery` for the type-level path. + * `TContract` through `mongoQueryOnValidatedContract` for the type-level path. */ export type DecodeFixtureContract = Omit, 'domain'> & { readonly roots: { diff --git a/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts b/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts index 45523b32b97f..d8a41d1a2cca 100644 --- a/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts +++ b/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts @@ -3,35 +3,70 @@ import { fileURLToPath } from 'node:url'; import { dirname, join } from 'pathe'; import { describe, expect, it } from 'vitest'; -const GUIDE_PATH = - 'upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md'; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../../..'); -const id = 'mongo-query-validates-contract'; +const familyPackage = ['@prisma', 'orm-family-mongo'].join('/'); +const facadePackage = ['@prisma', 'orm-mongo'].join('/'); -function detects(content: string): boolean { - const guide = readFileSync(join(repoRoot, GUIDE_PATH), 'utf-8'); +function detects(audience: 'app' | 'extension', id: string, content: string): boolean { + const path = `upgrade-instructions/pending/mongo-query-validates-contract/${audience}/instructions.md`; + const guide = readFileSync(join(repoRoot, path), 'utf-8'); const entry = guide.split(/\n {2}- id: /).find((block) => block.startsWith(`${id}\n`)); - if (entry === undefined) throw new Error(`The guide has no change ${id}`); + if (entry === undefined) throw new Error(`The ${audience} guide has no change ${id}`); const patterns = [...entry.matchAll(/^ {8}- '(.*)'$/gm)].map( ([, pattern]) => new RegExp((pattern ?? '').replaceAll("''", "'")), ); - if (patterns.length === 0) throw new Error(`The change ${id} has no detection`); + if (patterns.length === 0) throw new Error(`The ${audience} change ${id} has no detection`); return patterns.some((pattern) => pattern.test(content)); } -describe('the mongoQuery detection', () => { +describe('the app mongoQuery detection', () => { + const id = 'mongo-query-validates-contract'; + it.each([ ['a call', 'const query = mongoQuery({ contract });'], ['a call with a space', 'mongoQuery ({ contract })'], + ['an aliased import', `import { mongoQuery as query } from '${facadePackage}/query-builder';`], + [ + 'an import from the family package', + `import { acc, mongoQuery } from '${familyPackage}/query-builder';`, + ], ])('finds %s', (_name, content) => { - expect(detects(content)).toBe(true); + expect(detects('app', id, content)).toBe(true); }); it.each([ ['a method of the same name', 'db.mongoQuery({ contract })'], ['a longer name', 'myMongoQuery({ contract })'], - ['an import only', "import { acc } from 'somewhere';"], + ['an import of other names', `import { acc } from '${familyPackage}/query-builder';`], + ])('skips %s', (_name, content) => { + expect(detects('app', id, content)).toBe(false); + }); +}); + +describe('the extension mongoQuery detection', () => { + const id = 'family-query-builder-drops-mongo-query'; + + it.each([ + [ + 'an import from the family package', + `import { mongoQuery } from '${familyPackage}/query-builder';`, + ], + [ + 'an aliased import from the family package', + `import { mongoQuery as query } from '${familyPackage}/query-builder';`, + ], + [ + 'an import from the internal package', + "import { mongoQuery } from '@internal/mongo-query-builder';", + ], + ])('finds %s', (_name, content) => { + expect(detects('extension', id, content)).toBe(true); + }); + + it.each([ + ['an import from the facade', `import { mongoQuery } from '${facadePackage}/query-builder';`], + ['an import of other names', `import { acc } from '${familyPackage}/query-builder';`], ])('skips %s', (_name, content) => { - expect(detects(content)).toBe(false); + expect(detects('extension', id, content)).toBe(false); }); }); diff --git a/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md b/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md index d3e8602509d2..83eadb32db8d 100644 --- a/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md +++ b/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md @@ -2,11 +2,13 @@ changes: - id: mongo-query-validates-contract summary: | - `mongoQuery({ contract })` from `@prisma/orm-mongo/query-builder` now validates the contract as `mongo({ contract })` does, and throws `CONTRACT.VALIDATION_FAILED` for a malformed one. A value from `contractFromJson(...)` or `defineContract(...)` is not validated again. If you pass raw `contract.json`, or an object you built by hand, pass `contractFromJson(json)` instead. The detection matches files that call `mongoQuery(`. + `mongoQuery({ contract })` from `@prisma/orm-mongo/query-builder` now validates the contract as `mongo({ contract })` does, and throws `CONTRACT.VALIDATION_FAILED` for a malformed one. A value from `contractFromJson(...)` or `defineContract(...)` is not validated again. If you pass raw `contract.json`, or an object you built by hand, pass `contractFromJson(json)` instead. The family package `@prisma/orm-family-mongo/query-builder` no longer exports `mongoQuery`; import it from `@prisma/orm-mongo/query-builder`. The detection matches files that call `mongoQuery(`, import it under another name, or import it from the family package. detection: glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs}" matches: - '(? Date: Fri, 23 Oct 2026 14:50:27 +0200 Subject: [PATCH 09/19] TML-3686: Examples import from published packages, and the upgrade detection covers jsx, namespace imports and any stack receiver Signed-off-by: willbot Signed-off-by: Will Madden --- .../subsystems/10. MongoDB Family.md | 3 +-- docs/reference/Mongo Pipeline Builder.md | 12 +++++------ .../5-query-builders/query-builder/README.md | 2 +- packages/3-extensions/pgvector/README.md | 4 ++-- .../detection.test.ts | 20 +++++++++++++++++++ .../stack-driver-required/detection.test.ts | 6 +++++- .../app/instructions.md | 5 +++-- .../extension/instructions.md | 5 +++-- .../extension/instructions.md | 4 ++-- 9 files changed, 43 insertions(+), 18 deletions(-) diff --git a/docs/architecture docs/subsystems/10. MongoDB Family.md b/docs/architecture docs/subsystems/10. MongoDB Family.md index adbd0173995e..de4955f17188 100644 --- a/docs/architecture docs/subsystems/10. MongoDB Family.md +++ b/docs/architecture docs/subsystems/10. MongoDB Family.md @@ -357,8 +357,7 @@ Below the ORM and above the raw command AST sits `@internal/mongo-query-builder` Applications use `mongoQuery` from `@prisma/orm-mongo/query-builder`. It validates the contract first, with the same validator and error as `mongo({ contract })`, and skips a value that was already validated, such as one from `contractFromJson`. The package's own builder, `mongoQueryOnValidatedContract`, is on the internal entry `@internal/mongo-query-builder/internal/on-validated-contract` (published as `@prisma/orm-family-mongo/query-builder/internal/on-validated-contract`) and does not validate. ```typescript -import { acc } from '@internal/mongo-query-builder'; -import { mongoQuery } from '@internal/mongo/query-builder'; +import { acc, mongoQuery } from '@prisma/orm-mongo/query-builder'; const q = mongoQuery({ contract }); // from contractFromJson(json) or defineContract diff --git a/docs/reference/Mongo Pipeline Builder.md b/docs/reference/Mongo Pipeline Builder.md index f36a3a474dd2..79345aa66f5e 100644 --- a/docs/reference/Mongo Pipeline Builder.md +++ b/docs/reference/Mongo Pipeline Builder.md @@ -9,8 +9,8 @@ This page is a working reference: a concrete example to ground the mental model, Imagine an `orders` collection where each document has `customerId`, `amount`, and `status`. We want the top ten customers by total spend on active orders: ```ts -import { contractFromJson } from '@internal/mongo/static'; -import { mongoQuery } from '@internal/mongo/query-builder'; +import { contractFromJson } from '@prisma/orm-mongo/static'; +import { acc, mongoQuery } from '@prisma/orm-mongo/query-builder'; import type { Contract } from './contract'; import contractJson from './contract.json' with { type: 'json' }; @@ -73,7 +73,7 @@ Examples in the rest of this page assume: ```ts const orders = query.from('orders'); -import { acc, fn } from '@internal/mongo-query-builder'; +import { acc, fn } from '@prisma/orm-mongo/query-builder'; ``` `fn` is a namespace of typed expression helpers (`fn.toUpper(...)`, `fn.concat(...)`, `fn.literal(...)`); `acc` is the parallel namespace of accumulators (`acc.sum(...)`, `acc.count(...)`, …). Both produce `MongoAggExpr` AST nodes under the hood, but you rarely need to think about that. @@ -144,7 +144,7 @@ orders.sortByCount((f) => f.status).build(); For bucketing the typed surface gives you the chain entry point but the bucket boundaries / grouping expression flow as raw AST nodes (most commonly `MongoAggFieldRef.of('field')` — see [the primitives reference](./mongodb-primitives-reference.md)): ```ts -import { MongoAggFieldRef } from '@internal/mongo-query-ast/execution'; +import { MongoAggFieldRef } from '@prisma/orm-mongo/query-ast/execution'; orders .bucket({ groupBy: MongoAggFieldRef.of('amount'), boundaries: [0, 100, 1000] }) @@ -170,7 +170,7 @@ orders `graphLookup` is the recursive form. `unionWith` appends another collection's rows to the current pipeline. Both expose option objects rather than fluent chains: ```ts -import { MongoAggFieldRef } from '@internal/mongo-query-ast/execution'; +import { MongoAggFieldRef } from '@prisma/orm-mongo/query-ast/execution'; orders .graphLookup({ @@ -299,7 +299,7 @@ What is *not* statically guaranteed: Two levels of escape hatch are available when the typed surface does not reach a use case: ```ts -import { MongoLimitStage } from '@internal/mongo-query-ast/execution'; +import { MongoLimitStage } from '@prisma/orm-mongo/query-ast/execution'; // Append a raw stage; preserves the chain but collapses the shape unless // you supply a NewShape to pipe(). diff --git a/packages/2-mongo-family/5-query-builders/query-builder/README.md b/packages/2-mongo-family/5-query-builders/query-builder/README.md index b9ca466c9c18..74fdb25c45a9 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/README.md +++ b/packages/2-mongo-family/5-query-builders/query-builder/README.md @@ -7,7 +7,7 @@ A typed CRUD query builder for MongoDB contracts. Reads, writes, pipeline aggreg Applications use `mongoQuery` from `@prisma/orm-mongo/query-builder`, which validates the contract first. This package's builder, `mongoQueryOnValidatedContract`, lives on the internal entry `@internal/mongo-query-builder/internal/on-validated-contract` (published as `@prisma/orm-family-mongo/query-builder/internal/on-validated-contract`). It does not validate: pass it only a contract that is already validated, such as one from `contractFromJson`. ```typescript -import { mongoQuery } from '@prisma/orm-mongo/query-builder'; +import { acc, mongoQuery } from '@prisma/orm-mongo/query-builder'; const q = mongoQuery({ contract }); diff --git a/packages/3-extensions/pgvector/README.md b/packages/3-extensions/pgvector/README.md index 7a0cf70f7b57..0f10519a7052 100644 --- a/packages/3-extensions/pgvector/README.md +++ b/packages/3-extensions/pgvector/README.md @@ -91,8 +91,8 @@ The `vector(N)` factory is registered through the unified `CodecDescriptor<{ len Register the extension when creating the client: ```typescript -import postgres from '@internal/postgres/runtime'; -import pgvector from '@internal/extension-pgvector/runtime'; +import postgres from '@prisma/orm-postgres/runtime'; +import pgvector from '@prisma/orm-extension-pgvector/runtime'; const db = postgres({ contract, url, extensions: [pgvector] }); ``` diff --git a/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts b/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts index d8a41d1a2cca..d06d86e80104 100644 --- a/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts +++ b/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts @@ -30,6 +30,14 @@ describe('the app mongoQuery detection', () => { 'an import from the family package', `import { acc, mongoQuery } from '${familyPackage}/query-builder';`, ], + [ + 'a call through a namespace import of the facade', + `import * as builder from '${facadePackage}/query-builder';\nconst q = builder.mongoQuery({ contract });`, + ], + [ + 'a call through a namespace import of the family package', + `import * as qb from '${familyPackage}/query-builder';\nqb.mongoQuery({ contract });`, + ], ])('finds %s', (_name, content) => { expect(detects('app', id, content)).toBe(true); }); @@ -38,6 +46,10 @@ describe('the app mongoQuery detection', () => { ['a method of the same name', 'db.mongoQuery({ contract })'], ['a longer name', 'myMongoQuery({ contract })'], ['an import of other names', `import { acc } from '${familyPackage}/query-builder';`], + [ + 'a member call on another namespace', + `import * as builder from '${facadePackage}/query-builder';\nother.mongoQuery({ contract });`, + ], ])('skips %s', (_name, content) => { expect(detects('app', id, content)).toBe(false); }); @@ -59,12 +71,20 @@ describe('the extension mongoQuery detection', () => { 'an import from the internal package', "import { mongoQuery } from '@internal/mongo-query-builder';", ], + [ + 'a namespace import of the family package', + `import * as builder from '${familyPackage}/query-builder';`, + ], ])('finds %s', (_name, content) => { expect(detects('extension', id, content)).toBe(true); }); it.each([ ['an import from the facade', `import { mongoQuery } from '${facadePackage}/query-builder';`], + [ + 'a namespace import of the facade', + `import * as builder from '${facadePackage}/query-builder';`, + ], ['an import of other names', `import { acc } from '${familyPackage}/query-builder';`], ])('skips %s', (_name, content) => { expect(detects('extension', id, content)).toBe(false); diff --git a/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts b/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts index bebbd33d45e9..1acc2774d6fc 100644 --- a/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts +++ b/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts @@ -22,6 +22,8 @@ describe('the extension detection for a required stack driver', () => { it.each([ ['the type name', 'function build(stack: SqlExecutionStackWithDriver) {}'], ['a missing-driver check', 'if (!stack.driver) throw new Error();'], + ['a stack with a longer name', 'const driver = clientStack.driver.create();'], + ['a stack on this', 'if (!this.stack.driver) throw new Error();'], ])('finds %s', (_name, content) => { expect(detects(content)).toBe(true); }); @@ -29,7 +31,9 @@ describe('the extension detection for a required stack driver', () => { it.each([ ['the stack with no driver', 'function build(stack: SqlExecutionStack) {}'], ['a longer name', 'type MySqlExecutionStackWithDriverFactory = () => void;'], - ['another object', 'const d = runtimeStack.driverName;'], + ['another member', 'const d = runtimeStack.driverName;'], + ['the driver on this', 'this.driver.close();'], + ['the driver option', 'const d = options.driver;'], ])('skips %s', (_name, content) => { expect(detects(content)).toBe(false); }); diff --git a/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md b/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md index 83eadb32db8d..662f2d978139 100644 --- a/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md +++ b/upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md @@ -2,12 +2,13 @@ changes: - id: mongo-query-validates-contract summary: | - `mongoQuery({ contract })` from `@prisma/orm-mongo/query-builder` now validates the contract as `mongo({ contract })` does, and throws `CONTRACT.VALIDATION_FAILED` for a malformed one. A value from `contractFromJson(...)` or `defineContract(...)` is not validated again. If you pass raw `contract.json`, or an object you built by hand, pass `contractFromJson(json)` instead. The family package `@prisma/orm-family-mongo/query-builder` no longer exports `mongoQuery`; import it from `@prisma/orm-mongo/query-builder`. The detection matches files that call `mongoQuery(`, import it under another name, or import it from the family package. + `mongoQuery({ contract })` from `@prisma/orm-mongo/query-builder` now validates the contract as `mongo({ contract })` does, and throws `CONTRACT.VALIDATION_FAILED` for a malformed one. A value from `contractFromJson(...)` or `defineContract(...)` is not validated again. If you pass raw `contract.json`, or an object you built by hand, pass `contractFromJson(json)` instead. The family package `@prisma/orm-family-mongo/query-builder` no longer exports `mongoQuery`; import it from `@prisma/orm-mongo/query-builder`. The detection matches files that call `mongoQuery(`, import it under another name, call it through a namespace import, or import it from the family package. detection: - glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs}" + glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" matches: - '(? Date: Fri, 23 Oct 2026 15:14:06 +0200 Subject: [PATCH 10/19] TML-3689: The upgrade detection finds driver destructured from a stack Signed-off-by: willbot Signed-off-by: Will Madden --- .../stack-driver-required/detection.test.ts | 8 ++++++++ .../stack-driver-required/extension/instructions.md | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts b/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts index 1acc2774d6fc..1d36cb9111d3 100644 --- a/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts +++ b/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts @@ -24,6 +24,12 @@ describe('the extension detection for a required stack driver', () => { ['a missing-driver check', 'if (!stack.driver) throw new Error();'], ['a stack with a longer name', 'const driver = clientStack.driver.create();'], ['a stack on this', 'if (!this.stack.driver) throw new Error();'], + [ + 'driver destructured from the stack factory', + 'const { driver } = createSqlExecutionStack({ target, adapter });', + ], + ['driver destructured from a stack', 'const { driver, adapter } = stack;'], + ['driver renamed while destructured', 'const { adapter, driver: d } = clientStack;'], ])('finds %s', (_name, content) => { expect(detects(content)).toBe(true); }); @@ -34,6 +40,8 @@ describe('the extension detection for a required stack driver', () => { ['another member', 'const d = runtimeStack.driverName;'], ['the driver on this', 'this.driver.close();'], ['the driver option', 'const d = options.driver;'], + ['driver destructured from options', 'const { driver } = options;'], + ['another member destructured from a stack', 'const { adapter } = stack;'], ])('skips %s', (_name, content) => { expect(detects(content)).toBe(false); }); diff --git a/upgrade-instructions/pending/stack-driver-required/extension/instructions.md b/upgrade-instructions/pending/stack-driver-required/extension/instructions.md index ef44ef3e16d8..0b7315a558d5 100644 --- a/upgrade-instructions/pending/stack-driver-required/extension/instructions.md +++ b/upgrade-instructions/pending/stack-driver-required/extension/instructions.md @@ -2,12 +2,13 @@ changes: - id: sql-stack-with-driver-requires-driver summary: | - `SqlExecutionStackWithDriver` now requires its `driver`, and `createSqlExecutionStack(...)` returns it only when given a driver. Without a driver it returns `SqlExecutionStack`, which has no `driver` member. `createToSql(stack, contract)` takes `SqlExecutionStack`. Type a stack built without a driver as `SqlExecutionStack`, and remove checks such as `if (!stack.driver) throw ...` on a stack typed `SqlExecutionStackWithDriver`, which can no longer fail. The framework `ExecutionStack` now declares `driver` optional. The detection matches files that name `SqlExecutionStackWithDriver` or read `driver` from a stack, such as `stack.driver` or `clientStack.driver`. + `SqlExecutionStackWithDriver` now requires its `driver`, and `createSqlExecutionStack(...)` returns it only when given a driver. Without a driver it returns `SqlExecutionStack`, which has no `driver` member. `createToSql(stack, contract)` takes `SqlExecutionStack`. Type a stack built without a driver as `SqlExecutionStack`, and remove checks such as `if (!stack.driver) throw ...` on a stack typed `SqlExecutionStackWithDriver`, which can no longer fail. The framework `ExecutionStack` now declares `driver` optional. The detection matches files that name `SqlExecutionStackWithDriver` or read `driver` from a stack, such as `stack.driver`, `clientStack.driver` or `const { driver } = createSqlExecutionStack(...)`. detection: glob: "**/*.{ts,mts,cts,tsx}" matches: - '(? Date: Fri, 23 Oct 2026 15:21:19 +0200 Subject: [PATCH 11/19] TML-3689: Stack driver detection matches driver only as the destructured property key Signed-off-by: willbot Signed-off-by: Will Madden --- .../stack-driver-required/detection.test.ts | 1 + .../pending/stack-driver-required/extension/instructions.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts b/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts index 1d36cb9111d3..07daffcd52de 100644 --- a/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts +++ b/test/integration/test/upgrade-instructions/stack-driver-required/detection.test.ts @@ -42,6 +42,7 @@ describe('the extension detection for a required stack driver', () => { ['the driver option', 'const d = options.driver;'], ['driver destructured from options', 'const { driver } = options;'], ['another member destructured from a stack', 'const { adapter } = stack;'], + ['another member renamed to driver', 'const { adapter: driver } = stack;'], ])('skips %s', (_name, content) => { expect(detects(content)).toBe(false); }); diff --git a/upgrade-instructions/pending/stack-driver-required/extension/instructions.md b/upgrade-instructions/pending/stack-driver-required/extension/instructions.md index 0b7315a558d5..08b330ce800f 100644 --- a/upgrade-instructions/pending/stack-driver-required/extension/instructions.md +++ b/upgrade-instructions/pending/stack-driver-required/extension/instructions.md @@ -8,7 +8,7 @@ changes: matches: - '(? Date: Tue, 27 Oct 2026 20:33:38 +0100 Subject: [PATCH 12/19] TML-3686: mongoQuery takes an execution context, as the SQL builder does mongoQuery({ context }) reads the contract from a MongoDB execution context instead of taking { contract }. mongoStatic() and mongo() build it from the context they create after validating the contract, so applications use mongoStatic({ contract }).query or db.query. @prisma/orm-mongo/query-builder republishes the builder unchanged, as @prisma/orm-postgres/builder republishes the SQL builder; the facade wrapper, its entry and the internal subpath are removed. The upgrade fragments describe the new call. Signed-off-by: willbot Signed-off-by: Will Madden --- architecture.config.json | 6 - ...-machine pattern for typed DSL builders.md | 2 +- .../subsystems/10. MongoDB Family.md | 7 +- docs/reference/Mongo Pipeline Builder.md | 6 +- .../mongo-demo/test/cache-middleware.test.ts | 2 +- .../0-shared/publish-surface/src/shells.ts | 8 +- .../query-builder/DEVELOPING.md | 2 +- .../5-query-builders/query-builder/README.md | 9 +- .../query-builder/package.json | 1 - .../query-builder/src/exports/index.ts | 1 + .../exports/internal-on-validated-contract.ts | 1 - .../query-builder/src/lookup-builder.ts | 2 +- .../query-builder/src/query.ts | 13 +- .../query-builder/src/state-classes.ts | 6 +- .../test/builder-new-stages.test.ts | 4 +- .../query-builder/test/builder.test-d.ts | 111 +++++++++--------- .../query-builder/test/builder.test.ts | 12 +- .../query-builder/test/exports.test.ts | 14 --- .../test/field-accessor.test-d.ts | 12 +- .../test/find-and-modify.test.ts | 4 +- .../test/pipeline-updates.test.ts | 4 +- .../query-builder/test/raw-command.test.ts | 8 +- .../query-builder/test/result-shape.test.ts | 8 +- .../test/state-machine-surface.test-d.ts | 6 +- .../test/state-machine.test-d.ts | 24 ++-- .../query-builder/test/stored-names.test-d.ts | 10 +- .../query-builder/test/stored-names.test.ts | 4 +- .../test/structured-error-codes.test.ts | 12 +- .../query-builder/test/writes.test.ts | 4 +- .../query-builder/tsdown.config.ts | 1 - .../7-runtime/test/runtime-types.test-d.ts | 6 +- .../3-targets/mongo/9-facade/package.json | 1 - .../9-facade/src/exports/query-builder.ts | 2 - .../mongo/9-facade/src/static/mongo-query.ts | 17 --- .../mongo/9-facade/src/static/mongo-static.ts | 6 +- .../9-facade/test/contract-from-json.test.ts | 18 +++ .../mongo/9-facade/test/mongo-query.test.ts | 53 --------- .../mongo/9-facade/test/mongo.test.ts | 10 +- .../9-facade/test/structured-errors.test.ts | 8 +- .../3-targets/mongo/9-facade/tsdown.config.ts | 1 - .../@prisma/orm-family-mongo/package.json | 1 - .../test/mongo-runtime/query-builder.test.ts | 9 +- .../test/mongo/query-builder.test.ts | 9 +- .../runtime/decode-via-query-builder.test.ts | 11 +- .../mongo/runtime/decode.integration.test.ts | 6 +- .../fixtures/decode-fixture-contract.ts | 4 +- .../detection.test.ts | 49 ++++++++ .../detection.test.ts | 92 --------------- .../app/instructions.md | 30 +++++ .../extension/instructions.md | 30 +++++ .../app/instructions.md | 23 ---- .../extension/instructions.md | 21 ---- 52 files changed, 303 insertions(+), 408 deletions(-) delete mode 100644 packages/2-mongo-family/5-query-builders/query-builder/src/exports/internal-on-validated-contract.ts delete mode 100644 packages/2-mongo-family/5-query-builders/query-builder/test/exports.test.ts delete mode 100644 packages/3-targets/mongo/9-facade/src/exports/query-builder.ts delete mode 100644 packages/3-targets/mongo/9-facade/src/static/mongo-query.ts delete mode 100644 packages/3-targets/mongo/9-facade/test/mongo-query.test.ts create mode 100644 test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts delete mode 100644 test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts create mode 100644 upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md create mode 100644 upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md delete mode 100644 upgrade-instructions/pending/mongo-query-validates-contract/app/instructions.md delete mode 100644 upgrade-instructions/pending/mongo-query-validates-contract/extension/instructions.md diff --git a/architecture.config.json b/architecture.config.json index 2c71c3a67dc9..7e186154fa06 100644 --- a/architecture.config.json +++ b/architecture.config.json @@ -1596,12 +1596,6 @@ "layer": "adapters", "plane": "runtime" }, - { - "glob": "packages/3-targets/mongo/9-facade/src/exports/query-builder.ts", - "domain": "extensions", - "layer": "adapters", - "plane": "runtime" - }, { "glob": "packages/3-targets/mongo/9-facade/src/exports/target.ts", "domain": "extensions", diff --git a/docs/architecture docs/adrs/ADR 201 - State-machine pattern for typed DSL builders.md b/docs/architecture docs/adrs/ADR 201 - State-machine pattern for typed DSL builders.md index c1537d7449ab..88e553e57606 100644 --- a/docs/architecture docs/adrs/ADR 201 - State-machine pattern for typed DSL builders.md +++ b/docs/architecture docs/adrs/ADR 201 - State-machine pattern for typed DSL builders.md @@ -38,7 +38,7 @@ The chain has three qualitatively different vocabularies, so it gets three class - **`FilteredCollection`** — reached by `.match(...)` on a `CollectionHandle` (or chained match). Filtered writes (`updateMany`, `updateOne`, `deleteMany`, `deleteOne`), `upsertOne(updater)`, `findOneAndUpdate`, `findOneAndDelete`. Further `.match(...)` calls stay on `FilteredCollection` (AND-folded); any pipeline-stage method moves to `PipelineChain`. - **`PipelineChain`** — reached by any pipeline-stage method on either earlier state. Pipeline stages (`.sort`, `.limit`, `.group`, `.lookup`, …), `$merge`/`$out` write terminals, and read terminals (`.build()`, `.aggregate()`). Pipeline-style writes and find-and-modify are *conditional* — available only when the capability markers `U` and `F` are still "enabled"; `L` is a prefix tracker that conditions what a further `.match(...)` does to `U`. See § 2. -The user-facing entry point `mongoQuery({ contract }).from(name)` returns a `CollectionHandle`. Everything else is reached from there. +The user-facing entry point `db.query.from(name)` (or `mongoStatic({ contract }).query.from(name)`) returns a `CollectionHandle`. Everything else is reached from there. > Throughout this ADR a **terminal** is a method that ends the chain by returning a `MongoQueryPlan` (e.g. `.build()`, `.updateMany()`, `.findOneAndUpdate(...)`). A **stage** is a method that extends the chain and returns another builder class. diff --git a/docs/architecture docs/subsystems/10. MongoDB Family.md b/docs/architecture docs/subsystems/10. MongoDB Family.md index de4955f17188..7c5fe94232e5 100644 --- a/docs/architecture docs/subsystems/10. MongoDB Family.md +++ b/docs/architecture docs/subsystems/10. MongoDB Family.md @@ -354,12 +354,13 @@ Row type inference, custom collection subclasses, and the `include` interface al Below the ORM and above the raw command AST sits `@internal/mongo-query-builder`: a contract-bound, typed CRUD surface that produces the same `MongoQueryPlan` values the ORM emits. The ORM is model-centric and multi-collection (roots, includes, repositories); the query builder is collection-centric and single-command (reads, writes, find-and-modify, `$merge` / `$out`). Migrations, raw tooling, and other non-ORM callers author plans through the query builder rather than hand-building AST nodes. -Applications use `mongoQuery` from `@prisma/orm-mongo/query-builder`. It validates the contract first, with the same validator and error as `mongo({ contract })`, and skips a value that was already validated, such as one from `contractFromJson`. The package's own builder, `mongoQueryOnValidatedContract`, is on the internal entry `@internal/mongo-query-builder/internal/on-validated-contract` (published as `@prisma/orm-family-mongo/query-builder/internal/on-validated-contract`) and does not validate. +`mongoQuery({ context })` takes an execution context, as the SQL builder `sql({ context })` does; it reads the contract from the context. A context comes only from `mongoStatic({ contract })` or `mongo({ contract })`, which validate the contract once, so applications use `mongoStatic({ contract }).query` or `db.query`. `@prisma/orm-mongo/query-builder` republishes the builder unchanged. ```typescript -import { acc, mongoQuery } from '@prisma/orm-mongo/query-builder'; +import { acc } from '@prisma/orm-mongo/query-builder'; +import mongoStatic from '@prisma/orm-mongo/static'; -const q = mongoQuery({ contract }); // from contractFromJson(json) or defineContract +const q = mongoStatic({ contract }).query; // or db.query from mongo({ contract, url }) // Read — aggregation pipeline const analytics = q diff --git a/docs/reference/Mongo Pipeline Builder.md b/docs/reference/Mongo Pipeline Builder.md index 79345aa66f5e..33af22c32cbd 100644 --- a/docs/reference/Mongo Pipeline Builder.md +++ b/docs/reference/Mongo Pipeline Builder.md @@ -9,12 +9,12 @@ This page is a working reference: a concrete example to ground the mental model, Imagine an `orders` collection where each document has `customerId`, `amount`, and `status`. We want the top ten customers by total spend on active orders: ```ts -import { contractFromJson } from '@prisma/orm-mongo/static'; -import { acc, mongoQuery } from '@prisma/orm-mongo/query-builder'; +import mongoStatic, { contractFromJson } from '@prisma/orm-mongo/static'; +import { acc } from '@prisma/orm-mongo/query-builder'; import type { Contract } from './contract'; import contractJson from './contract.json' with { type: 'json' }; -const query = mongoQuery({ contract: contractFromJson(contractJson) }); +const { query } = mongoStatic({ contract: contractFromJson(contractJson) }); const plan = query .from('orders') diff --git a/examples/mongo-demo/test/cache-middleware.test.ts b/examples/mongo-demo/test/cache-middleware.test.ts index b8ee866626aa..8fa5876126da 100644 --- a/examples/mongo-demo/test/cache-middleware.test.ts +++ b/examples/mongo-demo/test/cache-middleware.test.ts @@ -81,7 +81,7 @@ describe('mongo-demo cache middleware integration', { executor: runtime, enums: buildMongoEnums(contract, context.codecs), }); - const query = mongoQuery({ contract }); + const query = mongoQuery({ context }); return { runtime, orm, query, driver, driverExecuteSpy }; } diff --git a/packages/0-shared/publish-surface/src/shells.ts b/packages/0-shared/publish-surface/src/shells.ts index 018d870cb28f..720ccae0437c 100644 --- a/packages/0-shared/publish-surface/src/shells.ts +++ b/packages/0-shared/publish-surface/src/shells.ts @@ -276,12 +276,7 @@ const SQL_QUERY_REEXPORTS: readonly ShellReexportMapping[] = [ */ const MONGO_QUERY_REEXPORTS: readonly ShellReexportMapping[] = [ { package: '@internal/mongo-orm', entry: 'orm' }, - { - package: '@internal/mongo-query-builder', - entry: 'query-builder', - root: false, - subpaths: ['contract-free'], - }, + { package: '@internal/mongo-query-builder', entry: 'query-builder' }, { package: '@internal/mongo-query-ast', entry: 'query-ast' }, { package: '@internal/mongo-value', entry: 'value' }, ]; @@ -553,7 +548,6 @@ export const publicShells: ReadonlyMap = new Map< dir: 'packages/2-mongo-family/5-query-builders/query-builder', name: '@internal/mongo-query-builder', entry: 'query-builder', - subpaths: ['.', 'contract-free', 'internal/on-validated-contract'], }, { dir: 'packages/2-mongo-family/6-transport/mongo-lowering', diff --git a/packages/2-mongo-family/5-query-builders/query-builder/DEVELOPING.md b/packages/2-mongo-family/5-query-builders/query-builder/DEVELOPING.md index 98bc790d2d85..7c134298e455 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/DEVELOPING.md +++ b/packages/2-mongo-family/5-query-builders/query-builder/DEVELOPING.md @@ -6,7 +6,7 @@ Internal architecture notes for contributors. | File | Responsibility | |------|---------------| -| `query.ts` | Entry point (`mongoQueryOnValidatedContract`, internal entry; it trusts the contract it is given) — creates the initial builder | +| `query.ts` | Entry point (`mongoQuery({ context })`) — reads the contract from the execution context, creates initial builder | | `builder.ts` | `PipelineBuilder` class — immutable; each stage method returns a new instance with the appended stage | | `types.ts` | Core type machinery: `DocField`, `DocShape`, `ModelToDocShape`, `ResolveRow`, shape transformers (`ProjectedShape`, `GroupedDocShape`, `UnwoundShape`, etc.) | | `field-proxy.ts` | `Proxy`-based `FieldProxy` — intercepts property access to produce `TypedAggExpr` nodes | diff --git a/packages/2-mongo-family/5-query-builders/query-builder/README.md b/packages/2-mongo-family/5-query-builders/query-builder/README.md index 74fdb25c45a9..b633d1faa999 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/README.md +++ b/packages/2-mongo-family/5-query-builders/query-builder/README.md @@ -4,12 +4,13 @@ A typed CRUD query builder for MongoDB contracts. Reads, writes, pipeline aggreg ## Quick start -Applications use `mongoQuery` from `@prisma/orm-mongo/query-builder`, which validates the contract first. This package's builder, `mongoQueryOnValidatedContract`, lives on the internal entry `@internal/mongo-query-builder/internal/on-validated-contract` (published as `@prisma/orm-family-mongo/query-builder/internal/on-validated-contract`). It does not validate: pass it only a contract that is already validated, such as one from `contractFromJson`. +`mongoQuery({ context })` takes an execution context, as `sql({ context })` does. Applications get the builder as `mongoStatic({ contract }).query` or `db.query`, whose context holds the contract `mongoStatic()` or `mongo()` validated. ```typescript -import { acc, mongoQuery } from '@prisma/orm-mongo/query-builder'; +import { acc } from '@prisma/orm-mongo/query-builder'; +import mongoStatic from '@prisma/orm-mongo/static'; -const q = mongoQuery({ contract }); +const q = mongoStatic({ contract }).query; // Read — aggregation pipeline const analytics = q @@ -111,7 +112,7 @@ The chain has three steps: The result-row encoding uses a `ModelArrayField` marker so `ResolveRow` produces `Array` — the field is **not** `unknown[]`. Marker behaviour is unchanged from the legacy shape: clears `UpdateEnabled` and `FindAndModifyEnabled`, sets `LeadingMatch` to `'past-leading'`, preserves the nested-shape parameter `N`. -**Limitation.** Typoed root names (`from('usexxxrs')`) are currently accepted at compile time and rejected at runtime. The constraint is a pre-existing limitation of `Contract.roots: Record` (shared with `mongoQueryOnValidatedContract.from('badname')`); compile-time rejection is tracked under [TML-2400](https://linear.app/prisma-company/issue/TML-2400). +**Limitation.** Typoed root names (`from('usexxxrs')`) are currently accepted at compile time and rejected at runtime. The constraint is a pre-existing limitation of `Contract.roots: Record` (shared with `mongoQuery.from('badname')`); compile-time rejection is tracked under [TML-2400](https://linear.app/prisma-company/issue/TML-2400). ## Field accessor diff --git a/packages/2-mongo-family/5-query-builders/query-builder/package.json b/packages/2-mongo-family/5-query-builders/query-builder/package.json index d3efbcc918f0..35f26263055d 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/package.json +++ b/packages/2-mongo-family/5-query-builders/query-builder/package.json @@ -45,7 +45,6 @@ "exports": { ".": "./dist/index.mjs", "./contract-free": "./dist/exports/contract-free.mjs", - "./internal/on-validated-contract": "./dist/internal/on-validated-contract.mjs", "./package.json": "./package.json" }, "engines": { diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/exports/index.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/exports/index.ts index e28e05e94e9c..ac02cf05bcdd 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/src/exports/index.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/src/exports/index.ts @@ -24,6 +24,7 @@ export type { } from '../lookup-builder'; export type { FindAndModifyEnabled, UpdateEnabled } from '../markers'; export type { QueryRoot } from '../query'; +export { mongoQuery } from '../query'; export type { ModelArrayField, ModelNestedShape, diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/exports/internal-on-validated-contract.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/exports/internal-on-validated-contract.ts deleted file mode 100644 index 7fcbada09dd1..000000000000 --- a/packages/2-mongo-family/5-query-builders/query-builder/src/exports/internal-on-validated-contract.ts +++ /dev/null @@ -1 +0,0 @@ -export { mongoQueryOnValidatedContract } from '../query'; diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.ts index 24c7241dde93..866d3a7273f9 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.ts @@ -112,7 +112,7 @@ export interface LookupBuilder< /** * Type of the `from` callable passed to `PipelineChain.lookup`'s outer * callback. The generic argument is inferred from a string-literal - * argument (the same pattern as `mongoQueryOnValidatedContract({ contract }).from('orders')`), + * argument (the same pattern as `mongoQuery({ context }).from('orders')`), * which grounds `RootName` into the returned `LookupBuilder` *before* * the inner `on(...)` callback is type-checked. This sequential * inference is what makes `foreign` resolve narrowly to the foreign diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts index f4a312b5be6e..8bd8d11542ab 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts @@ -10,7 +10,9 @@ import { contractError } from './contract-errors'; import { asMongoContract, type CollectionHandle, createCollectionHandle } from './state-classes'; /** - * The query builder over a contract that is already validated; it does not validate. Applications use `mongoQuery` from `@prisma/orm-mongo/query-builder`, which validates first. `mongoQueryOnValidatedContract(...).from(rootName)` yields the root state of the three-state machine (`CollectionHandle` → `FilteredCollection` → `PipelineChain`). + * Public entry point of the query builder. `mongoQuery(...).from(rootName)` + * yields the root state of the three-state machine + * (`CollectionHandle` → `FilteredCollection` → `PipelineChain`). * * `rawCommand(cmd)` is the escape hatch for cases the typed surface does * not cover (yet) — it accepts any `AnyMongoCommand` (typed CRUD or a @@ -27,10 +29,11 @@ export interface QueryRoot< rawCommand(command: C): MongoQueryPlan; } -export function mongoQueryOnValidatedContract< +/** Builds queries for the contract of `context`, which `mongoStatic({ contract })` and `mongo({ contract })` create after validating the contract. */ +export function mongoQuery< TContract extends MongoContractWithTypeMaps, ->(options: { readonly contract: TContract }): QueryRoot { - const { contract } = options; +>(options: { readonly context: { readonly contract: TContract } }): QueryRoot { + const { contract } = options.context; return { from(rootName: K) { return createCollectionHandle(contract, rootName); @@ -41,7 +44,7 @@ export function mongoQueryOnValidatedContract< if (!storageHash) { throw contractError( 'CONTRACT.VALIDATION_FAILED', - 'Contract is missing storage.storageHash. Pass a validated contract to mongoQueryOnValidatedContract().', + 'Contract is missing storage.storageHash. Pass a validated contract to mongoQuery().', ); } const meta: PlanMeta = { diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/state-classes.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/state-classes.ts index 19bec88fd0cb..c709642291f4 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/src/state-classes.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/src/state-classes.ts @@ -86,7 +86,7 @@ function writeMeta(storageHash: string): PlanMeta { /** * Root state of the query-builder state machine. Returned from - * `mongoQueryOnValidatedContract(...).from(name)` and bound to a single collection. + * `mongoQuery(...).from(name)` and bound to a single collection. * * Inherits the entire pipeline-stage surface from `PipelineChain` (since an * empty `CollectionHandle` is observably an empty pipeline). Adds: @@ -634,7 +634,7 @@ export interface BindingContext< /** * Construct a `CollectionHandle` from a validated contract + root name. - * Used by `mongoQueryOnValidatedContract(...).from(name)` to enter the state machine. + * Used by `mongoQuery(...).from(name)` to enter the state machine. */ export function createCollectionHandle< TContract extends MongoContractWithTypeMaps, @@ -662,7 +662,7 @@ export function createCollectionHandle< if (!c.storage?.storageHash) { throw contractError( 'CONTRACT.VALIDATION_FAILED', - 'Contract is missing storage.storageHash. Pass a validated contract to mongoQueryOnValidatedContract().', + 'Contract is missing storage.storageHash. Pass a validated contract to mongoQuery().', ); } return new CollectionHandle( diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts index aac69506def6..7c68e9e6a6a7 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts @@ -23,11 +23,11 @@ import { MongoVectorSearchStage, } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import { testContract } from './fixtures/test-contract'; function createOrdersBuilder() { - return mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); + return mongoQuery({ context: { contract: testContract } }).from('orders'); } describe('new stage builder methods', () => { diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts index bbeebdc08c5f..f00b4c8d1d76 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts @@ -3,7 +3,7 @@ import { MongoAggFieldRef, MongoLimitStage } from '@internal/mongo-query-ast/exe import { expectTypeOf } from 'vitest'; import { acc } from '../src/accumulator-helpers'; import { fn } from '../src/expression-helpers'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import type { TContract } from './fixtures/test-contract'; declare const contract: TContract; @@ -19,9 +19,17 @@ type OrderRow = { readonly tags: string[]; }; +describe('mongoQuery', () => { + it('takes an execution context, not a contract', () => { + expectTypeOf(mongoQuery({ context: { contract } }).from('orders')).not.toBeNever(); + // @ts-expect-error -- a contract alone is not accepted; build the context with mongoStatic() or mongo() + mongoQuery({ contract }); + }); +}); + describe('builder shape tests', () => { it('sort() only accepts keys from current shape', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); const builder = p.from('orders'); builder.sort({ amount: -1 }); builder.sort({ status: 1 }); @@ -30,7 +38,7 @@ describe('builder shape tests', () => { }); it('group() replaces shape — previous fields inaccessible', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); const grouped = p.from('orders').group((f) => ({ _id: f.customerId, total: acc.sum(f.amount), @@ -47,7 +55,7 @@ describe('builder shape tests', () => { }); it('addFields() extends shape', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); const extended = p.from('orders').addFields((f) => ({ fullName: fn.concat(f.status, fn.literal(' ')), })); @@ -57,7 +65,7 @@ describe('builder shape tests', () => { }); it('project() inclusion narrows shape', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); const projected = p.from('orders').project('status', 'amount'); projected.sort({ status: 1 }); @@ -68,7 +76,7 @@ describe('builder shape tests', () => { }); it('count() replaces shape with single field', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); const counted = p.from('orders').count('total'); counted.sort({ total: 1 }); @@ -77,7 +85,7 @@ describe('builder shape tests', () => { }); it('lookup() adds array field and preserves existing fields', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); const withLookup = p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -96,7 +104,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects bad local field at type level (AC-1 TC-1)', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -109,7 +117,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects bad foreign field at type level (AC-1 TC-2)', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -122,7 +130,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects non-leaf returns from on() (AC-2 TC-3)', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -136,11 +144,11 @@ describe('builder shape tests', () => { // Bad-from(name) typo rejection is intentionally not tested here — it is // deferred to TML-2400 (Contract.roots: Record widens - // keyof under intersection, identical to today's mongoQueryOnValidatedContract.from('bad') + // keyof under intersection, identical to today's mongoQuery.from('bad') // behaviour). See spec § Open Questions / Resolved decisions. it('replaceRoot() replaces entire shape', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); type NewShape = { readonly x: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; }; @@ -155,12 +163,12 @@ describe('builder shape tests', () => { describe('resolved row types', () => { it('from() → build() resolves to concrete field types', () => { - const plan = mongoQueryOnValidatedContract({ contract }).from('orders').build(); + const plan = mongoQuery({ context: { contract } }).from('orders').build(); expectTypeOf>().toEqualTypeOf(); }); it('match() preserves row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .match((f) => f.status.eq('active') as MongoFilterExpr) .build(); @@ -168,15 +176,12 @@ describe('resolved row types', () => { }); it('sort() preserves row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }) - .from('orders') - .sort({ amount: -1 }) - .build(); + const plan = mongoQuery({ context: { contract } }).from('orders').sort({ amount: -1 }).build(); expectTypeOf>().toEqualTypeOf(); }); it('limit() / skip() / sample() preserve row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .limit(10) .skip(5) @@ -186,7 +191,7 @@ describe('resolved row types', () => { }); it('addFields() extends row with new fields at correct types', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .addFields((f) => ({ fullName: fn.concat(f.status, fn.literal(' ')), @@ -206,7 +211,7 @@ describe('resolved row types', () => { }); it('project() inclusion narrows to selected fields at correct types', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .project('status', 'amount') .build(); @@ -218,7 +223,7 @@ describe('resolved row types', () => { }); it('project() computed includes expression fields at correct types', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .project((f) => ({ status: 1 as const, @@ -235,7 +240,7 @@ describe('resolved row types', () => { }); it('group() resolves _id at grouped-by field type, accumulators at correct types', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .group((f) => ({ _id: f.customerId, @@ -253,17 +258,17 @@ describe('resolved row types', () => { }); it('unwind() preserves row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }).from('orders').unwind('tags').build(); + const plan = mongoQuery({ context: { contract } }).from('orders').unwind('tags').build(); expectTypeOf>().toEqualTypeOf(); }); it('count() row type is { [field]: number }', () => { - const plan = mongoQueryOnValidatedContract({ contract }).from('orders').count('total').build(); + const plan = mongoQuery({ context: { contract } }).from('orders').count('total').build(); expectTypeOf>().toEqualTypeOf<{ total: number }>(); }); it('sortByCount() row type is { _id: ; count: number }', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .sortByCount((f) => f.status) .build(); @@ -274,7 +279,7 @@ describe('resolved row types', () => { }); it('lookup() resolves the as-named field to Array with concrete leaf types (AC-3 TC-4)', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .lookup((from) => from('users') @@ -302,7 +307,7 @@ describe('resolved row types', () => { }); it('lookup() resolves nested value-object fields on the foreign model recursively', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .lookup((from) => from('customers') @@ -341,7 +346,7 @@ describe('resolved row types', () => { }); it('entry-point query resolves value-object fields recursively (model-origin brand)', () => { - const plan = mongoQueryOnValidatedContract({ contract }).from('customers').build(); + const plan = mongoQuery({ context: { contract } }).from('customers').build(); expectTypeOf>().toEqualTypeOf<{ readonly _id: string; readonly name: string; @@ -362,7 +367,7 @@ describe('resolved row types', () => { }); it('addFields() preserves model-origin resolution and adds the new fields on top', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('customers') .addFields((f) => ({ upperName: fn.concat(f.name, fn.literal('!')), @@ -389,7 +394,7 @@ describe('resolved row types', () => { }); it('group() drops model-origin resolution (legitimate shape replacement)', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('customers') .group((f) => ({ _id: f.name, @@ -409,7 +414,7 @@ describe('resolved row types', () => { readonly x: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; readonly y: { readonly codecId: 'mongo/double@1'; readonly nullable: true }; }; - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .replaceRoot((f) => f.status) .build(); @@ -420,7 +425,7 @@ describe('resolved row types', () => { }); it('pipe() preserves row type by default', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .pipe(new MongoLimitStage(5)) .build(); @@ -431,7 +436,7 @@ describe('resolved row types', () => { type NewShape = { readonly a: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; }; - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .pipe(new MongoLimitStage(5)) .build(); @@ -439,14 +444,14 @@ describe('resolved row types', () => { }); it('nullable fields resolve to T | null', () => { - const plan = mongoQueryOnValidatedContract({ contract }).from('orders').build(); + const plan = mongoQuery({ context: { contract } }).from('orders').build(); type Row = PlanRow; expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); it('chained pipeline produces correct cumulative row types', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .match((f) => f.status.eq('active') as MongoFilterExpr) .group((f) => ({ @@ -465,7 +470,7 @@ describe('resolved row types', () => { }); it('execute() infers Row from build() plan type', () => { - const plan = mongoQueryOnValidatedContract({ contract }).from('orders').build(); + const plan = mongoQuery({ context: { contract } }).from('orders').build(); const execute = {} as (p: MongoQueryPlan) => Promise; const result = execute(plan); expectTypeOf[0]>().toEqualTypeOf(); @@ -474,7 +479,7 @@ describe('resolved row types', () => { describe('resolved row types — new stages', () => { it('redact() preserves row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .redact((f) => f.status) .build(); @@ -482,19 +487,17 @@ describe('resolved row types — new stages', () => { }); it('out() returns a write-terminal plan with an unknown row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }).from('orders').out('archive'); + const plan = mongoQuery({ context: { contract } }).from('orders').out('archive'); expectTypeOf>().toEqualTypeOf(); }); it('merge() returns a write-terminal plan with an unknown row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }) - .from('orders') - .merge({ into: 'summary' }); + const plan = mongoQuery({ context: { contract } }).from('orders').merge({ into: 'summary' }); expectTypeOf>().toEqualTypeOf(); }); it('unionWith() preserves row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .unionWith('archived_orders') .build(); @@ -502,7 +505,7 @@ describe('resolved row types — new stages', () => { }); it('densify() preserves row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .densify({ field: 'amount', range: { step: 10, bounds: 'full' } }) .build(); @@ -510,7 +513,7 @@ describe('resolved row types — new stages', () => { }); it('fill() preserves row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .fill({ sortBy: { amount: 1 }, output: { notes: { method: 'linear' } } }) .build(); @@ -518,7 +521,7 @@ describe('resolved row types — new stages', () => { }); it('search() preserves row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .search({ text: { query: 'test', path: 'status' } }) .build(); @@ -526,7 +529,7 @@ describe('resolved row types — new stages', () => { }); it('vectorSearch() preserves row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .vectorSearch({ index: 'idx', @@ -540,7 +543,7 @@ describe('resolved row types — new stages', () => { }); it('bucket() resets to untyped row', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .bucket({ groupBy: MongoAggFieldRef.of('amount'), boundaries: [0, 100, 500] }) .build(); @@ -548,7 +551,7 @@ describe('resolved row types — new stages', () => { }); it('bucketAuto() resets to untyped row', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .bucketAuto({ groupBy: MongoAggFieldRef.of('amount'), buckets: 5 }) .build(); @@ -556,7 +559,7 @@ describe('resolved row types — new stages', () => { }); it('facet() resets to untyped row', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .facet({ counts: [], top: [] }) .build(); @@ -564,7 +567,7 @@ describe('resolved row types — new stages', () => { }); it('geoNear() resets to untyped row', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .geoNear({ near: [0, 0], distanceField: 'dist' }) .build(); @@ -572,7 +575,7 @@ describe('resolved row types — new stages', () => { }); it('graphLookup() resets to untyped row', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .graphLookup({ from: 'categories', @@ -586,7 +589,7 @@ describe('resolved row types — new stages', () => { }); it('setWindowFields() resets to untyped row', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .setWindowFields({ sortBy: { amount: 1 }, output: {} }) .build(); @@ -594,7 +597,7 @@ describe('resolved row types — new stages', () => { }); it('searchMeta() resets to untyped row', () => { - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('orders') .searchMeta({ facet: { operator: {} } }) .build(); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts index c82cd1ae4fee..66451b805161 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts @@ -23,15 +23,15 @@ import { describe, expect, it } from 'vitest'; import { acc } from '../src/accumulator-helpers'; import { fn } from '../src/expression-helpers'; import type { LookupOnResult } from '../src/lookup-builder'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import { testContract } from './fixtures/test-contract'; function createOrdersBuilder() { - return mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); + return mongoQuery({ context: { contract: testContract } }).from('orders'); } function createCustomersBuilder() { - return mongoQueryOnValidatedContract({ contract: testContract }).from('customers'); + return mongoQuery({ context: { contract: testContract } }).from('customers'); } describe('PipelineChain', () => { @@ -385,16 +385,16 @@ describe('PipelineChain', () => { }); }); -describe('mongoQueryOnValidatedContract()', () => { +describe('mongoQuery()', () => { it('from() creates builder for known root', () => { - const p = mongoQueryOnValidatedContract({ contract: testContract }); + const p = mongoQuery({ context: { contract: testContract } }); const builder = p.from('orders'); const plan = builder.build(); expect(plan.collection).toBe('orders'); }); it('from() throws for unknown root', () => { - const p = mongoQueryOnValidatedContract({ contract: testContract }); + const p = mongoQuery({ context: { contract: testContract } }); expect(() => p.from('nonexistent' as 'orders')).toThrow('Unknown root'); }); }); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/exports.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/exports.test.ts deleted file mode 100644 index e681dc254308..000000000000 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/exports.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import * as root from '../src/exports/index'; -import * as onValidatedContract from '../src/exports/internal-on-validated-contract'; - -describe('the query builder entries', () => { - it('keep the builder that trusts its contract off the root entry', () => { - expect(Object.keys(root)).not.toContain('mongoQuery'); - expect(Object.keys(root)).not.toContain('mongoQueryOnValidatedContract'); - }); - - it('offer it on the internal on-validated-contract entry only', () => { - expect(Object.keys(onValidatedContract)).toEqual(['mongoQueryOnValidatedContract']); - }); -}); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts index 0824dfa2fb71..e42a7dc35916 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts @@ -6,7 +6,7 @@ import type { ObjectExpression, } from '../src/field-accessor'; import { createFieldAccessor } from '../src/field-accessor'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import type { ModelNestedShape, NestedDocShape, ObjectField } from '../src/resolve-path'; import type { DocField, ModelToDocShape } from '../src/types'; import type { TContract, TestContract } from './fixtures/test-contract'; @@ -212,20 +212,20 @@ describe('FieldAccessor.rawPath escape hatch', () => { describe('Pipeline integration — N threading', () => { it('CollectionHandle.match callback allows callable dot-path access', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); p.from('customers').match((f) => f('address.city').eq('London')); p.from('customers').match((f) => f('address.geo.lat').gt(0)); }); it('FilteredCollection.updateMany callback allows callable dot-path access', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); p.from('customers') .match((f) => f('address.city').eq('London')) .updateMany((f) => [f('address.zip').set('SW1'), f('stats.visits').inc(1)]); }); it('rejects bogus paths in match callback', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); p.from('customers').match((f) => // @ts-expect-error -- 'address.bogus' is not a valid path f('address.bogus').eq('x'), @@ -233,7 +233,7 @@ describe('Pipeline integration — N threading', () => { }); it('additive stages (sort/addFields/redact) preserve callable dot-paths', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); p.from('customers') .sort({ name: 1 }) .match((f) => f('address.city').eq('London')); @@ -244,7 +244,7 @@ describe('Pipeline integration — N threading', () => { }); it('replacement stages (group/project/replaceRoot) disable callable dot-paths', () => { - const p = mongoQueryOnValidatedContract({ contract }); + const p = mongoQuery({ context: { contract } }); const grouped = p.from('customers').group((f) => ({ _id: f.name, count: f._id })); // After group, N has been reset. The flat Shape no longer contains // value objects, and the callable form rejects any string. diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts index 720079a5e08c..0ef83fc9cf73 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts @@ -5,12 +5,12 @@ import { } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; import type { PipelineChain } from '../src/builder'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import type { ModelToDocShape } from '../src/types'; import type { TContract } from './fixtures/test-contract'; import { testContract } from './fixtures/test-contract'; -const orders = () => mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); +const orders = () => mongoQuery({ context: { contract: testContract } }).from('orders'); /** * After A25 (`CollectionHandle`/`FilteredCollection` start with diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts index c091cda124ff..6ab15783eb0c 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts @@ -7,13 +7,13 @@ import { } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; import type { PipelineChain } from '../src/builder'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import type { ModelToDocShape } from '../src/types'; import type { UpdaterResult } from '../src/update-ops'; import type { TContract } from './fixtures/test-contract'; import { testContract } from './fixtures/test-contract'; -const orders = () => mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); +const orders = () => mongoQuery({ context: { contract: testContract } }).from('orders'); /** * After A25 (`CollectionHandle`/`FilteredCollection` start with diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts index a541554071c4..5a2aa414dfb4 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts @@ -1,10 +1,10 @@ import { InsertOneCommand, RawAggregateCommand } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import type { TContract } from './fixtures/test-contract'; import { testContract } from './fixtures/test-contract'; -const root = () => mongoQueryOnValidatedContract({ contract: testContract }); +const root = () => mongoQuery({ context: { contract: testContract } }); describe('M5 raw escape hatch', () => { it('packages a typed CRUD command into a plan with lane: mongo-query', () => { @@ -26,8 +26,8 @@ describe('M5 raw escape hatch', () => { it('throws when the contract is missing a storageHash (signals an unvalidated contract)', () => { const cmd = new InsertOneCommand('orders', { status: 'new' }); expect(() => - mongoQueryOnValidatedContract({ - contract: { ...testContract, storage: {} } as unknown as TContract, + mongoQuery({ + context: { contract: { ...testContract, storage: {} } as unknown as TContract }, }).rawCommand(cmd), ).toThrow(/storageHash/); }); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts index e49677f052f2..a732440a0b80 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts @@ -1,6 +1,6 @@ import { MongoFieldFilter, MongoProjectStage } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import { contractFieldToMongoFieldShape, contractModelToMongoResultShape, @@ -197,12 +197,14 @@ describe('contractFieldToMongoFieldShape', () => { describe('PipelineChain build resultShape', () => { it('identity pipeline attaches document shape from bound model', () => { - const plan = mongoQueryOnValidatedContract({ contract: testContract }).from('orders').build(); + const plan = mongoQuery({ context: { contract: testContract } }) + .from('orders') + .build(); expect(plan.resultShape?.kind).toBe('document'); }); it('project stage reifies a document shape retaining _id and the projected field', () => { - const plan = mongoQueryOnValidatedContract({ contract: testContract }) + const plan = mongoQuery({ context: { contract: testContract } }) .from('orders') .match(MongoFieldFilter.eq('status', 'x')) .project('status') diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts index 2a79bf06fe89..b65ef432c94e 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts @@ -1,17 +1,17 @@ import { MongoFieldFilter } from '@internal/mongo-query-ast/execution'; import { describe, it } from 'vitest'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import type { TContract } from './fixtures/test-contract'; declare const contract: TContract; -const handle = () => mongoQueryOnValidatedContract({ contract }).from('orders'); +const handle = () => mongoQuery({ context: { contract } }).from('orders'); const filtered = () => handle().match(MongoFieldFilter.eq('status', 'new')); /** * Negative-surface type tests for the M1 state machine + M2 write methods. * * The point is to lock down which methods *don't* exist on each state, so a - * misuse like `mongoQueryOnValidatedContract(...).from('x').updateMany(...)` (writing to every + * misuse like `mongoQuery(...).from('x').updateMany(...)` (writing to every * doc through the wrong terminal) doesn't typecheck. Each `@ts-expect-error` * here is load-bearing — its absence would silently re-open a footgun the * spec set out to close. diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts index d6b9b15e6d8b..7e64731ef339 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts @@ -2,7 +2,7 @@ import { MongoFieldFilter } from '@internal/mongo-query-ast/execution'; import { describe, expectTypeOf, it } from 'vitest'; import type { PipelineChain } from '../src/builder'; import type { FindAndModifyEnabled, UpdateEnabled } from '../src/markers'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import type { CollectionHandle, FilteredCollection } from '../src/state-classes'; import type { TContract } from './fixtures/test-contract'; @@ -25,19 +25,19 @@ type GetF = describe('state machine', () => { it('from(name) returns CollectionHandle (root state) inheriting PipelineChain', () => { - const handle = mongoQueryOnValidatedContract({ contract }).from('orders'); + const handle = mongoQuery({ context: { contract } }).from('orders'); expectTypeOf(handle).toExtend>(); }); it('CollectionHandle.match(...) transitions to FilteredCollection', () => { - const filtered = mongoQueryOnValidatedContract({ contract }) + const filtered = mongoQuery({ context: { contract } }) .from('orders') .match(MongoFieldFilter.eq('status', 'active')); expectTypeOf(filtered).toExtend>(); }); it('FilteredCollection.match(...) stays in FilteredCollection (AND-folds)', () => { - const filtered = mongoQueryOnValidatedContract({ contract }) + const filtered = mongoQuery({ context: { contract } }) .from('orders') .match(MongoFieldFilter.eq('status', 'active')) .match(MongoFieldFilter.gt('amount', 100)); @@ -45,7 +45,7 @@ describe('state machine', () => { }); it('pipeline-stage methods drop out of the state-class subclasses', () => { - const sorted = mongoQueryOnValidatedContract({ contract }).from('orders').sort({ amount: -1 }); + const sorted = mongoQuery({ context: { contract } }).from('orders').sort({ amount: -1 }); // No longer a CollectionHandle/FilteredCollection — write/find-and-modify // surfaces have been left behind. expectTypeOf(sorted).not.toExtend>(); @@ -58,7 +58,7 @@ describe('state machine', () => { // find-and-modify terminals inherited from `PipelineChain` are gated off // by default, so a leading `.match(...)` is required before reaching them // via the `FilteredCollection` overrides (see ADR 201). - const handle = mongoQueryOnValidatedContract({ contract }).from('orders'); + const handle = mongoQuery({ context: { contract } }).from('orders'); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); }); @@ -70,7 +70,7 @@ describe('state machine', () => { // marker-gated PipelineChain versions), so the initial marker state // stays cleared to prevent accidental access to the PipelineChain // inheritance path. - const filtered = mongoQueryOnValidatedContract({ contract }) + const filtered = mongoQuery({ context: { contract } }) .from('orders') .match(MongoFieldFilter.eq('status', 'active')); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); @@ -78,23 +78,23 @@ describe('state machine', () => { }); it('marker table: limit() leaves both markers cleared', () => { - const limited = mongoQueryOnValidatedContract({ contract }).from('orders').limit(1); + const limited = mongoQuery({ context: { contract } }).from('orders').limit(1); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); }); it('marker table: sort / addFields / group all leave both markers cleared from .from()', () => { - const sorted = mongoQueryOnValidatedContract({ contract }).from('orders').sort({ amount: -1 }); + const sorted = mongoQuery({ context: { contract } }).from('orders').sort({ amount: -1 }); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); - const added = mongoQueryOnValidatedContract({ contract }) + const added = mongoQuery({ context: { contract } }) .from('orders') .addFields((f) => ({ doubled: f.amount })); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); - const grouped = mongoQueryOnValidatedContract({ contract }) + const grouped = mongoQuery({ context: { contract } }) .from('orders') .group((_f) => ({ _id: null })); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); @@ -106,7 +106,7 @@ describe('state machine', () => { // parameters here; the runtime / shape behaviour is asserted in // builder.test-d.ts. it('marker table: lookup() clears both markers', () => { - const looked = mongoQueryOnValidatedContract({ contract }) + const looked = mongoQuery({ context: { contract } }) .from('orders') .lookup((from) => from('users') diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts index 9baed8fe6844..801cf4f16538 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts @@ -1,6 +1,6 @@ import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; import { expectTypeOf } from 'vitest'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import type { ModelNestedShape } from '../src/resolve-path'; import type { ModelToDocShape } from '../src/types'; import { type TestContract, testContract } from './fixtures/test-contract'; @@ -38,12 +38,14 @@ describe('stored names', () => { }); it('exposes _id, not id, on a model whose id field is stored as _id', () => { - const plan = mongoQueryOnValidatedContract({ contract: testContract }).from('accounts').build(); + const plan = mongoQuery({ context: { contract: testContract } }) + .from('accounts') + .build(); expectTypeOf>().toEqualTypeOf(); }); it('accepts stored names and rejects domain field names in match', () => { - const accounts = mongoQueryOnValidatedContract({ contract: testContract }).from('accounts'); + const accounts = mongoQuery({ context: { contract: testContract } }).from('accounts'); accounts.match((f) => f._id.eq('a')); accounts.match((f) => f('home_address.city').eq('NYC')); // @ts-expect-error -- `id` is a domain field name, not a stored name @@ -53,7 +55,7 @@ describe('stored names', () => { }); it('keys a looked-up foreign row by stored name', () => { - const plan = mongoQueryOnValidatedContract({ contract: testContract }) + const plan = mongoQuery({ context: { contract: testContract } }) .from('orders') .lookup((from) => from('accounts') diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts index 0134909f8ac5..b9ddfb7b2108 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts @@ -4,11 +4,11 @@ import { MongoProjectStage, } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import { testContract } from './fixtures/test-contract'; describe('stored names', () => { - const accounts = () => mongoQueryOnValidatedContract({ contract: testContract }).from('accounts'); + const accounts = () => mongoQuery({ context: { contract: testContract } }).from('accounts'); it('keys the result shape of a model read by stored name', () => { expect(accounts().build().resultShape).toEqual({ diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts index 127c116cccd5..22302db30cba 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts @@ -2,12 +2,12 @@ import { InsertOneCommand } from '@internal/mongo-query-ast/execution'; import { isStructuredError } from '@internal/utils/structured-error'; import { describe, expect, it } from 'vitest'; import type { PipelineChain } from '../src/builder'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import type { ModelToDocShape } from '../src/types'; import type { TContract } from './fixtures/test-contract'; import { testContract, testContractJson } from './fixtures/test-contract'; -const orders = () => mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); +const orders = () => mongoQuery({ context: { contract: testContract } }).from('orders'); type FamReachableChain = PipelineChain< TContract, @@ -51,7 +51,7 @@ describe('structured error codes', () => { }); it('raises ORM.MODEL_UNKNOWN for an unknown root', () => { - const p = mongoQueryOnValidatedContract({ contract: testContract }); + const p = mongoQuery({ context: { contract: testContract } }); expect(codeOf(() => p.from('nonexistent' as 'orders'))).toBe('ORM.MODEL_UNKNOWN'); }); @@ -63,13 +63,13 @@ describe('structured error codes', () => { orders: { $ref: '#/domain/models/Ghost', model: 'Ghost' }, }, }; - const p = mongoQueryOnValidatedContract({ contract: patched as unknown as TContract }); + const p = mongoQuery({ context: { contract: patched as unknown as TContract } }); expect(codeOf(() => p.from('orders'))).toBe('CONTRACT.MODEL_UNKNOWN'); }); it('raises CONTRACT.VALIDATION_FAILED when the contract has no storageHash', () => { - const p = mongoQueryOnValidatedContract({ - contract: { ...testContract, storage: {} } as unknown as TContract, + const p = mongoQuery({ + context: { contract: { ...testContract, storage: {} } as unknown as TContract }, }); expect(codeOf(() => p.rawCommand(new InsertOneCommand('orders', { status: 'new' })))).toBe( 'CONTRACT.VALIDATION_FAILED', diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts index 25c2d5d237c9..8da3899f1403 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts @@ -9,10 +9,10 @@ import { UpdateOneCommand, } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; -import { mongoQueryOnValidatedContract } from '../src/query'; +import { mongoQuery } from '../src/query'; import { testContract } from './fixtures/test-contract'; -const orders = () => mongoQueryOnValidatedContract({ contract: testContract }).from('orders'); +const orders = () => mongoQuery({ context: { contract: testContract } }).from('orders'); describe('M2 write terminals', () => { describe('inserts (CollectionHandle)', () => { diff --git a/packages/2-mongo-family/5-query-builders/query-builder/tsdown.config.ts b/packages/2-mongo-family/5-query-builders/query-builder/tsdown.config.ts index d54fb798a19d..211121ea0e55 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/tsdown.config.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/tsdown.config.ts @@ -4,6 +4,5 @@ export default defineConfig({ entry: { index: 'src/exports/index.ts', 'exports/contract-free': 'src/exports/contract-free.ts', - 'internal/on-validated-contract': 'src/exports/internal-on-validated-contract.ts', }, }); diff --git a/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts b/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts index 7b9207f9d8dc..0ebefd95e23e 100644 --- a/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts +++ b/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts @@ -6,7 +6,7 @@ import type { MongoTypeMaps, } from '@internal/mongo-contract'; import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; -import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; +import { mongoQuery } from '@internal/mongo-query-builder'; import { expectTypeOf } from 'vitest'; import type { MongoRuntime } from '../src/mongo-runtime'; @@ -93,7 +93,7 @@ declare const contract: TContract; describe('runtime type safety', () => { it('query() returns AsyncIterableResult where Row matches build() row type', () => { - const plan = mongoQueryOnValidatedContract({ contract }).from('orders').build(); + const plan = mongoQuery({ context: { contract } }).from('orders').build(); type Row = PlanRow; expectTypeOf().toEqualTypeOf(); @@ -103,7 +103,7 @@ describe('runtime type safety', () => { }); it('query() result awaits to Row[]', () => { - const plan = mongoQueryOnValidatedContract({ contract }).from('orders').build(); + const plan = mongoQuery({ context: { contract } }).from('orders').build(); type Row = PlanRow; const runtime = {} as MongoRuntime; diff --git a/packages/3-targets/mongo/9-facade/package.json b/packages/3-targets/mongo/9-facade/package.json index 0c3746aa7cbc..2a49456ca522 100644 --- a/packages/3-targets/mongo/9-facade/package.json +++ b/packages/3-targets/mongo/9-facade/package.json @@ -69,7 +69,6 @@ "./contract-builder": "./dist/contract-builder.mjs", "./control": "./dist/control.mjs", "./family": "./dist/family.mjs", - "./query-builder": "./dist/query-builder.mjs", "./runtime": "./dist/runtime.mjs", "./static": "./dist/static.mjs", "./target": "./dist/target.mjs", diff --git a/packages/3-targets/mongo/9-facade/src/exports/query-builder.ts b/packages/3-targets/mongo/9-facade/src/exports/query-builder.ts deleted file mode 100644 index beb712d27e2d..000000000000 --- a/packages/3-targets/mongo/9-facade/src/exports/query-builder.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from '@internal/mongo-query-builder'; -export { mongoQuery } from '../static/mongo-query'; diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-query.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-query.ts deleted file mode 100644 index b3673fec2cb9..000000000000 --- a/packages/3-targets/mongo/9-facade/src/static/mongo-query.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { - AnyMongoTypeMaps, - MongoContract, - MongoContractWithTypeMaps, -} from '@internal/mongo-contract'; -import type { QueryRoot } from '@internal/mongo-query-builder'; -import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; -import { validateMongoContract } from './contract-from-json'; - -/** - * The typed query builder for `contract`. The contract is validated as `mongo({ contract })` validates it, once per contract value. - */ -export function mongoQuery< - TContract extends MongoContractWithTypeMaps, ->(options: { readonly contract: TContract }): QueryRoot { - return mongoQueryOnValidatedContract({ contract: validateMongoContract(options.contract) }); -} diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts index 83f595df7a59..edb8b9fc6030 100644 --- a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts +++ b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts @@ -8,7 +8,7 @@ import type { } from '@internal/mongo-contract'; import type { MongoRawClient } from '@internal/mongo-orm'; import { mongoRaw } from '@internal/mongo-orm'; -import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; +import { mongoQuery } from '@internal/mongo-query-builder'; import { buildMongoEnums, createMongoExecutionContext, @@ -30,7 +30,7 @@ export interface MongoStaticContext< readonly context: MongoExecutionContext; readonly contract: TContract; readonly enums: UnboundEnums; - readonly query: ReturnType>; + readonly query: ReturnType>; readonly raw: MongoRawClient; } @@ -56,7 +56,7 @@ export function buildMongoStaticContext< UnboundEnums, 'built from this contract domain; the mapped-type shape cannot be proven statically' >(unbound); - const query = mongoQueryOnValidatedContract({ contract }); + const query = mongoQuery({ context }); const raw = mongoRaw({ contract }); return { context, contract, enums, enumsByNamespace, query, raw }; } diff --git a/packages/3-targets/mongo/9-facade/test/contract-from-json.test.ts b/packages/3-targets/mongo/9-facade/test/contract-from-json.test.ts index 8fffdf15912e..83b4ea35d322 100644 --- a/packages/3-targets/mongo/9-facade/test/contract-from-json.test.ts +++ b/packages/3-targets/mongo/9-facade/test/contract-from-json.test.ts @@ -46,6 +46,24 @@ describe('contractFromJson', () => { }); }); +describe('the query builder of a static context', () => { + it('is never built for a malformed contract, because mongoStatic validates it first', () => { + expect(() => mongoStatic({ contract: { target: 'mongo' } as unknown as Contract })).toThrow( + expect.objectContaining({ code: 'CONTRACT.VALIDATION_FAILED' }), + ); + }); + + it('builds queries over the contract of its context', () => { + const { query, contract } = mongoStatic({ contract: authoredContract() }); + const root = Object.keys(contract.roots)[0] as keyof typeof contract.roots & string; + + expect(query.from(root).build()).toMatchObject({ + collection: 'accounts', + meta: { storageHash: contract.storage.storageHash, lane: 'mongo-query' }, + }); + }); +}); + describe('validating a contract once', () => { it('validates a contractFromJson value once for every client built from it', () => { const deserialize = vi.spyOn(MongoContractSerializer.prototype, 'deserializeContract'); diff --git a/packages/3-targets/mongo/9-facade/test/mongo-query.test.ts b/packages/3-targets/mongo/9-facade/test/mongo-query.test.ts deleted file mode 100644 index aeae9ff405c3..000000000000 --- a/packages/3-targets/mongo/9-facade/test/mongo-query.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { MongoContractSerializer } from '@internal/family-mongo/ir'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { Contract } from '../../../../2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract'; -import fixtureContractJson from '../../../../2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.json' with { - type: 'json', -}; -import { mongoQuery, PipelineChain } from '../src/exports/query-builder'; -import mongo from '../src/exports/runtime'; -import { contractFromJson } from '../src/exports/static'; - -afterEach(() => { - vi.restoreAllMocks(); -}); - -function thrownBy(run: () => unknown): unknown { - try { - run(); - } catch (error) { - return error; - } - throw new Error('nothing was thrown'); -} - -describe('mongoQuery({ contract })', () => { - it('rejects a malformed contract with the error mongo({ contract }) throws', () => { - const malformed = { target: 'mongo' } as unknown as Contract; - - const fromQuery = thrownBy(() => mongoQuery({ contract: malformed })); - const fromClient = thrownBy(() => mongo({ contract: malformed })); - - expect(fromQuery).toMatchObject({ code: 'CONTRACT.VALIDATION_FAILED' }); - expect({ - code: (fromQuery as { code: unknown }).code, - message: (fromQuery as Error).message, - }).toEqual({ - code: (fromClient as { code: unknown }).code, - message: (fromClient as Error).message, - }); - }); - - it('does not validate a contractFromJson value again', () => { - const contract = contractFromJson(structuredClone(fixtureContractJson)); - const deserialize = vi.spyOn(MongoContractSerializer.prototype, 'deserializeContract'); - - mongoQuery({ contract }); - - expect(deserialize).not.toHaveBeenCalled(); - }); - - it('still offers the rest of the query builder', () => { - expect(PipelineChain).toBeTypeOf('function'); - }); -}); diff --git a/packages/3-targets/mongo/9-facade/test/mongo.test.ts b/packages/3-targets/mongo/9-facade/test/mongo.test.ts index b86466c7a015..c5f1cf85e0db 100644 --- a/packages/3-targets/mongo/9-facade/test/mongo.test.ts +++ b/packages/3-targets/mongo/9-facade/test/mongo.test.ts @@ -20,7 +20,7 @@ const mocks = vi.hoisted(() => ({ driverFromDb: vi.fn(), deserializeContract: vi.fn(), mongoOrm: vi.fn(), - mongoQueryOnValidatedContract: vi.fn(), + mongoQuery: vi.fn(), mongoRaw: vi.fn(), })); @@ -59,8 +59,8 @@ vi.mock('@internal/mongo-orm', () => ({ mongoRaw: mocks.mongoRaw, })); -vi.mock('@internal/mongo-query-builder/internal/on-validated-contract', () => ({ - mongoQueryOnValidatedContract: mocks.mongoQueryOnValidatedContract, +vi.mock('@internal/mongo-query-builder', () => ({ + mongoQuery: mocks.mongoQuery, })); import mongo from '../src/runtime/mongo'; @@ -93,7 +93,7 @@ describe('mongo() facade', () => { mocks.driverFromDb.mockReturnValue({ id: 'driver-from-db', close: fakeDriverFromDbClose }); mocks.createMongoRuntime.mockReturnValue(fakeRuntime); mocks.mongoOrm.mockReturnValue(fakeOrm); - mocks.mongoQueryOnValidatedContract.mockReturnValue(fakeQuery); + mocks.mongoQuery.mockReturnValue(fakeQuery); mocks.mongoRaw.mockReturnValue(fakeRaw); fakeRaw.collection.mockClear(); fakeRuntime.close.mockClear(); @@ -107,7 +107,7 @@ describe('mongo() facade', () => { expect(db.orm).toBe(fakeOrm); expect(db.query).toBe(fakeQuery); expect(mocks.mongoOrm).toHaveBeenCalledTimes(1); - expect(mocks.mongoQueryOnValidatedContract).toHaveBeenCalledTimes(1); + expect(mocks.mongoQuery).toHaveBeenCalledTimes(1); expect(mocks.driverFromConnection).not.toHaveBeenCalled(); expect(mocks.driverFromDb).not.toHaveBeenCalled(); diff --git a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts index 60ebf90bf274..484419d090b8 100644 --- a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts +++ b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts @@ -19,7 +19,7 @@ const mocks = vi.hoisted(() => ({ deserializeContract: vi.fn(), mongoOrm: vi.fn(), mongoRaw: vi.fn(), - mongoQueryOnValidatedContract: vi.fn(), + mongoQuery: vi.fn(), })); vi.mock('@internal/adapter-mongo/runtime', () => ({ default: { id: 'adapter' } })); @@ -47,8 +47,8 @@ vi.mock('@internal/mongo-orm', () => ({ mongoOrm: mocks.mongoOrm, mongoRaw: mocks.mongoRaw, })); -vi.mock('@internal/mongo-query-builder/internal/on-validated-contract', () => ({ - mongoQueryOnValidatedContract: mocks.mongoQueryOnValidatedContract, +vi.mock('@internal/mongo-query-builder', () => ({ + mongoQuery: mocks.mongoQuery, })); import { resolveMongoBinding, resolveOptionalMongoBinding } from '../src/runtime/binding'; @@ -190,7 +190,7 @@ describe('facade lifecycle structured codes', () => { }); mocks.createMongoRuntime.mockReturnValue({ id: 'runtime' }); mocks.mongoOrm.mockReturnValue({ id: 'orm' }); - mocks.mongoQueryOnValidatedContract.mockReturnValue({ id: 'query' }); + mocks.mongoQuery.mockReturnValue({ id: 'query' }); mocks.mongoRaw.mockReturnValue({ id: 'raw' }); }); diff --git a/packages/3-targets/mongo/9-facade/tsdown.config.ts b/packages/3-targets/mongo/9-facade/tsdown.config.ts index eecbd9c1d392..3638dc0cbf61 100644 --- a/packages/3-targets/mongo/9-facade/tsdown.config.ts +++ b/packages/3-targets/mongo/9-facade/tsdown.config.ts @@ -7,7 +7,6 @@ export default defineConfig({ 'src/exports/contract-builder.ts', 'src/exports/control.ts', 'src/exports/family.ts', - 'src/exports/query-builder.ts', 'src/exports/runtime.ts', 'src/exports/static.ts', 'src/exports/target.ts', diff --git a/packages/9-public/@prisma/orm-family-mongo/package.json b/packages/9-public/@prisma/orm-family-mongo/package.json index bce5ea54ceb8..fd596b063aa1 100644 --- a/packages/9-public/@prisma/orm-family-mongo/package.json +++ b/packages/9-public/@prisma/orm-family-mongo/package.json @@ -77,7 +77,6 @@ "./query-ast/execution": "./dist/query-ast__execution.mjs", "./query-builder": "./dist/query-builder.mjs", "./query-builder/contract-free": "./dist/query-builder__contract-free.mjs", - "./query-builder/internal/on-validated-contract": "./dist/query-builder__internal__on-validated-contract.mjs", "./runtime": "./dist/runtime.mjs", "./schema-ir": "./dist/schema-ir.mjs", "./value": "./dist/value.mjs", diff --git a/test/integration/test/mongo-runtime/query-builder.test.ts b/test/integration/test/mongo-runtime/query-builder.test.ts index 2c6ddd80940f..0615b2ed3b0d 100644 --- a/test/integration/test/mongo-runtime/query-builder.test.ts +++ b/test/integration/test/mongo-runtime/query-builder.test.ts @@ -1,6 +1,6 @@ // Intentionally uses verbose mongo-contract-ts import: @internal/mongo/contract-builder's // defineContract wrap loses inline-model inference precision when consumers use -// mongoQueryOnValidatedContract chains (PlanRow row shapes collapse to _id: never / count: never). +// mongoQuery chains (PlanRow row shapes collapse to _id: never / count: never). // Tracked at https://linear.app/prisma-company/issue/TML-2633 — migrate to the facade form // once TML-2633 lands. @@ -14,8 +14,7 @@ import type { } from '@internal/mongo-contract'; import { defineContract, field, model, rel } from '@internal/mongo-contract-ts/contract-builder'; import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; -import { acc, fn } from '@internal/mongo-query-builder'; -import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; +import { acc, fn, mongoQuery } from '@internal/mongo-query-builder'; import mongoTargetPack from '@internal/target-mongo/pack'; import { timeouts } from '@repo/test-utils'; import { ObjectId } from 'mongodb'; @@ -181,7 +180,7 @@ const contract = defineContract({ }); describe('pipeline builder integration', { timeout: timeouts.spinUpMongoMemoryServer }, () => { - const p = mongoQueryOnValidatedContract({ contract: contractFromJson(contractJson) }); + const p = mongoQuery({ context: { contract: contractFromJson(contractJson) } }); it('match → group → sort → limit analytics query', async () => { await withMongod(async (ctx) => { @@ -359,7 +358,7 @@ describe('pipeline builder integration', { timeout: timeouts.spinUpMongoMemorySe }, ]); - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('tasks') .group((f) => ({ _id: f.type, diff --git a/test/integration/test/mongo/query-builder.test.ts b/test/integration/test/mongo/query-builder.test.ts index 853dac339e1e..eec705bf0b91 100644 --- a/test/integration/test/mongo/query-builder.test.ts +++ b/test/integration/test/mongo/query-builder.test.ts @@ -19,8 +19,7 @@ import { MongoRedactStage, MongoSortStage, } from '@internal/mongo-query-ast/execution'; -import { acc, fn } from '@internal/mongo-query-builder'; -import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; +import { acc, fn, mongoQuery } from '@internal/mongo-query-builder'; import { describe, expect, it } from 'vitest'; import { describeWithMongoDB } from './setup'; @@ -195,9 +194,7 @@ const contractJson = { // Helpers // --------------------------------------------------------------------------- -const query = mongoQueryOnValidatedContract({ - contract: contractFromJson(contractJson), -}); +const query = mongoQuery({ context: { contract: contractFromJson(contractJson) } }); function products() { return query.from('products'); @@ -259,7 +256,7 @@ const ORDERS = [ // Tests // --------------------------------------------------------------------------- -describeWithMongoDB('Pipeline builder integration (mongoQueryOnValidatedContract DSL)', (ctx) => { +describeWithMongoDB('Pipeline builder integration (mongoQuery DSL)', (ctx) => { async function seed() { const db = ctx.client.db(ctx.dbName); await db.collection('products').insertMany(PRODUCTS.map((p) => ({ ...p }))); diff --git a/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts b/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts index e51beefbe054..e141e45ad074 100644 --- a/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts +++ b/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts @@ -1,6 +1,6 @@ /** * End-to-end test for the user-facing path: contract types → - * `mongoQueryOnValidatedContract(...).from(...)..build()` → `runtime.query(plan)` → + * `mongoQuery(...).from(...)..build()` → `runtime.query(plan)` → * decoded rows. * * This file is the single artifact tying contract type inference, lane @@ -13,8 +13,7 @@ import { MongoContractSerializer } from '@internal/family-mongo/ir'; import { contractFromJson } from '@internal/mongo/static'; -import { acc } from '@internal/mongo-query-builder'; -import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; +import { acc, mongoQuery } from '@internal/mongo-query-builder'; import { MongoParamRef } from '@internal/mongo-value'; import { timeouts } from '@repo/test-utils'; import { ObjectId } from 'mongodb'; @@ -25,8 +24,8 @@ import { } from './fixtures/decode-fixture-contract'; import { withMongod } from './setup'; -const q = mongoQueryOnValidatedContract({ - contract: contractFromJson(decodeFixtureContractJson), +const q = mongoQuery({ + context: { contract: contractFromJson(decodeFixtureContractJson) }, }); describe('Mongo runtime decode integration via query-builder', { @@ -46,7 +45,7 @@ describe('Mongo runtime decode integration via query-builder', { embeddings: vec, }); - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('users') .match((f) => f['_id']!.eq(MongoParamRef.of(insert.insertedId, { codecId: 'mongo/objectId@1' })), diff --git a/test/integration/test/mongo/runtime/decode.integration.test.ts b/test/integration/test/mongo/runtime/decode.integration.test.ts index 7647dff7c531..994d1cb1050b 100644 --- a/test/integration/test/mongo/runtime/decode.integration.test.ts +++ b/test/integration/test/mongo/runtime/decode.integration.test.ts @@ -9,7 +9,7 @@ import { MongoMatchStage, RawAggregateCommand, } from '@internal/mongo-query-ast/execution'; -import { mongoQueryOnValidatedContract } from '@internal/mongo-query-builder/internal/on-validated-contract'; +import { mongoQuery } from '@internal/mongo-query-builder'; import { MongoParamRef } from '@internal/mongo-value'; import { timeouts } from '@repo/test-utils'; import { ObjectId } from 'mongodb'; @@ -38,7 +38,7 @@ describe('Mongo runtime decode integration', { timeout: timeouts.spinUpMongoMemo }); // User-facing path: build the plan through the query-builder so the Row type is contract-derived (no explicit annotation on execute). - const plan = mongoQueryOnValidatedContract({ contract }) + const plan = mongoQuery({ context: { contract } }) .from('users') .match((f) => f['_id']!.eq(MongoParamRef.of(insert.insertedId, { codecId: 'mongo/objectId@1' })), @@ -59,7 +59,7 @@ describe('Mongo runtime decode integration', { timeout: timeouts.spinUpMongoMemo it('decode failure surfaces RUNTIME.DECODE_FAILED with details and cause', async () => { await withMongod(async (ctx) => { - // The synthetic codec id (`test/throws-on-decode@1`) is not a contract field codec, so this test legitimately needs a stub `resultShape` — the user-facing `mongoQueryOnValidatedContract(...)` path can't construct a shape referencing a codec the contract doesn't declare. The tradeoff is intentional: this test exercises the failure-envelope plumbing, not the lane-population path. The other two integration tests in this file go through the + // The synthetic codec id (`test/throws-on-decode@1`) is not a contract field codec, so this test legitimately needs a stub `resultShape` — the user-facing `mongoQuery(...)` path can't construct a shape referencing a codec the contract doesn't declare. The tradeoff is intentional: this test exercises the failure-envelope plumbing, not the lane-population path. The other two integration tests in this file go through the // query-builder. const failing = mongoCodec({ typeId: 'test/throws-on-decode@1', diff --git a/test/integration/test/mongo/runtime/fixtures/decode-fixture-contract.ts b/test/integration/test/mongo/runtime/fixtures/decode-fixture-contract.ts index f7f8f5ae13f8..9f34dfaafe31 100644 --- a/test/integration/test/mongo/runtime/fixtures/decode-fixture-contract.ts +++ b/test/integration/test/mongo/runtime/fixtures/decode-fixture-contract.ts @@ -98,10 +98,10 @@ type DecodeFixtureStorage = { /** * Shared fixture contract for the decode integration tests. * - * Lives in a typed `.ts` so it can drive `mongoQueryOnValidatedContract({ contract })` row-type + * Lives in a typed `.ts` so it can drive `mongoQuery({ context: { contract } })` row-type * inference end-to-end. Call sites validate the JSON via the Mongo contract * serializer SPI to keep the runtime structural shape honest, then thread - * `TContract` through `mongoQueryOnValidatedContract` for the type-level path. + * `TContract` through `mongoQuery` for the type-level path. */ export type DecodeFixtureContract = Omit, 'domain'> & { readonly roots: { diff --git a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts new file mode 100644 index 000000000000..d621e7084122 --- /dev/null +++ b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts @@ -0,0 +1,49 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'pathe'; +import { describe, expect, it } from 'vitest'; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../../..'); +const id = 'mongo-query-takes-context'; +const facadePackage = ['@prisma', 'orm-mongo'].join('/'); +const familyPackage = ['@prisma', 'orm-family-mongo'].join('/'); + +function detects(audience: 'app' | 'extension', content: string): boolean { + const path = `upgrade-instructions/pending/mongo-query-takes-context/${audience}/instructions.md`; + const guide = readFileSync(join(repoRoot, path), 'utf-8'); + const entry = guide.split(/\n {2}- id: /).find((block) => block.startsWith(`${id}\n`)); + if (entry === undefined) throw new Error(`The ${audience} guide has no change ${id}`); + const patterns = [...entry.matchAll(/^ {8}- '(.*)'$/gm)].map( + ([, pattern]) => new RegExp((pattern ?? '').replaceAll("''", "'")), + ); + if (patterns.length === 0) throw new Error(`The ${audience} change ${id} has no detection`); + return patterns.some((pattern) => pattern.test(content)); +} + +describe.each(['app', 'extension'] as const)('the %s mongoQuery detection', (audience) => { + it.each([ + ['a call with a contract', 'const query = mongoQuery({ contract });'], + ['a call with a named contract', 'mongoQuery({ contract: contractFromJson(json) })'], + ['a call with a type argument', 'mongoQuery({\n contract,\n})'], + ['an aliased import', `import { mongoQuery as query } from '${facadePackage}/query-builder';`], + [ + 'a call through a namespace import', + `import * as builder from '${familyPackage}/query-builder';\nbuilder.mongoQuery({ context });`, + ], + ])('finds %s', (_name, content) => { + expect(detects(audience, content)).toBe(true); + }); + + it.each([ + ['a call with a context', 'const query = mongoQuery({ context });'], + ['the static query', 'const { query } = mongoStatic({ contract });'], + ['a method of the same name', 'db.mongoQuery({ contract })'], + ['a longer name', 'myMongoQuery({ contract })'], + [ + 'a member call on another namespace', + `import * as builder from '${facadePackage}/query-builder';\nother.mongoQuery({ context });`, + ], + ])('skips %s', (_name, content) => { + expect(detects(audience, content)).toBe(false); + }); +}); diff --git a/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts b/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts deleted file mode 100644 index d06d86e80104..000000000000 --- a/test/integration/test/upgrade-instructions/mongo-query-validates-contract/detection.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'pathe'; -import { describe, expect, it } from 'vitest'; - -const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../../..'); -const familyPackage = ['@prisma', 'orm-family-mongo'].join('/'); -const facadePackage = ['@prisma', 'orm-mongo'].join('/'); - -function detects(audience: 'app' | 'extension', id: string, content: string): boolean { - const path = `upgrade-instructions/pending/mongo-query-validates-contract/${audience}/instructions.md`; - const guide = readFileSync(join(repoRoot, path), 'utf-8'); - const entry = guide.split(/\n {2}- id: /).find((block) => block.startsWith(`${id}\n`)); - if (entry === undefined) throw new Error(`The ${audience} guide has no change ${id}`); - const patterns = [...entry.matchAll(/^ {8}- '(.*)'$/gm)].map( - ([, pattern]) => new RegExp((pattern ?? '').replaceAll("''", "'")), - ); - if (patterns.length === 0) throw new Error(`The ${audience} change ${id} has no detection`); - return patterns.some((pattern) => pattern.test(content)); -} - -describe('the app mongoQuery detection', () => { - const id = 'mongo-query-validates-contract'; - - it.each([ - ['a call', 'const query = mongoQuery({ contract });'], - ['a call with a space', 'mongoQuery ({ contract })'], - ['an aliased import', `import { mongoQuery as query } from '${facadePackage}/query-builder';`], - [ - 'an import from the family package', - `import { acc, mongoQuery } from '${familyPackage}/query-builder';`, - ], - [ - 'a call through a namespace import of the facade', - `import * as builder from '${facadePackage}/query-builder';\nconst q = builder.mongoQuery({ contract });`, - ], - [ - 'a call through a namespace import of the family package', - `import * as qb from '${familyPackage}/query-builder';\nqb.mongoQuery({ contract });`, - ], - ])('finds %s', (_name, content) => { - expect(detects('app', id, content)).toBe(true); - }); - - it.each([ - ['a method of the same name', 'db.mongoQuery({ contract })'], - ['a longer name', 'myMongoQuery({ contract })'], - ['an import of other names', `import { acc } from '${familyPackage}/query-builder';`], - [ - 'a member call on another namespace', - `import * as builder from '${facadePackage}/query-builder';\nother.mongoQuery({ contract });`, - ], - ])('skips %s', (_name, content) => { - expect(detects('app', id, content)).toBe(false); - }); -}); - -describe('the extension mongoQuery detection', () => { - const id = 'family-query-builder-drops-mongo-query'; - - it.each([ - [ - 'an import from the family package', - `import { mongoQuery } from '${familyPackage}/query-builder';`, - ], - [ - 'an aliased import from the family package', - `import { mongoQuery as query } from '${familyPackage}/query-builder';`, - ], - [ - 'an import from the internal package', - "import { mongoQuery } from '@internal/mongo-query-builder';", - ], - [ - 'a namespace import of the family package', - `import * as builder from '${familyPackage}/query-builder';`, - ], - ])('finds %s', (_name, content) => { - expect(detects('extension', id, content)).toBe(true); - }); - - it.each([ - ['an import from the facade', `import { mongoQuery } from '${facadePackage}/query-builder';`], - [ - 'a namespace import of the facade', - `import * as builder from '${facadePackage}/query-builder';`, - ], - ['an import of other names', `import { acc } from '${familyPackage}/query-builder';`], - ])('skips %s', (_name, content) => { - expect(detects('extension', id, content)).toBe(false); - }); -}); diff --git a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md new file mode 100644 index 000000000000..0c24a4d07161 --- /dev/null +++ b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md @@ -0,0 +1,30 @@ +--- +changes: + - id: mongo-query-takes-context + summary: | + `mongoQuery({ contract })` from `@prisma/orm-mongo/query-builder` no longer compiles: `mongoQuery` takes `{ context }`, an execution context. Replace `mongoQuery({ contract })` with `mongoStatic({ contract }).query` from `@prisma/orm-mongo/static`, or with `db.query` when you have a client from `mongo({ contract })`. Both validate the contract once. The detection matches files that call `mongoQuery` with a `contract` option, import it under another name, or call it through a namespace import. + detection: + glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" + matches: + - '(?()]*>)?\s*\(\s*\{\s*contract\b' + - '(?()]*>)?\s*\(\s*\{\s*contract\b' + - '(?(json)` instead. The family package `@prisma/orm-family-mongo/query-builder` no longer exports `mongoQuery`; import it from `@prisma/orm-mongo/query-builder`. The detection matches files that call `mongoQuery(`, import it under another name, call it through a namespace import, or import it from the family package. - detection: - glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" - matches: - - '(?(contractJson) }); -``` diff --git a/upgrade-instructions/pending/mongo-query-validates-contract/extension/instructions.md b/upgrade-instructions/pending/mongo-query-validates-contract/extension/instructions.md deleted file mode 100644 index afeeb2e09e22..000000000000 --- a/upgrade-instructions/pending/mongo-query-validates-contract/extension/instructions.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -changes: - - id: family-query-builder-drops-mongo-query - summary: | - `@prisma/orm-family-mongo/query-builder` (`@internal/mongo-query-builder`) no longer exports `mongoQuery`. Use `mongoQuery` from `@prisma/orm-mongo/query-builder`, which validates the contract once, as `mongo({ contract })` does. Extension code that builds plans on a contract it has already validated, such as one from `contractFromJson`, may instead use `mongoQueryOnValidatedContract` from the internal path `@prisma/orm-family-mongo/query-builder/internal/on-validated-contract`; it does not validate. The detection matches files that import `mongoQuery` from the family package, under its own name or another, and files that import the family package as a namespace. - detection: - glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" - matches: - - '\bimport\s*(?:type\s*)?\{[^}]*(? Date: Tue, 27 Oct 2026 20:47:06 +0100 Subject: [PATCH 13/19] TML-3686: MongoExecutionContext lives in query-ast without a stack, and mongoQuery takes it As SQL declares ExecutionContext in relational-core, MongoDB declares MongoExecutionContext in @internal/mongo-query-ast, with the contract, the codec lookup and applyMutationDefaults, and no stack. @internal/mongo-runtime still exports it. createMongoRuntime takes the stack as its own option, the way the SQL runtime receives its adapter beside the context, and every reader of context.stack uses the stack it now receives. mongoQuery takes { context: MongoExecutionContext }, so an object holding only a contract no longer compiles. Signed-off-by: willbot Signed-off-by: Will Madden --- .../mongo-demo/test/cache-middleware.test.ts | 2 +- .../4-query/query-ast/package.json | 1 + .../query-ast/src/execution-context.ts | 22 +++ .../query-ast/src/exports/execution.ts | 1 + .../test/integration/orm-ergonomics.test.ts | 2 +- .../orm/test/integration/polymorphism.test.ts | 2 +- .../query-builder/src/query.ts | 8 +- .../test/builder-new-stages.test.ts | 3 +- .../query-builder/test/builder.test-d.ts | 127 +++++++++++------- .../query-builder/test/builder.test.ts | 9 +- .../test/field-accessor.test-d.ts | 11 +- .../test/find-and-modify.test.ts | 3 +- .../test/fixtures/test-context.ts | 9 ++ .../test/pipeline-updates.test.ts | 3 +- .../query-builder/test/raw-command.test.ts | 5 +- .../query-builder/test/result-shape.test.ts | 5 +- .../test/state-machine-surface.test-d.ts | 3 +- .../test/state-machine.test-d.ts | 29 ++-- .../query-builder/test/stored-names.test-d.ts | 7 +- .../query-builder/test/stored-names.test.ts | 3 +- .../test/structured-error-codes.test.ts | 9 +- .../query-builder/test/writes.test.ts | 3 +- packages/2-mongo-family/7-runtime/README.md | 5 +- .../7-runtime/src/codecs/decoding.ts | 7 +- .../7-runtime/src/exports/index.ts | 3 +- .../7-runtime/src/mongo-enums.ts | 2 +- .../7-runtime/src/mongo-execution-stack.ts | 29 +--- .../7-runtime/src/mongo-runtime.ts | 22 +-- .../7-runtime/test/mongo-enums.test.ts | 2 +- .../test/mongo-middleware-fixtures.ts | 12 +- .../test/mongo-middleware.identity.test.ts | 12 +- .../7-runtime/test/mongo-middleware.test.ts | 28 ++-- .../test/mongo-runtime-abort.test.ts | 23 ++-- .../test/mongo-runtime.decode.test.ts | 2 +- .../test/mongo-runtime.finalization.test.ts | 36 +++-- .../test/mongo-runtime.statistics.test.ts | 24 ++-- .../test/runtime-options-shape.test-d.ts | 24 +++- .../7-runtime/test/runtime-types.test-d.ts | 8 +- .../mongo/9-facade/src/runtime/mongo.ts | 3 +- .../mongo/9-facade/src/static/mongo-static.ts | 4 +- pnpm-lock.yaml | 3 + test/integration/test/_harness/mongo.ts | 2 +- .../cross-family-middleware.test.ts | 14 +- .../test/mongo-runtime/query-builder.test.ts | 10 +- .../test/mongo/query-builder.test.ts | 6 +- .../mongo/runtime/content-hash-guard.test.ts | 1 + .../runtime/decode-via-query-builder.test.ts | 14 +- .../mongo/runtime/decode.integration.test.ts | 6 +- .../execute-param-mutator-wiring.test.ts | 20 +-- .../runtime/mongo-execution-stack.test.ts | 4 +- test/integration/test/mongo/runtime/setup.ts | 2 +- test/integration/test/mongo/setup.ts | 4 +- .../detection.test.ts | 28 +++- .../app/instructions.md | 18 ++- .../extension/instructions.md | 18 ++- 55 files changed, 407 insertions(+), 256 deletions(-) create mode 100644 packages/2-mongo-family/4-query/query-ast/src/execution-context.ts create mode 100644 packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts diff --git a/examples/mongo-demo/test/cache-middleware.test.ts b/examples/mongo-demo/test/cache-middleware.test.ts index 8fa5876126da..10555513f6a5 100644 --- a/examples/mongo-demo/test/cache-middleware.test.ts +++ b/examples/mongo-demo/test/cache-middleware.test.ts @@ -75,7 +75,7 @@ describe('mongo-demo cache middleware integration', { const driver = await createMongoDriver(replSet.getUri(), dbName); const driverExecuteSpy = vi.spyOn(driver, 'execute'); const cache = createCacheMiddleware(); - const runtime = createMongoRuntime({ context, driver, middleware: [cache] }); + const runtime = createMongoRuntime({ context, stack, driver, middleware: [cache] }); const orm = mongoOrm({ contract, executor: runtime, diff --git a/packages/2-mongo-family/4-query/query-ast/package.json b/packages/2-mongo-family/4-query/query-ast/package.json index 04f1892309fc..23fadca54c2d 100644 --- a/packages/2-mongo-family/4-query/query-ast/package.json +++ b/packages/2-mongo-family/4-query/query-ast/package.json @@ -18,6 +18,7 @@ "dependencies": { "@internal/contract": "workspace:8.0.0-rc.20", "@internal/framework-components": "workspace:8.0.0-rc.20", + "@internal/mongo-codec": "workspace:8.0.0-rc.20", "@internal/mongo-contract": "workspace:8.0.0-rc.20", "@internal/mongo-value": "workspace:8.0.0-rc.20", "@internal/utils": "workspace:8.0.0-rc.20", diff --git a/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts new file mode 100644 index 000000000000..7191f73356fd --- /dev/null +++ b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts @@ -0,0 +1,22 @@ +import type { MutationDefaults } from '@internal/framework-components/runtime'; +import type { MongoCodec } from '@internal/mongo-codec'; + +/** + * Read-only view of the codec registry exposed on `MongoExecutionContext`. + * + * Hides `register()` and the iterator from public surface — users do not mutate the per-execution codec registry. + */ +export interface MongoCodecLookup { + get(id: string): MongoCodec | undefined; + has(id: string): boolean; +} + +/** + * The context the query lanes build against: the user's contract, a read-only lookup over the codec registry composed from every stack contributor, and `applyMutationDefaults`, which fills the contract's execution defaults from the composed generators. The runtime creates it with `createMongoExecutionContext`. + * + * Mirrors SQL's `ExecutionContext`: it holds no runtime concerns such as the adapter or the stack; the runtime receives the stack as its own option. + */ +export interface MongoExecutionContext extends MutationDefaults { + readonly contract: TContract; + readonly codecs: MongoCodecLookup; +} diff --git a/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts b/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts index f8a314111f7a..bcf76279755e 100644 --- a/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts +++ b/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts @@ -26,6 +26,7 @@ export { UpdateManyCommand, UpdateOneCommand, } from '../commands'; +export type { MongoCodecLookup, MongoExecutionContext } from '../execution-context'; export type { MongoFilterExpr } from '../filter-expressions'; export { isMongoFilterExpr, diff --git a/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts index cffb5b5caeb9..434a29c0278c 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts @@ -54,7 +54,7 @@ describe('ORM ergonomics integration', { }); const context = createMongoExecutionContext({ contract: {}, stack }); const driver = await createMongoDriver(replSet.getUri(), dbName); - runtime = createMongoRuntime({ context, driver }); + runtime = createMongoRuntime({ context, stack, driver }); enums = buildMongoEnums(contract, context.codecs); }, timeouts.spinUpMongoMemoryServer); diff --git a/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts index cdf8d8ea34fb..f60fb5596f2a 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts @@ -41,7 +41,7 @@ describe('Mongo ORM polymorphism integration', { }); const context = createMongoExecutionContext({ contract: {}, stack }); const driver = await createMongoDriver(replSet.getUri(), dbName); - runtime = createMongoRuntime({ context, driver }); + runtime = createMongoRuntime({ context, stack, driver }); enums = buildMongoEnums(contract, context.codecs); }, timeouts.spinUpMongoMemoryServer); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts b/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts index 8bd8d11542ab..f8e00519bb94 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/src/query.ts @@ -5,7 +5,11 @@ import type { MongoContractWithTypeMaps, RootModelName, } from '@internal/mongo-contract'; -import type { AnyMongoCommand, MongoQueryPlan } from '@internal/mongo-query-ast/execution'; +import type { + AnyMongoCommand, + MongoExecutionContext, + MongoQueryPlan, +} from '@internal/mongo-query-ast/execution'; import { contractError } from './contract-errors'; import { asMongoContract, type CollectionHandle, createCollectionHandle } from './state-classes'; @@ -32,7 +36,7 @@ export interface QueryRoot< /** Builds queries for the contract of `context`, which `mongoStatic({ contract })` and `mongo({ contract })` create after validating the contract. */ export function mongoQuery< TContract extends MongoContractWithTypeMaps, ->(options: { readonly context: { readonly contract: TContract } }): QueryRoot { +>(options: { readonly context: MongoExecutionContext }): QueryRoot { const { contract } = options.context; return { from(rootName: K) { diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts index 7c68e9e6a6a7..5bf4b78db015 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts @@ -24,10 +24,11 @@ import { } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; import { mongoQuery } from '../src/query'; +import { contextOf } from './fixtures/test-context'; import { testContract } from './fixtures/test-contract'; function createOrdersBuilder() { - return mongoQuery({ context: { contract: testContract } }).from('orders'); + return mongoQuery({ context: contextOf(testContract) }).from('orders'); } describe('new stage builder methods', () => { diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts index f00b4c8d1d76..f514e09119be 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts @@ -1,12 +1,18 @@ -import type { MongoFilterExpr, MongoQueryPlan } from '@internal/mongo-query-ast/execution'; +import type { + MongoExecutionContext, + MongoFilterExpr, + MongoQueryPlan, +} from '@internal/mongo-query-ast/execution'; import { MongoAggFieldRef, MongoLimitStage } from '@internal/mongo-query-ast/execution'; import { expectTypeOf } from 'vitest'; import { acc } from '../src/accumulator-helpers'; import { fn } from '../src/expression-helpers'; import { mongoQuery } from '../src/query'; +import { contextOf } from './fixtures/test-context'; import type { TContract } from './fixtures/test-contract'; declare const contract: TContract; +declare const context: MongoExecutionContext; type PlanRow

= P extends MongoQueryPlan ? R : never; @@ -21,15 +27,17 @@ type OrderRow = { describe('mongoQuery', () => { it('takes an execution context, not a contract', () => { - expectTypeOf(mongoQuery({ context: { contract } }).from('orders')).not.toBeNever(); + expectTypeOf(mongoQuery({ context }).from('orders')).not.toBeNever(); // @ts-expect-error -- a contract alone is not accepted; build the context with mongoStatic() or mongo() mongoQuery({ contract }); + // @ts-expect-error -- an object holding only a contract is not an execution context + mongoQuery({ context: { contract } }); }); }); describe('builder shape tests', () => { it('sort() only accepts keys from current shape', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); const builder = p.from('orders'); builder.sort({ amount: -1 }); builder.sort({ status: 1 }); @@ -38,7 +46,7 @@ describe('builder shape tests', () => { }); it('group() replaces shape — previous fields inaccessible', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); const grouped = p.from('orders').group((f) => ({ _id: f.customerId, total: acc.sum(f.amount), @@ -55,7 +63,7 @@ describe('builder shape tests', () => { }); it('addFields() extends shape', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); const extended = p.from('orders').addFields((f) => ({ fullName: fn.concat(f.status, fn.literal(' ')), })); @@ -65,7 +73,7 @@ describe('builder shape tests', () => { }); it('project() inclusion narrows shape', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); const projected = p.from('orders').project('status', 'amount'); projected.sort({ status: 1 }); @@ -76,7 +84,7 @@ describe('builder shape tests', () => { }); it('count() replaces shape with single field', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); const counted = p.from('orders').count('total'); counted.sort({ total: 1 }); @@ -85,7 +93,7 @@ describe('builder shape tests', () => { }); it('lookup() adds array field and preserves existing fields', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); const withLookup = p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -104,7 +112,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects bad local field at type level (AC-1 TC-1)', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -117,7 +125,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects bad foreign field at type level (AC-1 TC-2)', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -130,7 +138,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects non-leaf returns from on() (AC-2 TC-3)', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -148,7 +156,7 @@ describe('builder shape tests', () => { // behaviour). See spec § Open Questions / Resolved decisions. it('replaceRoot() replaces entire shape', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); type NewShape = { readonly x: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; }; @@ -163,12 +171,14 @@ describe('builder shape tests', () => { describe('resolved row types', () => { it('from() → build() resolves to concrete field types', () => { - const plan = mongoQuery({ context: { contract } }).from('orders').build(); + const plan = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .build(); expectTypeOf>().toEqualTypeOf(); }); it('match() preserves row type', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .match((f) => f.status.eq('active') as MongoFilterExpr) .build(); @@ -176,12 +186,15 @@ describe('resolved row types', () => { }); it('sort() preserves row type', () => { - const plan = mongoQuery({ context: { contract } }).from('orders').sort({ amount: -1 }).build(); + const plan = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .sort({ amount: -1 }) + .build(); expectTypeOf>().toEqualTypeOf(); }); it('limit() / skip() / sample() preserve row type', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .limit(10) .skip(5) @@ -191,7 +204,7 @@ describe('resolved row types', () => { }); it('addFields() extends row with new fields at correct types', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .addFields((f) => ({ fullName: fn.concat(f.status, fn.literal(' ')), @@ -211,7 +224,7 @@ describe('resolved row types', () => { }); it('project() inclusion narrows to selected fields at correct types', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .project('status', 'amount') .build(); @@ -223,7 +236,7 @@ describe('resolved row types', () => { }); it('project() computed includes expression fields at correct types', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .project((f) => ({ status: 1 as const, @@ -240,7 +253,7 @@ describe('resolved row types', () => { }); it('group() resolves _id at grouped-by field type, accumulators at correct types', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .group((f) => ({ _id: f.customerId, @@ -258,17 +271,23 @@ describe('resolved row types', () => { }); it('unwind() preserves row type', () => { - const plan = mongoQuery({ context: { contract } }).from('orders').unwind('tags').build(); + const plan = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .unwind('tags') + .build(); expectTypeOf>().toEqualTypeOf(); }); it('count() row type is { [field]: number }', () => { - const plan = mongoQuery({ context: { contract } }).from('orders').count('total').build(); + const plan = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .count('total') + .build(); expectTypeOf>().toEqualTypeOf<{ total: number }>(); }); it('sortByCount() row type is { _id: ; count: number }', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .sortByCount((f) => f.status) .build(); @@ -279,7 +298,7 @@ describe('resolved row types', () => { }); it('lookup() resolves the as-named field to Array with concrete leaf types (AC-3 TC-4)', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .lookup((from) => from('users') @@ -307,7 +326,7 @@ describe('resolved row types', () => { }); it('lookup() resolves nested value-object fields on the foreign model recursively', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .lookup((from) => from('customers') @@ -346,7 +365,9 @@ describe('resolved row types', () => { }); it('entry-point query resolves value-object fields recursively (model-origin brand)', () => { - const plan = mongoQuery({ context: { contract } }).from('customers').build(); + const plan = mongoQuery({ context: contextOf(contract) }) + .from('customers') + .build(); expectTypeOf>().toEqualTypeOf<{ readonly _id: string; readonly name: string; @@ -367,7 +388,7 @@ describe('resolved row types', () => { }); it('addFields() preserves model-origin resolution and adds the new fields on top', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('customers') .addFields((f) => ({ upperName: fn.concat(f.name, fn.literal('!')), @@ -394,7 +415,7 @@ describe('resolved row types', () => { }); it('group() drops model-origin resolution (legitimate shape replacement)', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('customers') .group((f) => ({ _id: f.name, @@ -414,7 +435,7 @@ describe('resolved row types', () => { readonly x: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; readonly y: { readonly codecId: 'mongo/double@1'; readonly nullable: true }; }; - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .replaceRoot((f) => f.status) .build(); @@ -425,7 +446,7 @@ describe('resolved row types', () => { }); it('pipe() preserves row type by default', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .pipe(new MongoLimitStage(5)) .build(); @@ -436,7 +457,7 @@ describe('resolved row types', () => { type NewShape = { readonly a: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; }; - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .pipe(new MongoLimitStage(5)) .build(); @@ -444,14 +465,16 @@ describe('resolved row types', () => { }); it('nullable fields resolve to T | null', () => { - const plan = mongoQuery({ context: { contract } }).from('orders').build(); + const plan = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .build(); type Row = PlanRow; expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); it('chained pipeline produces correct cumulative row types', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .match((f) => f.status.eq('active') as MongoFilterExpr) .group((f) => ({ @@ -470,7 +493,9 @@ describe('resolved row types', () => { }); it('execute() infers Row from build() plan type', () => { - const plan = mongoQuery({ context: { contract } }).from('orders').build(); + const plan = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .build(); const execute = {} as (p: MongoQueryPlan) => Promise; const result = execute(plan); expectTypeOf[0]>().toEqualTypeOf(); @@ -479,7 +504,7 @@ describe('resolved row types', () => { describe('resolved row types — new stages', () => { it('redact() preserves row type', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .redact((f) => f.status) .build(); @@ -487,17 +512,21 @@ describe('resolved row types — new stages', () => { }); it('out() returns a write-terminal plan with an unknown row type', () => { - const plan = mongoQuery({ context: { contract } }).from('orders').out('archive'); + const plan = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .out('archive'); expectTypeOf>().toEqualTypeOf(); }); it('merge() returns a write-terminal plan with an unknown row type', () => { - const plan = mongoQuery({ context: { contract } }).from('orders').merge({ into: 'summary' }); + const plan = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .merge({ into: 'summary' }); expectTypeOf>().toEqualTypeOf(); }); it('unionWith() preserves row type', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .unionWith('archived_orders') .build(); @@ -505,7 +534,7 @@ describe('resolved row types — new stages', () => { }); it('densify() preserves row type', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .densify({ field: 'amount', range: { step: 10, bounds: 'full' } }) .build(); @@ -513,7 +542,7 @@ describe('resolved row types — new stages', () => { }); it('fill() preserves row type', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .fill({ sortBy: { amount: 1 }, output: { notes: { method: 'linear' } } }) .build(); @@ -521,7 +550,7 @@ describe('resolved row types — new stages', () => { }); it('search() preserves row type', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .search({ text: { query: 'test', path: 'status' } }) .build(); @@ -529,7 +558,7 @@ describe('resolved row types — new stages', () => { }); it('vectorSearch() preserves row type', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .vectorSearch({ index: 'idx', @@ -543,7 +572,7 @@ describe('resolved row types — new stages', () => { }); it('bucket() resets to untyped row', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .bucket({ groupBy: MongoAggFieldRef.of('amount'), boundaries: [0, 100, 500] }) .build(); @@ -551,7 +580,7 @@ describe('resolved row types — new stages', () => { }); it('bucketAuto() resets to untyped row', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .bucketAuto({ groupBy: MongoAggFieldRef.of('amount'), buckets: 5 }) .build(); @@ -559,7 +588,7 @@ describe('resolved row types — new stages', () => { }); it('facet() resets to untyped row', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .facet({ counts: [], top: [] }) .build(); @@ -567,7 +596,7 @@ describe('resolved row types — new stages', () => { }); it('geoNear() resets to untyped row', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .geoNear({ near: [0, 0], distanceField: 'dist' }) .build(); @@ -575,7 +604,7 @@ describe('resolved row types — new stages', () => { }); it('graphLookup() resets to untyped row', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .graphLookup({ from: 'categories', @@ -589,7 +618,7 @@ describe('resolved row types — new stages', () => { }); it('setWindowFields() resets to untyped row', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .setWindowFields({ sortBy: { amount: 1 }, output: {} }) .build(); @@ -597,7 +626,7 @@ describe('resolved row types — new stages', () => { }); it('searchMeta() resets to untyped row', () => { - const plan = mongoQuery({ context: { contract } }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .searchMeta({ facet: { operator: {} } }) .build(); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts index 66451b805161..51f47a8d4f9a 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts @@ -24,14 +24,15 @@ import { acc } from '../src/accumulator-helpers'; import { fn } from '../src/expression-helpers'; import type { LookupOnResult } from '../src/lookup-builder'; import { mongoQuery } from '../src/query'; +import { contextOf } from './fixtures/test-context'; import { testContract } from './fixtures/test-contract'; function createOrdersBuilder() { - return mongoQuery({ context: { contract: testContract } }).from('orders'); + return mongoQuery({ context: contextOf(testContract) }).from('orders'); } function createCustomersBuilder() { - return mongoQuery({ context: { contract: testContract } }).from('customers'); + return mongoQuery({ context: contextOf(testContract) }).from('customers'); } describe('PipelineChain', () => { @@ -387,14 +388,14 @@ describe('PipelineChain', () => { describe('mongoQuery()', () => { it('from() creates builder for known root', () => { - const p = mongoQuery({ context: { contract: testContract } }); + const p = mongoQuery({ context: contextOf(testContract) }); const builder = p.from('orders'); const plan = builder.build(); expect(plan.collection).toBe('orders'); }); it('from() throws for unknown root', () => { - const p = mongoQuery({ context: { contract: testContract } }); + const p = mongoQuery({ context: contextOf(testContract) }); expect(() => p.from('nonexistent' as 'orders')).toThrow('Unknown root'); }); }); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts index e42a7dc35916..49ac60ddf49f 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts @@ -9,6 +9,7 @@ import { createFieldAccessor } from '../src/field-accessor'; import { mongoQuery } from '../src/query'; import type { ModelNestedShape, NestedDocShape, ObjectField } from '../src/resolve-path'; import type { DocField, ModelToDocShape } from '../src/types'; +import { contextOf } from './fixtures/test-context'; import type { TContract, TestContract } from './fixtures/test-contract'; declare const contract: TContract; @@ -212,20 +213,20 @@ describe('FieldAccessor.rawPath escape hatch', () => { describe('Pipeline integration — N threading', () => { it('CollectionHandle.match callback allows callable dot-path access', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); p.from('customers').match((f) => f('address.city').eq('London')); p.from('customers').match((f) => f('address.geo.lat').gt(0)); }); it('FilteredCollection.updateMany callback allows callable dot-path access', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); p.from('customers') .match((f) => f('address.city').eq('London')) .updateMany((f) => [f('address.zip').set('SW1'), f('stats.visits').inc(1)]); }); it('rejects bogus paths in match callback', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); p.from('customers').match((f) => // @ts-expect-error -- 'address.bogus' is not a valid path f('address.bogus').eq('x'), @@ -233,7 +234,7 @@ describe('Pipeline integration — N threading', () => { }); it('additive stages (sort/addFields/redact) preserve callable dot-paths', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); p.from('customers') .sort({ name: 1 }) .match((f) => f('address.city').eq('London')); @@ -244,7 +245,7 @@ describe('Pipeline integration — N threading', () => { }); it('replacement stages (group/project/replaceRoot) disable callable dot-paths', () => { - const p = mongoQuery({ context: { contract } }); + const p = mongoQuery({ context: contextOf(contract) }); const grouped = p.from('customers').group((f) => ({ _id: f.name, count: f._id })); // After group, N has been reset. The flat Shape no longer contains // value objects, and the callable form rejects any string. diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts index 0ef83fc9cf73..6261226ec9c4 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts @@ -7,10 +7,11 @@ import { describe, expect, it } from 'vitest'; import type { PipelineChain } from '../src/builder'; import { mongoQuery } from '../src/query'; import type { ModelToDocShape } from '../src/types'; +import { contextOf } from './fixtures/test-context'; import type { TContract } from './fixtures/test-contract'; import { testContract } from './fixtures/test-contract'; -const orders = () => mongoQuery({ context: { contract: testContract } }).from('orders'); +const orders = () => mongoQuery({ context: contextOf(testContract) }).from('orders'); /** * After A25 (`CollectionHandle`/`FilteredCollection` start with diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts new file mode 100644 index 000000000000..162a5e5d129b --- /dev/null +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts @@ -0,0 +1,9 @@ +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; + +export function contextOf(contract: TContract): MongoExecutionContext { + return { + contract, + codecs: { get: () => undefined, has: () => false }, + applyMutationDefaults: () => [], + }; +} diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts index 6ab15783eb0c..95146c4b219e 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts @@ -10,10 +10,11 @@ import type { PipelineChain } from '../src/builder'; import { mongoQuery } from '../src/query'; import type { ModelToDocShape } from '../src/types'; import type { UpdaterResult } from '../src/update-ops'; +import { contextOf } from './fixtures/test-context'; import type { TContract } from './fixtures/test-contract'; import { testContract } from './fixtures/test-contract'; -const orders = () => mongoQuery({ context: { contract: testContract } }).from('orders'); +const orders = () => mongoQuery({ context: contextOf(testContract) }).from('orders'); /** * After A25 (`CollectionHandle`/`FilteredCollection` start with diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts index 5a2aa414dfb4..dd4594786ee1 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts @@ -1,10 +1,11 @@ import { InsertOneCommand, RawAggregateCommand } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; import { mongoQuery } from '../src/query'; +import { contextOf } from './fixtures/test-context'; import type { TContract } from './fixtures/test-contract'; import { testContract } from './fixtures/test-contract'; -const root = () => mongoQuery({ context: { contract: testContract } }); +const root = () => mongoQuery({ context: contextOf(testContract) }); describe('M5 raw escape hatch', () => { it('packages a typed CRUD command into a plan with lane: mongo-query', () => { @@ -27,7 +28,7 @@ describe('M5 raw escape hatch', () => { const cmd = new InsertOneCommand('orders', { status: 'new' }); expect(() => mongoQuery({ - context: { contract: { ...testContract, storage: {} } as unknown as TContract }, + context: contextOf({ ...testContract, storage: {} } as unknown as TContract), }).rawCommand(cmd), ).toThrow(/storageHash/); }); diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts index a732440a0b80..6351e3b33bbe 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts @@ -5,6 +5,7 @@ import { contractFieldToMongoFieldShape, contractModelToMongoResultShape, } from '../src/result-shape'; +import { contextOf } from './fixtures/test-context'; import { testContract } from './fixtures/test-contract'; describe('contractModelToMongoResultShape', () => { @@ -197,14 +198,14 @@ describe('contractFieldToMongoFieldShape', () => { describe('PipelineChain build resultShape', () => { it('identity pipeline attaches document shape from bound model', () => { - const plan = mongoQuery({ context: { contract: testContract } }) + const plan = mongoQuery({ context: contextOf(testContract) }) .from('orders') .build(); expect(plan.resultShape?.kind).toBe('document'); }); it('project stage reifies a document shape retaining _id and the projected field', () => { - const plan = mongoQuery({ context: { contract: testContract } }) + const plan = mongoQuery({ context: contextOf(testContract) }) .from('orders') .match(MongoFieldFilter.eq('status', 'x')) .project('status') diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts index b65ef432c94e..28eb646a54fd 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts @@ -1,10 +1,11 @@ import { MongoFieldFilter } from '@internal/mongo-query-ast/execution'; import { describe, it } from 'vitest'; import { mongoQuery } from '../src/query'; +import { contextOf } from './fixtures/test-context'; import type { TContract } from './fixtures/test-contract'; declare const contract: TContract; -const handle = () => mongoQuery({ context: { contract } }).from('orders'); +const handle = () => mongoQuery({ context: contextOf(contract) }).from('orders'); const filtered = () => handle().match(MongoFieldFilter.eq('status', 'new')); /** diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts index 7e64731ef339..172c2bc87d09 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts @@ -4,6 +4,7 @@ import type { PipelineChain } from '../src/builder'; import type { FindAndModifyEnabled, UpdateEnabled } from '../src/markers'; import { mongoQuery } from '../src/query'; import type { CollectionHandle, FilteredCollection } from '../src/state-classes'; +import { contextOf } from './fixtures/test-context'; import type { TContract } from './fixtures/test-contract'; declare const contract: TContract; @@ -25,19 +26,19 @@ type GetF = describe('state machine', () => { it('from(name) returns CollectionHandle (root state) inheriting PipelineChain', () => { - const handle = mongoQuery({ context: { contract } }).from('orders'); + const handle = mongoQuery({ context: contextOf(contract) }).from('orders'); expectTypeOf(handle).toExtend>(); }); it('CollectionHandle.match(...) transitions to FilteredCollection', () => { - const filtered = mongoQuery({ context: { contract } }) + const filtered = mongoQuery({ context: contextOf(contract) }) .from('orders') .match(MongoFieldFilter.eq('status', 'active')); expectTypeOf(filtered).toExtend>(); }); it('FilteredCollection.match(...) stays in FilteredCollection (AND-folds)', () => { - const filtered = mongoQuery({ context: { contract } }) + const filtered = mongoQuery({ context: contextOf(contract) }) .from('orders') .match(MongoFieldFilter.eq('status', 'active')) .match(MongoFieldFilter.gt('amount', 100)); @@ -45,7 +46,9 @@ describe('state machine', () => { }); it('pipeline-stage methods drop out of the state-class subclasses', () => { - const sorted = mongoQuery({ context: { contract } }).from('orders').sort({ amount: -1 }); + const sorted = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .sort({ amount: -1 }); // No longer a CollectionHandle/FilteredCollection — write/find-and-modify // surfaces have been left behind. expectTypeOf(sorted).not.toExtend>(); @@ -58,7 +61,7 @@ describe('state machine', () => { // find-and-modify terminals inherited from `PipelineChain` are gated off // by default, so a leading `.match(...)` is required before reaching them // via the `FilteredCollection` overrides (see ADR 201). - const handle = mongoQuery({ context: { contract } }).from('orders'); + const handle = mongoQuery({ context: contextOf(contract) }).from('orders'); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); }); @@ -70,7 +73,7 @@ describe('state machine', () => { // marker-gated PipelineChain versions), so the initial marker state // stays cleared to prevent accidental access to the PipelineChain // inheritance path. - const filtered = mongoQuery({ context: { contract } }) + const filtered = mongoQuery({ context: contextOf(contract) }) .from('orders') .match(MongoFieldFilter.eq('status', 'active')); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); @@ -78,23 +81,27 @@ describe('state machine', () => { }); it('marker table: limit() leaves both markers cleared', () => { - const limited = mongoQuery({ context: { contract } }).from('orders').limit(1); + const limited = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .limit(1); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); }); it('marker table: sort / addFields / group all leave both markers cleared from .from()', () => { - const sorted = mongoQuery({ context: { contract } }).from('orders').sort({ amount: -1 }); + const sorted = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .sort({ amount: -1 }); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); - const added = mongoQuery({ context: { contract } }) + const added = mongoQuery({ context: contextOf(contract) }) .from('orders') .addFields((f) => ({ doubled: f.amount })); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); expectTypeOf>().toEqualTypeOf<'fam-cleared' & FindAndModifyEnabled>(); - const grouped = mongoQuery({ context: { contract } }) + const grouped = mongoQuery({ context: contextOf(contract) }) .from('orders') .group((_f) => ({ _id: null })); expectTypeOf>().toEqualTypeOf<'update-cleared' & UpdateEnabled>(); @@ -106,7 +113,7 @@ describe('state machine', () => { // parameters here; the runtime / shape behaviour is asserted in // builder.test-d.ts. it('marker table: lookup() clears both markers', () => { - const looked = mongoQuery({ context: { contract } }) + const looked = mongoQuery({ context: contextOf(contract) }) .from('orders') .lookup((from) => from('users') diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts index 801cf4f16538..6604d324769c 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test-d.ts @@ -3,6 +3,7 @@ import { expectTypeOf } from 'vitest'; import { mongoQuery } from '../src/query'; import type { ModelNestedShape } from '../src/resolve-path'; import type { ModelToDocShape } from '../src/types'; +import { contextOf } from './fixtures/test-context'; import { type TestContract, testContract } from './fixtures/test-contract'; type PlanRow

= P extends MongoQueryPlan ? R : never; @@ -38,14 +39,14 @@ describe('stored names', () => { }); it('exposes _id, not id, on a model whose id field is stored as _id', () => { - const plan = mongoQuery({ context: { contract: testContract } }) + const plan = mongoQuery({ context: contextOf(testContract) }) .from('accounts') .build(); expectTypeOf>().toEqualTypeOf(); }); it('accepts stored names and rejects domain field names in match', () => { - const accounts = mongoQuery({ context: { contract: testContract } }).from('accounts'); + const accounts = mongoQuery({ context: contextOf(testContract) }).from('accounts'); accounts.match((f) => f._id.eq('a')); accounts.match((f) => f('home_address.city').eq('NYC')); // @ts-expect-error -- `id` is a domain field name, not a stored name @@ -55,7 +56,7 @@ describe('stored names', () => { }); it('keys a looked-up foreign row by stored name', () => { - const plan = mongoQuery({ context: { contract: testContract } }) + const plan = mongoQuery({ context: contextOf(testContract) }) .from('orders') .lookup((from) => from('accounts') diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts index b9ddfb7b2108..ee1b0f798f53 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/stored-names.test.ts @@ -5,10 +5,11 @@ import { } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; import { mongoQuery } from '../src/query'; +import { contextOf } from './fixtures/test-context'; import { testContract } from './fixtures/test-contract'; describe('stored names', () => { - const accounts = () => mongoQuery({ context: { contract: testContract } }).from('accounts'); + const accounts = () => mongoQuery({ context: contextOf(testContract) }).from('accounts'); it('keys the result shape of a model read by stored name', () => { expect(accounts().build().resultShape).toEqual({ diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts index 22302db30cba..ad7edd71cf3e 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/structured-error-codes.test.ts @@ -4,10 +4,11 @@ import { describe, expect, it } from 'vitest'; import type { PipelineChain } from '../src/builder'; import { mongoQuery } from '../src/query'; import type { ModelToDocShape } from '../src/types'; +import { contextOf } from './fixtures/test-context'; import type { TContract } from './fixtures/test-contract'; import { testContract, testContractJson } from './fixtures/test-contract'; -const orders = () => mongoQuery({ context: { contract: testContract } }).from('orders'); +const orders = () => mongoQuery({ context: contextOf(testContract) }).from('orders'); type FamReachableChain = PipelineChain< TContract, @@ -51,7 +52,7 @@ describe('structured error codes', () => { }); it('raises ORM.MODEL_UNKNOWN for an unknown root', () => { - const p = mongoQuery({ context: { contract: testContract } }); + const p = mongoQuery({ context: contextOf(testContract) }); expect(codeOf(() => p.from('nonexistent' as 'orders'))).toBe('ORM.MODEL_UNKNOWN'); }); @@ -63,13 +64,13 @@ describe('structured error codes', () => { orders: { $ref: '#/domain/models/Ghost', model: 'Ghost' }, }, }; - const p = mongoQuery({ context: { contract: patched as unknown as TContract } }); + const p = mongoQuery({ context: contextOf(patched as unknown as TContract) }); expect(codeOf(() => p.from('orders'))).toBe('CONTRACT.MODEL_UNKNOWN'); }); it('raises CONTRACT.VALIDATION_FAILED when the contract has no storageHash', () => { const p = mongoQuery({ - context: { contract: { ...testContract, storage: {} } as unknown as TContract }, + context: contextOf({ ...testContract, storage: {} } as unknown as TContract), }); expect(codeOf(() => p.rawCommand(new InsertOneCommand('orders', { status: 'new' })))).toBe( 'CONTRACT.VALIDATION_FAILED', diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts index 8da3899f1403..77b321ae76a9 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts @@ -10,9 +10,10 @@ import { } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; import { mongoQuery } from '../src/query'; +import { contextOf } from './fixtures/test-context'; import { testContract } from './fixtures/test-contract'; -const orders = () => mongoQuery({ context: { contract: testContract } }).from('orders'); +const orders = () => mongoQuery({ context: contextOf(testContract) }).from('orders'); describe('M2 write terminals', () => { describe('inserts (CollectionHandle)', () => { diff --git a/packages/2-mongo-family/7-runtime/README.md b/packages/2-mongo-family/7-runtime/README.md index 5f0e27543860..8d2f1b0bece8 100644 --- a/packages/2-mongo-family/7-runtime/README.md +++ b/packages/2-mongo-family/7-runtime/README.md @@ -38,6 +38,7 @@ const context = createMongoExecutionContext({ contract, stack }); const runtime = createMongoRuntime({ context, + stack, driver: await createMongoDriver(url, dbName), }); ``` @@ -46,9 +47,9 @@ Custom or third-party codecs (encryption, vendor scalars) are contributed via an ## Responsibilities -- **Stack/context composition**: `createMongoExecutionStack` and `createMongoExecutionContext` mirror SQL's `createSqlExecutionStack` / `createExecutionContext`. The context aggregates codec contributions from `[stack.target, stack.adapter, ...stack.extensions]` into a single `MongoCodecRegistry`. +- **Stack/context composition**: `createMongoExecutionStack` and `createMongoExecutionContext` mirror SQL's `createSqlExecutionStack` / `createExecutionContext`. The context aggregates codec contributions from `[stack.target, stack.adapter, ...stack.extensions]` into a single `MongoCodecRegistry`. The `MongoExecutionContext` type is declared in `@internal/mongo-query-ast`, as SQL's `ExecutionContext` is declared in relational-core, and holds no stack: the query lanes build against it, and the runtime receives the stack as its own option. - **Enum accessors**: `buildMongoEnums(contract, context.codecs)` builds the contract's enum accessors by namespace: what `db.enums` holds and what `mongoOrm({ enums })` checks a written enum value against. A contract whose enum codec the context lacks is refused here with `RUNTIME.CODEC_DESCRIPTOR_MISSING`. -- **Runtime executor**: `createMongoRuntime({ context, driver, ... })` composes context and driver into a `MongoRuntime` with separate `query(plan)` and `execute(plan)` operations accepting `MongoQueryPlan` from `@internal/mongo-query-ast`. `query()` yields decoded rows, while `execute()` returns statement statistics. Both operations reach the adapter via `context.stack.adapter` (instantiated lazily through the stack's `create(stack)` factory), lower the plan, and run the wire command on the driver; `query()` **optionally decodes** each row when `plan.resultShape` is present. +- **Runtime executor**: `createMongoRuntime({ context, stack, driver, ... })` composes context, stack and driver into a `MongoRuntime` with separate `query(plan)` and `execute(plan)` operations accepting `MongoQueryPlan` from `@internal/mongo-query-ast`. `query()` yields decoded rows, while `execute()` returns statement statistics. Both operations use the adapter the runtime creates from `stack.adapter` through the stack's `create(stack)` factory, lower the plan, and run the wire command on the driver; `query()` **optionally decodes** each row when `plan.resultShape` is present. - **Operation flow**: Row-producing work uses `query(plan)` and the query middleware lifecycle; statistics-producing work uses `execute(plan)` and the execute middleware lifecycle. Each operation allocates its own `CodecCallContext` and threads it through its codec dispatches. - **Lowering**: Happens in the adapter (`lower(plan)`), wrapped by the runtime's `lower` override into a `MongoExecutionPlan`. - **Middleware lifecycle inheritance**: `MongoRuntime` extends `RuntimeCore` and runs every `query()` and `execute()` as a statement activity: `activityStarted`, `beforeEncode` (before `resolveParams`), `intercept.onQuery` / `intercept.onExecute`, `onRow`, `activityEnded`, then `activityFinalized` right away, because Mongo has no transactions. Mongo does **not** override `runBeforeCompile` (Mongo middleware has no `beforeCompile` hook today). diff --git a/packages/2-mongo-family/7-runtime/src/codecs/decoding.ts b/packages/2-mongo-family/7-runtime/src/codecs/decoding.ts index 59ce5d67185f..049bd1d51023 100644 --- a/packages/2-mongo-family/7-runtime/src/codecs/decoding.ts +++ b/packages/2-mongo-family/7-runtime/src/codecs/decoding.ts @@ -1,11 +1,14 @@ import type { CodecCallContext } from '@internal/framework-components/codec'; import { runtimeError } from '@internal/framework-components/runtime'; -import type { MongoFieldShape, MongoResultShape } from '@internal/mongo-query-ast/execution'; +import type { + MongoCodecLookup, + MongoFieldShape, + MongoResultShape, +} from '@internal/mongo-query-ast/execution'; import { blindCast } from '@internal/utils/casts'; import { ifDefined } from '@internal/utils/defined'; import { isInternalError } from '@internal/utils/internal-error'; import { isStructuredError } from '@internal/utils/structured-error'; -import type { MongoCodecLookup } from '../mongo-execution-stack'; const WIRE_PREVIEW_LIMIT = 100; diff --git a/packages/2-mongo-family/7-runtime/src/exports/index.ts b/packages/2-mongo-family/7-runtime/src/exports/index.ts index 3163bce41193..b12404145cd5 100644 --- a/packages/2-mongo-family/7-runtime/src/exports/index.ts +++ b/packages/2-mongo-family/7-runtime/src/exports/index.ts @@ -1,10 +1,9 @@ export type { RuntimeTargetInstance } from '@internal/framework-components/execution'; +export type { MongoCodecLookup, MongoExecutionContext } from '@internal/mongo-query-ast/execution'; export type { MongoEnumAccessors } from '../mongo-enums'; export { buildMongoEnums } from '../mongo-enums'; export type { MongoExecutionPlan } from '../mongo-execution-plan'; export type { - MongoCodecLookup, - MongoExecutionContext, MongoExecutionStack, MongoRuntimeAdapterDescriptor, MongoRuntimeAdapterInstance, diff --git a/packages/2-mongo-family/7-runtime/src/mongo-enums.ts b/packages/2-mongo-family/7-runtime/src/mongo-enums.ts index 2bfe288ede29..900a91e2299d 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-enums.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-enums.ts @@ -1,7 +1,7 @@ import { buildEnumsMapForNamespace, type EnumAccessor } from '@internal/contract/enum-accessor'; import type { Contract } from '@internal/contract/types'; import { runtimeError } from '@internal/framework-components/runtime'; -import type { MongoCodecLookup } from './mongo-execution-stack'; +import type { MongoCodecLookup } from '@internal/mongo-query-ast/execution'; /** A contract's enum accessors by namespace and enum name. */ export type MongoEnumAccessors = Readonly>>>; diff --git a/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts index 49ed4905be79..bebe27847ff6 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts @@ -15,13 +15,13 @@ import { assertMutationDefaultGeneratorsAvailable, collectMutationDefaultGenerators, type MutationDefaultGeneratorContributor, - type MutationDefaults, type MutationDefaultsOptions, runtimeError, } from '@internal/framework-components/runtime'; import type { MongoCodec } from '@internal/mongo-codec'; import { type MongoCodecRegistry, newMongoCodecRegistry } from '@internal/mongo-codec'; import type { MongoAdapter } from '@internal/mongo-lowering'; +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { blindCast } from '@internal/utils/casts'; /** @@ -107,37 +107,13 @@ export function createMongoExecutionStack(op >(stack); } -/** - * Read-only view of the codec registry exposed on `MongoExecutionContext`. - * - * Hides `register()` and the iterator from public surface — users do not mutate the per-execution codec registry. Internal aggregation in `createMongoExecutionContext` keeps using the full `MongoCodecRegistry` (it needs `register()`). - */ -export interface MongoCodecLookup { - get(id: string): MongoCodec | undefined; - has(id: string): boolean; -} - -/** - * Per-execution context aggregated from a `MongoExecutionStack`. - * - * Carries the user's contract, a read-only lookup over the codec registry composed from every stack contributor, a back-reference to the stack itself so the runtime can reach the adapter without users threading it explicitly, and `applyMutationDefaults`, which fills the contract's execution defaults from the composed generators. - * - * Mirrors SQL's `ExecutionContext` in role; Mongo's flavour is leaner because there are no parameterised codecs or JSON-schema validators in scope yet. - */ -export interface MongoExecutionContext - extends MutationDefaults { - readonly contract: TContract; - readonly codecs: MongoCodecLookup; - readonly stack: MongoExecutionStack; -} - export function createMongoExecutionContext< TContract = unknown, TTargetId extends string = 'mongo', >(options: { readonly contract: TContract; readonly stack: MongoExecutionStack; -}): MongoExecutionContext { +}): MongoExecutionContext { const registry = newMongoCodecRegistry(); const owners = new Map(); @@ -170,7 +146,6 @@ export function createMongoExecutionContext< return Object.freeze({ contract: options.contract, codecs: registry, - stack: options.stack, applyMutationDefaults: (mutation: MutationDefaultsOptions) => applyMutationDefaults(execution, generators, mutation), }); diff --git a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts index e87979bb139f..8716a481c732 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts @@ -14,13 +14,17 @@ import { streamStatement, } from '@internal/framework-components/runtime'; import type { MongoAdapter, MongoDriver } from '@internal/mongo-lowering'; -import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; +import type { + MongoCodecLookup, + MongoExecutionContext, + MongoQueryPlan, +} from '@internal/mongo-query-ast/execution'; import { blindCast } from '@internal/utils/casts'; import { ifDefined } from '@internal/utils/defined'; import { decodeMongoRow } from './codecs/decoding'; import { computeMongoContentHash } from './content-hash'; import type { MongoExecutionPlan } from './mongo-execution-plan'; -import type { MongoCodecLookup, MongoExecutionContext } from './mongo-execution-stack'; +import type { MongoExecutionStack } from './mongo-execution-stack'; import type { MongoMiddleware, MongoMiddlewareContext } from './mongo-middleware'; import { createMongoParamRefMutator, @@ -65,15 +69,16 @@ function invalidStatisticsResult(commandKind: string, countField: string): Error * Mongo runtime options. * * The runtime takes a {@link MongoExecutionContext} (built via - * `createMongoExecutionContext`) and a driver. Codec resolution flows from - * the context — there is no `codecs` field on this options bag. The adapter - * is reached via `context.stack.adapter` (instantiated lazily through the - * stack's `create(stack)` factory). See ADR — Mongo result-shape as a + * `createMongoExecutionContext`), the execution stack the context was built + * from, and a driver. Codec resolution flows from the context — there is no + * `codecs` field on this options bag. The adapter comes from `stack.adapter` + * (instantiated through the stack's `create(stack)` factory). See ADR — Mongo result-shape as a * structural plan field, § Codec registry: stack aggregation, not user * threading. */ export interface MongoRuntimeOptions { readonly context: MongoExecutionContext; + readonly stack: MongoExecutionStack; readonly driver: MongoDriver; readonly middleware?: readonly MongoMiddleware[]; readonly mode?: RuntimeMode; @@ -117,7 +122,7 @@ class MongoRuntimeImpl constructor(options: MongoRuntimeOptions) { const middleware = options.middleware ?? []; - const targetId = options.context.stack.target.targetId; + const targetId = options.stack.target.targetId; for (const mw of middleware) { checkMiddlewareCompatibility(mw, 'mongo', targetId); } @@ -145,8 +150,7 @@ class MongoRuntimeImpl super({ middleware, ctx }); this.#contextFields = ctx; - const adapterDescriptor = options.context.stack.adapter; - const adapterInstance = adapterDescriptor.create(options.context.stack); + const adapterInstance = options.stack.adapter.create(options.stack); this.#adapter = adapterInstance; this.#driver = options.driver; this.#codecs = options.context.codecs; diff --git a/packages/2-mongo-family/7-runtime/test/mongo-enums.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-enums.test.ts index 114771ac8024..13f64ae46a24 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-enums.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-enums.test.ts @@ -1,8 +1,8 @@ import type { ContractEnum } from '@internal/contract/types'; import { type DataTypeValue, dataType, readJsonString } from '@internal/framework-components/codec'; +import type { MongoCodecLookup } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; import { buildMongoEnums } from '../src/mongo-enums'; -import type { MongoCodecLookup } from '../src/mongo-execution-stack'; const level: ContractEnum = { codecId: 'test/level@1', diff --git a/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts b/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts index f596985f3ed5..25568a52bd68 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts @@ -2,21 +2,20 @@ import type { PlanMeta } from '@internal/contract/types'; import type { CodecCallContext } from '@internal/framework-components/codec'; import { type MongoCodecRegistry, newMongoCodecRegistry } from '@internal/mongo-codec'; import type { MongoAdapter, MongoDriver, MongoLoweredDraft } from '@internal/mongo-lowering'; -import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; +import type { MongoExecutionContext, MongoQueryPlan } from '@internal/mongo-query-ast/execution'; import { AggregateWireCommand, type AnyMongoDmlWireCommand } from '@internal/mongo-wire'; import { vi } from 'vitest'; import type { - MongoExecutionContext, MongoExecutionStack, MongoRuntimeAdapterDescriptor, MongoRuntimeAdapterInstance, MongoRuntimeTargetDescriptor, } from '../src/mongo-execution-stack'; -export function makeContext( +export function makeExecution( _adapter: MongoAdapter, wireCommand: AnyMongoDmlWireCommand = new AggregateWireCommand('users', []), -): MongoExecutionContext { +): { readonly context: MongoExecutionContext; readonly stack: MongoExecutionStack } { const codecs: MongoCodecRegistry = newMongoCodecRegistry(); const adapterInstance: MongoRuntimeAdapterInstance<'mongo'> = { familyId: 'mongo', @@ -57,7 +56,10 @@ export function makeContext( driver: undefined, extensions: [], }; - return Object.freeze({ contract: {}, codecs, stack, applyMutationDefaults: () => [] }); + return { + context: Object.freeze({ contract: {}, codecs, applyMutationDefaults: () => [] }), + stack, + }; } export const baseMeta: PlanMeta = { diff --git a/packages/2-mongo-family/7-runtime/test/mongo-middleware.identity.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-middleware.identity.test.ts index 45f03aef8235..9946eb8001f1 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-middleware.identity.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-middleware.identity.test.ts @@ -5,7 +5,7 @@ import { createMockAdapter, createMockDriver, createPlan, - makeContext, + makeExecution, } from './mongo-middleware-fixtures'; describe('MongoRuntime statement activity identity', () => { @@ -33,7 +33,7 @@ describe('MongoRuntime statement activity identity', () => { it('gives every hook of one query the same activity id and context', async () => { const log: Observation[] = []; const runtime = createMongoRuntime({ - context: makeContext(createMockAdapter()), + ...makeExecution(createMockAdapter()), driver: createMockDriver([{ _id: '1' }]), middleware: [observerMiddleware(log)], }); @@ -51,7 +51,7 @@ describe('MongoRuntime statement activity identity', () => { it('gives two queries of the same plan instance distinct activities', async () => { const log: Observation[] = []; const runtime = createMongoRuntime({ - context: makeContext(createMockAdapter()), + ...makeExecution(createMockAdapter()), driver: createMockDriver([{ _id: '1' }]), middleware: [observerMiddleware(log)], }); @@ -77,7 +77,7 @@ describe('MongoRuntime middleware compatibility validation', () => { const middleware: MongoMiddleware = { name: 'generic' }; expect(() => createMongoRuntime({ - context: makeContext(createMockAdapter()), + ...makeExecution(createMockAdapter()), driver: createMockDriver(), middleware: [middleware], }), @@ -88,7 +88,7 @@ describe('MongoRuntime middleware compatibility validation', () => { const middleware: MongoMiddleware = { name: 'mongo-specific', familyId: 'mongo' }; expect(() => createMongoRuntime({ - context: makeContext(createMockAdapter()), + ...makeExecution(createMockAdapter()), driver: createMockDriver(), middleware: [middleware], }), @@ -103,7 +103,7 @@ describe('MongoRuntime middleware compatibility validation', () => { } as unknown as MongoMiddleware; expect(() => createMongoRuntime({ - context: makeContext(createMockAdapter()), + ...makeExecution(createMockAdapter()), driver: createMockDriver(), middleware: [middleware], }), diff --git a/packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts index 4b64152fcc23..64efff0c3a7c 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts @@ -8,7 +8,7 @@ import { createMockAdapter, createMockDriver, createPlan, - makeContext, + makeExecution, } from './mongo-middleware-fixtures'; describe('MongoRuntime middleware lifecycle', () => { @@ -41,7 +41,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: createMockDriver([{ _id: '1', name: 'Alice' }]), middleware: [middleware], }); @@ -64,7 +64,7 @@ describe('MongoRuntime middleware lifecycle', () => { it('works with no middleware', async () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: createMockDriver([{ _id: '1' }]), }); @@ -80,9 +80,13 @@ describe('MongoRuntime middleware lifecycle', () => { const calls: string[] = []; const contexts: MongoMiddlewareContext[] = []; const driver = createMockDriver([{ deletedCount: 99 }]); - const context = makeContext(createMockAdapter(), new DeleteOneWireCommand('users', { id: 1 })); + const { context, stack } = makeExecution( + createMockAdapter(), + new DeleteOneWireCommand('users', { id: 1 }), + ); const runtime = createMongoRuntime({ context, + stack, driver, middleware: [ { @@ -157,7 +161,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: createMockDriver([]), middleware: [middleware], }); @@ -193,7 +197,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: failingDriver, middleware: [middleware], }); @@ -226,7 +230,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: failingDriver, middleware: [middleware], }); @@ -258,7 +262,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: failingDriver, middleware: [middleware], }); @@ -283,7 +287,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: createMockDriver([{ _id: '1' }, { _id: '2' }, { _id: '3' }]), middleware: [middleware], }); @@ -306,7 +310,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: createMockDriver([]), middleware: [middleware], mode: 'permissive', @@ -334,7 +338,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: createMockDriver([]), middleware: [middleware], }); @@ -360,7 +364,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: createMockDriver([{ _id: '1' }, { _id: '2' }, { _id: '3' }]), middleware: [middleware], }); diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts index a7af332a028f..a837733fe39d 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts @@ -2,11 +2,10 @@ import type { PlanMeta } from '@internal/contract/types'; import type { CodecCallContext } from '@internal/framework-components/codec'; import { type MongoCodecRegistry, newMongoCodecRegistry } from '@internal/mongo-codec'; import type { MongoAdapter, MongoDriver, MongoLoweredDraft } from '@internal/mongo-lowering'; -import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; +import type { MongoExecutionContext, MongoQueryPlan } from '@internal/mongo-query-ast/execution'; import type { AnyMongoWireCommand } from '@internal/mongo-wire'; import { describe, expect, it, vi } from 'vitest'; import type { - MongoExecutionContext, MongoExecutionStack, MongoRuntimeAdapterDescriptor, MongoRuntimeAdapterInstance, @@ -57,7 +56,10 @@ function recordingAdapter(): RecordingAdapter { return { adapter, observed, resolveCallCount }; } -function makeContext(adapter: MongoAdapter): MongoExecutionContext { +function makeExecution(adapter: MongoAdapter): { + readonly context: MongoExecutionContext; + readonly stack: MongoExecutionStack; +} { const codecs: MongoCodecRegistry = newMongoCodecRegistry(); const adapterInstance: MongoRuntimeAdapterInstance<'mongo'> = { familyId: 'mongo', @@ -90,7 +92,10 @@ function makeContext(adapter: MongoAdapter): MongoExecutionContext { driver: undefined, extensions: [], }; - return Object.freeze({ contract: {}, codecs, stack, applyMutationDefaults: () => [] }); + return { + context: Object.freeze({ contract: {}, codecs, applyMutationDefaults: () => [] }), + stack, + }; } function rowsDriver(rows: Record[] = []): MongoDriver { @@ -116,7 +121,7 @@ describe('MongoRuntime operations abort + ctx threading', () => { it('query(plan) with no options threads a ctx whose signal is undefined', async () => { const { adapter, observed } = recordingAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: rowsDriver([{ _id: '1' }]), }); @@ -129,7 +134,7 @@ describe('MongoRuntime operations abort + ctx threading', () => { it('query(plan, undefined) and query(plan, {}) thread ctx with undefined signal', async () => { const { adapter, observed } = recordingAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: rowsDriver([]), }); @@ -143,7 +148,7 @@ describe('MongoRuntime operations abort + ctx threading', () => { it('threads { signal } through query → resolveParams as a CodecCallContext (signal identity preserved)', async () => { const { adapter, observed } = recordingAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: rowsDriver([{ _id: '1' }]), }); @@ -159,7 +164,7 @@ describe('MongoRuntime operations abort + ctx threading', () => { const { adapter, resolveCallCount } = recordingAdapter(); const driver = rowsDriver([{ _id: '1' }]); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver, }); @@ -185,7 +190,7 @@ describe('MongoRuntime operations abort + ctx threading', () => { it('rejects already-aborted execute before lowering or driver delegation', async () => { const { adapter, resolveCallCount } = recordingAdapter(); const driver = rowsDriver([{ modifiedCount: 1 }]); - const runtime = createMongoRuntime({ context: makeContext(adapter), driver }); + const runtime = createMongoRuntime({ ...makeExecution(adapter), driver }); const controller = new AbortController(); const reason = new Error('statistics execution aborted'); controller.abort(reason); diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime.decode.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime.decode.test.ts index 74530fea9a0f..cab8f6296960 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime.decode.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime.decode.test.ts @@ -64,7 +64,7 @@ function runtimeOver(driver: MongoDriver) { }, }); const context = createMongoExecutionContext({ contract: {}, stack }); - return createMongoRuntime({ context, driver }); + return createMongoRuntime({ context, stack, driver }); } function driverYielding(rows: readonly Record[]): MongoDriver { diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts index f8147b6afb28..dbda05f4c75a 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts @@ -6,14 +6,14 @@ import type { } from '@internal/framework-components/runtime'; import { mongoCodec, newMongoCodecRegistry } from '@internal/mongo-codec'; import type { MongoAdapter, MongoDriver, MongoLoweredDraft } from '@internal/mongo-lowering'; -import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; +import type { MongoExecutionContext, MongoQueryPlan } from '@internal/mongo-query-ast/execution'; import { AggregateWireCommand, type AnyMongoDmlWireCommand, DeleteOneWireCommand, } from '@internal/mongo-wire'; import { describe, expect, it, vi } from 'vitest'; -import type { MongoExecutionContext } from '../src/mongo-execution-stack'; +import type { MongoExecutionStack } from '../src/mongo-execution-stack'; import type { MongoMiddleware, MongoMiddlewareContext } from '../src/mongo-middleware'; import { createMongoRuntime } from '../src/mongo-runtime'; @@ -63,10 +63,10 @@ function codecsWithFailingDecode() { return registry; } -function makeContext( +function makeExecution( wireCommand: AnyMongoDmlWireCommand, onResolveParams: () => void = () => {}, -): MongoExecutionContext { +): { readonly context: MongoExecutionContext; readonly stack: MongoExecutionStack } { const adapter = { familyId: 'mongo', targetId: 'mongo', @@ -84,8 +84,7 @@ function makeContext( }), } as unknown as MongoAdapter; return { - contract: {}, - codecs: codecsWithFailingDecode(), + context: { contract: {}, codecs: codecsWithFailingDecode(), applyMutationDefaults: () => [] }, stack: { target: { kind: 'target', @@ -107,9 +106,8 @@ function makeContext( }, driver: undefined, extensions: [], - }, - applyMutationDefaults: () => [], - } as unknown as MongoExecutionContext; + } as unknown as MongoExecutionStack, + }; } function createDriver(results: Record[], failure?: Error): MongoDriver { @@ -160,7 +158,7 @@ describe('MongoRuntime statement finalization', () => { it('finalizes a query committed right after it ends', async () => { const events: HookEvent[] = []; const runtime = createMongoRuntime({ - context: makeContext(new AggregateWireCommand('users', [])), + ...makeExecution(new AggregateWireCommand('users', [])), driver: createDriver([{ _id: '1' }, { _id: '2' }]), middleware: [recorder(events)], }); @@ -173,7 +171,7 @@ describe('MongoRuntime statement finalization', () => { it('finalizes a query unknown right after it fails when the driver throws', async () => { const events: HookEvent[] = []; const runtime = createMongoRuntime({ - context: makeContext(new AggregateWireCommand('users', [])), + ...makeExecution(new AggregateWireCommand('users', [])), driver: createDriver([{ _id: '1' }], new Error('driver failure')), middleware: [recorder(events)], }); @@ -186,7 +184,7 @@ describe('MongoRuntime statement finalization', () => { it('ends a query stopped and finalizes it unknown when the caller stops reading early', async () => { const events: HookEvent[] = []; const runtime = createMongoRuntime({ - context: makeContext(new AggregateWireCommand('users', [])), + ...makeExecution(new AggregateWireCommand('users', [])), driver: createDriver([{ _id: '1' }, { _id: '2' }]), middleware: [recorder(events)], }); @@ -202,7 +200,7 @@ describe('MongoRuntime statement finalization', () => { it('finalizes a query unknown when a row fails to decode', async () => { const events: HookEvent[] = []; const runtime = createMongoRuntime({ - context: makeContext(new AggregateWireCommand('users', [])), + ...makeExecution(new AggregateWireCommand('users', [])), driver: createDriver([{ name: 'alice' }]), middleware: [recorder(events)], }); @@ -217,7 +215,7 @@ describe('MongoRuntime statement finalization', () => { it('finalizes a query unknown when the signal aborts between rows', async () => { const events: HookEvent[] = []; const runtime = createMongoRuntime({ - context: makeContext(new AggregateWireCommand('users', [])), + ...makeExecution(new AggregateWireCommand('users', [])), driver: createDriver([{ _id: '1' }, { _id: '2' }]), middleware: [recorder(events)], }); @@ -239,7 +237,7 @@ describe('MongoRuntime statement finalization', () => { it('finalizes an execute committed right after it ends', async () => { const events: HookEvent[] = []; const runtime = createMongoRuntime({ - context: makeContext(new DeleteOneWireCommand('users', { id: 1 })), + ...makeExecution(new DeleteOneWireCommand('users', { id: 1 })), driver: createDriver([{ deletedCount: 1 }]), middleware: [recorder(events)], }); @@ -252,7 +250,7 @@ describe('MongoRuntime statement finalization', () => { it('finalizes an execute unknown when the driver throws', async () => { const events: HookEvent[] = []; const runtime = createMongoRuntime({ - context: makeContext(new DeleteOneWireCommand('users', { id: 1 })), + ...makeExecution(new DeleteOneWireCommand('users', { id: 1 })), driver: createDriver([], new Error('driver failure')), middleware: [recorder(events)], }); @@ -266,9 +264,7 @@ describe('MongoRuntime statement finalization', () => { const events: HookEvent[] = []; const controller = new AbortController(); const runtime = createMongoRuntime({ - context: makeContext(new DeleteOneWireCommand('users', { id: 1 }), () => - controller.abort(), - ), + ...makeExecution(new DeleteOneWireCommand('users', { id: 1 }), () => controller.abort()), driver: createDriver([{ deletedCount: 1 }]), middleware: [recorder(events)], }); @@ -293,7 +289,7 @@ describe('MongoRuntime statement finalization', () => { }, }; const runtime = createMongoRuntime({ - context: makeContext(new DeleteOneWireCommand('users', { id: 1 })), + ...makeExecution(new DeleteOneWireCommand('users', { id: 1 })), driver: createDriver([{ deletedCount: 1 }]), middleware: [failingBeforeHook, recorder(events)], }); diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime.statistics.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime.statistics.test.ts index 77698ae74aa4..86224c6c9dd1 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime.statistics.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime.statistics.test.ts @@ -10,14 +10,14 @@ import { createMockAdapter, createMockDriver, createPlan, - makeContext, + makeExecution, } from './mongo-middleware-fixtures'; describe('MongoRuntime statement statistics', () => { it('maps updateOne modifiedCount to affectedRows', async () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext( + ...makeExecution( adapter, new UpdateOneWireCommand('users', { id: 1 }, { $set: { active: true } }), ), @@ -30,10 +30,7 @@ describe('MongoRuntime statement statistics', () => { it('maps update modifiedCount to affectedRows', async () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext( - adapter, - new UpdateManyWireCommand('users', {}, { $set: { active: true } }), - ), + ...makeExecution(adapter, new UpdateManyWireCommand('users', {}, { $set: { active: true } })), driver: createMockDriver([{ matchedCount: 6, modifiedCount: 4 }]), }); @@ -43,7 +40,7 @@ describe('MongoRuntime statement statistics', () => { it('maps deleteOne deletedCount to affectedRows', async () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter, new DeleteOneWireCommand('users', { id: 1 })), + ...makeExecution(adapter, new DeleteOneWireCommand('users', { id: 1 })), driver: createMockDriver([{ deletedCount: 1 }]), }); @@ -53,7 +50,7 @@ describe('MongoRuntime statement statistics', () => { it('maps delete deletedCount to affectedRows', async () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter, new DeleteManyWireCommand('users', {})), + ...makeExecution(adapter, new DeleteManyWireCommand('users', {})), driver: createMockDriver([{ deletedCount: 3 }]), }); @@ -64,7 +61,7 @@ describe('MongoRuntime statement statistics', () => { const adapter = createMockAdapter(); const driver = createMockDriver([{ count: 5 }]); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver, }); @@ -76,7 +73,7 @@ describe('MongoRuntime statement statistics', () => { it('rejects zero statistics results', async () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter, new DeleteOneWireCommand('users', { id: 1 })), + ...makeExecution(adapter, new DeleteOneWireCommand('users', { id: 1 })), driver: createMockDriver([]), }); @@ -88,7 +85,7 @@ describe('MongoRuntime statement statistics', () => { it('rejects multiple statistics results', async () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext( + ...makeExecution( adapter, new UpdateOneWireCommand('users', { id: 1 }, { $set: { active: true } }), ), @@ -103,10 +100,7 @@ describe('MongoRuntime statement statistics', () => { it('rejects update statistics without modifiedCount', async () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext( - adapter, - new UpdateManyWireCommand('users', {}, { $set: { active: true } }), - ), + ...makeExecution(adapter, new UpdateManyWireCommand('users', {}, { $set: { active: true } })), driver: createMockDriver([{ matchedCount: 2 }]), }); diff --git a/packages/2-mongo-family/7-runtime/test/runtime-options-shape.test-d.ts b/packages/2-mongo-family/7-runtime/test/runtime-options-shape.test-d.ts index f131cf989646..a269b63b5df2 100644 --- a/packages/2-mongo-family/7-runtime/test/runtime-options-shape.test-d.ts +++ b/packages/2-mongo-family/7-runtime/test/runtime-options-shape.test-d.ts @@ -1,18 +1,30 @@ import type { MongoCodecRegistry } from '@internal/mongo-codec'; -import { test } from 'vitest'; -import type { MongoExecutionContext, MongoRuntimeOptions } from '../src/exports/index'; +import { expectTypeOf, test } from 'vitest'; +import type { + MongoExecutionContext, + MongoExecutionStack, + MongoRuntimeOptions, +} from '../src/exports/index'; import { createMongoRuntime } from '../src/mongo-runtime'; declare const ctx: MongoExecutionContext; +declare const stack: MongoExecutionStack; + +test('the execution context carries no stack; the runtime takes it as its own option', () => { + expectTypeOf().not.toHaveProperty('stack'); + expectTypeOf().toEqualTypeOf(); +}); test('runtime options accept only context, driver, middleware, and mode', () => { createMongoRuntime({ context: ctx, + stack, driver: {} as never, }); const validOptions = { context: ctx, + stack, driver: {} as never, middleware: [ { @@ -28,6 +40,7 @@ test('runtime options accept only context, driver, middleware, and mode', () => createMongoRuntime({ context: ctx, + stack, driver: {} as never, // @ts-expect-error `codecs` is not a property of MongoRuntimeOptions — // codecs are aggregated via `createMongoExecutionContext` and reached @@ -37,22 +50,25 @@ test('runtime options accept only context, driver, middleware, and mode', () => createMongoRuntime({ context: ctx, + stack, driver: {} as never, // @ts-expect-error `adapter` is not a property of MongoRuntimeOptions — - // the adapter is reached via `context.stack.adapter`. + // the adapter is reached via `stack.adapter`. adapter: {} as never, }); createMongoRuntime({ context: ctx, + stack, driver: {} as never, // @ts-expect-error `targetId` is not a property of MongoRuntimeOptions — - // the target is reached via `context.stack.target.targetId`. + // the target is reached via `stack.target.targetId`. targetId: 'mongo', }); createMongoRuntime({ context: ctx, + stack, driver: {} as never, // @ts-expect-error `contract` is not a property of MongoRuntimeOptions — // the contract is reached via `context.contract`. diff --git a/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts b/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts index 0ebefd95e23e..6b32d5617945 100644 --- a/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts +++ b/packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts @@ -5,7 +5,7 @@ import type { MongoContractWithTypeMaps, MongoTypeMaps, } from '@internal/mongo-contract'; -import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; +import type { MongoExecutionContext, MongoQueryPlan } from '@internal/mongo-query-ast/execution'; import { mongoQuery } from '@internal/mongo-query-builder'; import { expectTypeOf } from 'vitest'; import type { MongoRuntime } from '../src/mongo-runtime'; @@ -89,11 +89,11 @@ type PlanRow

= P extends MongoQueryPlan ? R : type OrderRow = { readonly _id: string; readonly status: string; readonly amount: number }; -declare const contract: TContract; +declare const context: MongoExecutionContext; describe('runtime type safety', () => { it('query() returns AsyncIterableResult where Row matches build() row type', () => { - const plan = mongoQuery({ context: { contract } }).from('orders').build(); + const plan = mongoQuery({ context }).from('orders').build(); type Row = PlanRow; expectTypeOf().toEqualTypeOf(); @@ -103,7 +103,7 @@ describe('runtime type safety', () => { }); it('query() result awaits to Row[]', () => { - const plan = mongoQuery({ context: { contract } }).from('orders').build(); + const plan = mongoQuery({ context }).from('orders').build(); type Row = PlanRow; const runtime = {} as MongoRuntime; diff --git a/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts b/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts index 44cb1343e851..69e2075402d3 100644 --- a/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts +++ b/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts @@ -86,7 +86,7 @@ export default function mongo< const contract = validateMongoContract(options.contract); let binding = resolveOptionalMongoBinding(options); - const { context, query, enums, enumsByNamespace, raw } = + const { context, stack, query, enums, enumsByNamespace, raw } = buildMongoStaticContext(contract); // Single source of truth for the lifecycle. `runtimePromise` is the in-flight @@ -106,6 +106,7 @@ export default function mongo< } return createMongoRuntime({ context, + stack, driver, ...ifDefined('mode', options.mode), ...ifDefined('middleware', options.middleware), diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts index edb8b9fc6030..9bdb3ce62df9 100644 --- a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts +++ b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts @@ -15,6 +15,7 @@ import { createMongoExecutionStack, type MongoEnumAccessors, type MongoExecutionContext, + type MongoExecutionStack, } from '@internal/mongo-runtime'; import mongoRuntimeTarget from '@internal/target-mongo/runtime'; import { assertDefined } from '@internal/utils/assertions'; @@ -39,6 +40,7 @@ export interface MongoClientStaticContext< TContract extends MongoContractWithTypeMaps, > extends MongoStaticContext { readonly enumsByNamespace: MongoEnumAccessors; + readonly stack: MongoExecutionStack; } export function buildMongoStaticContext< @@ -58,7 +60,7 @@ export function buildMongoStaticContext< >(unbound); const query = mongoQuery({ context }); const raw = mongoRaw({ contract }); - return { context, contract, enums, enumsByNamespace, query, raw }; + return { context, contract, enums, enumsByNamespace, query, raw, stack }; } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 723d44061a42..2044b664ed16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2129,6 +2129,9 @@ importers: '@internal/framework-components': specifier: workspace:8.0.0-rc.20 version: link:../../../1-framework/1-core/framework-components + '@internal/mongo-codec': + specifier: workspace:8.0.0-rc.20 + version: link:../../1-foundation/mongo-codec '@internal/mongo-contract': specifier: workspace:8.0.0-rc.20 version: link:../../1-foundation/mongo-contract diff --git a/test/integration/test/_harness/mongo.ts b/test/integration/test/_harness/mongo.ts index a32baede4fbb..a789f8450974 100644 --- a/test/integration/test/_harness/mongo.ts +++ b/test/integration/test/_harness/mongo.ts @@ -159,7 +159,7 @@ export async function withMongoPort< }); const context = createMongoExecutionContext({ contract, stack }); const driver = await createMongoDriver(connectionUri, dbName); - const runtime = createMongoRuntime({ context, driver }); + const runtime = createMongoRuntime({ context, stack, driver }); try { const db = mongoOrm({ diff --git a/test/integration/test/cross-package/cross-family-middleware.test.ts b/test/integration/test/cross-package/cross-family-middleware.test.ts index 16a528e80fc6..08d35dfab2e7 100644 --- a/test/integration/test/cross-package/cross-family-middleware.test.ts +++ b/test/integration/test/cross-package/cross-family-middleware.test.ts @@ -14,6 +14,7 @@ import { createMongoExecutionStack, createMongoRuntime, type MongoExecutionContext, + type MongoExecutionStack, type MongoRuntimeAdapterDescriptor, } from '@internal/mongo-runtime'; import mongoRuntimeTarget from '@internal/target-mongo/runtime'; @@ -162,12 +163,15 @@ function mockAdapterDescriptor(adapter: MongoAdapter): MongoRuntimeAdapterDescri }; } -function makeMongoContext(adapter: MongoAdapter): MongoExecutionContext { +function makeMongoExecution(adapter: MongoAdapter): { + readonly context: MongoExecutionContext; + readonly stack: MongoExecutionStack; +} { const stack = createMongoExecutionStack({ target: mongoRuntimeTarget, adapter: mockAdapterDescriptor(adapter), }); - return createMongoExecutionContext({ contract: {}, stack }); + return { context: createMongoExecutionContext({ contract: {}, stack }), stack }; } function createMockMongoDriver(rows: Record[] = []): MongoDriver { @@ -246,7 +250,7 @@ describe('cross-family middleware proof', () => { const mongoAdapter = createMockMongoAdapter(); const mongoRuntime = createMongoRuntime({ - context: makeMongoContext(mongoAdapter), + ...makeMongoExecution(mongoAdapter), driver: createMockMongoDriver([ { _id: '1', name: 'Bob' }, { _id: '2', name: 'Carol' }, @@ -281,7 +285,7 @@ describe('cross-family middleware proof', () => { const mongoAdapter = createMockMongoAdapter(); const mongoRuntime = createMongoRuntime({ - context: makeMongoContext(mongoAdapter), + ...makeMongoExecution(mongoAdapter), driver: createMockMongoDriver([{ _id: '1' }]), middleware: [middleware], }); @@ -343,7 +347,7 @@ describe('cross-family middleware proof', () => { const mongoAdapter = createMockMongoAdapter(); const mongoDriver = createMockMongoDriver([{ _id: 'should-not-appear' }]); const mongoRuntime = createMongoRuntime({ - context: makeMongoContext(mongoAdapter), + ...makeMongoExecution(mongoAdapter), driver: mongoDriver, middleware: [interceptor, observer], }); diff --git a/test/integration/test/mongo-runtime/query-builder.test.ts b/test/integration/test/mongo-runtime/query-builder.test.ts index 0615b2ed3b0d..71861f4efe02 100644 --- a/test/integration/test/mongo-runtime/query-builder.test.ts +++ b/test/integration/test/mongo-runtime/query-builder.test.ts @@ -6,7 +6,7 @@ import { crossRef, type NamespaceId } from '@internal/contract/types'; import mongoFamilyPack from '@internal/family-mongo/pack'; -import { contractFromJson } from '@internal/mongo/static'; +import mongoStatic, { contractFromJson } from '@internal/mongo/static'; import type { AnyMongoTypeMaps, MongoContract, @@ -14,7 +14,7 @@ import type { } from '@internal/mongo-contract'; import { defineContract, field, model, rel } from '@internal/mongo-contract-ts/contract-builder'; import type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; -import { acc, fn, mongoQuery } from '@internal/mongo-query-builder'; +import { acc, fn } from '@internal/mongo-query-builder'; import mongoTargetPack from '@internal/target-mongo/pack'; import { timeouts } from '@repo/test-utils'; import { ObjectId } from 'mongodb'; @@ -180,7 +180,7 @@ const contract = defineContract({ }); describe('pipeline builder integration', { timeout: timeouts.spinUpMongoMemoryServer }, () => { - const p = mongoQuery({ context: { contract: contractFromJson(contractJson) } }); + const p = mongoStatic({ contract: contractFromJson(contractJson) }).query; it('match → group → sort → limit analytics query', async () => { await withMongod(async (ctx) => { @@ -358,8 +358,8 @@ describe('pipeline builder integration', { timeout: timeouts.spinUpMongoMemorySe }, ]); - const plan = mongoQuery({ context: { contract } }) - .from('tasks') + const plan = mongoStatic({ contract }) + .query.from('tasks') .group((f) => ({ _id: f.type, count: acc.count(), diff --git a/test/integration/test/mongo/query-builder.test.ts b/test/integration/test/mongo/query-builder.test.ts index eec705bf0b91..eea3dd433fb5 100644 --- a/test/integration/test/mongo/query-builder.test.ts +++ b/test/integration/test/mongo/query-builder.test.ts @@ -1,5 +1,5 @@ import { crossRef, type NamespaceId } from '@internal/contract/types'; -import { contractFromJson } from '@internal/mongo/static'; +import mongoStatic, { contractFromJson } from '@internal/mongo/static'; import type { MongoContract, MongoContractWithTypeMaps, @@ -19,7 +19,7 @@ import { MongoRedactStage, MongoSortStage, } from '@internal/mongo-query-ast/execution'; -import { acc, fn, mongoQuery } from '@internal/mongo-query-builder'; +import { acc, fn } from '@internal/mongo-query-builder'; import { describe, expect, it } from 'vitest'; import { describeWithMongoDB } from './setup'; @@ -194,7 +194,7 @@ const contractJson = { // Helpers // --------------------------------------------------------------------------- -const query = mongoQuery({ context: { contract: contractFromJson(contractJson) } }); +const query = mongoStatic({ contract: contractFromJson(contractJson) }).query; function products() { return query.from('products'); diff --git a/test/integration/test/mongo/runtime/content-hash-guard.test.ts b/test/integration/test/mongo/runtime/content-hash-guard.test.ts index 0bddfad14a1e..41760f046b02 100644 --- a/test/integration/test/mongo/runtime/content-hash-guard.test.ts +++ b/test/integration/test/mongo/runtime/content-hash-guard.test.ts @@ -43,6 +43,7 @@ describe('Mongo runtime content hash before param resolution', () => { } as unknown as MongoDriver; const runtime = createMongoRuntime({ context, + stack, driver, middleware: [middleware], }); diff --git a/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts b/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts index e141e45ad074..6f570d2601a6 100644 --- a/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts +++ b/test/integration/test/mongo/runtime/decode-via-query-builder.test.ts @@ -12,8 +12,8 @@ */ import { MongoContractSerializer } from '@internal/family-mongo/ir'; -import { contractFromJson } from '@internal/mongo/static'; -import { acc, mongoQuery } from '@internal/mongo-query-builder'; +import mongoStatic, { contractFromJson } from '@internal/mongo/static'; +import { acc } from '@internal/mongo-query-builder'; import { MongoParamRef } from '@internal/mongo-value'; import { timeouts } from '@repo/test-utils'; import { ObjectId } from 'mongodb'; @@ -24,9 +24,9 @@ import { } from './fixtures/decode-fixture-contract'; import { withMongod } from './setup'; -const q = mongoQuery({ - context: { contract: contractFromJson(decodeFixtureContractJson) }, -}); +const q = mongoStatic({ + contract: contractFromJson(decodeFixtureContractJson), +}).query; describe('Mongo runtime decode integration via query-builder', { timeout: timeouts.spinUpMongoMemoryServer, @@ -45,8 +45,8 @@ describe('Mongo runtime decode integration via query-builder', { embeddings: vec, }); - const plan = mongoQuery({ context: { contract } }) - .from('users') + const plan = mongoStatic({ contract }) + .query.from('users') .match((f) => f['_id']!.eq(MongoParamRef.of(insert.insertedId, { codecId: 'mongo/objectId@1' })), ) diff --git a/test/integration/test/mongo/runtime/decode.integration.test.ts b/test/integration/test/mongo/runtime/decode.integration.test.ts index 994d1cb1050b..0d703e239822 100644 --- a/test/integration/test/mongo/runtime/decode.integration.test.ts +++ b/test/integration/test/mongo/runtime/decode.integration.test.ts @@ -1,6 +1,7 @@ import { MongoContractSerializer } from '@internal/family-mongo/ir'; import { type DataTypeValue, dataType, readJsonString } from '@internal/framework-components/codec'; import { isRuntimeError } from '@internal/framework-components/runtime'; +import mongoStatic from '@internal/mongo/static'; import { mongoCodec } from '@internal/mongo-codec'; import type { MongoResultShape } from '@internal/mongo-query-ast/execution'; import { @@ -9,7 +10,6 @@ import { MongoMatchStage, RawAggregateCommand, } from '@internal/mongo-query-ast/execution'; -import { mongoQuery } from '@internal/mongo-query-builder'; import { MongoParamRef } from '@internal/mongo-value'; import { timeouts } from '@repo/test-utils'; import { ObjectId } from 'mongodb'; @@ -38,8 +38,8 @@ describe('Mongo runtime decode integration', { timeout: timeouts.spinUpMongoMemo }); // User-facing path: build the plan through the query-builder so the Row type is contract-derived (no explicit annotation on execute). - const plan = mongoQuery({ context: { contract } }) - .from('users') + const plan = mongoStatic({ contract }) + .query.from('users') .match((f) => f['_id']!.eq(MongoParamRef.of(insert.insertedId, { codecId: 'mongo/objectId@1' })), ) diff --git a/test/integration/test/mongo/runtime/execute-param-mutator-wiring.test.ts b/test/integration/test/mongo/runtime/execute-param-mutator-wiring.test.ts index 92fbf254380a..ec53fac07e70 100644 --- a/test/integration/test/mongo/runtime/execute-param-mutator-wiring.test.ts +++ b/test/integration/test/mongo/runtime/execute-param-mutator-wiring.test.ts @@ -9,6 +9,7 @@ import { createMongoExecutionStack, createMongoRuntime, type MongoExecutionContext, + type MongoExecutionStack, } from '@internal/mongo-runtime'; import { MongoParamRef } from '@internal/mongo-value'; import type { AnyMongoWireCommand } from '@internal/mongo-wire'; @@ -25,12 +26,15 @@ const baseMeta: PlanMeta = { lane: 'orm', }; -function buildStackContext(): MongoExecutionContext { +function buildExecution(): { + readonly context: MongoExecutionContext; + readonly stack: MongoExecutionStack; +} { const stack = createMongoExecutionStack({ target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - return createMongoExecutionContext({ contract: {}, stack }); + return { context: createMongoExecutionContext({ contract: {}, stack }), stack }; } function recordingDriver(): { @@ -66,7 +70,7 @@ describe('MongoRuntime execute param-mutator wiring', () => { const { driver } = recordingDriver(); const runtime = createMongoRuntime({ - context: buildStackContext(), + ...buildExecution(), driver, middleware: [middleware], }); @@ -106,7 +110,7 @@ describe('MongoRuntime execute param-mutator wiring', () => { const { driver, commands } = recordingDriver(); const runtime = createMongoRuntime({ - context: buildStackContext(), + ...buildExecution(), driver, middleware: [middleware], }); @@ -136,8 +140,8 @@ describe('MongoRuntime execute param-mutator wiring', () => { let structuralDraft: MongoLoweredDraft | undefined; const resolveDraftRefs: MongoLoweredDraft[] = []; - const context = buildStackContext(); - const adapterInstance = context.stack.adapter.create(context.stack); + const { stack } = buildExecution(); + const adapterInstance = stack.adapter.create(stack); const structuralLowerSpy = vi.fn((plan) => { structuralDraft = adapterInstance.structuralLower(plan); return structuralDraft; @@ -161,7 +165,7 @@ describe('MongoRuntime execute param-mutator wiring', () => { const patchedContext = createMongoExecutionContext({ contract: {}, stack: patchedStack }); const { driver } = recordingDriver(); - const runtime = createMongoRuntime({ context: patchedContext, driver }); + const runtime = createMongoRuntime({ context: patchedContext, stack: patchedStack, driver }); await runtime .query({ @@ -205,7 +209,7 @@ describe('MongoRuntime execute param-mutator wiring', () => { }; const { driver } = recordingDriver(); const runtime = createMongoRuntime({ - context: buildStackContext(), + ...buildExecution(), driver, middleware: [middleware, observer], }); diff --git a/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts b/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts index 963857dce850..26a91a839f4d 100644 --- a/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts +++ b/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts @@ -149,7 +149,7 @@ describe('createMongoExecutionContext', () => { }); }); - it('returns a frozen { contract, codecs, stack } object', () => { + it('returns a frozen { contract, codecs } object with no stack', () => { const stack = createMongoExecutionStack({ target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, @@ -158,7 +158,7 @@ describe('createMongoExecutionContext', () => { const context = createMongoExecutionContext({ contract, stack }); expect(Object.isFrozen(context)).toBe(true); expect(context.contract).toBe(contract); - expect(context.stack).toBe(stack); + expect(Object.keys(context)).not.toContain('stack'); }); it('preserves the TContract type on context.contract (no unknown widening)', () => { diff --git a/test/integration/test/mongo/runtime/setup.ts b/test/integration/test/mongo/runtime/setup.ts index 1abf4c549a89..5ff3c9c39306 100644 --- a/test/integration/test/mongo/runtime/setup.ts +++ b/test/integration/test/mongo/runtime/setup.ts @@ -54,7 +54,7 @@ export async function withMongod(fn: (ctx: MongodContext) => Promise): Pro }); const context = createMongoExecutionContext({ contract: {}, stack }); const driver = await createMongoDriver(connectionUri, dbName); - const runtime = createMongoRuntime({ context, driver }); + const runtime = createMongoRuntime({ context, stack, driver }); // Cast back to `MongoCodecRegistry` — the test setup is the one place that // legitimately mutates the per-execution registry mid-flight (to install diff --git a/test/integration/test/mongo/setup.ts b/test/integration/test/mongo/setup.ts index 4dc6bdb64015..5ccddc65d349 100644 --- a/test/integration/test/mongo/setup.ts +++ b/test/integration/test/mongo/setup.ts @@ -36,7 +36,7 @@ export async function withMongod(fn: (ctx: MongodContext) => Promise): Pro }); const context = createMongoExecutionContext({ contract: {}, stack }); const driver = await createMongoDriver(connectionUri, dbName); - runtime = createMongoRuntime({ context, driver }); + runtime = createMongoRuntime({ context, stack, driver }); const ctx: MongodContext = { connectionUri, dbName, client, runtime, codecs: context.codecs }; return await fn(ctx); @@ -96,7 +96,7 @@ export function describeWithMongoDB(name: string, fn: (ctx: MongodContext) => vo }); const context = createMongoExecutionContext({ contract: {}, stack }); const driver = await createMongoDriver(replSet.getUri(), dbName); - runtime = createMongoRuntime({ context, driver }); + runtime = createMongoRuntime({ context, stack, driver }); codecs = context.codecs; }, timeouts.spinUpMongoMemoryServer); diff --git a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts index d621e7084122..4c094a3f1805 100644 --- a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts +++ b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts @@ -8,15 +8,15 @@ const id = 'mongo-query-takes-context'; const facadePackage = ['@prisma', 'orm-mongo'].join('/'); const familyPackage = ['@prisma', 'orm-family-mongo'].join('/'); -function detects(audience: 'app' | 'extension', content: string): boolean { +function detects(audience: 'app' | 'extension', content: string, change = id): boolean { const path = `upgrade-instructions/pending/mongo-query-takes-context/${audience}/instructions.md`; const guide = readFileSync(join(repoRoot, path), 'utf-8'); - const entry = guide.split(/\n {2}- id: /).find((block) => block.startsWith(`${id}\n`)); - if (entry === undefined) throw new Error(`The ${audience} guide has no change ${id}`); + const entry = guide.split(/\n {2}- id: /).find((block) => block.startsWith(`${change}\n`)); + if (entry === undefined) throw new Error(`The ${audience} guide has no change ${change}`); const patterns = [...entry.matchAll(/^ {8}- '(.*)'$/gm)].map( ([, pattern]) => new RegExp((pattern ?? '').replaceAll("''", "'")), ); - if (patterns.length === 0) throw new Error(`The ${audience} change ${id} has no detection`); + if (patterns.length === 0) throw new Error(`The ${audience} change ${change} has no detection`); return patterns.some((pattern) => pattern.test(content)); } @@ -47,3 +47,23 @@ describe.each(['app', 'extension'] as const)('the %s mongoQuery detection', (aud expect(detects(audience, content)).toBe(false); }); }); + +describe.each(['app', 'extension'] as const)('the %s runtime stack detection', (audience) => { + const change = 'mongo-runtime-takes-stack'; + + it.each([ + ['a runtime built from a context', 'const runtime = createMongoRuntime({ context, driver });'], + ['the stack read from a context', 'const adapter = context.stack.adapter;'], + ['the stack read from a named context', 'mongoContext.stack.target.targetId'], + ])('finds %s', (_name, content) => { + expect(detects(audience, content, change)).toBe(true); + }); + + it.each([ + ['a stack read from a stack', 'stack.adapter.create(stack)'], + ['a method of the same name', 'factory.createMongoRuntime({ context })'], + ['a stack on another object', 'options.stack.adapter'], + ])('skips %s', (_name, content) => { + expect(detects(audience, content, change)).toBe(false); + }); +}); diff --git a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md index 0c24a4d07161..fb97a97d041d 100644 --- a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md +++ b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md @@ -9,9 +9,17 @@ changes: - '(?()]*>)?\s*\(\s*\{\s*contract\b' - '(?()]*>)?\s*\(\s*\{\s*contract\b' - '(? Date: Tue, 27 Oct 2026 20:56:37 +0100 Subject: [PATCH 14/19] TML-3689: createSqlExecutionStack builds the stack through createExecutionStack, so its driver gets the target session settings The overloads built the stack object directly, which skipped the framework factory that wraps the driver to create it with the session settings the target declares. A Supabase client on a caller pool then lost DateStyle ISO after RESET ALL. Signed-off-by: willbot Signed-off-by: Will Madden --- .../2-sql-family/5-runtime/src/sql-context.ts | 29 ++++++++++--------- .../5-runtime/test/execution-stack.test.ts | 24 +++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/packages/2-sql-family/5-runtime/src/sql-context.ts b/packages/2-sql-family/5-runtime/src/sql-context.ts index 550d86b2ff48..35a88ce546d2 100644 --- a/packages/2-sql-family/5-runtime/src/sql-context.ts +++ b/packages/2-sql-family/5-runtime/src/sql-context.ts @@ -12,15 +12,16 @@ import { checkContractComponentRequirements, mergeCapabilityMatrices, } from '@internal/framework-components/components'; -import type { - RuntimeAdapterDescriptor, - RuntimeAdapterInstance, - RuntimeDriverDescriptor, - RuntimeDriverInstance, - RuntimeExtensionDescriptor, - RuntimeExtensionInstance, - RuntimeTargetDescriptor, - RuntimeTargetInstance, +import { + createExecutionStack, + type RuntimeAdapterDescriptor, + type RuntimeAdapterInstance, + type RuntimeDriverDescriptor, + type RuntimeDriverInstance, + type RuntimeExtensionDescriptor, + type RuntimeExtensionInstance, + type RuntimeTargetDescriptor, + type RuntimeTargetInstance, } from '@internal/framework-components/execution'; import { applyMutationDefaults, @@ -155,12 +156,14 @@ export function createSqlExecutionStack( readonly driver?: SqlExecutionStackWithDriver['driver']; }, ): SqlExecutionStack | SqlExecutionStackWithDriver { - const stack = { + const stack = createExecutionStack({ target: options.target, adapter: options.adapter, - extensions: options.extensions ?? [], - }; - return options.driver === undefined ? stack : { ...stack, driver: options.driver }; + driver: options.driver, + extensions: options.extensions, + }); + const { driver } = stack; + return driver === undefined ? stack : { ...stack, driver }; } export type { ExecutionContext }; diff --git a/packages/2-sql-family/5-runtime/test/execution-stack.test.ts b/packages/2-sql-family/5-runtime/test/execution-stack.test.ts index c5c562e0b57c..15f0761935b4 100644 --- a/packages/2-sql-family/5-runtime/test/execution-stack.test.ts +++ b/packages/2-sql-family/5-runtime/test/execution-stack.test.ts @@ -129,6 +129,30 @@ describe('createExecutionStack', () => { }); describe('createSqlExecutionStack', () => { + it('creates its driver with the session settings the target declares', () => { + const received: unknown[] = []; + const driver = { + kind: 'driver' as const, + id: 'test-driver', + version: '0.0.1', + familyId: 'sql' as const, + targetId: 'postgres' as const, + create: (...args: unknown[]) => { + received.push(args); + return {} as never; + }, + }; + const stack = createSqlExecutionStack({ + target: { ...createStubTargetDescriptor(), sessionSettings: { DateStyle: 'ISO' } }, + adapter: createStubAdapterDescriptor(), + driver, + }); + + stack.driver.create({ cursor: { disabled: true } }); + + expect(received).toEqual([[{ cursor: { disabled: true } }, { DateStyle: 'ISO' }]]); + }); + it('preserves descriptor references and defaults extensions', () => { const target = createStubTargetDescriptor(); const adapter = createStubAdapterDescriptor(); From 09f121e8b207e6f18f7bb40b1976c23d610a0eea Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 15:32:32 +0100 Subject: [PATCH 15/19] TML-3686: the Mongo runtime takes an adapter, and mongoOrm and mongoRaw take a context createMongoRuntime takes { context, adapter, driver }, as the SQL runtime does; it reads the target from context.contract.target. mongo() creates the adapter. MongoExecutionStack loses its unused driver field. mongoOrm and mongoRaw take { context } instead of { contract }; mongoOrm fills execution defaults through the context, so its mutationDefaults option and ORM.MUTATION_DEFAULTS_MISSING are gone. Upgrade fragments and detection tests cover each change. Signed-off-by: willbot Signed-off-by: Will Madden --- ...R 175 - Shared ORM Collection interface.md | 2 +- ...on packages snapshot their own contract.md | 6 +-- ...erators are a framework runtime concern.md | 2 +- .../subsystems/10. MongoDB Family.md | 4 +- docs/reference/error-reference.md | 4 -- examples/mongo-demo/README.md | 2 +- .../mongo-demo/test/cache-middleware.test.ts | 9 +++- .../5-query-builders/orm/src/mongo-orm.ts | 20 +++----- .../5-query-builders/orm/src/mongo-raw.ts | 5 +- .../5-query-builders/orm/src/orm-errors.ts | 3 +- .../test/collection.mutation-defaults.test.ts | 37 ++------------- .../test/integration/orm-ergonomics.test.ts | 44 ++++++++--------- .../orm/test/integration/polymorphism.test.ts | 22 +++++---- .../orm/test/mongo-orm-options.test-d.ts | 16 ++++++- .../orm/test/mongo-raw.test.ts | 16 +++++-- packages/2-mongo-family/7-runtime/README.md | 6 +-- .../7-runtime/src/mongo-execution-stack.ts | 28 ++--------- .../7-runtime/src/mongo-runtime.ts | 20 +++----- .../test/mongo-middleware-fixtures.ts | 47 ++++--------------- .../7-runtime/test/mongo-middleware.test.ts | 21 ++++++++- .../test/mongo-runtime-abort.test.ts | 47 +++---------------- .../test/mongo-runtime.decode.test.ts | 5 +- .../test/mongo-runtime.finalization.test.ts | 34 ++++---------- .../test/runtime-options-shape.test-d.ts | 38 +++++++-------- .../mongo/9-facade/src/runtime/mongo.ts | 5 +- .../mongo/9-facade/src/static/mongo-static.ts | 2 +- .../mongo/9-facade/test/enum-members.test.ts | 8 ++-- .../mongo/9-facade/test/mongo.test.ts | 20 ++++++-- .../9-facade/test/structured-errors.test.ts | 5 +- test/integration/test/_harness/mongo.ts | 5 +- .../cross-family-middleware.test.ts | 11 +++-- test/integration/test/mongo/no-emit.test.ts | 7 +-- test/integration/test/mongo/orm.test.ts | 24 +++++----- .../mongo/runtime/content-hash-guard.test.ts | 5 +- .../execute-param-mutator-wiring.test.ts | 29 ++++++++---- .../runtime/mongo-execution-stack.test.ts | 4 +- test/integration/test/mongo/runtime/setup.ts | 5 +- test/integration/test/mongo/setup.ts | 45 ++++++++++++++---- .../detection.test.ts | 39 ++++++++++++++- .../value-objects/value-objects.e2e.test.ts | 12 +++-- .../value-objects.integration.test.ts | 17 ++++--- .../app/instructions.md | 42 ++++++++++++++--- .../extension/instructions.md | 42 ++++++++++++++--- 43 files changed, 408 insertions(+), 357 deletions(-) diff --git a/docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.md b/docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.md index 384a9089397b..50ea879e770c 100644 --- a/docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.md +++ b/docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.md @@ -32,7 +32,7 @@ const user = await db.User.byEmail('alice@example.com').first(); **Mongo ORM (target — not yet implemented):** ```typescript -const db = mongoOrm({ contract, runtime }); +const db = mongoOrm({ context, executor: runtime, enums }); // Same custom collection pattern class UserCollection extends Collection { diff --git a/docs/architecture docs/adrs/ADR 197 - Migration packages snapshot their own contract.md b/docs/architecture docs/adrs/ADR 197 - Migration packages snapshot their own contract.md index 414bf820c764..73d87fbbe7e0 100644 --- a/docs/architecture docs/adrs/ADR 197 - Migration packages snapshot their own contract.md +++ b/docs/architecture docs/adrs/ADR 197 - Migration packages snapshot their own contract.md @@ -19,7 +19,7 @@ The migration file imports the contract *locally*, not from the project root: import type { Contract } from './contract' import contractJson from './contract.json' with { type: 'json' } -const raw = mongoRaw({ contract: contractJson as Contract }) +const { raw } = mongoStatic({ contract: contractJson as Contract }) ``` `contract.json` and `contract.d.ts` are copies — snapshots of the project's emitted contract when the migration was scaffolded. The migration never reaches outside its own directory for type information. @@ -88,8 +88,8 @@ import type { Contract as Intermediate } from './intermediate' import contractJson from './contract.json' with { type: 'json' } import intermediateJson from './intermediate.json' with { type: 'json' } -const dest = mongoRaw({ contract: contractJson as Contract }) -const mid = mongoRaw({ contract: intermediateJson as Intermediate }) +const dest = mongoStatic({ contract: contractJson as Contract }).raw +const mid = mongoStatic({ contract: intermediateJson as Intermediate }).raw ``` Multiple intermediate contracts are supported — the user names them and emits them independently. Each is a self-contained contract snapshot that lives in the migration directory. diff --git a/docs/architecture docs/adrs/ADR 256 - Mutation-default generators are a framework runtime concern.md b/docs/architecture docs/adrs/ADR 256 - Mutation-default generators are a framework runtime concern.md index 6e6a7abddc6c..12acd036bf62 100644 --- a/docs/architecture docs/adrs/ADR 256 - Mutation-default generators are a framework runtime concern.md +++ b/docs/architecture docs/adrs/ADR 256 - Mutation-default generators are a framework runtime concern.md @@ -112,7 +112,7 @@ For each default of `(namespace, entry)` with a phase for `op` (`onCreate` for ` - **A field is applied at most once per call**, even when two defaults name it. - **`namespace` is required.** Refs are namespace-scoped, so only defaults declared for `(namespace, entry)` apply; that is what tells two same-named tables in different schemas apart. Requiring it keeps the coordinate part of every match: a missing namespace is a caller bug, not a quiet fallback to matching by entry name alone. -`MutationDefaults` is the interface an ORM depends on: `applyMutationDefaults(options): ReadonlyArray`. The SQL lane `ExecutionContext` and `MongoExecutionContext` extend it, and the Mongo ORM takes it as `mongoOrm({ contract, executor, mutationDefaults })`. +`MutationDefaults` is the interface an ORM depends on: `applyMutationDefaults(options): ReadonlyArray`. The SQL lane `ExecutionContext` and `MongoExecutionContext` extend it, and the Mongo ORM reads it from the context it takes, `mongoOrm({ context, executor, enums })`, as the SQL ORM does. ## The execution section diff --git a/docs/architecture docs/subsystems/10. MongoDB Family.md b/docs/architecture docs/subsystems/10. MongoDB Family.md index ded210b4d988..37bdc2cdb971 100644 --- a/docs/architecture docs/subsystems/10. MongoDB Family.md +++ b/docs/architecture docs/subsystems/10. MongoDB Family.md @@ -290,7 +290,7 @@ The contract records each generated field in an optional `execution` section, wi Because a ref names a collection and a field, a default applies to every document in the collection. On a polymorphic model a generated field therefore belongs on the base model, and every variant create fills it; a generator attribute or generating type alias on a variant's own field is rejected (`PSL_GENERATOR_ON_VARIANT_FIELD` in PSL, `CONTRACT.DEFAULT_INVALID` with `reason: 'executionDefaults-on-variant'` in TS). Two models stored in one collection that declare the same field with identical defaults share one ref; different defaults are rejected (`PSL_GENERATOR_CONFLICT`, or `reason: 'executionDefaults-conflict'`). Such a collection's validator accepts a document of either model (see [Schema validation](#schema-validation-jsonschema)). -At runtime, `createMongoExecutionContext` collects `mutationDefaultGenerators` from the target, adapter and extensions (the adapter registers `timestampNow`) and throws `RUNTIME.MUTATION_DEFAULT_GENERATOR_MISSING` if the contract needs a generator no component provides; the registry, the check, and the apply loop are the framework's shared mutation-default runtime ([Runtime & Middleware Framework](4.%20Runtime%20%26%20Middleware%20Framework.md#mutation-defaults), [ADR 256](../adrs/ADR%20256%20-%20Mutation-default%20generators%20are%20a%20framework%20runtime%20concern.md)). The context implements the framework `MutationDefaults` interface, which the ORM depends on without depending on the Mongo runtime, and `mongoOrm({ contract, executor, mutationDefaults })` calls it on every write (`mongo()` passes the context; a hand-built `mongoOrm()` over a contract with execution defaults and no `mutationDefaults` throws `ORM.MUTATION_DEFAULTS_MISSING`), with the same rules as SQL: +At runtime, `createMongoExecutionContext` collects `mutationDefaultGenerators` from the target, adapter and extensions (the adapter registers `timestampNow`) and throws `RUNTIME.MUTATION_DEFAULT_GENERATOR_MISSING` if the contract needs a generator no component provides; the registry, the check, and the apply loop are the framework's shared mutation-default runtime ([Runtime & Middleware Framework](4.%20Runtime%20%26%20Middleware%20Framework.md#mutation-defaults), [ADR 256](../adrs/ADR%20256%20-%20Mutation-default%20generators%20are%20a%20framework%20runtime%20concern.md)). The context implements the framework `MutationDefaults` interface, which the ORM depends on without depending on the Mongo runtime, and `mongoOrm({ context, executor, enums })` calls it on every write, with the same rules as SQL: - `create`, `createAll`, `createAndCount` and the insert half of `upsert` fill `onCreate` fields; `update`, `updateAll`, `updateAndCount` and the update half of `upsert` add `onUpdate` fields to `$set`. - A field the write sets explicitly keeps its value. The ORM drops `undefined` values before it applies defaults, so a field set to `undefined` gets its default. An update whose payload touches no field gets no generated values. @@ -318,7 +318,7 @@ db.posts.updateMany( The ORM presents the same chaining API to consumers regardless of database family. A developer querying MongoDB uses the same `where()`, `select()`, `include()`, `orderBy()`, `limit()`, and `all()` methods as one querying Postgres: ```typescript -const db = mongoOrm({ contract, executor: runtime, enums: buildMongoEnums(contract, context.codecs) }); +const db = mongoOrm({ context, executor: runtime, enums: buildMongoEnums(contract, context.codecs) }); class UserCollection extends Collection { admins() { return this.where({ kind: 'admin' }); } diff --git a/docs/reference/error-reference.md b/docs/reference/error-reference.md index e6ea563ddac1..1c6b68ca8e27 100644 --- a/docs/reference/error-reference.md +++ b/docs/reference/error-reference.md @@ -1084,10 +1084,6 @@ The Mongo ORM client was asked to operate on a model name that is not in the con `create()` or `createAndCount()` was called with zero rows; at least one row of data is required. Payload: `method`, `namespaceId`, `tableName`. -### ORM.MUTATION_DEFAULTS_MISSING - -`mongoOrm()` was built over a contract with execution defaults (fields such as `temporal.CreatedAt` that the ORM fills on write) without `mutationDefaults`, so those fields would never be written. Pass the execution context, `mongoOrm({ contract, executor, mutationDefaults: context })`, or create the client with `mongo()`. Payload: `fields` (`.` for each default). - ### ORM.MUTATION_ROW_MISSING A mutation that expected the database to return a row got none: `create()`/`upsert()` read-back, MTI base or variant INSERT, or a nested create. The Prisma-classic analogue of P2025. Payload: `operation`, `model`, `tableName`, `phase`. diff --git a/examples/mongo-demo/README.md b/examples/mongo-demo/README.md index 1d9bc7ca593f..42797ef4dab5 100644 --- a/examples/mongo-demo/README.md +++ b/examples/mongo-demo/README.md @@ -107,7 +107,7 @@ The Mongo query builder doesn't yet expose a chainable `.annotate(...)` surface 1. Builds the execution stack from the Mongo target, adapter and driver, and the runtime that executes queries (with the cache middleware) 2. Exposes the ORM with typed collection accessors (`db.orm.users`, `db.orm.posts`), the query builder (`db.query`) and the enums (`db.enums`) -`mongo()` also fills generated fields such as `temporal.CreatedAt` on write. If you build the ORM yourself with `mongoOrm()` rather than through `mongo()`, pass the execution context as `mutationDefaults` and the contract's enum accessors as `enums`: `mongoOrm({ contract, executor, mutationDefaults: context, enums: buildMongoEnums(contract, context.codecs) })`, with `buildMongoEnums` from `@prisma/orm-mongo/family-runtime`. The ORM checks a written enum value against those accessors, which are the ones `db.enums` holds. It fills generated fields such as `temporal.CreatedAt`, and `mongoOrm()` refuses a contract that has them when the option is missing (`ORM.MUTATION_DEFAULTS_MISSING`). +`mongo()` also fills generated fields such as `temporal.CreatedAt` on write. If you build the ORM yourself with `mongoOrm()` rather than through `mongo()`, pass the execution context and the contract's enum accessors: `mongoOrm({ context, executor, enums: buildMongoEnums(contract, context.codecs) })`, with `buildMongoEnums` from `@prisma/orm-mongo/family-runtime`. The ORM fills generated fields such as `temporal.CreatedAt` through the context, and checks a written enum value against the accessors, which are the ones `db.enums` holds. ## Key files diff --git a/examples/mongo-demo/test/cache-middleware.test.ts b/examples/mongo-demo/test/cache-middleware.test.ts index 10555513f6a5..8a9f711092ae 100644 --- a/examples/mongo-demo/test/cache-middleware.test.ts +++ b/examples/mongo-demo/test/cache-middleware.test.ts @@ -75,9 +75,14 @@ describe('mongo-demo cache middleware integration', { const driver = await createMongoDriver(replSet.getUri(), dbName); const driverExecuteSpy = vi.spyOn(driver, 'execute'); const cache = createCacheMiddleware(); - const runtime = createMongoRuntime({ context, stack, driver, middleware: [cache] }); + const runtime = createMongoRuntime({ + context, + adapter: stack.adapter.create(stack), + driver, + middleware: [cache], + }); const orm = mongoOrm({ - contract, + context, executor: runtime, enums: buildMongoEnums(contract, context.codecs), }); diff --git a/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts b/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts index 3ecfc4052a66..51129dd78cf9 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts @@ -1,10 +1,10 @@ -import type { MutationDefaults } from '@internal/framework-components/runtime'; import type { AnyMongoTypeMaps, MongoContract, MongoContractWithTypeMaps, RootModelName, } from '@internal/mongo-contract'; +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { blindCast } from '@internal/utils/casts'; import type { MongoCollection, MongoOrmEnums } from './collection'; import { createMongoCollection } from './collection'; @@ -12,10 +12,9 @@ import type { MongoQueryExecutor } from './executor'; import { ormError } from './orm-errors'; export interface MongoOrmOptions { - readonly contract: TContract; + /** The validated contract, its codecs, and the execution defaults filled on writes. */ + readonly context: MongoExecutionContext; readonly executor: MongoQueryExecutor; - /** Fills the contract's execution defaults on writes. Without it, no generated values are applied. */ - readonly mutationDefaults?: MutationDefaults; /** The contract's enum accessors, which a written enum value is checked against: `buildMongoEnums(contract, context.codecs)`, the accessors `db.enums` holds. */ readonly enums: MongoOrmEnums; } @@ -46,15 +45,8 @@ function assertEnumsComplete(contract: MongoContract, enums: MongoOrmEnums): voi export function mongoOrm< TContract extends MongoContractWithTypeMaps, >(options: MongoOrmOptions): MongoOrmClient { - const { contract, executor, mutationDefaults, enums } = options; - const executionDefaults = contract.execution?.mutations.defaults ?? []; - if (executionDefaults.length > 0 && mutationDefaults === undefined) { - throw ormError( - 'ORM.MUTATION_DEFAULTS_MISSING', - 'The contract has execution defaults (fields such as temporal.CreatedAt that the ORM fills on write), but mongoOrm() was built without mutationDefaults, so those fields would never be written. Pass the execution context: mongoOrm({ contract, executor, mutationDefaults: context }), or create the client with mongo().', - { meta: { fields: executionDefaults.map((d) => `${d.ref.entry}.${d.ref.field}`) } }, - ); - } + const { context, executor, enums } = options; + const { contract } = context; assertEnumsComplete(contract, enums); const client: Record = {}; @@ -67,7 +59,7 @@ export function mongoOrm< >(rootRef.model), executor, enums, - mutationDefaults, + context, ); } diff --git a/packages/2-mongo-family/5-query-builders/orm/src/mongo-raw.ts b/packages/2-mongo-family/5-query-builders/orm/src/mongo-raw.ts index 54325c4f0e36..c5d6a17d9088 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/mongo-raw.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/mongo-raw.ts @@ -4,6 +4,7 @@ import { type MongoContract, mongoModelsOf, } from '@internal/mongo-contract'; +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { ormError } from './orm-errors'; import { createRawMongoCollection, type RawMongoCollection } from './raw-collection'; @@ -12,9 +13,9 @@ export interface MongoRawClient { } export function mongoRaw(options: { - contract: TContract; + readonly context: MongoExecutionContext; }): MongoRawClient { - const { contract } = options; + const { contract } = options.context; return { collection(rootName: K): RawMongoCollection { diff --git a/packages/2-mongo-family/5-query-builders/orm/src/orm-errors.ts b/packages/2-mongo-family/5-query-builders/orm/src/orm-errors.ts index b1ac8839f5b7..7e7bfcfbf302 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/orm-errors.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/orm-errors.ts @@ -12,8 +12,7 @@ type OrmSubcode = | 'OPERATION_UNSUPPORTED' | 'INCLUDE_UNSUPPORTED' | 'FIELD_IMMUTABLE' - | 'FIELD_UNKNOWN' - | 'MUTATION_DEFAULTS_MISSING'; + | 'FIELD_UNKNOWN'; export type OrmCode = `ORM.${OrmSubcode}`; diff --git a/packages/2-mongo-family/5-query-builders/orm/test/collection.mutation-defaults.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/collection.mutation-defaults.test.ts index c839679b1470..4e59ed5aaf98 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/collection.mutation-defaults.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/collection.mutation-defaults.test.ts @@ -18,6 +18,8 @@ import type { MongoQueryExecutor } from '../src/executor'; import { mongoOrm } from '../src/mongo-orm'; import { noEnums } from './no-enums'; +const noCodecs = { get: () => undefined, has: () => false }; + const contract = ormContractJson as unknown as Contract; const userData = { @@ -300,45 +302,14 @@ describe('ORM upsert applies both halves', () => { }); describe('mongoOrm', () => { - it('refuses a contract with execution defaults when mutationDefaults is missing', () => { - const { executor } = recordingExecutor(); - const withDefaults = { - ...contract, - execution: { - executionHash: 'test', - mutations: { - defaults: [ - { - ref: { namespace: '__unbound__', entry: 'users', field: 'loginCount' }, - onCreate: { kind: 'generator', id: 'timestampNow' }, - }, - ], - }, - }, - } as unknown as Contract; - expect(() => mongoOrm({ contract: withDefaults, executor, enums: noEnums })).toThrow( - expect.objectContaining({ - code: 'ORM.MUTATION_DEFAULTS_MISSING', - message: - 'The contract has execution defaults (fields such as temporal.CreatedAt that the ORM fills on write), but mongoOrm() was built without mutationDefaults, so those fields would never be written. Pass the execution context: mongoOrm({ contract, executor, mutationDefaults: context }), or create the client with mongo().', - }), - ); - }); - - it('builds a contract without execution defaults without mutationDefaults', () => { - const { executor } = recordingExecutor(); - expect(() => mongoOrm({ contract, executor, enums: noEnums })).not.toThrow(); - }); - - it('passes mutationDefaults to every root collection', async () => { + it('fills writes from the execution context it is given', async () => { const { executor, plans } = recordingExecutor([ { insertedId: 'id-1', document: { _id: 'id-1' } }, ]); const orm = mongoOrm({ - contract, + context: { ...fakeMutationDefaults(), contract, codecs: noCodecs }, executor, enums: noEnums, - mutationDefaults: fakeMutationDefaults(), }); await orm.users.create(input(userData)); expect(unwrap(commandOf(plans, 'insertOne')['document'])).toMatchObject({ loginCount: 7 }); diff --git a/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts index 434a29c0278c..47c309447ddd 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts @@ -1,5 +1,6 @@ import mongoRuntimeAdapter from '@internal/adapter-mongo/runtime'; import { createMongoDriver } from '@internal/driver-mongo'; +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { buildMongoEnums, createMongoExecutionContext, @@ -39,6 +40,7 @@ describe('ORM ergonomics integration', { let client: MongoClient; let runtime: MongoRuntime; let enums: MongoOrmEnums; + let context: MongoExecutionContext; const dbName = 'orm_ergonomics_test'; beforeAll(async () => { @@ -52,9 +54,9 @@ describe('ORM ergonomics integration', { target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - const context = createMongoExecutionContext({ contract: {}, stack }); + context = createMongoExecutionContext({ contract, stack }); const driver = await createMongoDriver(replSet.getUri(), dbName); - runtime = createMongoRuntime({ context, stack, driver }); + runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); enums = buildMongoEnums(contract, context.codecs); }, timeouts.spinUpMongoMemoryServer); @@ -68,7 +70,7 @@ describe('ORM ergonomics integration', { describe('write results decode through codecs (issue #577)', () => { it('create() returns _id as the decoded hex string a read returns', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const created = await orm.users.create(defaultUserData); expect(typeof created._id).toBe('string'); const fetched = await orm.users.where({ _id: created._id as string }).first(); @@ -76,7 +78,7 @@ describe('ORM ergonomics integration', { }); it('createAll() returns decoded _ids', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const rows: Record[] = []; for await (const row of orm.users.createAll([ defaultUserData, @@ -91,7 +93,7 @@ describe('ORM ergonomics integration', { }); it('update() returns the document decoded like a read', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const created = await orm.users.create(defaultUserData); const updated = await orm.users .where({ _id: created._id as string }) @@ -101,7 +103,7 @@ describe('ORM ergonomics integration', { }); it('delete() returns the document decoded like a read', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const created = await orm.users.create(defaultUserData); const deleted = await orm.users.where({ _id: created._id as string }).delete(); expect(deleted).not.toBeNull(); @@ -111,7 +113,7 @@ describe('ORM ergonomics integration', { describe('codec-aware where()', () => { it('retrieves document by ObjectId field using object where', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create(defaultUserData); const found = await orm.users.where({ _id: user._id as string }).first(); expect(found).not.toBeNull(); @@ -119,7 +121,7 @@ describe('ORM ergonomics integration', { }); it('retrieves document by string field using object where', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); await orm.users.create(defaultUserData); const found = await orm.users.where({ name: 'Alice' }).first(); expect(found).not.toBeNull(); @@ -127,7 +129,7 @@ describe('ORM ergonomics integration', { }); it('firstOrThrow returns the matching document and rejects when none matches', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); await orm.users.create(defaultUserData); await orm.users.create({ ...defaultUserData, name: 'Bob', email: 'bob@test.com' }); const found = await orm.users.where({ name: 'Bob' }).firstOrThrow(); @@ -142,7 +144,7 @@ describe('ORM ergonomics integration', { }); it('filters by multiple fields using object where', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); await orm.users.create(defaultUserData); await orm.users.create({ ...defaultUserData, name: 'Bob', email: 'bob@test.com' }); const found = await orm.users.where({ name: 'Alice', email: 'alice@test.com' }).first(); @@ -153,7 +155,7 @@ describe('ORM ergonomics integration', { describe('field accessor mutations', () => { it('$push adds element to array field', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create(defaultUserData); const updated = await orm.users .where({ _id: user._id as string }) @@ -166,7 +168,7 @@ describe('ORM ergonomics integration', { }); it('$pull removes element from array field', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create({ ...defaultUserData, tags: ['admin', 'editor'] }); await orm.users.where({ _id: user._id as string }).update((u) => [u.tags.pull('admin')]); @@ -176,7 +178,7 @@ describe('ORM ergonomics integration', { }); it('$inc increments numeric field', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create(defaultUserData); await orm.users.where({ _id: user._id as string }).update((u) => [u.loginCount.inc(1)]); @@ -186,7 +188,7 @@ describe('ORM ergonomics integration', { }); it('dot-path $set updates nested value object field', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create({ ...defaultUserData, homeAddress: { city: 'SF', country: 'US' }, @@ -201,7 +203,7 @@ describe('ORM ergonomics integration', { }); it('multiple operations in one callback are applied together', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create(defaultUserData); await orm.users .where({ _id: user._id as string }) @@ -214,7 +216,7 @@ describe('ORM ergonomics integration', { }); it('updateAll with callback updates multiple documents', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); await orm.users.create(defaultUserData); await orm.users.create({ ...defaultUserData, name: 'Bob', email: 'bob@test.com' }); @@ -235,7 +237,7 @@ describe('ORM ergonomics integration', { describe('upsert() dot-path guard', () => { it('throws when callback uses a dot-path operation', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); await orm.users.create(defaultUserData); await expect( orm.users.where({ name: 'Alice' }).upsert({ @@ -248,7 +250,7 @@ describe('ORM ergonomics integration', { describe('reference relation include', () => { it('include() on 1:N relation returns array of related documents', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create(defaultUserData); await orm.tasks.create({ @@ -274,7 +276,7 @@ describe('ORM ergonomics integration', { }); it('include() on 1:N returns empty array when no related documents', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create(defaultUserData); const result = await orm.users @@ -287,7 +289,7 @@ describe('ORM ergonomics integration', { }); it('select() keeps a 1:N include whose join key is not selected', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create(defaultUserData); await orm.tasks.create({ title: 'Task 1', @@ -310,7 +312,7 @@ describe('ORM ergonomics integration', { }); it('select() keeps an N:1 include whose join key is not selected', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create(defaultUserData); await orm.tasks.create({ title: 'Task 1', diff --git a/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts index f60fb5596f2a..82f7987c7b63 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts @@ -1,5 +1,6 @@ import mongoRuntimeAdapter from '@internal/adapter-mongo/runtime'; import { createMongoDriver } from '@internal/driver-mongo'; +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { buildMongoEnums, createMongoExecutionContext, @@ -26,6 +27,7 @@ describe('Mongo ORM polymorphism integration', { let client: MongoClient; let runtime: MongoRuntime; let enums: MongoOrmEnums; + let context: MongoExecutionContext; const dbName = 'polymorphism_test'; beforeAll(async () => { @@ -39,9 +41,9 @@ describe('Mongo ORM polymorphism integration', { target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - const context = createMongoExecutionContext({ contract: {}, stack }); + context = createMongoExecutionContext({ contract, stack }); const driver = await createMongoDriver(replSet.getUri(), dbName); - runtime = createMongoRuntime({ context, stack, driver }); + runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); enums = buildMongoEnums(contract, context.codecs); }, timeouts.spinUpMongoMemoryServer); @@ -54,7 +56,7 @@ describe('Mongo ORM polymorphism integration', { }, timeouts.spinUpMongoMemoryServer); it('base query returns rows with discriminator values', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -81,7 +83,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant("bug") filters to only Bug rows', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -108,7 +110,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant("feature") filters to only Feature rows', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -135,7 +137,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant create injects discriminator and persists it', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -159,7 +161,7 @@ describe('Mongo ORM polymorphism integration', { }); it('round-trip: create via variant, read back via base', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -190,7 +192,7 @@ describe('Mongo ORM polymorphism integration', { }); it('non-polymorphic model unaffected by polymorphism changes', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); await orm.users.createAll([ { @@ -216,7 +218,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant().first() returns narrowed result', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -238,7 +240,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant createAll injects discriminator into each document', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, executor: runtime, enums }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', diff --git a/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts b/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts index e693feb31f07..26c66edf931a 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts @@ -1,15 +1,29 @@ +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { test } from 'vitest'; import type { Contract } from '../../../1-foundation/mongo-contract/test/fixtures/orm-contract'; +import type { MongoOrmEnums } from '../src/collection'; import { createMongoCollection } from '../src/collection'; import type { MongoQueryExecutor } from '../src/executor'; import { mongoOrm } from '../src/mongo-orm'; +import { mongoRaw } from '../src/mongo-raw'; declare const contract: Contract; +declare const context: MongoExecutionContext; declare const executor: MongoQueryExecutor; +declare const enums: MongoOrmEnums; test("mongoOrm() and createMongoCollection() require the contract's enum accessors", () => { // @ts-expect-error a written enum value is checked against the contract's enum accessors - mongoOrm({ contract, executor }); + mongoOrm({ context, executor }); // @ts-expect-error a written enum value is checked against the contract's enum accessors createMongoCollection(contract, 'User', executor); }); + +test('mongoOrm() and mongoRaw() take an execution context, not a contract', () => { + mongoOrm({ context, executor, enums }); + mongoRaw({ context }); + // @ts-expect-error a contract alone is not accepted; build the context with mongoStatic() or mongo() + mongoOrm({ contract, executor, enums }); + // @ts-expect-error a contract alone is not accepted; build the context with mongoStatic() or mongo() + mongoRaw({ contract }); +}); diff --git a/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts index b5330c5b3087..416309f38aa7 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts @@ -1,18 +1,28 @@ import type { MongoContract } from '@internal/mongo-contract'; -import type { RawMongoCommand } from '@internal/mongo-query-ast/execution'; +import type { MongoExecutionContext, RawMongoCommand } from '@internal/mongo-query-ast/execution'; import { blindCast } from '@internal/utils/casts'; import { describe, expect, it } from 'vitest'; import type { Contract } from '../../../1-foundation/mongo-contract/test/fixtures/orm-contract'; import ormContractJson from '../../../1-foundation/mongo-contract/test/fixtures/orm-contract.json'; import { mongoRaw } from '../src/mongo-raw'; +function contextOf( + contract: TContract, +): MongoExecutionContext { + return { + contract, + codecs: { get: () => undefined, has: () => false }, + applyMutationDefaults: () => [], + }; +} + // JSON import loses literal types; typed Contract .d.ts is the source of truth const contract = blindCast( ormContractJson, ); describe('mongoRaw', () => { - const raw = mongoRaw({ contract }); + const raw = mongoRaw({ context: contextOf(contract) }); describe('collection name resolution', () => { it('resolves root name to storage collection via contract', () => { @@ -25,7 +35,7 @@ describe('mongoRaw', () => { ...ormContractJson, roots: { ghost: { namespace: '__unbound__', model: 'NoSuchModel' } }, } as unknown as MongoContract; - const badRaw = mongoRaw({ contract: badContract }); + const badRaw = mongoRaw({ context: contextOf(badContract) }); expect(() => badRaw.collection('ghost')).toThrow( 'Unknown model "NoSuchModel" for root "ghost"', ); diff --git a/packages/2-mongo-family/7-runtime/README.md b/packages/2-mongo-family/7-runtime/README.md index 8d2f1b0bece8..073923f8da39 100644 --- a/packages/2-mongo-family/7-runtime/README.md +++ b/packages/2-mongo-family/7-runtime/README.md @@ -38,7 +38,7 @@ const context = createMongoExecutionContext({ contract, stack }); const runtime = createMongoRuntime({ context, - stack, + adapter: stack.adapter.create(stack), driver: await createMongoDriver(url, dbName), }); ``` @@ -47,9 +47,9 @@ Custom or third-party codecs (encryption, vendor scalars) are contributed via an ## Responsibilities -- **Stack/context composition**: `createMongoExecutionStack` and `createMongoExecutionContext` mirror SQL's `createSqlExecutionStack` / `createExecutionContext`. The context aggregates codec contributions from `[stack.target, stack.adapter, ...stack.extensions]` into a single `MongoCodecRegistry`. The `MongoExecutionContext` type is declared in `@internal/mongo-query-ast`, as SQL's `ExecutionContext` is declared in relational-core, and holds no stack: the query lanes build against it, and the runtime receives the stack as its own option. +- **Stack/context composition**: `createMongoExecutionStack` and `createMongoExecutionContext` mirror SQL's `createSqlExecutionStack` / `createExecutionContext`. The context aggregates codec contributions from `[stack.target, stack.adapter, ...stack.extensions]` into a single `MongoCodecRegistry`. The `MongoExecutionContext` type is declared in `@internal/mongo-query-ast`, as SQL's `ExecutionContext` is declared in relational-core, and holds no stack: the query lanes build against it, and the runtime receives the adapter as its own option. - **Enum accessors**: `buildMongoEnums(contract, context.codecs)` builds the contract's enum accessors by namespace: what `db.enums` holds and what `mongoOrm({ enums })` checks a written enum value against. A contract whose enum codec the context lacks is refused here with `RUNTIME.CODEC_DESCRIPTOR_MISSING`. -- **Runtime executor**: `createMongoRuntime({ context, stack, driver, ... })` composes context, stack and driver into a `MongoRuntime` with separate `query(plan)` and `execute(plan)` operations accepting `MongoQueryPlan` from `@internal/mongo-query-ast`. `query()` yields decoded rows, while `execute()` returns statement statistics. Both operations use the adapter the runtime creates from `stack.adapter` through the stack's `create(stack)` factory, lower the plan, and run the wire command on the driver; `query()` **optionally decodes** each row when `plan.resultShape` is present. +- **Runtime executor**: `createMongoRuntime({ context, adapter, driver, ... })` composes context, adapter and driver into a `MongoRuntime`, as the SQL runtime does. The caller creates the adapter with `stack.adapter.create(stack)`, and the runtime checks middleware against the target named by `context.contract.target`. The runtime has separate `query(plan)` and `execute(plan)` operations accepting `MongoQueryPlan` from `@internal/mongo-query-ast`. `query()` yields decoded rows, while `execute()` returns statement statistics. Both operations lower the plan with the adapter, and run the wire command on the driver; `query()` **optionally decodes** each row when `plan.resultShape` is present. - **Operation flow**: Row-producing work uses `query(plan)` and the query middleware lifecycle; statistics-producing work uses `execute(plan)` and the execute middleware lifecycle. Each operation allocates its own `CodecCallContext` and threads it through its codec dispatches. - **Lowering**: Happens in the adapter (`lower(plan)`), wrapped by the runtime's `lower` override into a `MongoExecutionPlan`. - **Middleware lifecycle inheritance**: `MongoRuntime` extends `RuntimeCore` and runs every `query()` and `execute()` as a statement activity: `activityStarted`, `beforeEncode` (before `resolveParams`), `intercept.onQuery` / `intercept.onExecute`, `onRow`, `activityEnded`, then `activityFinalized` right away, because Mongo has no transactions. Mongo does **not** override `runBeforeCompile` (Mongo middleware has no `beforeCompile` hook today). diff --git a/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts index bebe27847ff6..30b594fd6106 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts @@ -3,8 +3,6 @@ import { createExecutionStack, type RuntimeAdapterDescriptor, type RuntimeAdapterInstance, - type RuntimeDriverDescriptor, - type RuntimeDriverInstance, type RuntimeExtensionDescriptor, type RuntimeExtensionInstance, type RuntimeTargetDescriptor, @@ -66,45 +64,25 @@ export interface MongoRuntimeExtensionDescriptor { readonly target: MongoRuntimeTargetDescriptor; readonly adapter: MongoRuntimeAdapterDescriptor; - readonly driver: - | RuntimeDriverDescriptor< - 'mongo', - TTargetId, - unknown, - RuntimeDriverInstance<'mongo', TTargetId> - > - | undefined; readonly extensions: readonly MongoRuntimeExtensionDescriptor[]; } export function createMongoExecutionStack(options: { readonly target: MongoRuntimeTargetDescriptor; readonly adapter: MongoRuntimeAdapterDescriptor; - readonly driver?: - | RuntimeDriverDescriptor< - 'mongo', - TTargetId, - unknown, - RuntimeDriverInstance<'mongo', TTargetId> - > - | undefined; readonly extensions?: readonly MongoRuntimeExtensionDescriptor[] | undefined; }): MongoExecutionStack { - const stack = createExecutionStack({ + const { target, adapter, extensions } = createExecutionStack({ target: options.target, adapter: options.adapter, - driver: options.driver, extensions: options.extensions, }); - return blindCast< - MongoExecutionStack, - 'createExecutionStack keeps the Mongo descriptors it was given' - >(stack); + return { target, adapter, extensions }; } export function createMongoExecutionContext< diff --git a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts index 8716a481c732..54eb43673706 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts @@ -13,6 +13,7 @@ import { type StatementActivityRun, streamStatement, } from '@internal/framework-components/runtime'; +import type { MongoContract } from '@internal/mongo-contract'; import type { MongoAdapter, MongoDriver } from '@internal/mongo-lowering'; import type { MongoCodecLookup, @@ -24,7 +25,6 @@ import { ifDefined } from '@internal/utils/defined'; import { decodeMongoRow } from './codecs/decoding'; import { computeMongoContentHash } from './content-hash'; import type { MongoExecutionPlan } from './mongo-execution-plan'; -import type { MongoExecutionStack } from './mongo-execution-stack'; import type { MongoMiddleware, MongoMiddlewareContext } from './mongo-middleware'; import { createMongoParamRefMutator, @@ -68,17 +68,11 @@ function invalidStatisticsResult(commandKind: string, countField: string): Error /** * Mongo runtime options. * - * The runtime takes a {@link MongoExecutionContext} (built via - * `createMongoExecutionContext`), the execution stack the context was built - * from, and a driver. Codec resolution flows from the context — there is no - * `codecs` field on this options bag. The adapter comes from `stack.adapter` - * (instantiated through the stack's `create(stack)` factory). See ADR — Mongo result-shape as a - * structural plan field, § Codec registry: stack aggregation, not user - * threading. + * The runtime takes a {@link MongoExecutionContext} (built via `createMongoExecutionContext`), an adapter, and a driver, as the SQL runtime does. Codecs and the target come from the context. See ADR — Mongo result-shape as a structural plan field, § Codec registry: stack aggregation, not user threading. */ export interface MongoRuntimeOptions { - readonly context: MongoExecutionContext; - readonly stack: MongoExecutionStack; + readonly context: MongoExecutionContext; + readonly adapter: MongoAdapter; readonly driver: MongoDriver; readonly middleware?: readonly MongoMiddleware[]; readonly mode?: RuntimeMode; @@ -122,9 +116,8 @@ class MongoRuntimeImpl constructor(options: MongoRuntimeOptions) { const middleware = options.middleware ?? []; - const targetId = options.stack.target.targetId; for (const mw of middleware) { - checkMiddlewareCompatibility(mw, 'mongo', targetId); + checkMiddlewareCompatibility(mw, 'mongo', options.context.contract.target); } const ctx: ActivityContextFields = { @@ -150,8 +143,7 @@ class MongoRuntimeImpl super({ middleware, ctx }); this.#contextFields = ctx; - const adapterInstance = options.stack.adapter.create(options.stack); - this.#adapter = adapterInstance; + this.#adapter = options.adapter; this.#driver = options.driver; this.#codecs = options.context.codecs; } diff --git a/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts b/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts index 25568a52bd68..d84324f17b42 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts @@ -1,28 +1,21 @@ import type { PlanMeta } from '@internal/contract/types'; import type { CodecCallContext } from '@internal/framework-components/codec'; import { type MongoCodecRegistry, newMongoCodecRegistry } from '@internal/mongo-codec'; +import type { MongoContract } from '@internal/mongo-contract'; import type { MongoAdapter, MongoDriver, MongoLoweredDraft } from '@internal/mongo-lowering'; import type { MongoExecutionContext, MongoQueryPlan } from '@internal/mongo-query-ast/execution'; import { AggregateWireCommand, type AnyMongoDmlWireCommand } from '@internal/mongo-wire'; import { vi } from 'vitest'; -import type { - MongoExecutionStack, - MongoRuntimeAdapterDescriptor, - MongoRuntimeAdapterInstance, - MongoRuntimeTargetDescriptor, -} from '../src/mongo-execution-stack'; + +export const testContract = { target: 'mongo' } as unknown as MongoContract; export function makeExecution( _adapter: MongoAdapter, wireCommand: AnyMongoDmlWireCommand = new AggregateWireCommand('users', []), -): { readonly context: MongoExecutionContext; readonly stack: MongoExecutionStack } { +): { readonly context: MongoExecutionContext; readonly adapter: MongoAdapter } { const codecs: MongoCodecRegistry = newMongoCodecRegistry(); - const adapterInstance: MongoRuntimeAdapterInstance<'mongo'> = { - familyId: 'mongo', - targetId: 'mongo', - lower: vi.fn( - async () => wireCommand, - ) as unknown as MongoRuntimeAdapterInstance<'mongo'>['lower'], + const adapter: MongoAdapter = { + lower: vi.fn(async () => wireCommand) as unknown as MongoAdapter['lower'], structuralLower: vi.fn( (plan: MongoQueryPlan): MongoLoweredDraft => ({ kind: 'rawAggregate', @@ -32,33 +25,9 @@ export function makeExecution( ), resolveParams: vi.fn(async (_draft: MongoLoweredDraft, _ctx: CodecCallContext) => wireCommand), }; - const target: MongoRuntimeTargetDescriptor<'mongo'> = { - kind: 'target', - id: 'mongo', - familyId: 'mongo', - targetId: 'mongo', - version: '0.0.1', - codecs: () => newMongoCodecRegistry(), - create: () => ({ familyId: 'mongo', targetId: 'mongo' }), - }; - const adapterDescriptor: MongoRuntimeAdapterDescriptor<'mongo'> = { - kind: 'adapter', - id: 'mongo', - familyId: 'mongo', - targetId: 'mongo', - version: '0.0.1', - codecs: () => newMongoCodecRegistry(), - create: () => adapterInstance, - }; - const stack: MongoExecutionStack<'mongo'> = { - target, - adapter: adapterDescriptor, - driver: undefined, - extensions: [], - }; return { - context: Object.freeze({ contract: {}, codecs, applyMutationDefaults: () => [] }), - stack, + context: Object.freeze({ contract: testContract, codecs, applyMutationDefaults: () => [] }), + adapter, }; } diff --git a/packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts index 64efff0c3a7c..cf5fe67405a5 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts @@ -76,17 +76,34 @@ describe('MongoRuntime middleware lifecycle', () => { expect(results).toHaveLength(1); }); + it('checks middleware against the target named by the context contract', () => { + const execution = makeExecution(createMockAdapter()); + const driver = createMockDriver(); + const middlewareFor = (targetId: string): MongoMiddleware => ({ + name: `for-${targetId}`, + familyId: 'mongo', + targetId, + }); + + expect(() => + createMongoRuntime({ ...execution, driver, middleware: [middlewareFor('mongo')] }), + ).not.toThrow(); + expect(() => + createMongoRuntime({ ...execution, driver, middleware: [middlewareFor('other')] }), + ).toThrow(expect.objectContaining({ code: 'RUNTIME.MIDDLEWARE_TARGET_MISMATCH' })); + }); + it('intercepts execute statistics through intercept.onExecute only', async () => { const calls: string[] = []; const contexts: MongoMiddlewareContext[] = []; const driver = createMockDriver([{ deletedCount: 99 }]); - const { context, stack } = makeExecution( + const { context, adapter } = makeExecution( createMockAdapter(), new DeleteOneWireCommand('users', { id: 1 }), ); const runtime = createMongoRuntime({ context, - stack, + adapter, driver, middleware: [ { diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts index a837733fe39d..8b5154a76f11 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts @@ -1,17 +1,13 @@ import type { PlanMeta } from '@internal/contract/types'; import type { CodecCallContext } from '@internal/framework-components/codec'; import { type MongoCodecRegistry, newMongoCodecRegistry } from '@internal/mongo-codec'; +import type { MongoContract } from '@internal/mongo-contract'; import type { MongoAdapter, MongoDriver, MongoLoweredDraft } from '@internal/mongo-lowering'; import type { MongoExecutionContext, MongoQueryPlan } from '@internal/mongo-query-ast/execution'; import type { AnyMongoWireCommand } from '@internal/mongo-wire'; import { describe, expect, it, vi } from 'vitest'; -import type { - MongoExecutionStack, - MongoRuntimeAdapterDescriptor, - MongoRuntimeAdapterInstance, - MongoRuntimeTargetDescriptor, -} from '../src/mongo-execution-stack'; import { createMongoRuntime } from '../src/mongo-runtime'; +import { testContract } from './mongo-middleware-fixtures'; const baseMeta: PlanMeta = { target: 'mongo', @@ -57,44 +53,13 @@ function recordingAdapter(): RecordingAdapter { } function makeExecution(adapter: MongoAdapter): { - readonly context: MongoExecutionContext; - readonly stack: MongoExecutionStack; + readonly context: MongoExecutionContext; + readonly adapter: MongoAdapter; } { const codecs: MongoCodecRegistry = newMongoCodecRegistry(); - const adapterInstance: MongoRuntimeAdapterInstance<'mongo'> = { - familyId: 'mongo', - targetId: 'mongo', - lower: adapter.lower.bind(adapter), - structuralLower: adapter.structuralLower.bind(adapter), - resolveParams: adapter.resolveParams.bind(adapter), - }; - const target: MongoRuntimeTargetDescriptor<'mongo'> = { - kind: 'target', - id: 'mongo', - familyId: 'mongo', - targetId: 'mongo', - version: '0.0.1', - codecs: () => newMongoCodecRegistry(), - create: () => ({ familyId: 'mongo', targetId: 'mongo' }), - }; - const adapterDescriptor: MongoRuntimeAdapterDescriptor<'mongo'> = { - kind: 'adapter', - id: 'mongo', - familyId: 'mongo', - targetId: 'mongo', - version: '0.0.1', - codecs: () => newMongoCodecRegistry(), - create: () => adapterInstance, - }; - const stack: MongoExecutionStack<'mongo'> = { - target, - adapter: adapterDescriptor, - driver: undefined, - extensions: [], - }; return { - context: Object.freeze({ contract: {}, codecs, applyMutationDefaults: () => [] }), - stack, + context: Object.freeze({ contract: testContract, codecs, applyMutationDefaults: () => [] }), + adapter, }; } diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime.decode.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime.decode.test.ts index cab8f6296960..a84c9a1d2854 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime.decode.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime.decode.test.ts @@ -11,6 +11,7 @@ import { type MongoRuntimeAdapterInstance, } from '../src/mongo-execution-stack'; import { createMongoRuntime } from '../src/mongo-runtime'; +import { testContract } from './mongo-middleware-fixtures'; const textType = dataType('test/text', { read: (json) => readJsonString('test/text', json) }); @@ -63,8 +64,8 @@ function runtimeOver(driver: MongoDriver) { create: () => adapterInstance, }, }); - const context = createMongoExecutionContext({ contract: {}, stack }); - return createMongoRuntime({ context, stack, driver }); + const context = createMongoExecutionContext({ contract: testContract, stack }); + return createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); } function driverYielding(rows: readonly Record[]): MongoDriver { diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts index dbda05f4c75a..8dad785a0629 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts @@ -5,6 +5,7 @@ import type { ActivityFinalizedResult, } from '@internal/framework-components/runtime'; import { mongoCodec, newMongoCodecRegistry } from '@internal/mongo-codec'; +import type { MongoContract } from '@internal/mongo-contract'; import type { MongoAdapter, MongoDriver, MongoLoweredDraft } from '@internal/mongo-lowering'; import type { MongoExecutionContext, MongoQueryPlan } from '@internal/mongo-query-ast/execution'; import { @@ -13,9 +14,9 @@ import { DeleteOneWireCommand, } from '@internal/mongo-wire'; import { describe, expect, it, vi } from 'vitest'; -import type { MongoExecutionStack } from '../src/mongo-execution-stack'; import type { MongoMiddleware, MongoMiddlewareContext } from '../src/mongo-middleware'; import { createMongoRuntime } from '../src/mongo-runtime'; +import { testContract } from './mongo-middleware-fixtures'; const textType = dataType('test/text', { read: (json) => readJsonString('test/text', json) }); @@ -66,7 +67,7 @@ function codecsWithFailingDecode() { function makeExecution( wireCommand: AnyMongoDmlWireCommand, onResolveParams: () => void = () => {}, -): { readonly context: MongoExecutionContext; readonly stack: MongoExecutionStack } { +): { readonly context: MongoExecutionContext; readonly adapter: MongoAdapter } { const adapter = { familyId: 'mongo', targetId: 'mongo', @@ -84,29 +85,12 @@ function makeExecution( }), } as unknown as MongoAdapter; return { - context: { contract: {}, codecs: codecsWithFailingDecode(), applyMutationDefaults: () => [] }, - stack: { - target: { - kind: 'target', - id: 'mongo', - familyId: 'mongo', - targetId: 'mongo', - version: '0.0.1', - codecs: () => newMongoCodecRegistry(), - create: () => ({ familyId: 'mongo', targetId: 'mongo' }), - }, - adapter: { - kind: 'adapter', - id: 'mongo', - familyId: 'mongo', - targetId: 'mongo', - version: '0.0.1', - codecs: () => newMongoCodecRegistry(), - create: () => adapter, - }, - driver: undefined, - extensions: [], - } as unknown as MongoExecutionStack, + context: { + contract: testContract, + codecs: codecsWithFailingDecode(), + applyMutationDefaults: () => [], + }, + adapter, }; } diff --git a/packages/2-mongo-family/7-runtime/test/runtime-options-shape.test-d.ts b/packages/2-mongo-family/7-runtime/test/runtime-options-shape.test-d.ts index a269b63b5df2..dddd7f60f5ea 100644 --- a/packages/2-mongo-family/7-runtime/test/runtime-options-shape.test-d.ts +++ b/packages/2-mongo-family/7-runtime/test/runtime-options-shape.test-d.ts @@ -1,4 +1,6 @@ import type { MongoCodecRegistry } from '@internal/mongo-codec'; +import type { MongoContract } from '@internal/mongo-contract'; +import type { MongoAdapter } from '@internal/mongo-lowering'; import { expectTypeOf, test } from 'vitest'; import type { MongoExecutionContext, @@ -7,24 +9,22 @@ import type { } from '../src/exports/index'; import { createMongoRuntime } from '../src/mongo-runtime'; -declare const ctx: MongoExecutionContext; +declare const ctx: MongoExecutionContext; +declare const adapter: MongoAdapter; declare const stack: MongoExecutionStack; -test('the execution context carries no stack; the runtime takes it as its own option', () => { +test('the runtime takes the adapter beside the context, as the SQL runtime does', () => { expectTypeOf().not.toHaveProperty('stack'); - expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().not.toHaveProperty('driver'); }); -test('runtime options accept only context, driver, middleware, and mode', () => { - createMongoRuntime({ - context: ctx, - stack, - driver: {} as never, - }); +test('runtime options accept only context, adapter, driver, middleware, and mode', () => { + createMongoRuntime({ context: ctx, adapter, driver: {} as never }); const validOptions = { context: ctx, - stack, + adapter, driver: {} as never, middleware: [ { @@ -40,7 +40,7 @@ test('runtime options accept only context, driver, middleware, and mode', () => createMongoRuntime({ context: ctx, - stack, + adapter, driver: {} as never, // @ts-expect-error `codecs` is not a property of MongoRuntimeOptions — // codecs are aggregated via `createMongoExecutionContext` and reached @@ -50,30 +50,28 @@ test('runtime options accept only context, driver, middleware, and mode', () => createMongoRuntime({ context: ctx, - stack, + adapter, driver: {} as never, - // @ts-expect-error `adapter` is not a property of MongoRuntimeOptions — - // the adapter is reached via `stack.adapter`. - adapter: {} as never, + // @ts-expect-error `stack` is not a property of MongoRuntimeOptions — + // the client creates the adapter from its stack and passes the adapter. + stack, }); createMongoRuntime({ context: ctx, - stack, + adapter, driver: {} as never, // @ts-expect-error `targetId` is not a property of MongoRuntimeOptions — - // the target is reached via `stack.target.targetId`. + // the target is read from `context.contract.target`. targetId: 'mongo', }); createMongoRuntime({ context: ctx, - stack, + adapter, driver: {} as never, // @ts-expect-error `contract` is not a property of MongoRuntimeOptions — // the contract is reached via `context.contract`. contract: {}, }); }); - -declare const _options: MongoRuntimeOptions; diff --git a/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts b/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts index 69e2075402d3..79994ce5aeef 100644 --- a/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts +++ b/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts @@ -106,7 +106,7 @@ export default function mongo< } return createMongoRuntime({ context, - stack, + adapter: stack.adapter.create(stack), driver, ...ifDefined('mode', options.mode), ...ifDefined('middleware', options.middleware), @@ -151,9 +151,8 @@ export default function mongo< } const orm = mongoOrm({ - contract, + context, executor: { query: queryRows, execute: executeStats }, - mutationDefaults: context, enums: enumsByNamespace, }); diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts index 9bdb3ce62df9..ff161318ade2 100644 --- a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts +++ b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts @@ -59,7 +59,7 @@ export function buildMongoStaticContext< 'built from this contract domain; the mapped-type shape cannot be proven statically' >(unbound); const query = mongoQuery({ context }); - const raw = mongoRaw({ contract }); + const raw = mongoRaw({ context }); return { context, contract, enums, enumsByNamespace, query, raw, stack }; } diff --git a/packages/3-targets/mongo/9-facade/test/enum-members.test.ts b/packages/3-targets/mongo/9-facade/test/enum-members.test.ts index 2daa677a9855..ed3547a1b949 100644 --- a/packages/3-targets/mongo/9-facade/test/enum-members.test.ts +++ b/packages/3-targets/mongo/9-facade/test/enum-members.test.ts @@ -104,7 +104,7 @@ describe('db.enums member types', () => { describe('the Mongo ORM checks a written enum value against the accessors db.enums holds', () => { it('passes a member to the database and refuses a value outside the enum', async () => { const orm = mongoOrm({ - contract, + context, executor: unreachableDatabase, enums: buildMongoEnums(contract, context.codecs), }); @@ -126,12 +126,12 @@ describe('the Mongo ORM checks a written enum value against the accessors db.enu message: `mongoOrm() has no accessor for enum ${enumName} in namespace __unbound__, so it cannot check a value written to a field of that enum. Pass the contract's enum accessors: enums: buildMongoEnums(contract, context.codecs).`, meta: { argument: 'enums', namespace: '__unbound__', enum: enumName }, }); - expect(() => mongoOrm({ contract, executor: unreachableDatabase, enums: {} })).toThrow( + expect(() => mongoOrm({ context, executor: unreachableDatabase, enums: {} })).toThrow( refusalFor('TextLevel'), ); expect(() => mongoOrm({ - contract, + context, executor: unreachableDatabase, enums: { __unbound__: Object.defineProperties({}, withoutDouble) }, }), @@ -154,7 +154,7 @@ describe('the Mongo ORM checks a written enum value against the accessors db.enu ), ); expect(() => - mongoOrm({ contract, executor: unreachableDatabase, enums: { __unbound__: unread } }), + mongoOrm({ context, executor: unreachableDatabase, enums: { __unbound__: unread } }), ).not.toThrow(); }); diff --git a/packages/3-targets/mongo/9-facade/test/mongo.test.ts b/packages/3-targets/mongo/9-facade/test/mongo.test.ts index c5f1cf85e0db..63dc5d3a24f6 100644 --- a/packages/3-targets/mongo/9-facade/test/mongo.test.ts +++ b/packages/3-targets/mongo/9-facade/test/mongo.test.ts @@ -76,6 +76,8 @@ const fakeDriverFromDbClose = vi.fn().mockResolvedValue(undefined); const fakeOrm = { id: 'orm-instance' }; const fakeQuery = { id: 'query-instance' }; const fakeRaw = { id: 'raw-instance', collection: vi.fn() }; +const fakeAdapter = { id: 'adapter-instance' }; +const fakeStack = { id: 'stack-instance', adapter: { create: () => fakeAdapter } }; describe('mongo() facade', () => { beforeEach(() => { @@ -84,7 +86,7 @@ describe('mongo() facade', () => { } mocks.deserializeContract.mockReturnValue(fakeContract); - mocks.createMongoExecutionStack.mockReturnValue({ id: 'stack-instance' }); + mocks.createMongoExecutionStack.mockReturnValue(fakeStack); mocks.createMongoExecutionContext.mockReturnValue({ id: 'context-instance' }); mocks.driverFromConnection.mockResolvedValue({ id: 'driver-from-url', @@ -137,18 +139,19 @@ describe('mongo() facade', () => { expect(mocks.createMongoExecutionContext).toHaveBeenCalledTimes(1); expect(mocks.createMongoExecutionContext).toHaveBeenCalledWith({ contract: fakeContract, - stack: { id: 'stack-instance' }, + stack: fakeStack, }); expect(mocks.createMongoRuntime).toHaveBeenCalledTimes(1); expect(mocks.createMongoRuntime).toHaveBeenCalledWith( expect.objectContaining({ context: { id: 'context-instance' }, + adapter: fakeAdapter, }), ); // The user-facing options bag never sees `codecs`. const runtimeArgs = mocks.createMongoRuntime.mock.calls[0]?.[0] as Record; expect(runtimeArgs).not.toHaveProperty('codecs'); - expect(runtimeArgs).not.toHaveProperty('adapter'); + expect(runtimeArgs).not.toHaveProperty('stack'); expect(runtimeArgs).not.toHaveProperty('targetId'); }); @@ -507,12 +510,21 @@ describe('mongo() facade', () => { expect(db.context).toEqual({ id: 'context-instance' }); }); + it('builds query, raw and orm from the one execution context', () => { + mongo({ contract: fakeContract, url: 'mongodb://localhost:27017/mydb' }); + const context = { id: 'context-instance' }; + expect(mocks.mongoQuery).toHaveBeenCalledWith({ context }); + expect(mocks.mongoRaw).toHaveBeenCalledWith({ context }); + expect(mocks.mongoOrm).toHaveBeenCalledWith(expect.objectContaining({ context })); + expect(mocks.mongoOrm.mock.calls[0]?.[0]).not.toHaveProperty('contract'); + }); + it('createMongoExecutionContext is called upfront (before runtime()) with the resolved contract', () => { mongo({ contract: fakeContract, url: 'mongodb://localhost:27017/mydb' }); expect(mocks.createMongoExecutionContext).toHaveBeenCalledTimes(1); expect(mocks.createMongoExecutionContext).toHaveBeenCalledWith({ contract: fakeContract, - stack: { id: 'stack-instance' }, + stack: fakeStack, }); }); diff --git a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts index 484419d090b8..be9e8a9bfc27 100644 --- a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts +++ b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts @@ -182,7 +182,10 @@ describe('facade lifecycle structured codes', () => { fn.mockReset(); } mocks.deserializeContract.mockReturnValue(fakeContract); - mocks.createMongoExecutionStack.mockReturnValue({ id: 'stack' }); + mocks.createMongoExecutionStack.mockReturnValue({ + id: 'stack', + adapter: { create: () => ({ id: 'adapter' }) }, + }); mocks.createMongoExecutionContext.mockReturnValue({ id: 'context' }); mocks.driverFromConnection.mockResolvedValue({ id: 'driver', diff --git a/test/integration/test/_harness/mongo.ts b/test/integration/test/_harness/mongo.ts index a789f8450974..b6abf8e33bb6 100644 --- a/test/integration/test/_harness/mongo.ts +++ b/test/integration/test/_harness/mongo.ts @@ -159,13 +159,12 @@ export async function withMongoPort< }); const context = createMongoExecutionContext({ contract, stack }); const driver = await createMongoDriver(connectionUri, dbName); - const runtime = createMongoRuntime({ context, stack, driver }); + const runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); try { const db = mongoOrm({ - contract, + context, executor: runtime, - mutationDefaults: context, enums: buildMongoEnums(contract, context.codecs), }); const mongoDb = client.db(dbName); diff --git a/test/integration/test/cross-package/cross-family-middleware.test.ts b/test/integration/test/cross-package/cross-family-middleware.test.ts index 08d35dfab2e7..b8ea18678b7f 100644 --- a/test/integration/test/cross-package/cross-family-middleware.test.ts +++ b/test/integration/test/cross-package/cross-family-middleware.test.ts @@ -14,11 +14,11 @@ import { createMongoExecutionStack, createMongoRuntime, type MongoExecutionContext, - type MongoExecutionStack, type MongoRuntimeAdapterDescriptor, } from '@internal/mongo-runtime'; import mongoRuntimeTarget from '@internal/target-mongo/runtime'; import { describe, expect, it, vi } from 'vitest'; +import { fixtureContract } from '../mongo/setup'; /** * Minimal cross-family observer for tests. Records `beforeEncode` and @@ -164,14 +164,17 @@ function mockAdapterDescriptor(adapter: MongoAdapter): MongoRuntimeAdapterDescri } function makeMongoExecution(adapter: MongoAdapter): { - readonly context: MongoExecutionContext; - readonly stack: MongoExecutionStack; + readonly context: MongoExecutionContext; + readonly adapter: MongoAdapter; } { const stack = createMongoExecutionStack({ target: mongoRuntimeTarget, adapter: mockAdapterDescriptor(adapter), }); - return { context: createMongoExecutionContext({ contract: {}, stack }), stack }; + return { + context: createMongoExecutionContext({ contract: fixtureContract, stack }), + adapter: stack.adapter.create(stack), + }; } function createMockMongoDriver(rows: Record[] = []): MongoDriver { diff --git a/test/integration/test/mongo/no-emit.test.ts b/test/integration/test/mongo/no-emit.test.ts index 93c73f593727..5907d9c25e6a 100644 --- a/test/integration/test/mongo/no-emit.test.ts +++ b/test/integration/test/mongo/no-emit.test.ts @@ -3,7 +3,7 @@ import { buildMongoEnums } from '@internal/mongo-runtime'; import { ObjectId } from 'mongodb'; import { expect, expectTypeOf, it } from 'vitest'; import { contract } from './fixtures/contract'; -import { describeWithMongoDB } from './setup'; +import { describeWithMongoDB, executionContextFor } from './setup'; describeWithMongoDB('Mongo no-emit integration', (ctx) => { it('mongoOrm executes with a builder-authored contract directly', async () => { @@ -27,10 +27,11 @@ describeWithMongoDB('Mongo no-emit integration', (ctx) => { comments: [{ _id: commentId, text: 'Investigating', createdAt: new Date('2025-01-01') }], }); + const context = executionContextFor(contract); const orm = mongoOrm({ - contract, + context, executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), + enums: buildMongoEnums(contract, context.codecs), }); const tasks = await orm.tasks.include('assignee').all(); diff --git a/test/integration/test/mongo/orm.test.ts b/test/integration/test/mongo/orm.test.ts index 2306e04a4953..5c786be4141e 100644 --- a/test/integration/test/mongo/orm.test.ts +++ b/test/integration/test/mongo/orm.test.ts @@ -58,7 +58,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { ]); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -77,7 +77,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { ]); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -105,7 +105,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { }); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -130,7 +130,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { }); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -155,7 +155,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { }); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -188,7 +188,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { ]); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -216,7 +216,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { }); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -242,7 +242,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { ]); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -262,7 +262,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { ]); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -283,7 +283,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { ]); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -301,7 +301,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { ]); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); @@ -336,7 +336,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { }); const orm = mongoOrm({ - contract, + context: ctx.context, executor: ctx.runtime, enums: buildMongoEnums(contract, ctx.codecs), }); diff --git a/test/integration/test/mongo/runtime/content-hash-guard.test.ts b/test/integration/test/mongo/runtime/content-hash-guard.test.ts index 41760f046b02..40888e219ed2 100644 --- a/test/integration/test/mongo/runtime/content-hash-guard.test.ts +++ b/test/integration/test/mongo/runtime/content-hash-guard.test.ts @@ -13,6 +13,7 @@ import { MongoParamRef } from '@internal/mongo-value'; import type { AnyMongoWireCommand } from '@internal/mongo-wire'; import mongoRuntimeTarget from '@internal/target-mongo/runtime'; import { describe, expect, it, vi } from 'vitest'; +import { fixtureContract } from '../setup'; const baseMeta: PlanMeta = { target: 'mongo', @@ -34,7 +35,7 @@ describe('Mongo runtime content hash before param resolution', () => { target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - const context = createMongoExecutionContext({ contract: {}, stack }); + const context = createMongoExecutionContext({ contract: fixtureContract, stack }); const driver = { execute: vi.fn(async function* (_command: AnyMongoWireCommand) { yield { insertedId: 'x' }; @@ -43,7 +44,7 @@ describe('Mongo runtime content hash before param resolution', () => { } as unknown as MongoDriver; const runtime = createMongoRuntime({ context, - stack, + adapter: stack.adapter.create(stack), driver, middleware: [middleware], }); diff --git a/test/integration/test/mongo/runtime/execute-param-mutator-wiring.test.ts b/test/integration/test/mongo/runtime/execute-param-mutator-wiring.test.ts index ec53fac07e70..b6b154f310e0 100644 --- a/test/integration/test/mongo/runtime/execute-param-mutator-wiring.test.ts +++ b/test/integration/test/mongo/runtime/execute-param-mutator-wiring.test.ts @@ -1,7 +1,7 @@ import mongoRuntimeAdapter from '@internal/adapter-mongo/runtime'; import type { PlanMeta } from '@internal/contract/types'; import type { CodecCallContext } from '@internal/framework-components/codec'; -import type { MongoDriver, MongoLoweredDraft } from '@internal/mongo-lowering'; +import type { MongoAdapter, MongoDriver, MongoLoweredDraft } from '@internal/mongo-lowering'; import { InsertOneCommand } from '@internal/mongo-query-ast/execution'; import type { MongoMiddleware } from '@internal/mongo-runtime'; import { @@ -9,13 +9,13 @@ import { createMongoExecutionStack, createMongoRuntime, type MongoExecutionContext, - type MongoExecutionStack, } from '@internal/mongo-runtime'; import { MongoParamRef } from '@internal/mongo-value'; import type { AnyMongoWireCommand } from '@internal/mongo-wire'; import { InsertOneWireCommand } from '@internal/mongo-wire'; import mongoRuntimeTarget from '@internal/target-mongo/runtime'; import { describe, expect, it, vi } from 'vitest'; +import { fixtureContract } from '../setup'; const BULK_CODEC_ID = 'test/bulk-transform'; @@ -27,14 +27,17 @@ const baseMeta: PlanMeta = { }; function buildExecution(): { - readonly context: MongoExecutionContext; - readonly stack: MongoExecutionStack; + readonly context: MongoExecutionContext; + readonly adapter: MongoAdapter; } { const stack = createMongoExecutionStack({ target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - return { context: createMongoExecutionContext({ contract: {}, stack }), stack }; + return { + context: createMongoExecutionContext({ contract: fixtureContract, stack }), + adapter: stack.adapter.create(stack), + }; } function recordingDriver(): { @@ -140,8 +143,9 @@ describe('MongoRuntime execute param-mutator wiring', () => { let structuralDraft: MongoLoweredDraft | undefined; const resolveDraftRefs: MongoLoweredDraft[] = []; - const { stack } = buildExecution(); - const adapterInstance = stack.adapter.create(stack); + const adapterInstance = mongoRuntimeAdapter.create( + createMongoExecutionStack({ target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter }), + ); const structuralLowerSpy = vi.fn((plan) => { structuralDraft = adapterInstance.structuralLower(plan); return structuralDraft; @@ -162,10 +166,17 @@ describe('MongoRuntime execute param-mutator wiring', () => { }), }, }); - const patchedContext = createMongoExecutionContext({ contract: {}, stack: patchedStack }); + const patchedContext = createMongoExecutionContext({ + contract: fixtureContract, + stack: patchedStack, + }); const { driver } = recordingDriver(); - const runtime = createMongoRuntime({ context: patchedContext, stack: patchedStack, driver }); + const runtime = createMongoRuntime({ + context: patchedContext, + adapter: patchedStack.adapter.create(patchedStack), + driver, + }); await runtime .query({ diff --git a/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts b/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts index 26a91a839f4d..47c0d0fe424b 100644 --- a/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts +++ b/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts @@ -30,14 +30,14 @@ const STANDARD_CODEC_IDS = [ ]; describe('createMongoExecutionStack', () => { - it('builds a stack from target + adapter (no extensions, no driver)', () => { + it('builds a stack from target + adapter (no extensions)', () => { const stack = createMongoExecutionStack({ target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); expect(stack.target).toBe(mongoRuntimeTarget); expect(stack.adapter).toBe(mongoRuntimeAdapter); - expect(stack.driver).toBeUndefined(); + expect(stack).not.toHaveProperty('driver'); expect(stack.extensions).toEqual([]); }); diff --git a/test/integration/test/mongo/runtime/setup.ts b/test/integration/test/mongo/runtime/setup.ts index 5ff3c9c39306..996b8ed33f1b 100644 --- a/test/integration/test/mongo/runtime/setup.ts +++ b/test/integration/test/mongo/runtime/setup.ts @@ -12,6 +12,7 @@ import mongoRuntimeTarget from '@internal/target-mongo/runtime'; import { timeouts } from '@repo/test-utils'; import { MongoClient } from 'mongodb'; import { MongoMemoryReplSet } from 'mongodb-memory-server'; +import { fixtureContract } from '../setup'; export interface MongodContext { readonly connectionUri: string; @@ -52,9 +53,9 @@ export async function withMongod(fn: (ctx: MongodContext) => Promise): Pro target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - const context = createMongoExecutionContext({ contract: {}, stack }); + const context = createMongoExecutionContext({ contract: fixtureContract, stack }); const driver = await createMongoDriver(connectionUri, dbName); - const runtime = createMongoRuntime({ context, stack, driver }); + const runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); // Cast back to `MongoCodecRegistry` — the test setup is the one place that // legitimately mutates the per-execution registry mid-flight (to install diff --git a/test/integration/test/mongo/setup.ts b/test/integration/test/mongo/setup.ts index 5ccddc65d349..cbd9fb6deaf2 100644 --- a/test/integration/test/mongo/setup.ts +++ b/test/integration/test/mongo/setup.ts @@ -1,10 +1,12 @@ import mongoRuntimeAdapter from '@internal/adapter-mongo/runtime'; import { createMongoDriver } from '@internal/driver-mongo'; +import { MongoContractSerializer } from '@internal/family-mongo/ir'; import { createMongoExecutionContext, createMongoExecutionStack, createMongoRuntime, type MongoCodecLookup, + type MongoExecutionContext, type MongoRuntime, } from '@internal/mongo-runtime'; import mongoRuntimeTarget from '@internal/target-mongo/runtime'; @@ -12,6 +14,22 @@ import { timeouts } from '@repo/test-utils'; import { MongoClient } from 'mongodb'; import { MongoMemoryReplSet } from 'mongodb-memory-server'; import { afterAll, beforeAll, beforeEach, describe } from 'vitest'; +import type { Contract } from './fixtures/generated/contract'; +import contractJson from './fixtures/generated/contract.json'; + +export const fixtureContract = new MongoContractSerializer().deserializeContract( + contractJson, +) as Contract; + +export function executionContextFor( + contract: TContract, +): MongoExecutionContext { + const stack = createMongoExecutionStack({ + target: mongoRuntimeTarget, + adapter: mongoRuntimeAdapter, + }); + return createMongoExecutionContext({ contract, stack }); +} export async function withMongod(fn: (ctx: MongodContext) => Promise): Promise { let replSet: MongoMemoryReplSet | undefined; @@ -34,11 +52,18 @@ export async function withMongod(fn: (ctx: MongodContext) => Promise): Pro target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - const context = createMongoExecutionContext({ contract: {}, stack }); + const context = createMongoExecutionContext({ contract: fixtureContract, stack }); const driver = await createMongoDriver(connectionUri, dbName); - runtime = createMongoRuntime({ context, stack, driver }); + runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); - const ctx: MongodContext = { connectionUri, dbName, client, runtime, codecs: context.codecs }; + const ctx: MongodContext = { + connectionUri, + dbName, + client, + runtime, + context, + codecs: context.codecs, + }; return await fn(ctx); } finally { await runtime?.close(); @@ -52,6 +77,8 @@ export interface MongodContext { readonly dbName: string; readonly client: MongoClient; readonly runtime: MongoRuntime; + /** The execution context over the fixture contract, which `mongoOrm()` and `mongoRaw()` take. */ + readonly context: MongoExecutionContext; /** The execution context's codecs, which `buildMongoEnums()` reads enum members through. */ readonly codecs: MongoCodecLookup; } @@ -61,7 +88,7 @@ export function describeWithMongoDB(name: string, fn: (ctx: MongodContext) => vo let replSet: MongoMemoryReplSet; let client: MongoClient; let runtime: MongoRuntime; - let codecs: MongoCodecLookup; + let context: MongoExecutionContext; const dbName = 'test'; const ctx: MongodContext = { @@ -75,8 +102,11 @@ export function describeWithMongoDB(name: string, fn: (ctx: MongodContext) => vo get runtime() { return runtime; }, + get context() { + return context; + }, get codecs() { - return codecs; + return context.codecs; }, }; @@ -94,10 +124,9 @@ export function describeWithMongoDB(name: string, fn: (ctx: MongodContext) => vo target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - const context = createMongoExecutionContext({ contract: {}, stack }); + context = createMongoExecutionContext({ contract: fixtureContract, stack }); const driver = await createMongoDriver(replSet.getUri(), dbName); - runtime = createMongoRuntime({ context, stack, driver }); - codecs = context.codecs; + runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); }, timeouts.spinUpMongoMemoryServer); beforeEach(async () => { diff --git a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts index 4c094a3f1805..fbdc08656d60 100644 --- a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts +++ b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts @@ -48,13 +48,45 @@ describe.each(['app', 'extension'] as const)('the %s mongoQuery detection', (aud }); }); -describe.each(['app', 'extension'] as const)('the %s runtime stack detection', (audience) => { - const change = 'mongo-runtime-takes-stack'; +describe.each(['app', 'extension'] as const)('the %s ORM and raw detection', (audience) => { + const change = 'mongo-orm-takes-context'; + + it.each([ + ['an ORM built from a contract', 'mongoOrm({ contract, executor, enums })'], + ['an ORM with a type argument', 'mongoOrm({\n contract,\n executor,\n})'], + ['a raw client built from a contract', 'const raw = mongoRaw({ contract });'], + ['the mutationDefaults option', 'mongoOrm({ context, executor, mutationDefaults: context })'], + ['the removed error code', "if (error.code === 'ORM.MUTATION_DEFAULTS_MISSING') {}"], + ])('finds %s', (_name, content) => { + expect(detects(audience, content, change)).toBe(true); + }); + + it.each([ + ['an ORM built from a context', 'mongoOrm({ context, executor, enums })'], + ['a raw client built from a context', 'mongoRaw({ context })'], + ['the static raw client', 'const { raw } = mongoStatic({ contract });'], + ['a method of the same name', 'db.mongoOrm({ contract })'], + ])('skips %s', (_name, content) => { + expect(detects(audience, content, change)).toBe(false); + }); +}); + +describe.each(['app', 'extension'] as const)('the %s runtime adapter detection', (audience) => { + const change = 'mongo-runtime-takes-adapter'; it.each([ ['a runtime built from a context', 'const runtime = createMongoRuntime({ context, driver });'], ['the stack read from a context', 'const adapter = context.stack.adapter;'], ['the stack read from a named context', 'mongoContext.stack.target.targetId'], + ['a context type with a target id', "type Ctx = MongoExecutionContext;"], + [ + 'a context type with a generic contract and a target id', + 'MongoExecutionContext, TTargetId>', + ], + [ + 'a stack built with a driver', + 'createMongoExecutionStack({\n target,\n adapter,\n driver: mongoDriver,\n})', + ], ])('finds %s', (_name, content) => { expect(detects(audience, content, change)).toBe(true); }); @@ -63,6 +95,9 @@ describe.each(['app', 'extension'] as const)('the %s runtime stack detection', ( ['a stack read from a stack', 'stack.adapter.create(stack)'], ['a method of the same name', 'factory.createMongoRuntime({ context })'], ['a stack on another object', 'options.stack.adapter'], + ['a context type over one contract', 'MongoExecutionContext'], + ['a context type over a generic contract', 'MongoExecutionContext>'], + ['a stack built without a driver', 'createMongoExecutionStack({ target, adapter })'], ])('skips %s', (_name, content) => { expect(detects(audience, content, change)).toBe(false); }); diff --git a/test/integration/test/value-objects/value-objects.e2e.test.ts b/test/integration/test/value-objects/value-objects.e2e.test.ts index c29bd5f71ef1..5291ff8f41da 100644 --- a/test/integration/test/value-objects/value-objects.e2e.test.ts +++ b/test/integration/test/value-objects/value-objects.e2e.test.ts @@ -11,7 +11,7 @@ import postgresTarget from '@internal/target-postgres/runtime'; import { timeouts, withClient, withDevDatabase } from '@repo/test-utils'; import { describe, expect, expectTypeOf, it } from 'vitest'; import { TestSqlContractSerializer as SqlContractSerializer } from '../../../../packages/2-sql-family/9-family/test/test-sql-contract-serializer'; -import { describeWithMongoDB } from '../mongo/setup'; +import { describeWithMongoDB, executionContextFor } from '../mongo/setup'; import { setupTestDatabase } from '../utils'; import type { Contract as MongoVOContract } from './fixtures/generated/mongo-contract.d'; import mongoContractJson from './fixtures/generated/mongo-contract.json'; @@ -24,10 +24,11 @@ describeWithMongoDB('value objects e2e: Mongo → real DB → typed ORM', (ctx) ) as unknown as MongoVOContract; it('create and read value objects with correct types', async () => { + const context = executionContextFor(contract); const ormClient = mongoOrm({ - contract, + context, executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), + enums: buildMongoEnums(contract, context.codecs), }); const shopCollection = ormClient['shop']!; @@ -79,10 +80,11 @@ describeWithMongoDB('value objects e2e: Mongo → real DB → typed ORM', (ctx) }); it('non-null value object field roundtrips through update', async () => { + const context = executionContextFor(contract); const ormClient = mongoOrm({ - contract, + context, executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), + enums: buildMongoEnums(contract, context.codecs), }); const shopCollection = ormClient['shop']!; diff --git a/test/integration/test/value-objects/value-objects.integration.test.ts b/test/integration/test/value-objects/value-objects.integration.test.ts index 798e0b23ad56..e08dbef9bcdc 100644 --- a/test/integration/test/value-objects/value-objects.integration.test.ts +++ b/test/integration/test/value-objects/value-objects.integration.test.ts @@ -32,7 +32,7 @@ import { createPostgresBuiltinCodecLookup } from '@internal/target-postgres/code import { postgresDataTypes } from '@internal/target-postgres/data-types'; import { postgresCreateNamespace } from '@internal/target-postgres/types'; import { describe, expect, it } from 'vitest'; -import { describeWithMongoDB } from '../mongo/setup'; +import { describeWithMongoDB, executionContextFor } from '../mongo/setup'; const mongoPsl = ` model User { @@ -213,10 +213,11 @@ describeWithMongoDB('value objects: end-to-end Mongo', (ctx) => { const validated = { contract: new MongoContractSerializer().deserializeContract(contract) }; + const context = executionContextFor(validated.contract); const orm = mongoOrm({ - contract: validated.contract, + context, executor: ctx.runtime, - enums: buildMongoEnums(validated.contract, ctx.codecs), + enums: buildMongoEnums(validated.contract, context.codecs), }); const userCollection = orm['User']!; @@ -248,10 +249,11 @@ describeWithMongoDB('value objects: end-to-end Mongo', (ctx) => { if (!result.ok) throw new Error(`Interpretation failed: ${result.failure.summary}`); const validated = { contract: new MongoContractSerializer().deserializeContract(result.value) }; + const context = executionContextFor(validated.contract); const orm = mongoOrm({ - contract: validated.contract, + context, executor: ctx.runtime, - enums: buildMongoEnums(validated.contract, ctx.codecs), + enums: buildMongoEnums(validated.contract, context.codecs), }); const userCollection = orm['User']!; @@ -291,10 +293,11 @@ type Address { if (!result.ok) throw new Error(`Interpretation failed: ${result.failure.summary}`); const validated = { contract: new MongoContractSerializer().deserializeContract(result.value) }; + const context = executionContextFor(validated.contract); const orm = mongoOrm({ - contract: validated.contract, + context, executor: ctx.runtime, - enums: buildMongoEnums(validated.contract, ctx.codecs), + enums: buildMongoEnums(validated.contract, context.codecs), }); const userCollection = orm['User']!; diff --git a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md index fb97a97d041d..898c7751947a 100644 --- a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md +++ b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md @@ -9,38 +9,66 @@ changes: - '(?()]*>)?\s*\(\s*\{\s*contract\b' - '(?()]*>)?\s*\(\s*\{\s*contract\b' + - '(?`. `MongoExecutionStack` no longer has a `driver` member, and `createMongoExecutionStack()` no longer takes a `driver` option. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments, or pass a driver to `createMongoExecutionStack`. detection: glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" matches: - '(?]|<(?:[^<>]|<[^<>]*>)*>)*,' + - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\bdriver\b' --- -# mongoQuery takes an execution context, and the runtime takes the stack +# The MongoDB query surfaces and runtime take an execution context -`mongoQuery` now takes `{ context }`, a MongoDB execution context, instead of `{ contract }`, as the SQL builder `sql({ context })` does. The context comes from `mongoStatic({ contract })` or `mongo({ contract })`, which validate the contract once. +`mongoQuery`, `mongoRaw` and `mongoOrm` take `{ context }`, a MongoDB execution context, instead of `{ contract }`, as `sql({ context })` and the SQL ORM do. The context comes from `mongoStatic({ contract })` or `mongo({ contract })`, which validate the contract once. ```ts // Before import { mongoQuery } from '@prisma/orm-mongo/query-builder'; const query = mongoQuery({ contract }); +const raw = mongoRaw({ contract }); // After import mongoStatic from '@prisma/orm-mongo/static'; -const { query } = mongoStatic({ contract }); +const { query, raw } = mongoStatic({ contract }); // Or, with a client const db = mongo({ contract, url }); db.query.from('orders'); ``` -The runtime receives the stack beside the context: +Code that builds the ORM itself passes the context, which also fills generated fields: ```ts +// Before +const orm = mongoOrm({ contract, executor, mutationDefaults: context, enums }); + +// After +const orm = mongoOrm({ context, executor, enums }); +``` + +The runtime takes an adapter beside the context, as the SQL runtime does, and reads the target from the context's contract: + +```ts +// Before +const runtime = createMongoRuntime({ context, stack, driver }); + +// After const stack = createMongoExecutionStack({ target, adapter }); const context = createMongoExecutionContext({ contract, stack }); -const runtime = createMongoRuntime({ context, stack, driver }); +const runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); ``` + +`MongoExecutionContext` takes one type parameter, the contract: `MongoExecutionContext`. diff --git a/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md b/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md index 538790b63f87..81af5e59424b 100644 --- a/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md +++ b/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md @@ -9,38 +9,66 @@ changes: - '(?()]*>)?\s*\(\s*\{\s*contract\b' - '(?()]*>)?\s*\(\s*\{\s*contract\b' + - '(?`. `MongoExecutionContext` is declared in `@prisma/orm-family-mongo/query-ast/execution` (`@internal/mongo-query-ast/execution`), still exported from the runtime entry; it has no `stack` member and no second type parameter (`TTargetId`). `MongoExecutionStack` has no `driver` member, and `createMongoExecutionStack()` takes no `driver` option. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments, or pass a driver to `createMongoExecutionStack`. detection: glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" matches: - '(?]|<(?:[^<>]|<[^<>]*>)*>)*,' + - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\bdriver\b' --- -# mongoQuery takes an execution context, and the runtime takes the stack +# The MongoDB query surfaces and runtime take an execution context -`mongoQuery` now takes `{ context }`, a MongoDB execution context, instead of `{ contract }`, as the SQL builder `sql({ context })` does. The context comes from `mongoStatic({ contract })` or `mongo({ contract })`, which validate the contract once. +`mongoQuery`, `mongoRaw` and `mongoOrm` take `{ context }`, a MongoDB execution context, instead of `{ contract }`, as `sql({ context })` and the SQL ORM do. The context comes from `mongoStatic({ contract })` or `mongo({ contract })`, which validate the contract once. ```ts // Before import { mongoQuery } from '@prisma/orm-mongo/query-builder'; const query = mongoQuery({ contract }); +const raw = mongoRaw({ contract }); // After import mongoStatic from '@prisma/orm-mongo/static'; -const { query } = mongoStatic({ contract }); +const { query, raw } = mongoStatic({ contract }); // Or, with a client const db = mongo({ contract, url }); db.query.from('orders'); ``` -The runtime receives the stack beside the context: +Code that builds the ORM itself passes the context, which also fills generated fields: ```ts +// Before +const orm = mongoOrm({ contract, executor, mutationDefaults: context, enums }); + +// After +const orm = mongoOrm({ context, executor, enums }); +``` + +The runtime takes an adapter beside the context, as the SQL runtime does, and reads the target from the context's contract: + +```ts +// Before +const runtime = createMongoRuntime({ context, stack, driver }); + +// After const stack = createMongoExecutionStack({ target, adapter }); const context = createMongoExecutionContext({ contract, stack }); -const runtime = createMongoRuntime({ context, stack, driver }); +const runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); ``` + +`MongoExecutionContext` takes one type parameter, the contract: `MongoExecutionContext`. From 6acd2917dae74ab1f492f93212b8e4d12fea23dc Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 16:04:22 +0100 Subject: [PATCH 16/19] TML-3686: mongoOrm takes a context and a runtime, as the SQL orm() does mongoOrm({ context, runtime }) replaces mongoOrm({ context, executor, enums }); the ORM builds enum accessors from the context. createMongoCollection takes ({ context, runtime }, modelName), like a SQL Collection, so a collection always fills generated fields. MongoExecutionContext constrains its contract to a MongoContract, as SQL ExecutionContext does; buildMongoEnums moves beside it in query-ast. A test shows mongoStatic and mongo reject a malformed contract with the same code and message. Signed-off-by: willbot Signed-off-by: Will Madden --- ...R 175 - Shared ORM Collection interface.md | 2 +- ...erators are a framework runtime concern.md | 2 +- .../subsystems/10. MongoDB Family.md | 4 +- docs/reference/error-reference.md | 2 +- examples/mongo-demo/README.md | 2 +- .../mongo-demo/test/cache-middleware.test.ts | 7 +- .../4-query/query-ast/README.md | 1 + .../query-ast/src/execution-context.ts | 6 +- .../query-ast/src/exports/execution.ts | 2 + .../query-ast}/src/mongo-enums.ts | 2 +- .../query-ast}/test/mongo-enums.test.ts | 2 +- .../5-query-builders/orm/README.md | 2 +- .../5-query-builders/orm/src/collection.ts | 66 +++--- .../5-query-builders/orm/src/exports/index.ts | 2 +- .../5-query-builders/orm/src/mongo-orm.ts | 32 +-- .../orm/test/collection-context.ts | 29 +++ .../test/collection.mutation-defaults.test.ts | 11 +- .../orm/test/collection.param-refs.test.ts | 20 +- .../orm/test/collection.stored-names.test.ts | 31 +-- .../orm/test/collection.test.ts | 222 ++++++++++-------- .../test/collection.variant-fields.test.ts | 9 +- .../test/integration/orm-ergonomics.test.ts | 42 ++-- .../orm/test/integration/polymorphism.test.ts | 20 +- .../orm/test/mongo-orm-options.test-d.ts | 38 ++- .../5-query-builders/orm/test/no-enums.ts | 4 - .../test/fixtures/test-context.ts | 5 +- packages/2-mongo-family/7-runtime/README.md | 2 +- .../7-runtime/src/exports/index.ts | 9 +- .../7-runtime/src/mongo-execution-stack.ts | 14 +- .../7-runtime/src/mongo-runtime.ts | 3 +- .../7-runtime/test/execution-context.test.ts | 19 +- .../mongo/9-facade/src/runtime/mongo.ts | 6 +- .../mongo/9-facade/src/static/mongo-static.ts | 9 +- .../test/collection-mutation-defaults.test.ts | 58 +++++ .../9-facade/test/contract-from-json.test.ts | 16 ++ .../mongo/9-facade/test/enum-members.test.ts | 60 +---- .../mongo/9-facade/test/mongo.test.ts | 2 +- test/integration/test/_harness/mongo.ts | 7 +- test/integration/test/mongo/no-emit.test.ts | 7 +- test/integration/test/mongo/orm.test.ts | 73 +----- .../runtime/mongo-execution-stack.test.ts | 19 +- .../mutation-default-generators.test.ts | 11 +- test/integration/test/mongo/setup.ts | 3 +- .../temporal-generators.e2e.test.ts | 2 +- .../detection.test.ts | 17 +- .../value-objects/value-objects.e2e.test.ts | 13 +- .../value-objects.integration.test.ts | 19 +- .../app/instructions.md | 16 +- .../extension/instructions.md | 16 +- 49 files changed, 483 insertions(+), 483 deletions(-) rename packages/2-mongo-family/{7-runtime => 4-query/query-ast}/src/mongo-enums.ts (94%) rename packages/2-mongo-family/{7-runtime => 4-query/query-ast}/test/mongo-enums.test.ts (96%) create mode 100644 packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts delete mode 100644 packages/2-mongo-family/5-query-builders/orm/test/no-enums.ts create mode 100644 packages/3-targets/mongo/9-facade/test/collection-mutation-defaults.test.ts diff --git a/docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.md b/docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.md index 50ea879e770c..e7b9c4e37050 100644 --- a/docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.md +++ b/docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.md @@ -32,7 +32,7 @@ const user = await db.User.byEmail('alice@example.com').first(); **Mongo ORM (target — not yet implemented):** ```typescript -const db = mongoOrm({ context, executor: runtime, enums }); +const db = mongoOrm({ context, runtime }); // Same custom collection pattern class UserCollection extends Collection { diff --git a/docs/architecture docs/adrs/ADR 256 - Mutation-default generators are a framework runtime concern.md b/docs/architecture docs/adrs/ADR 256 - Mutation-default generators are a framework runtime concern.md index 12acd036bf62..0831cef705eb 100644 --- a/docs/architecture docs/adrs/ADR 256 - Mutation-default generators are a framework runtime concern.md +++ b/docs/architecture docs/adrs/ADR 256 - Mutation-default generators are a framework runtime concern.md @@ -112,7 +112,7 @@ For each default of `(namespace, entry)` with a phase for `op` (`onCreate` for ` - **A field is applied at most once per call**, even when two defaults name it. - **`namespace` is required.** Refs are namespace-scoped, so only defaults declared for `(namespace, entry)` apply; that is what tells two same-named tables in different schemas apart. Requiring it keeps the coordinate part of every match: a missing namespace is a caller bug, not a quiet fallback to matching by entry name alone. -`MutationDefaults` is the interface an ORM depends on: `applyMutationDefaults(options): ReadonlyArray`. The SQL lane `ExecutionContext` and `MongoExecutionContext` extend it, and the Mongo ORM reads it from the context it takes, `mongoOrm({ context, executor, enums })`, as the SQL ORM does. +`MutationDefaults` is the interface an ORM depends on: `applyMutationDefaults(options): ReadonlyArray`. The SQL lane `ExecutionContext` and `MongoExecutionContext` extend it, and the Mongo ORM reads it from the context it takes, `mongoOrm({ context, runtime })`, as the SQL ORM reads it from `orm({ context, runtime })`. ## The execution section diff --git a/docs/architecture docs/subsystems/10. MongoDB Family.md b/docs/architecture docs/subsystems/10. MongoDB Family.md index 37bdc2cdb971..5c38adc398c9 100644 --- a/docs/architecture docs/subsystems/10. MongoDB Family.md +++ b/docs/architecture docs/subsystems/10. MongoDB Family.md @@ -290,7 +290,7 @@ The contract records each generated field in an optional `execution` section, wi Because a ref names a collection and a field, a default applies to every document in the collection. On a polymorphic model a generated field therefore belongs on the base model, and every variant create fills it; a generator attribute or generating type alias on a variant's own field is rejected (`PSL_GENERATOR_ON_VARIANT_FIELD` in PSL, `CONTRACT.DEFAULT_INVALID` with `reason: 'executionDefaults-on-variant'` in TS). Two models stored in one collection that declare the same field with identical defaults share one ref; different defaults are rejected (`PSL_GENERATOR_CONFLICT`, or `reason: 'executionDefaults-conflict'`). Such a collection's validator accepts a document of either model (see [Schema validation](#schema-validation-jsonschema)). -At runtime, `createMongoExecutionContext` collects `mutationDefaultGenerators` from the target, adapter and extensions (the adapter registers `timestampNow`) and throws `RUNTIME.MUTATION_DEFAULT_GENERATOR_MISSING` if the contract needs a generator no component provides; the registry, the check, and the apply loop are the framework's shared mutation-default runtime ([Runtime & Middleware Framework](4.%20Runtime%20%26%20Middleware%20Framework.md#mutation-defaults), [ADR 256](../adrs/ADR%20256%20-%20Mutation-default%20generators%20are%20a%20framework%20runtime%20concern.md)). The context implements the framework `MutationDefaults` interface, which the ORM depends on without depending on the Mongo runtime, and `mongoOrm({ context, executor, enums })` calls it on every write, with the same rules as SQL: +At runtime, `createMongoExecutionContext` collects `mutationDefaultGenerators` from the target, adapter and extensions (the adapter registers `timestampNow`) and throws `RUNTIME.MUTATION_DEFAULT_GENERATOR_MISSING` if the contract needs a generator no component provides; the registry, the check, and the apply loop are the framework's shared mutation-default runtime ([Runtime & Middleware Framework](4.%20Runtime%20%26%20Middleware%20Framework.md#mutation-defaults), [ADR 256](../adrs/ADR%20256%20-%20Mutation-default%20generators%20are%20a%20framework%20runtime%20concern.md)). The context implements the framework `MutationDefaults` interface, which the ORM depends on without depending on the Mongo runtime, and `mongoOrm({ context, runtime })` calls it on every write, with the same rules as SQL: - `create`, `createAll`, `createAndCount` and the insert half of `upsert` fill `onCreate` fields; `update`, `updateAll`, `updateAndCount` and the update half of `upsert` add `onUpdate` fields to `$set`. - A field the write sets explicitly keeps its value. The ORM drops `undefined` values before it applies defaults, so a field set to `undefined` gets its default. An update whose payload touches no field gets no generated values. @@ -318,7 +318,7 @@ db.posts.updateMany( The ORM presents the same chaining API to consumers regardless of database family. A developer querying MongoDB uses the same `where()`, `select()`, `include()`, `orderBy()`, `limit()`, and `all()` methods as one querying Postgres: ```typescript -const db = mongoOrm({ context, executor: runtime, enums: buildMongoEnums(contract, context.codecs) }); +const db = mongoOrm({ context, runtime }); class UserCollection extends Collection { admins() { return this.where({ kind: 'admin' }); } diff --git a/docs/reference/error-reference.md b/docs/reference/error-reference.md index 1c6b68ca8e27..25b3e5463b3c 100644 --- a/docs/reference/error-reference.md +++ b/docs/reference/error-reference.md @@ -1026,7 +1026,7 @@ An aggregate was invoked for an operation/input pair the composed target declare ### ORM.ARGUMENT_INVALID -A method argument on the ORM client, or on the `sql()` / Mongo query-builder DSLs, is malformed or missing a required part: a `null` where-arg, `upsert()` without conflict columns or without a create value for a conflict column, a custom collection given as an instance / for a nonexistent model in a client's `collections` option or `orm({ collections })`, invalid builder argument shapes, `$and`/`$or` with no expressions, a limit, offset or skip that is negative or not an integer, malformed lookup/group/update specs, a row-locking method (`forUpdate()`, `forNoKeyUpdate()`, `forShare()`, `forKeyShare()`) given both `nowait` and `skipLocked`, or a `variant()` call whose value is not a declared discriminator value of the receiver model, or on a model with no discriminator (SQL and Mongo ORMs; the payload adds `value` and `declaredValues`). The Mongo ORM also raises it when `mongoOrm()` is given `enums` without an accessor for an enum the contract declares (`argument: 'enums'`, `namespace`, `enum`), and when a write through `createMongoCollection()` finds no accessor for the field's enum (`argument: 'enums'`, `enum`); pass `buildMongoEnums(contract, context.codecs)`. For SQL, the limit/offset check runs in relational-core when the `SelectAst` is constructed, so every SQL lane and target raises it before any SQL is rendered. That check also refuses integers above `Number.MAX_SAFE_INTEGER`, and does not check a limit or offset bound as a parameter. Payload: `method`, `argument` (`limit` or `offset` for the SQL limit/offset check), `model`, `column`, `key`. The `sql()` builder also raises it when a table's `indexes` is read by a name that more than one of its indexes shares (payload: `namespaceId`, `tableName`, `index`). It also raises it when a query is built with a column of an alias that is not one of the query's sources, such as one from an index read from `table.as('other').indexes` (payload: `alias`, `column`, `sources`). +A method argument on the ORM client, or on the `sql()` / Mongo query-builder DSLs, is malformed or missing a required part: a `null` where-arg, `upsert()` without conflict columns or without a create value for a conflict column, a custom collection given as an instance / for a nonexistent model in a client's `collections` option or `orm({ collections })`, invalid builder argument shapes, `$and`/`$or` with no expressions, a limit, offset or skip that is negative or not an integer, malformed lookup/group/update specs, a row-locking method (`forUpdate()`, `forNoKeyUpdate()`, `forShare()`, `forKeyShare()`) given both `nowait` and `skipLocked`, or a `variant()` call whose value is not a declared discriminator value of the receiver model, or on a model with no discriminator (SQL and Mongo ORMs; the payload adds `value` and `declaredValues`). For SQL, the limit/offset check runs in relational-core when the `SelectAst` is constructed, so every SQL lane and target raises it before any SQL is rendered. That check also refuses integers above `Number.MAX_SAFE_INTEGER`, and does not check a limit or offset bound as a parameter. Payload: `method`, `argument` (`limit` or `offset` for the SQL limit/offset check), `model`, `column`, `key`. The `sql()` builder also raises it when a table's `indexes` is read by a name that more than one of its indexes shares (payload: `namespaceId`, `tableName`, `index`). It also raises it when a query is built with a column of an alias that is not one of the query's sources, such as one from an index read from `table.as('other').indexes` (payload: `alias`, `column`, `sources`). ### ORM.CAPABILITY_MISSING diff --git a/examples/mongo-demo/README.md b/examples/mongo-demo/README.md index 42797ef4dab5..6a3019c32186 100644 --- a/examples/mongo-demo/README.md +++ b/examples/mongo-demo/README.md @@ -107,7 +107,7 @@ The Mongo query builder doesn't yet expose a chainable `.annotate(...)` surface 1. Builds the execution stack from the Mongo target, adapter and driver, and the runtime that executes queries (with the cache middleware) 2. Exposes the ORM with typed collection accessors (`db.orm.users`, `db.orm.posts`), the query builder (`db.query`) and the enums (`db.enums`) -`mongo()` also fills generated fields such as `temporal.CreatedAt` on write. If you build the ORM yourself with `mongoOrm()` rather than through `mongo()`, pass the execution context and the contract's enum accessors: `mongoOrm({ context, executor, enums: buildMongoEnums(contract, context.codecs) })`, with `buildMongoEnums` from `@prisma/orm-mongo/family-runtime`. The ORM fills generated fields such as `temporal.CreatedAt` through the context, and checks a written enum value against the accessors, which are the ones `db.enums` holds. +`mongo()` also fills generated fields such as `temporal.CreatedAt` on write. If you build the ORM yourself with `mongoOrm()` rather than through `mongo()`, pass the execution context and the runtime, as with the SQL `orm()`: `mongoOrm({ context, runtime })`. The ORM fills generated fields such as `temporal.CreatedAt` through the context, and checks a written enum value against the contract's enums. ## Key files diff --git a/examples/mongo-demo/test/cache-middleware.test.ts b/examples/mongo-demo/test/cache-middleware.test.ts index 8a9f711092ae..3ca6c3e89953 100644 --- a/examples/mongo-demo/test/cache-middleware.test.ts +++ b/examples/mongo-demo/test/cache-middleware.test.ts @@ -3,7 +3,6 @@ import { cacheAnnotation, createCacheMiddleware } from '@prisma/orm-extension-mi import mongoRuntimeAdapter from '@prisma/orm-mongo/adapter/runtime'; import { createMongoDriver } from '@prisma/orm-mongo/driver'; import { - buildMongoEnums, createMongoExecutionContext, createMongoExecutionStack, createMongoRuntime, @@ -81,11 +80,7 @@ describe('mongo-demo cache middleware integration', { driver, middleware: [cache], }); - const orm = mongoOrm({ - context, - executor: runtime, - enums: buildMongoEnums(contract, context.codecs), - }); + const orm = mongoOrm({ context, runtime: runtime }); const query = mongoQuery({ context }); return { runtime, orm, query, driver, driverExecuteSpy }; } diff --git a/packages/2-mongo-family/4-query/query-ast/README.md b/packages/2-mongo-family/4-query/query-ast/README.md index ed7444195ceb..e7ddaf0b9e2b 100644 --- a/packages/2-mongo-family/4-query/query-ast/README.md +++ b/packages/2-mongo-family/4-query/query-ast/README.md @@ -9,5 +9,6 @@ Typed AST for MongoDB filters, pipeline stages, and command variants, plus the u - **Commands**: `AnyMongoCommand` and related command types (reads, writes, aggregates) carried on a plan - **Query plan**: `MongoQueryPlan` — a branded typed representation with shape `{ collection, command: AnyMongoCommand, meta: PlanMeta }` - **Visitors**: `MongoFilterVisitor`, `MongoFilterRewriter`, `MongoStageVisitor` interfaces for traversing and transforming AST nodes +- **Execution context**: `MongoExecutionContext` — the contract, a read-only codec lookup and the mutation defaults the query lanes build against, as SQL's `ExecutionContext` is; `buildMongoEnums(contract, codecs)` builds the contract's enum accessors from it Lowering typed nodes to wire BSON is performed by `@internal/adapter-mongo` via `lower(plan)` on `MongoQueryPlan`, which dispatches on `command.kind` and uses internal helpers for filters and pipelines. diff --git a/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts index 7191f73356fd..cb6930cc57e0 100644 --- a/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts +++ b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts @@ -1,5 +1,6 @@ import type { MutationDefaults } from '@internal/framework-components/runtime'; import type { MongoCodec } from '@internal/mongo-codec'; +import type { MongoContract } from '@internal/mongo-contract'; /** * Read-only view of the codec registry exposed on `MongoExecutionContext`. @@ -14,9 +15,10 @@ export interface MongoCodecLookup { /** * The context the query lanes build against: the user's contract, a read-only lookup over the codec registry composed from every stack contributor, and `applyMutationDefaults`, which fills the contract's execution defaults from the composed generators. The runtime creates it with `createMongoExecutionContext`. * - * Mirrors SQL's `ExecutionContext`: it holds no runtime concerns such as the adapter or the stack; the runtime receives the stack as its own option. + * Mirrors SQL's `ExecutionContext`: it holds no runtime concerns such as the adapter or the stack; the runtime receives the adapter as its own option. */ -export interface MongoExecutionContext extends MutationDefaults { +export interface MongoExecutionContext + extends MutationDefaults { readonly contract: TContract; readonly codecs: MongoCodecLookup; } diff --git a/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts b/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts index bcf76279755e..bb553ed3ed16 100644 --- a/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts +++ b/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts @@ -37,6 +37,8 @@ export { MongoNotExpr, MongoOrExpr, } from '../filter-expressions'; +export type { MongoEnumAccessors } from '../mongo-enums'; +export { buildMongoEnums } from '../mongo-enums'; export type { MongoQueryPlan } from '../query-plan'; export type { RawMongoCommand } from '../raw-commands'; export { diff --git a/packages/2-mongo-family/7-runtime/src/mongo-enums.ts b/packages/2-mongo-family/4-query/query-ast/src/mongo-enums.ts similarity index 94% rename from packages/2-mongo-family/7-runtime/src/mongo-enums.ts rename to packages/2-mongo-family/4-query/query-ast/src/mongo-enums.ts index 900a91e2299d..12b6780887d5 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-enums.ts +++ b/packages/2-mongo-family/4-query/query-ast/src/mongo-enums.ts @@ -1,7 +1,7 @@ import { buildEnumsMapForNamespace, type EnumAccessor } from '@internal/contract/enum-accessor'; import type { Contract } from '@internal/contract/types'; import { runtimeError } from '@internal/framework-components/runtime'; -import type { MongoCodecLookup } from '@internal/mongo-query-ast/execution'; +import type { MongoCodecLookup } from './execution-context'; /** A contract's enum accessors by namespace and enum name. */ export type MongoEnumAccessors = Readonly>>>; diff --git a/packages/2-mongo-family/7-runtime/test/mongo-enums.test.ts b/packages/2-mongo-family/4-query/query-ast/test/mongo-enums.test.ts similarity index 96% rename from packages/2-mongo-family/7-runtime/test/mongo-enums.test.ts rename to packages/2-mongo-family/4-query/query-ast/test/mongo-enums.test.ts index 13f64ae46a24..d5323acbc20a 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-enums.test.ts +++ b/packages/2-mongo-family/4-query/query-ast/test/mongo-enums.test.ts @@ -1,7 +1,7 @@ import type { ContractEnum } from '@internal/contract/types'; import { type DataTypeValue, dataType, readJsonString } from '@internal/framework-components/codec'; -import type { MongoCodecLookup } from '@internal/mongo-query-ast/execution'; import { describe, expect, it } from 'vitest'; +import type { MongoCodecLookup } from '../src/execution-context'; import { buildMongoEnums } from '../src/mongo-enums'; const level: ContractEnum = { diff --git a/packages/2-mongo-family/5-query-builders/orm/README.md b/packages/2-mongo-family/5-query-builders/orm/README.md index 49084898573f..079145fdf88b 100644 --- a/packages/2-mongo-family/5-query-builders/orm/README.md +++ b/packages/2-mongo-family/5-query-builders/orm/README.md @@ -4,7 +4,7 @@ MongoDB ORM client for Prisma 8. ## Responsibilities -- **ORM client factory**: `mongoOrm()` creates a typed client with root-based collection accessors; it takes the contract's enum accessors (`enums`, from `buildMongoEnums(contract, context.codecs)` in `@internal/mongo-runtime`) and refuses a written value outside a field's enum +- **ORM client factory**: `mongoOrm({ context, runtime })` creates a typed client with root-based collection accessors, as the SQL `orm({ context, runtime })` does. It fills generated fields through the context and refuses a written value outside a field's enum. `createMongoCollection({ context, runtime }, modelName)` creates one collection the same way - **Typed queries**: `findMany` with equality filters (`MongoWhereFilter`) and reference includes (`MongoIncludeSpec`) - **Row type inference**: `InferFullRow` (scalar fields + embedded documents), `InferRootRow` (discriminated union for polymorphic roots), `IncludeResultFields` - **Polymorphic narrowing**: Discriminator field carries literal variant values, enabling TypeScript `switch`/`if` narrowing diff --git a/packages/2-mongo-family/5-query-builders/orm/src/collection.ts b/packages/2-mongo-family/5-query-builders/orm/src/collection.ts index 2bbb60d087f4..37969deb33da 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/collection.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/collection.ts @@ -1,4 +1,3 @@ -import type { EnumAccessor } from '@internal/contract/enum-accessor'; import { type ContractField, type ContractModel, @@ -29,12 +28,15 @@ import type { AnyMongoCommand, InsertManyResult, InsertOneResult, + MongoEnumAccessors, + MongoExecutionContext, MongoFieldShape, MongoFilterExpr, MongoQueryPlan, MongoResultShape, } from '@internal/mongo-query-ast/execution'; import { + buildMongoEnums, DeleteManyCommand, FindOneAndDeleteCommand, FindOneAndUpdateCommand, @@ -239,12 +241,11 @@ function topLevelUpdateFields( return fields; } -/** - * The contract's enum accessors by namespace and enum name, as `db.enums` holds them. The ORM checks a written enum value against them. - */ -export type MongoOrmEnums = Readonly< - Record>>> ->; +/** What a collection runs against: the execution context, whose contract, codecs and mutation defaults it uses, and the runtime that executes its plans. Mirrors the SQL ORM's `CollectionContext`. */ +export interface MongoCollectionContext { + readonly context: MongoExecutionContext; + readonly runtime: MongoQueryExecutor; +} function describeValue(value: unknown): string { if (typeof value === 'bigint') return `${value}n`; @@ -268,26 +269,26 @@ class MongoCollectionImpl< { readonly #contract: TContract; readonly #modelName: ModelName; + readonly #ctx: MongoCollectionContext; readonly #executor: MongoQueryExecutor; - readonly #mutationDefaults: MutationDefaults | undefined; - readonly #enums: MongoOrmEnums; + readonly #mutationDefaults: MutationDefaults; + readonly #enums: MongoEnumAccessors; #collectionName: string; #state: MongoCollectionState; #variantValue: string | undefined; constructor( - contract: TContract, + ctx: MongoCollectionContext, modelName: ModelName, - executor: MongoQueryExecutor, - mutationDefaults: MutationDefaults | undefined, - enums: MongoOrmEnums, + enums: MongoEnumAccessors, ) { - this.#contract = contract; + this.#ctx = ctx; + this.#contract = ctx.context.contract; this.#modelName = modelName; - this.#executor = executor; - this.#mutationDefaults = mutationDefaults; + this.#executor = ctx.runtime; + this.#mutationDefaults = ctx.context; this.#enums = enums; - this.#collectionName = resolveCollectionName(modelOf(contract, modelName), modelName); + this.#collectionName = resolveCollectionName(modelOf(this.#contract, modelName), modelName); this.#state = emptyCollectionState(); } @@ -1135,15 +1136,8 @@ class MongoCollectionImpl< if (valueSet === undefined || valueSet.entityKind !== 'enum' || value === null) return; const contractEnum = this.#contract.domain.namespaces[valueSet.namespaceId]?.enum?.[valueSet.entityName]; - if (contractEnum === undefined) return; const accessor = this.#enums[valueSet.namespaceId]?.[valueSet.entityName]; - if (accessor === undefined) { - throw ormError( - 'ORM.ARGUMENT_INVALID', - `The ORM has no accessor for enum ${valueSet.entityName}, so it cannot check the value written to ${path} in collection '${this.#collectionName}'. Pass the contract's enum accessors: enums: buildMongoEnums(contract, context.codecs).`, - { meta: { argument: 'enums', enum: valueSet.entityName } }, - ); - } + if (contractEnum === undefined || accessor === undefined) return; const values: readonly unknown[] = field.many && Array.isArray(value) ? value : [value]; const outside = values.findIndex((entry) => entry !== null && !accessor.has(entry)); if (outside === -1) return; @@ -1266,7 +1260,7 @@ class MongoCollectionImpl< defaultValueCache: Map, ): Record { const applied = - this.#mutationDefaults?.applyMutationDefaults({ + this.#mutationDefaults.applyMutationDefaults({ op, namespace: UNBOUND_NAMESPACE_ID, entry: this.#collectionName, @@ -1440,10 +1434,8 @@ class MongoCollectionImpl< overrides: Partial, ): MongoCollectionImpl { const instance = new MongoCollectionImpl( - this.#contract, + this.#ctx, this.#modelName, - this.#executor, - this.#mutationDefaults, this.#enums, ); instance.#state = { ...this.#state, ...overrides }; @@ -1457,10 +1449,8 @@ class MongoCollectionImpl< variantValue: string, ): MongoCollectionImpl { const instance = new MongoCollectionImpl( - this.#contract, + this.#ctx, this.#modelName, - this.#executor, - this.#mutationDefaults, this.#enums, ); instance.#state = { ...this.#state, ...overrides }; @@ -1470,15 +1460,17 @@ class MongoCollectionImpl< } } +/** A collection of `modelName` over the context's contract, which fills generated fields from the context's mutation defaults and checks written enum values against the contract's enums. */ export function createMongoCollection< TContract extends MongoContractWithTypeMaps, ModelName extends string & keyof MongoModelsMap, >( - contract: TContract, + ctx: MongoCollectionContext, modelName: ModelName, - executor: MongoQueryExecutor, - enums: MongoOrmEnums, - mutationDefaults?: MutationDefaults, ): MongoCollection { - return new MongoCollectionImpl(contract, modelName, executor, mutationDefaults, enums); + return new MongoCollectionImpl( + ctx, + modelName, + buildMongoEnums(ctx.context.contract, ctx.context.codecs), + ); } diff --git a/packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts b/packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts index 82a64ff06405..8fd7d5a65435 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts @@ -1,6 +1,6 @@ export type { MongoQueryPlan } from '@internal/mongo-query-ast/execution'; export type { SimplifyDeep } from '@internal/utils/simplify-deep'; -export type { MongoCollection, MongoOrmEnums } from '../collection'; +export type { MongoCollection, MongoCollectionContext } from '../collection'; export { createMongoCollection } from '../collection'; export { compileMongoQuery } from '../compile'; export type { MongoQueryExecutor } from '../executor'; diff --git a/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts b/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts index 51129dd78cf9..a1c4ba7cad3a 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts @@ -6,17 +6,14 @@ import type { } from '@internal/mongo-contract'; import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { blindCast } from '@internal/utils/casts'; -import type { MongoCollection, MongoOrmEnums } from './collection'; +import type { MongoCollection } from './collection'; import { createMongoCollection } from './collection'; import type { MongoQueryExecutor } from './executor'; -import { ormError } from './orm-errors'; export interface MongoOrmOptions { /** The validated contract, its codecs, and the execution defaults filled on writes. */ readonly context: MongoExecutionContext; - readonly executor: MongoQueryExecutor; - /** The contract's enum accessors, which a written enum value is checked against: `buildMongoEnums(contract, context.codecs)`, the accessors `db.enums` holds. */ - readonly enums: MongoOrmEnums; + readonly runtime: MongoQueryExecutor; } export type MongoOrmClient< @@ -28,38 +25,19 @@ export type MongoOrmClient< >; }; -function assertEnumsComplete(contract: MongoContract, enums: MongoOrmEnums): void { - for (const [namespaceId, namespace] of Object.entries(contract.domain.namespaces)) { - const accessors = enums[namespaceId] ?? {}; - for (const enumName of Object.keys(namespace.enum ?? {})) { - if (enumName in accessors) continue; - throw ormError( - 'ORM.ARGUMENT_INVALID', - `mongoOrm() has no accessor for enum ${enumName} in namespace ${namespaceId}, so it cannot check a value written to a field of that enum. Pass the contract's enum accessors: enums: buildMongoEnums(contract, context.codecs).`, - { meta: { argument: 'enums', namespace: namespaceId, enum: enumName } }, - ); - } - } -} - export function mongoOrm< TContract extends MongoContractWithTypeMaps, >(options: MongoOrmOptions): MongoOrmClient { - const { context, executor, enums } = options; - const { contract } = context; - assertEnumsComplete(contract, enums); + const { context, runtime } = options; const client: Record = {}; - for (const [rootName, rootRef] of Object.entries(contract.roots)) { + for (const [rootName, rootRef] of Object.entries(context.contract.roots)) { client[rootName] = createMongoCollection( - contract, + { context, runtime }, blindCast< RootModelName, 'roots entries are CrossReferences; rootRef.model is a valid RootModelName for this contract' >(rootRef.model), - executor, - enums, - context, ); } diff --git a/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts b/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts new file mode 100644 index 000000000000..65c1e4d07ca8 --- /dev/null +++ b/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts @@ -0,0 +1,29 @@ +import type { MutationDefaults } from '@internal/framework-components/runtime'; +import type { MongoContract } from '@internal/mongo-contract'; +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; +import type { MongoCollectionContext } from '../src/collection'; +import type { MongoQueryExecutor } from '../src/executor'; + +const noCodecs = { get: () => undefined, has: () => false }; +const noMutationDefaults: MutationDefaults = { applyMutationDefaults: () => [] }; + +/** An execution context over a contract without enums, with no codecs and the given mutation defaults. */ +export function contextOf( + contract: TContract, + mutationDefaults: MutationDefaults = noMutationDefaults, +): MongoExecutionContext { + return { + contract, + codecs: noCodecs, + applyMutationDefaults: (options) => mutationDefaults.applyMutationDefaults(options), + }; +} + +/** A collection context over a contract without enums. */ +export function collectionContext( + contract: TContract, + runtime: MongoQueryExecutor, + mutationDefaults?: MutationDefaults, +): MongoCollectionContext { + return { context: contextOf(contract, mutationDefaults), runtime }; +} diff --git a/packages/2-mongo-family/5-query-builders/orm/test/collection.mutation-defaults.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/collection.mutation-defaults.test.ts index 4e59ed5aaf98..372962128987 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/collection.mutation-defaults.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/collection.mutation-defaults.test.ts @@ -16,9 +16,7 @@ import ormContractJson from '../../../1-foundation/mongo-contract/test/fixtures/ import { createMongoCollection } from '../src/collection'; import type { MongoQueryExecutor } from '../src/executor'; import { mongoOrm } from '../src/mongo-orm'; -import { noEnums } from './no-enums'; - -const noCodecs = { get: () => undefined, has: () => false }; +import { collectionContext, contextOf } from './collection-context'; const contract = ormContractJson as unknown as Contract; @@ -107,7 +105,7 @@ function commandOf(plans: readonly MongoQueryPlan[], kind: string) { } function users(executor: MongoQueryExecutor, defaults: MutationDefaults) { - return createMongoCollection(contract, 'User', executor, noEnums, defaults); + return createMongoCollection(collectionContext(contract, executor, defaults), 'User'); } const byEmail = MongoFieldFilter.eq('email', 'a@b.c'); @@ -307,9 +305,8 @@ describe('mongoOrm', () => { { insertedId: 'id-1', document: { _id: 'id-1' } }, ]); const orm = mongoOrm({ - context: { ...fakeMutationDefaults(), contract, codecs: noCodecs }, - executor, - enums: noEnums, + context: contextOf(contract, fakeMutationDefaults()), + runtime: executor, }); await orm.users.create(input(userData)); expect(unwrap(commandOf(plans, 'insertOne')['document'])).toMatchObject({ loginCount: 7 }); diff --git a/packages/2-mongo-family/5-query-builders/orm/test/collection.param-refs.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/collection.param-refs.test.ts index 526929b90f83..f97bca1b8352 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/collection.param-refs.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/collection.param-refs.test.ts @@ -10,7 +10,7 @@ import type { Contract } from '../../../1-foundation/mongo-contract/test/fixture import ormContractJson from '../../../1-foundation/mongo-contract/test/fixtures/orm-contract.json'; import { createMongoCollection } from '../src/collection'; import type { MongoQueryExecutor } from '../src/executor'; -import { noEnums } from './no-enums'; +import { collectionContext } from './collection-context'; const contract = ormContractJson as unknown as Contract; @@ -58,7 +58,7 @@ const withoutCodec = { name: undefined, collection: undefined, codecId: undefine describe('parameters the Mongo ORM builds', () => { it('name each field of a create by its path, nested value-object fields included', async () => { const { executor, plans } = recordingExecutor(); - await createMongoCollection(contract, 'User', executor, noEnums).create(user); + await createMongoCollection(collectionContext(contract, executor), 'User').create(user); expect(labels(plans)).toEqual([ { name: 'name', collection: 'users', codecId: string }, @@ -72,7 +72,7 @@ describe('parameters the Mongo ORM builds', () => { it('name the fields of a where filter and a delete filter', async () => { const { executor, plans } = recordingExecutor(); - const users = createMongoCollection(contract, 'User', executor, noEnums); + const users = createMongoCollection(collectionContext(contract, executor), 'User'); await users.where({ email: 'a@b.c' }).all().toArray(); await users.where({ name: 'Alice' }).delete(); @@ -84,7 +84,7 @@ describe('parameters the Mongo ORM builds', () => { it('name the fields of $set, $inc, $push and a dot-path $set', async () => { const { executor, plans } = recordingExecutor(); - await createMongoCollection(contract, 'User', executor, noEnums) + await createMongoCollection(collectionContext(contract, executor), 'User') .where({ email: 'a@b.c' }) .update((u) => [ u.name.set('Bob'), @@ -104,7 +104,7 @@ describe('parameters the Mongo ORM builds', () => { it('name the fields of an upsert, in both $set and $setOnInsert', async () => { const { executor, plans } = recordingExecutor(); - await createMongoCollection(contract, 'User', executor, noEnums) + await createMongoCollection(collectionContext(contract, executor), 'User') .where({ email: 'a@b.c' }) .upsert({ create: user, update: { name: 'Bob' } }); @@ -127,7 +127,7 @@ describe('parameters the Mongo ORM builds', () => { it('name the base fields and the discriminator of a variant create', async () => { const { executor, plans } = recordingExecutor(); - await createMongoCollection(contract, 'Task', executor, noEnums) + await createMongoCollection(collectionContext(contract, executor), 'Task') .variant('bug') .create({ title: 'Crash', assigneeId: 'a1', severity: 'high', comments: [] }); @@ -142,7 +142,7 @@ describe('parameters the Mongo ORM builds', () => { it('encode the values of a filter expression through the codec of the field it names', async () => { const { executor, plans } = recordingExecutor(); - await createMongoCollection(contract, 'User', executor, noEnums) + await createMongoCollection(collectionContext(contract, executor), 'User') .where( MongoAndExpr.of([ MongoFieldFilter.eq('_id', '65f0000000000000000000ab'), @@ -163,7 +163,7 @@ describe('parameters the Mongo ORM builds', () => { it('encode each element of a whole-list comparison once, wrapped or not', async () => { const { executor, plans } = recordingExecutor(); - await createMongoCollection(contract, 'User', executor, noEnums) + await createMongoCollection(collectionContext(contract, executor), 'User') .where(MongoFieldFilter.eq('tags', [new MongoParamRef('x'), 'y'])) .all() .toArray(); @@ -179,7 +179,7 @@ describe('parameters the Mongo ORM builds', () => { describe('null in the Mongo ORM', () => { it('is refused for a required field on create and update, naming the field', async () => { const { executor, plans } = recordingExecutor(); - const users = createMongoCollection(contract, 'User', executor, noEnums); + const users = createMongoCollection(collectionContext(contract, executor), 'User'); const refused = { code: 'RUNTIME.ENCODE_FAILED', message: @@ -201,7 +201,7 @@ describe('null in the Mongo ORM', () => { it('is written without a codec to a nullable field and compared without one in a filter', async () => { const { executor, plans } = recordingExecutor(); - const users = createMongoCollection(contract, 'User', executor, noEnums); + const users = createMongoCollection(collectionContext(contract, executor), 'User'); await users.create({ ...user, homeAddress: null }); await users .where({ loginCount: null as never }) diff --git a/packages/2-mongo-family/5-query-builders/orm/test/collection.stored-names.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/collection.stored-names.test.ts index b2ab4ac08100..3c06db2c699f 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/collection.stored-names.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/collection.stored-names.test.ts @@ -16,9 +16,9 @@ import { MongoParamRef } from '@internal/mongo-value'; import { describe, expect, it } from 'vitest'; import { createMongoCollection } from '../src/collection'; import type { MongoQueryExecutor } from '../src/executor'; +import { collectionContext } from './collection-context'; import type { Contract } from './fixtures/stored-names-contract'; import storedNamesContractJson from './fixtures/stored-names-contract.json'; -import { noEnums } from './no-enums'; const contract = storedNamesContractJson as unknown as Contract; @@ -94,13 +94,15 @@ const userRow = { describe('a model whose fields are stored under other names', () => { it('reads each document as a row of fields, leaving out stored names no field stores', async () => { const { executor } = recordingExecutor([{ ...storedUser, legacy: true }]); - const rows = await createMongoCollection(contract, 'User', executor, noEnums).all().toArray(); + const rows = await createMongoCollection(collectionContext(contract, executor), 'User') + .all() + .toArray(); expect(rows).toEqual([userRow]); }); it('filters, selects and sorts by stored names', async () => { const { executor, plans } = recordingExecutor([]); - await createMongoCollection(contract, 'User', executor, noEnums) + await createMongoCollection(collectionContext(contract, executor), 'User') .where({ id: 'u1' }) .select('displayName') .orderBy({ createdAt: -1 }) @@ -121,7 +123,7 @@ describe('a model whose fields are stored under other names', () => { it('translates the field of a raw filter expression', async () => { const { executor, plans } = recordingExecutor([]); - await createMongoCollection(contract, 'User', executor, noEnums) + await createMongoCollection(collectionContext(contract, executor), 'User') .where(MongoFieldFilter.eq('home.city', 'London')) .all() .toArray(); @@ -142,7 +144,7 @@ describe('a model whose fields are stored under other names', () => { it('translates field references inside $expr, leaving variables alone', async () => { const { executor, plans } = recordingExecutor([]); - await createMongoCollection(contract, 'User', executor, noEnums) + await createMongoCollection(collectionContext(contract, executor), 'User') .where( MongoExprFilter.of( MongoAggOperator.of('$eq', [ @@ -176,7 +178,7 @@ describe('a model whose fields are stored under other names', () => { 'refuses %s, a name every object inherits, as a field it does not have', async (name) => { const { executor } = recordingExecutor(); - const users = createMongoCollection(contract, 'User', executor, noEnums); + const users = createMongoCollection(collectionContext(contract, executor), 'User'); const unknown = expect.objectContaining({ code: 'ORM.FIELD_UNKNOWN', message: `Model "User" has no field "${name}"`, @@ -193,7 +195,9 @@ describe('a model whose fields are stored under other names', () => { it('refuses a stored name passed as a field, naming the field that stores it', () => { const { executor } = recordingExecutor(); expect(() => - createMongoCollection(contract, 'User', executor, noEnums).where({ _id: 'u1' } as never), + createMongoCollection(collectionContext(contract, executor), 'User').where({ + _id: 'u1', + } as never), ).toThrow( expect.objectContaining({ code: 'ORM.FIELD_UNKNOWN', @@ -206,11 +210,8 @@ describe('a model whose fields are stored under other names', () => { it('writes a create under stored names, applies execution defaults to the stored document, and returns a row', async () => { const { executor, plans } = recordingExecutor([{ insertedId: 'u1', document: storedUser }]); const row = await createMongoCollection( - contract, + collectionContext(contract, executor, createdAtDefaults), 'User', - executor, - noEnums, - createdAtDefaults, ).create({ displayName: 'Ada', email: null, @@ -229,7 +230,9 @@ describe('a model whose fields are stored under other names', () => { it('updates and upserts under stored names, and refuses a change to the field stored as _id', async () => { const { executor, plans } = recordingExecutor([storedUser], [storedUser]); - const users = createMongoCollection(contract, 'User', executor, noEnums).where({ id: 'u1' }); + const users = createMongoCollection(collectionContext(contract, executor), 'User').where({ + id: 'u1', + }); const updated = await users.update((u) => [u.displayName.set('Grace')]); const upserted = await users.upsert({ create: { displayName: 'Ada', createdAt: 'now' } as never, @@ -268,7 +271,7 @@ describe('a model whose fields are stored under other names', () => { const { executor, plans } = recordingExecutor([ { _id: 'p1', author_id: 'u1', title: 'Hi', author: storedUser }, ]); - const rows = await createMongoCollection(contract, 'Post', executor, noEnums) + const rows = await createMongoCollection(collectionContext(contract, executor), 'Post') .include('author') .all() .toArray(); @@ -282,7 +285,7 @@ describe('a model whose fields are stored under other names', () => { it('reads and writes a variant by the stored discriminator and variant field names', async () => { const storedBug = { _id: 't1', _kind: 'bug', task_title: 'Crash', bug_severity: 'high' }; const { executor, plans } = recordingExecutor([storedBug], [storedBug], [{ insertedCount: 1 }]); - const tasks = createMongoCollection(contract, 'Task', executor, noEnums); + const tasks = createMongoCollection(collectionContext(contract, executor), 'Task'); const read = await tasks.all().toArray(); const created = await tasks.variant('bug').all().toArray(); await tasks.variant('bug').createAndCount([{ title: 'Crash', severity: 'high' } as never]); diff --git a/packages/2-mongo-family/5-query-builders/orm/test/collection.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/collection.test.ts index 3fb451bd8a7a..6d0e9fb27951 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/collection.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/collection.test.ts @@ -27,7 +27,7 @@ import { type FieldExpression, type FieldOperation, } from '../src/field-accessor'; -import { noEnums } from './no-enums'; +import { collectionContext } from './collection-context'; const contract = ormContractJson as unknown as Contract; @@ -139,14 +139,14 @@ function createMockExecutor( describe('MongoCollection chaining', () => { it('returns a new instance from where()', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const filtered = col.where(MongoFieldFilter.eq('name', 'Alice')); expect(filtered).not.toBe(col); }); it('accumulates filters from multiple where() calls', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums) + const col = createMongoCollection(collectionContext(contract, executor), 'User') .where(MongoFieldFilter.eq('name', 'Alice')) .where(MongoFieldFilter.gte('email', 'a')); col.all(); @@ -156,7 +156,7 @@ describe('MongoCollection chaining', () => { it('returns a new instance from select()', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const selected = col.select('name'); expect(selected).not.toBe(col); selected.all(); @@ -165,7 +165,7 @@ describe('MongoCollection chaining', () => { it('accumulates fields across multiple select() calls', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums) + const col = createMongoCollection(collectionContext(contract, executor), 'User') .select('name') .select('_id'); col.all(); @@ -175,7 +175,7 @@ describe('MongoCollection chaining', () => { it('returns a new instance from orderBy()', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const ordered = col.orderBy({ name: 1 }); expect(ordered).not.toBe(col); ordered.all(); @@ -185,7 +185,7 @@ describe('MongoCollection chaining', () => { it('merges orderBy across calls', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums) + const col = createMongoCollection(collectionContext(contract, executor), 'User') .orderBy({ name: 1 }) .orderBy({ email: -1 }); col.all(); @@ -195,7 +195,7 @@ describe('MongoCollection chaining', () => { it('returns a new instance from limit()', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const limited = col.limit(10); expect(limited).not.toBe(col); limited.all(); @@ -205,7 +205,7 @@ describe('MongoCollection chaining', () => { it('returns a new instance from offset()', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const skipped = col.offset(5); expect(skipped).not.toBe(col); skipped.all(); @@ -215,7 +215,7 @@ describe('MongoCollection chaining', () => { it('does not mutate original instance', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); col.where(MongoFieldFilter.eq('name', 'Alice')); col.all(); expect(executor.lastStages!).toHaveLength(0); @@ -223,7 +223,7 @@ describe('MongoCollection chaining', () => { it('chains where, orderBy, limit, offset together', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums) + const col = createMongoCollection(collectionContext(contract, executor), 'User') .where(MongoFieldFilter.eq('name', 'Alice')) .orderBy({ name: 1 }) .offset(10) @@ -237,7 +237,7 @@ describe('MongoCollection chaining', () => { describe('MongoCollection object-based where()', () => { it('produces eq filter with string codecId for string field', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums).where({ + const col = createMongoCollection(collectionContext(contract, executor), 'User').where({ name: 'Alice', }); col.all(); @@ -255,7 +255,7 @@ describe('MongoCollection object-based where()', () => { it('produces eq filter with objectId codecId for ObjectId field', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'Task', executor, noEnums).where({ + const col = createMongoCollection(collectionContext(contract, executor), 'Task').where({ assigneeId: 'abc123', }); col.all(); @@ -273,7 +273,7 @@ describe('MongoCollection object-based where()', () => { it('preserves exact scalar-list equality while wrapping each non-null operand element', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums).where({ + const col = createMongoCollection(collectionContext(contract, executor), 'User').where({ tags: ['admin', null as never, 'editor'], }); col.all(); @@ -301,7 +301,7 @@ describe('MongoCollection object-based where()', () => { const editor = new MongoParamRef('editor', { codecId: 'mongo/string@1' }); const operands = [admin, null, editor]; const filter = MongoFieldFilter.in('tags', operands); - createMongoCollection(contract, 'User', executor, noEnums).where(filter).all(); + createMongoCollection(collectionContext(contract, executor), 'User').where(filter).all(); const match = executor.lastStages![0] as MongoMatchStage; expect(match.filter).toBe(filter); expect(match.filter).toMatchObject({ value: operands }); @@ -313,7 +313,7 @@ describe('MongoCollection object-based where()', () => { it('produces AND of multiple eq filters for multi-field object', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums).where({ + const col = createMongoCollection(collectionContext(contract, executor), 'User').where({ name: 'Alice', email: 'a@b.c', }); @@ -331,7 +331,7 @@ describe('MongoCollection object-based where()', () => { it('chains with MongoFilterExpr where()', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums) + const col = createMongoCollection(collectionContext(contract, executor), 'User') .where({ name: 'Alice' }) .where(MongoFieldFilter.gte('email', 'a')); col.all(); @@ -341,7 +341,7 @@ describe('MongoCollection object-based where()', () => { it('chains MongoFilterExpr where() then object where()', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums) + const col = createMongoCollection(collectionContext(contract, executor), 'User') .where(MongoFieldFilter.eq('_id', 'id-1')) .where({ name: 'Alice' }); col.all(); @@ -478,14 +478,14 @@ describe('compileFieldOperations()', () => { describe('MongoCollection variant()', () => { it('returns a new instance from variant()', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'Task', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'Task'); const narrowed = col.variant('bug'); expect(narrowed).not.toBe(col); }); it('injects discriminator eq filter for the variant value', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'Task', executor, noEnums).variant('bug'); + const col = createMongoCollection(collectionContext(contract, executor), 'Task').variant('bug'); col.all(); const match = executor.lastStages![0] as MongoMatchStage; expect(match.filter.kind).toBe('field'); @@ -497,7 +497,7 @@ describe('MongoCollection variant()', () => { it('does not mutate original collection', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'Task', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'Task'); col.variant('bug'); col.all(); expect(executor.lastStages!).toHaveLength(0); @@ -505,7 +505,7 @@ describe('MongoCollection variant()', () => { it('composes with where()', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'Task', executor, noEnums) + const col = createMongoCollection(collectionContext(contract, executor), 'Task') .variant('feature') .where(MongoFieldFilter.eq('title', 'Login')); col.all(); @@ -515,7 +515,7 @@ describe('MongoCollection variant()', () => { it('throws when a variant is already selected', () => { const executor = createMockExecutor(); - const bugs = createMongoCollection(contract, 'Task', executor, noEnums).variant( + const bugs = createMongoCollection(collectionContext(contract, executor), 'Task').variant( 'bug', ) as unknown as MongoCollection; @@ -537,7 +537,7 @@ describe('MongoCollection variant()', () => { it('keeps a discriminator where() written before variant()', () => { const executor = createMockExecutor(); - createMongoCollection(contract, 'Task', executor, noEnums) + createMongoCollection(collectionContext(contract, executor), 'Task') .where(MongoFieldFilter.eq('type', 'feature')) .variant('bug') .all(); @@ -560,7 +560,7 @@ describe('MongoCollection variant()', () => { it('throws when the model has no discriminator', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); expect(() => col.variant('bug' as never)).toThrow( expect.objectContaining({ code: 'ORM.ARGUMENT_INVALID', @@ -578,7 +578,7 @@ describe('MongoCollection variant()', () => { it('throws for an undeclared discriminator value', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'Task', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'Task'); const variantModelName = 'Bug'; expect(() => col.variant(variantModelName as never)).toThrow( expect.objectContaining({ @@ -598,7 +598,7 @@ describe('MongoCollection variant()', () => { it('create() injects discriminator value into the document', async () => { const executor = createMockExecutor([{ insertedId: 'new-id', document: { _id: 'new-id' } }]); - const col = createMongoCollection(contract, 'Task', executor, noEnums).variant('bug'); + const col = createMongoCollection(collectionContext(contract, executor), 'Task').variant('bug'); await col.create({ title: 'Fix crash', severity: 'high', assigneeId: 'u1' } as never); const command = executor.plans[0]!.command; expect(command.kind).toBe('insertOne'); @@ -611,7 +611,7 @@ describe('MongoCollection variant()', () => { const executor = createMockExecutor([ { insertedId: 'new-id', document: { _id: 'new-id', title: 'Fix crash', type: 'bug' } }, ]); - const col = createMongoCollection(contract, 'Task', executor, noEnums).variant('bug'); + const col = createMongoCollection(collectionContext(contract, executor), 'Task').variant('bug'); const result = await col.create({ title: 'Fix crash', severity: 'high', @@ -631,7 +631,7 @@ describe('MongoCollection variant()', () => { ], }, ]); - const col = createMongoCollection(contract, 'Task', executor, noEnums).variant('bug'); + const col = createMongoCollection(collectionContext(contract, executor), 'Task').variant('bug'); const rows: unknown[] = []; for await (const row of col.createAll([ { title: 'Bug 1', severity: 'low', assigneeId: 'u1' }, @@ -648,7 +648,9 @@ describe('MongoCollection variant()', () => { describe('MongoCollection include()', () => { it('adds a relation include', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'Task', executor, noEnums).include('assignee'); + const col = createMongoCollection(collectionContext(contract, executor), 'Task').include( + 'assignee', + ); col.all(); const lookup = executor.lastStages!.find((s) => s.kind === 'lookup') as MongoLookupStage; expect(lookup.from).toBe('users'); @@ -659,21 +661,23 @@ describe('MongoCollection include()', () => { it('throws for unknown relation', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'Task', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'Task'); // @ts-expect-error 'nonexistent' is not a valid reference relation key expect(() => col.include('nonexistent')).toThrow('Unknown relation'); }); it('throws for embed relation', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'Task', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'Task'); // @ts-expect-error 'comments' is an embed relation, not a reference relation expect(() => col.include('comments')).toThrow('embed relation'); }); it('produces $lookup without $unwind for 1:N reference relation', () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums).include('tasks'); + const col = createMongoCollection(collectionContext(contract, executor), 'User').include( + 'tasks', + ); col.all(); const stages = executor.lastStages!; const lookup = stages.find((s) => s.kind === 'lookup') as MongoLookupStage; @@ -689,7 +693,7 @@ describe('MongoCollection include()', () => { describe('MongoCollection terminal methods', () => { it('all() executes the compiled plan', () => { const executor = createMockExecutor([{ _id: '1', name: 'Alice', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); col.all(); expect(executor.lastPlan).toBeDefined(); expect(executor.lastPlan!.collection).toBe('users'); @@ -701,21 +705,21 @@ describe('MongoCollection terminal methods', () => { { _id: '1', name: 'Alice', email: 'a@b.c' }, { _id: '2', name: 'Bob', email: 'b@b.c' }, ]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const result = await col.first(); expect(result).toEqual({ _id: '1', name: 'Alice', email: 'a@b.c' }); }); it('first() returns null when no results', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const result = await col.first(); expect(result).toBeNull(); }); it('first() sets limit 1 on the compiled plan', async () => { const executor = createMockExecutor([{ _id: '1', name: 'Alice', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.first(); const limitStage = executor.lastStages!.find((s) => s.kind === 'limit') as | MongoLimitStage @@ -728,13 +732,13 @@ describe('MongoCollection terminal methods', () => { { _id: '1', name: 'Alice', email: 'a@b.c' }, { _id: '2', name: 'Bob', email: 'b@b.c' }, ]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); expect(await col.firstOrThrow()).toEqual({ _id: '1', name: 'Alice', email: 'a@b.c' }); }); it('firstOrThrow() rejects with RUNTIME.NO_ROWS when no results', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect(col.firstOrThrow()).rejects.toMatchObject({ code: 'RUNTIME.NO_ROWS', message: 'Expected at least one row, but none were returned', @@ -745,10 +749,10 @@ describe('MongoCollection terminal methods', () => { it('firstOrThrow() issues the plan of first()', async () => { const row = { _id: '1', title: 'Crash', type: 'bug', assigneeId: '2' }; const executor = createMockExecutor([row], [row], [row], [row]); - const users = createMongoCollection(contract, 'User', executor, noEnums).where({ + const users = createMongoCollection(collectionContext(contract, executor), 'User').where({ name: 'Alice', }); - const bugs = createMongoCollection(contract, 'Task', executor, noEnums) + const bugs = createMongoCollection(collectionContext(contract, executor), 'Task') .variant('bug') .include('assignee') .limit(99); @@ -770,7 +774,7 @@ describe('MongoCollection write methods', () => { it('returns the written document the insert result carries, without a read', async () => { const stored = { _id: 'new-id-1', ...defaultUserData, loginCount: 1 }; const executor = createMockExecutor([{ insertedId: 'new-id-1', document: stored }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const result = await col.create(defaultUserData); expect(result).toEqual(stored); expect(executor.plans.map((plan) => plan.command.kind)).toEqual(['insertOne']); @@ -778,14 +782,14 @@ describe('MongoCollection write methods', () => { it('sends an InsertOneCommand', async () => { const executor = createMockExecutor([{ insertedId: 'id', document: { _id: 'id' } }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.create({ ...defaultUserData, name: 'Bob', email: 'b@b.c' }); expect(executor.plans[0]?.command).toMatchObject({ kind: 'insertOne', collection: 'users' }); }); it('attaches codecId from contract fields to MongoParamRef in document', async () => { const executor = createMockExecutor([{ insertedId: 'id', document: { _id: 'id' } }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.create(defaultUserData); const command = executor.plans[0]!.command; expect(command.kind).toBe('insertOne'); @@ -801,7 +805,10 @@ describe('MongoCollection write methods', () => { it('wraps scalar-list elements independently and leaves null unencoded', async () => { const executor = createMockExecutor([{ insertedId: 'id' }]); - const col = createMongoCollection(nullableScalarListContract, 'User', executor, noEnums); + const col = createMongoCollection( + collectionContext(nullableScalarListContract, executor), + 'User', + ); await col.create({ ...defaultUserData, tags: ['a', null as never, 'b'] }); const command = executor.lastCommand!; expect(command.kind).toBe('insertOne'); @@ -824,7 +831,10 @@ describe('MongoCollection write methods', () => { it('preserves nullable value-object list elements and wraps nested scalar leaves', async () => { const executor = createMockExecutor([{ insertedId: 'id' }]); - const col = createMongoCollection(nullableValueObjectListContract, 'User', executor, noEnums); + const col = createMongoCollection( + collectionContext(nullableValueObjectListContract, executor), + 'User', + ); await col.create({ ...defaultUserData, homeAddress: nullableValueObjectList as never, @@ -838,7 +848,7 @@ describe('MongoCollection write methods', () => { it('attaches objectId codecId for ObjectId-typed fields', async () => { const executor = createMockExecutor([{ insertedId: 'id', document: { _id: 'id' } }]); - const col = createMongoCollection(contract, 'Task', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'Task'); await col.create({ title: 'Fix bug', assigneeId: 'abc123', type: 'bug' }); const command = executor.plans[0]!.command; expect(command.kind).toBe('insertOne'); @@ -851,7 +861,7 @@ describe('MongoCollection write methods', () => { it('attaches a result shape decoding insertedId and the document through the model, as a row', async () => { const executor = createMockExecutor([{ insertedId: 'id', document: { _id: 'id' } }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.create(defaultUserData); expect(executor.plans[0]!.resultShape).toMatchObject({ kind: 'document', @@ -880,7 +890,7 @@ describe('MongoCollection write methods', () => { ], }, ]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const rows: unknown[] = []; for await (const row of col.createAll([ defaultUserData, @@ -899,7 +909,7 @@ describe('MongoCollection write methods', () => { const executor = createMockExecutor([ { insertedIds: ['id-1'], insertedCount: 1, documents: [{ _id: 'id-1' }] }, ]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); for await (const _row of col.createAll([defaultUserData])) { // drain } @@ -924,7 +934,7 @@ describe('MongoCollection write methods', () => { describe('createAndCount()', () => { it('returns the count of inserted documents', async () => { const executor = createMockExecutor([{ insertedIds: ['a', 'b'], insertedCount: 2 }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const count = await col.createAndCount([ defaultUserData, { ...defaultUserData, name: 'Bob', email: 'b@b.c' }, @@ -936,13 +946,13 @@ describe('MongoCollection write methods', () => { describe('update()', () => { it('throws without .where()', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect(col.update({ name: 'Changed' })).rejects.toThrow('requires a .where()'); }); it('returns updated row via findOneAndUpdate', async () => { const executor = createMockExecutor([{ _id: 'id-1', name: 'Updated', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const result = await col .where(MongoFieldFilter.eq('_id', 'id-1')) .update({ name: 'Updated' }); @@ -952,7 +962,7 @@ describe('MongoCollection write methods', () => { it('passes MongoFilterExpr to command', async () => { const executor = createMockExecutor([{ _id: 'id-1', name: 'Updated', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('_id', 'id-1')).update({ name: 'Updated' }); const command = executor.lastCommand!; expect(command.kind).toBe('findOneAndUpdate'); @@ -964,14 +974,14 @@ describe('MongoCollection write methods', () => { it('returns null when no match', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const result = await col.where(MongoFieldFilter.eq('_id', 'missing')).update({ name: 'X' }); expect(result).toBeNull(); }); it('attaches codecId to $set fields from contract', async () => { const executor = createMockExecutor([{ _id: 'id-1', name: 'Updated', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('_id', 'id-1')).update({ name: 'Updated' }); const command = executor.lastCommand!; expect(command.kind).toBe('findOneAndUpdate'); @@ -985,7 +995,10 @@ describe('MongoCollection write methods', () => { it('preserves nullable value-object list elements in object updates', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(nullableValueObjectListContract, 'User', executor, noEnums); + const col = createMongoCollection( + collectionContext(nullableValueObjectListContract, executor), + 'User', + ); await col.where(MongoFieldFilter.eq('_id', 'id-1')).update({ homeAddress: nullableValueObjectList as never, }); @@ -1000,7 +1013,7 @@ describe('MongoCollection write methods', () => { it('attaches the model result shape so the returned document decodes like a read', async () => { const executor = createMockExecutor([{ _id: 'id-1', name: 'Updated', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('_id', 'id-1')).update({ name: 'Updated' }); const shape = executor.lastPlan!.resultShape; expect(shape).toBeDefined(); @@ -1018,7 +1031,7 @@ describe('MongoCollection write methods', () => { describe('update() with callback', () => { it('produces correct update doc from field operations', async () => { const executor = createMockExecutor([{ _id: 'id-1', name: 'Updated' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col .where(MongoFieldFilter.eq('_id', 'id-1')) .update((u) => [u.name.set('Updated'), u.loginCount.inc(1)]); @@ -1033,7 +1046,7 @@ describe('MongoCollection write methods', () => { it('applies codec to callback operations for scalar fields', async () => { const executor = createMockExecutor([{ _id: 'id-1', name: 'Updated' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('_id', 'id-1')).update((u) => [u.name.set('Updated')]); const command = executor.lastCommand!; if (command.kind === 'findOneAndUpdate') { @@ -1044,7 +1057,10 @@ describe('MongoCollection write methods', () => { it('wraps callback scalar-list replacement elements independently', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(nullableScalarListContract, 'User', executor, noEnums); + const col = createMongoCollection( + collectionContext(nullableScalarListContract, executor), + 'User', + ); await col .where(MongoFieldFilter.eq('_id', 'id-1')) .update((u) => [u.tags.set(['admin', null as never, 'editor'])]); @@ -1072,7 +1088,10 @@ describe('MongoCollection write methods', () => { it('preserves nullable value-object list elements in callback $set', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(nullableValueObjectListContract, 'User', executor, noEnums); + const col = createMongoCollection( + collectionContext(nullableValueObjectListContract, executor), + 'User', + ); await col .where(MongoFieldFilter.eq('_id', 'id-1')) .update((u) => [u.homeAddress.set(nullableValueObjectList as never)]); @@ -1087,7 +1106,7 @@ describe('MongoCollection write methods', () => { it('preserves null in top-level nullable value-object callback $set', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('_id', 'id-1')).update((u) => [u.homeAddress.set(null)]); const command = executor.lastCommand!; expect(command.kind).toBe('findOneAndUpdate'); @@ -1098,7 +1117,7 @@ describe('MongoCollection write methods', () => { it('produces $push operations from callback', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('_id', 'id-1')).update((u) => [u.tags.push('admin')]); const command = executor.lastCommand!; if (command.kind === 'findOneAndUpdate') { @@ -1116,7 +1135,10 @@ describe('MongoCollection write methods', () => { it('leaves null callback $push elements unwrapped', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(nullableScalarListContract, 'User', executor, noEnums); + const col = createMongoCollection( + collectionContext(nullableScalarListContract, executor), + 'User', + ); await col .where(MongoFieldFilter.eq('_id', 'id-1')) .update((u) => [u.tags.push(null as never)]); @@ -1128,7 +1150,7 @@ describe('MongoCollection write methods', () => { it('does not attach codecId to $unset sentinel value', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('_id', 'id-1')).update((u) => [u.name.unset()]); const command = executor.lastCommand!; if (command.kind === 'findOneAndUpdate') { @@ -1142,7 +1164,7 @@ describe('MongoCollection write methods', () => { it('produces dot-path operations from callback', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col .where(MongoFieldFilter.eq('_id', 'id-1')) .update((u) => [u('homeAddress.city').set('NYC')]); @@ -1156,7 +1178,7 @@ describe('MongoCollection write methods', () => { it('normalizes empty callback to { $set: {} }', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('_id', 'id-1')).update(() => []); const command = executor.lastCommand!; if (command.kind === 'findOneAndUpdate') { @@ -1166,7 +1188,7 @@ describe('MongoCollection write methods', () => { it('wraps value-object payload through codec in set()', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col .where(MongoFieldFilter.eq('_id', 'id-1')) .update((u) => [u.homeAddress.set({ city: 'NYC', country: 'US' })]); @@ -1192,7 +1214,7 @@ describe('MongoCollection write methods', () => { { _id: 'id-2', name: 'Bob', loginCount: 1 }, ], ); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const rows: unknown[] = []; for await (const row of col .where(MongoFieldFilter.eq('email', 'a@b.c')) @@ -1208,7 +1230,7 @@ describe('MongoCollection write methods', () => { describe('updateAndCount() with callback', () => { it('produces correct update doc from field operations', async () => { const executor = createMockExecutor({ affectedRows: 1 }); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const count = await col .where(MongoFieldFilter.eq('email', 'a')) .updateAndCount((u) => [u.name.set('X')]); @@ -1220,7 +1242,7 @@ describe('MongoCollection write methods', () => { describe('upsert() with callback', () => { it('uses field operations for update part', async () => { const executor = createMockExecutor([{ _id: 'new-id' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('email', 'a@b.c')).upsert({ create: defaultUserData, update: (u: FieldAccessor) => [u.loginCount.inc(1)], @@ -1235,7 +1257,7 @@ describe('MongoCollection write methods', () => { it('throws when callback produces dot-path operations', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect( col.where(MongoFieldFilter.eq('email', 'a@b.c')).upsert({ create: { ...defaultUserData, homeAddress: { city: 'SF', country: 'US' } }, @@ -1248,13 +1270,13 @@ describe('MongoCollection write methods', () => { describe('updateAndCount()', () => { it('throws without .where()', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect(col.updateAndCount({ name: 'X' })).rejects.toThrow('requires a .where()'); }); it('returns the modified count', async () => { const executor = createMockExecutor({ affectedRows: 3 }); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const count = await col .where(MongoFieldFilter.eq('email', 'a')) .updateAndCount({ name: 'X' }); @@ -1266,13 +1288,13 @@ describe('MongoCollection write methods', () => { describe('delete()', () => { it('throws without .where()', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect(col.delete()).rejects.toThrow('requires a .where()'); }); it('returns deleted row via findOneAndDelete', async () => { const executor = createMockExecutor([{ _id: 'id-1', name: 'Alice', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const result = await col.where(MongoFieldFilter.eq('_id', 'id-1')).delete(); expect(result).toEqual({ _id: 'id-1', name: 'Alice', email: 'a@b.c' }); expect(executor.lastCommand!.kind).toBe('findOneAndDelete'); @@ -1280,7 +1302,7 @@ describe('MongoCollection write methods', () => { it('passes MongoFilterExpr to command', async () => { const executor = createMockExecutor([{ _id: 'id-1', name: 'Alice', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('_id', 'id-1')).delete(); const command = executor.lastCommand!; expect(command.kind).toBe('findOneAndDelete'); @@ -1291,14 +1313,14 @@ describe('MongoCollection write methods', () => { it('returns null when no match', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const result = await col.where(MongoFieldFilter.eq('_id', 'none')).delete(); expect(result).toBeNull(); }); it('attaches the model result shape so the returned document decodes like a read', async () => { const executor = createMockExecutor([{ _id: 'id-1', name: 'Alice', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('_id', 'id-1')).delete(); const shape = executor.lastPlan!.resultShape; expect(shape).toBeDefined(); @@ -1309,13 +1331,13 @@ describe('MongoCollection write methods', () => { describe('deleteAndCount()', () => { it('throws without .where()', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect(col.deleteAndCount()).rejects.toThrow('requires a .where()'); }); it('returns the deleted count', async () => { const executor = createMockExecutor({ affectedRows: 2 }); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const count = await col.where(MongoFieldFilter.eq('email', 'x')).deleteAndCount(); expect(count).toBe(2); expect(executor.lastOperation).toBe('execute'); @@ -1325,7 +1347,7 @@ describe('MongoCollection write methods', () => { describe('upsert()', () => { it('sends findOneAndUpdate with upsert true', async () => { const executor = createMockExecutor([{ _id: 'new-id', name: 'Alice', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const result = await col.where(MongoFieldFilter.eq('email', 'a@b.c')).upsert({ create: defaultUserData, update: { name: 'Alice Updated' }, @@ -1336,7 +1358,7 @@ describe('MongoCollection write methods', () => { it('attaches the model result shape so the returned document decodes like a read', async () => { const executor = createMockExecutor([{ _id: 'new-id', name: 'Alice', email: 'a@b.c' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.where(MongoFieldFilter.eq('email', 'a@b.c')).upsert({ create: defaultUserData, update: { name: 'Alice Updated' }, @@ -1348,7 +1370,7 @@ describe('MongoCollection write methods', () => { it('throws without .where()', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect( col.upsert({ create: { ...defaultUserData, name: 'A' }, @@ -1360,7 +1382,7 @@ describe('MongoCollection write methods', () => { describe('windowing rejection on mutations', () => { function withFilter(executor: MongoQueryExecutor) { - return createMongoCollection(contract, 'User', executor, noEnums).where( + return createMongoCollection(collectionContext(contract, executor), 'User').where( MongoFieldFilter.eq('name', 'Alice'), ); } @@ -1425,7 +1447,7 @@ describe('MongoCollection write methods', () => { { _id: 'id-2', name: 'Updated', email: 'b@b.c' }, ], ); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const rows: unknown[] = []; for await (const row of col .where(MongoFieldFilter.eq('name', 'Alice')) @@ -1448,7 +1470,7 @@ describe('MongoCollection write methods', () => { describe('include rejection on write terminals', () => { function taskWithInclude(executor: MongoQueryExecutor) { - return createMongoCollection(contract, 'Task', executor, noEnums) + return createMongoCollection(collectionContext(contract, executor), 'Task') .where(MongoFieldFilter.eq('title', 'test')) .include('assignee'); } @@ -1456,7 +1478,7 @@ describe('MongoCollection write methods', () => { it('create() throws with .include()', async () => { const executor = createMockExecutor(); await expect( - createMongoCollection(contract, 'Task', executor, noEnums) + createMongoCollection(collectionContext(contract, executor), 'Task') .include('assignee') .create({ title: 'test', type: 'bug', assigneeId: 'u1' }), ).rejects.toThrow('include'); @@ -1465,7 +1487,7 @@ describe('MongoCollection write methods', () => { it('createAll() throws with .include()', () => { const executor = createMockExecutor(); expect(() => - createMongoCollection(contract, 'Task', executor, noEnums) + createMongoCollection(collectionContext(contract, executor), 'Task') .include('assignee') .createAll([{ title: 'test', type: 'bug', assigneeId: 'u1' }]), ).toThrow('include'); @@ -1495,7 +1517,7 @@ describe('MongoCollection write methods', () => { describe('undefined normalization on create paths', () => { it('create() leaves an undefined value out of the inserted document', async () => { const executor = createMockExecutor([{ insertedId: 'new-id', document: { _id: 'new-id' } }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const input = { name: 'Alice', email: 'a@b.c', extra: undefined } as Record; await col.create(input as never); const command = executor.plans[0]!.command; @@ -1509,7 +1531,7 @@ describe('MongoCollection write methods', () => { const executor = createMockExecutor([ { insertedIds: ['id-1'], insertedCount: 1, documents: [{ _id: 'id-1' }] }, ]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const input = [{ name: 'Alice', email: 'a@b.c', extra: undefined }] as Record< string, unknown @@ -1527,7 +1549,7 @@ describe('MongoCollection write methods', () => { describe('_id rejection on update paths', () => { it('update() throws when _id is in update data', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect( col.where(MongoFieldFilter.eq('_id', 'id-1')).update({ _id: 'new-id', name: 'X' }), ).rejects.toThrow('_id'); @@ -1535,7 +1557,7 @@ describe('MongoCollection write methods', () => { it('updateAndCount() throws when _id is in update data', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect( col.where(MongoFieldFilter.eq('_id', 'id-1')).updateAndCount({ _id: 'new-id' }), ).rejects.toThrow('_id'); @@ -1543,7 +1565,7 @@ describe('MongoCollection write methods', () => { it('updateAll() throws when _id is in update data', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const result = col .where(MongoFieldFilter.eq('_id', 'id-1')) .updateAll({ _id: 'new-id', name: 'X' }); @@ -1556,7 +1578,7 @@ describe('MongoCollection write methods', () => { it('upsert() throws when _id is in update data', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect( col.where(MongoFieldFilter.eq('email', 'a@b.c')).upsert({ create: defaultUserData, @@ -1567,7 +1589,7 @@ describe('MongoCollection write methods', () => { it('update() with callback throws when _id is targeted', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect( col.where(MongoFieldFilter.eq('_id', 'id-1')).update((u) => [u._id.set('new-id')]), ).rejects.toThrow('_id'); @@ -1575,7 +1597,7 @@ describe('MongoCollection write methods', () => { it('updateAll() with callback throws when _id is targeted', async () => { const executor = createMockExecutor([{ _id: 'id-1' }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); const result = col .where(MongoFieldFilter.eq('_id', 'id-1')) .updateAll((u: FieldAccessor) => [u._id.set('new-id')]); @@ -1588,7 +1610,7 @@ describe('MongoCollection write methods', () => { it('upsert() with callback throws when _id is targeted', async () => { const executor = createMockExecutor(); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await expect( col.where(MongoFieldFilter.eq('email', 'a@b.c')).upsert({ create: defaultUserData, @@ -1601,7 +1623,7 @@ describe('MongoCollection write methods', () => { describe('immutability', () => { it('write methods do not mutate collection state', async () => { const executor = createMockExecutor([{ insertedId: 'x', document: { _id: 'x' } }]); - const col = createMongoCollection(contract, 'User', executor, noEnums); + const col = createMongoCollection(collectionContext(contract, executor), 'User'); await col.create(defaultUserData); const filtered = col.where(MongoFieldFilter.eq('name', 'Alice')); expect(filtered).not.toBe(col); diff --git a/packages/2-mongo-family/5-query-builders/orm/test/collection.variant-fields.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/collection.variant-fields.test.ts index e1054a63374c..38ec0cf46f24 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/collection.variant-fields.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/collection.variant-fields.test.ts @@ -6,7 +6,7 @@ import type { Contract } from '../../../1-foundation/mongo-contract/test/fixture import ormContractJson from '../../../1-foundation/mongo-contract/test/fixtures/orm-contract.json'; import { createMongoCollection } from '../src/collection'; import type { MongoQueryExecutor } from '../src/executor'; -import { noEnums } from './no-enums'; +import { collectionContext } from './collection-context'; const contract = ormContractJson as unknown as Contract; @@ -37,7 +37,7 @@ function paramRefs(value: unknown): MongoParamRef[] { describe('a variant collection', () => { it("encodes a field declared only on the variant with the variant field's codec", async () => { const { executor, plans } = recordingExecutor(); - await createMongoCollection(contract, 'Task', executor, noEnums) + await createMongoCollection(collectionContext(contract, executor), 'Task') .variant('bug') .create({ title: 'Crash', assigneeId: 'a1', severity: 'high', comments: [] }); @@ -53,7 +53,10 @@ describe('a variant collection', () => { it("decodes a field declared only on the variant with the variant field's codec", async () => { const { executor, plans } = recordingExecutor(); - await createMongoCollection(contract, 'Task', executor, noEnums).variant('bug').all().toArray(); + await createMongoCollection(collectionContext(contract, executor), 'Task') + .variant('bug') + .all() + .toArray(); expect(plans[0]?.resultShape).toMatchObject({ kind: 'document', diff --git a/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts index 47c309447ddd..6d0982ad5b94 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts @@ -2,7 +2,6 @@ import mongoRuntimeAdapter from '@internal/adapter-mongo/runtime'; import { createMongoDriver } from '@internal/driver-mongo'; import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { - buildMongoEnums, createMongoExecutionContext, createMongoExecutionStack, createMongoRuntime, @@ -15,7 +14,6 @@ import { MongoMemoryReplSet } from 'mongodb-memory-server'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import type { Contract } from '../../../../1-foundation/mongo-contract/test/fixtures/orm-contract'; import ormContractJson from '../../../../1-foundation/mongo-contract/test/fixtures/orm-contract.json'; -import type { MongoOrmEnums } from '../../src/collection'; import type { FieldAccessor } from '../../src/field-accessor'; import { mongoOrm } from '../../src/mongo-orm'; @@ -39,7 +37,6 @@ describe('ORM ergonomics integration', { let replSet: MongoMemoryReplSet; let client: MongoClient; let runtime: MongoRuntime; - let enums: MongoOrmEnums; let context: MongoExecutionContext; const dbName = 'orm_ergonomics_test'; @@ -57,7 +54,6 @@ describe('ORM ergonomics integration', { context = createMongoExecutionContext({ contract, stack }); const driver = await createMongoDriver(replSet.getUri(), dbName); runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); - enums = buildMongoEnums(contract, context.codecs); }, timeouts.spinUpMongoMemoryServer); beforeEach(async () => { @@ -70,7 +66,7 @@ describe('ORM ergonomics integration', { describe('write results decode through codecs (issue #577)', () => { it('create() returns _id as the decoded hex string a read returns', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const created = await orm.users.create(defaultUserData); expect(typeof created._id).toBe('string'); const fetched = await orm.users.where({ _id: created._id as string }).first(); @@ -78,7 +74,7 @@ describe('ORM ergonomics integration', { }); it('createAll() returns decoded _ids', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const rows: Record[] = []; for await (const row of orm.users.createAll([ defaultUserData, @@ -93,7 +89,7 @@ describe('ORM ergonomics integration', { }); it('update() returns the document decoded like a read', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const created = await orm.users.create(defaultUserData); const updated = await orm.users .where({ _id: created._id as string }) @@ -103,7 +99,7 @@ describe('ORM ergonomics integration', { }); it('delete() returns the document decoded like a read', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const created = await orm.users.create(defaultUserData); const deleted = await orm.users.where({ _id: created._id as string }).delete(); expect(deleted).not.toBeNull(); @@ -113,7 +109,7 @@ describe('ORM ergonomics integration', { describe('codec-aware where()', () => { it('retrieves document by ObjectId field using object where', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create(defaultUserData); const found = await orm.users.where({ _id: user._id as string }).first(); expect(found).not.toBeNull(); @@ -121,7 +117,7 @@ describe('ORM ergonomics integration', { }); it('retrieves document by string field using object where', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); await orm.users.create(defaultUserData); const found = await orm.users.where({ name: 'Alice' }).first(); expect(found).not.toBeNull(); @@ -129,7 +125,7 @@ describe('ORM ergonomics integration', { }); it('firstOrThrow returns the matching document and rejects when none matches', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); await orm.users.create(defaultUserData); await orm.users.create({ ...defaultUserData, name: 'Bob', email: 'bob@test.com' }); const found = await orm.users.where({ name: 'Bob' }).firstOrThrow(); @@ -144,7 +140,7 @@ describe('ORM ergonomics integration', { }); it('filters by multiple fields using object where', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); await orm.users.create(defaultUserData); await orm.users.create({ ...defaultUserData, name: 'Bob', email: 'bob@test.com' }); const found = await orm.users.where({ name: 'Alice', email: 'alice@test.com' }).first(); @@ -155,7 +151,7 @@ describe('ORM ergonomics integration', { describe('field accessor mutations', () => { it('$push adds element to array field', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create(defaultUserData); const updated = await orm.users .where({ _id: user._id as string }) @@ -168,7 +164,7 @@ describe('ORM ergonomics integration', { }); it('$pull removes element from array field', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create({ ...defaultUserData, tags: ['admin', 'editor'] }); await orm.users.where({ _id: user._id as string }).update((u) => [u.tags.pull('admin')]); @@ -178,7 +174,7 @@ describe('ORM ergonomics integration', { }); it('$inc increments numeric field', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create(defaultUserData); await orm.users.where({ _id: user._id as string }).update((u) => [u.loginCount.inc(1)]); @@ -188,7 +184,7 @@ describe('ORM ergonomics integration', { }); it('dot-path $set updates nested value object field', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create({ ...defaultUserData, homeAddress: { city: 'SF', country: 'US' }, @@ -203,7 +199,7 @@ describe('ORM ergonomics integration', { }); it('multiple operations in one callback are applied together', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create(defaultUserData); await orm.users .where({ _id: user._id as string }) @@ -216,7 +212,7 @@ describe('ORM ergonomics integration', { }); it('updateAll with callback updates multiple documents', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); await orm.users.create(defaultUserData); await orm.users.create({ ...defaultUserData, name: 'Bob', email: 'bob@test.com' }); @@ -237,7 +233,7 @@ describe('ORM ergonomics integration', { describe('upsert() dot-path guard', () => { it('throws when callback uses a dot-path operation', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); await orm.users.create(defaultUserData); await expect( orm.users.where({ name: 'Alice' }).upsert({ @@ -250,7 +246,7 @@ describe('ORM ergonomics integration', { describe('reference relation include', () => { it('include() on 1:N relation returns array of related documents', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create(defaultUserData); await orm.tasks.create({ @@ -276,7 +272,7 @@ describe('ORM ergonomics integration', { }); it('include() on 1:N returns empty array when no related documents', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create(defaultUserData); const result = await orm.users @@ -289,7 +285,7 @@ describe('ORM ergonomics integration', { }); it('select() keeps a 1:N include whose join key is not selected', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create(defaultUserData); await orm.tasks.create({ title: 'Task 1', @@ -312,7 +308,7 @@ describe('ORM ergonomics integration', { }); it('select() keeps an N:1 include whose join key is not selected', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create(defaultUserData); await orm.tasks.create({ title: 'Task 1', diff --git a/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts index 82f7987c7b63..9006183c4657 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/integration/polymorphism.test.ts @@ -2,7 +2,6 @@ import mongoRuntimeAdapter from '@internal/adapter-mongo/runtime'; import { createMongoDriver } from '@internal/driver-mongo'; import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { - buildMongoEnums, createMongoExecutionContext, createMongoExecutionStack, createMongoRuntime, @@ -15,7 +14,6 @@ import { MongoMemoryReplSet } from 'mongodb-memory-server'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import type { Contract } from '../../../../1-foundation/mongo-contract/test/fixtures/orm-contract'; import ormContractJson from '../../../../1-foundation/mongo-contract/test/fixtures/orm-contract.json'; -import type { MongoOrmEnums } from '../../src/collection'; import { mongoOrm } from '../../src/mongo-orm'; const contract = ormContractJson as unknown as Contract; @@ -26,7 +24,6 @@ describe('Mongo ORM polymorphism integration', { let replSet: MongoMemoryReplSet; let client: MongoClient; let runtime: MongoRuntime; - let enums: MongoOrmEnums; let context: MongoExecutionContext; const dbName = 'polymorphism_test'; @@ -44,7 +41,6 @@ describe('Mongo ORM polymorphism integration', { context = createMongoExecutionContext({ contract, stack }); const driver = await createMongoDriver(replSet.getUri(), dbName); runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); - enums = buildMongoEnums(contract, context.codecs); }, timeouts.spinUpMongoMemoryServer); beforeEach(async () => { @@ -56,7 +52,7 @@ describe('Mongo ORM polymorphism integration', { }, timeouts.spinUpMongoMemoryServer); it('base query returns rows with discriminator values', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -83,7 +79,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant("bug") filters to only Bug rows', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -110,7 +106,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant("feature") filters to only Feature rows', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -137,7 +133,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant create injects discriminator and persists it', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -161,7 +157,7 @@ describe('Mongo ORM polymorphism integration', { }); it('round-trip: create via variant, read back via base', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -192,7 +188,7 @@ describe('Mongo ORM polymorphism integration', { }); it('non-polymorphic model unaffected by polymorphism changes', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); await orm.users.createAll([ { @@ -218,7 +214,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant().first() returns narrowed result', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -240,7 +236,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant createAll injects discriminator into each document', async () => { - const orm = mongoOrm({ context, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', diff --git a/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts b/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts index 26c66edf931a..ce36ba89be45 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts @@ -1,7 +1,9 @@ -import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; -import { test } from 'vitest'; +import type { + MongoEnumAccessors, + MongoExecutionContext, +} from '@internal/mongo-query-ast/execution'; +import { expectTypeOf, test } from 'vitest'; import type { Contract } from '../../../1-foundation/mongo-contract/test/fixtures/orm-contract'; -import type { MongoOrmEnums } from '../src/collection'; import { createMongoCollection } from '../src/collection'; import type { MongoQueryExecutor } from '../src/executor'; import { mongoOrm } from '../src/mongo-orm'; @@ -9,21 +11,33 @@ import { mongoRaw } from '../src/mongo-raw'; declare const contract: Contract; declare const context: MongoExecutionContext; -declare const executor: MongoQueryExecutor; -declare const enums: MongoOrmEnums; +declare const runtime: MongoQueryExecutor; +declare const enums: MongoEnumAccessors; -test("mongoOrm() and createMongoCollection() require the contract's enum accessors", () => { - // @ts-expect-error a written enum value is checked against the contract's enum accessors - mongoOrm({ context, executor }); - // @ts-expect-error a written enum value is checked against the contract's enum accessors - createMongoCollection(contract, 'User', executor); +test('mongoOrm() takes a context and a runtime, as the SQL orm() does', () => { + mongoOrm({ context, runtime }); + // @ts-expect-error the enum accessors come from the context + mongoOrm({ context, runtime, enums }); + // @ts-expect-error the option is named runtime, as in the SQL orm() + mongoOrm({ context, executor: runtime }); }); test('mongoOrm() and mongoRaw() take an execution context, not a contract', () => { - mongoOrm({ context, executor, enums }); mongoRaw({ context }); // @ts-expect-error a contract alone is not accepted; build the context with mongoStatic() or mongo() - mongoOrm({ contract, executor, enums }); + mongoOrm({ contract, runtime }); // @ts-expect-error a contract alone is not accepted; build the context with mongoStatic() or mongo() mongoRaw({ contract }); }); + +test('createMongoCollection() takes a context and a runtime, as a SQL Collection does', () => { + const users = createMongoCollection({ context, runtime }, 'User'); + expectTypeOf(users.create).toBeFunction(); + // @ts-expect-error a contract alone is not accepted; the context carries the mutation defaults + createMongoCollection(contract, 'User', runtime, enums); +}); + +test('the execution context is constrained to a Mongo contract, as the SQL ExecutionContext is', () => { + // @ts-expect-error the contract must be a Mongo contract + type _NotAContract = MongoExecutionContext<{ readonly anything: true }>; +}); diff --git a/packages/2-mongo-family/5-query-builders/orm/test/no-enums.ts b/packages/2-mongo-family/5-query-builders/orm/test/no-enums.ts deleted file mode 100644 index 930b5b3f75af..000000000000 --- a/packages/2-mongo-family/5-query-builders/orm/test/no-enums.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { MongoOrmEnums } from '../src/collection'; - -/** The enum accessors of a contract without enums. */ -export const noEnums: MongoOrmEnums = {}; diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts index 162a5e5d129b..937b9f7045cc 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts @@ -1,6 +1,9 @@ +import type { MongoContract } from '@internal/mongo-contract'; import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; -export function contextOf(contract: TContract): MongoExecutionContext { +export function contextOf( + contract: TContract, +): MongoExecutionContext { return { contract, codecs: { get: () => undefined, has: () => false }, diff --git a/packages/2-mongo-family/7-runtime/README.md b/packages/2-mongo-family/7-runtime/README.md index 073923f8da39..1360d64ccbdf 100644 --- a/packages/2-mongo-family/7-runtime/README.md +++ b/packages/2-mongo-family/7-runtime/README.md @@ -48,7 +48,7 @@ Custom or third-party codecs (encryption, vendor scalars) are contributed via an ## Responsibilities - **Stack/context composition**: `createMongoExecutionStack` and `createMongoExecutionContext` mirror SQL's `createSqlExecutionStack` / `createExecutionContext`. The context aggregates codec contributions from `[stack.target, stack.adapter, ...stack.extensions]` into a single `MongoCodecRegistry`. The `MongoExecutionContext` type is declared in `@internal/mongo-query-ast`, as SQL's `ExecutionContext` is declared in relational-core, and holds no stack: the query lanes build against it, and the runtime receives the adapter as its own option. -- **Enum accessors**: `buildMongoEnums(contract, context.codecs)` builds the contract's enum accessors by namespace: what `db.enums` holds and what `mongoOrm({ enums })` checks a written enum value against. A contract whose enum codec the context lacks is refused here with `RUNTIME.CODEC_DESCRIPTOR_MISSING`. +- **Enum accessors**: `buildMongoEnums(contract, context.codecs)`, declared in `@internal/mongo-query-ast` and exported here, builds the contract's enum accessors by namespace: what `db.enums` holds and what the ORM checks a written enum value against. A contract whose enum codec the context lacks is refused here with `RUNTIME.CODEC_DESCRIPTOR_MISSING`. - **Runtime executor**: `createMongoRuntime({ context, adapter, driver, ... })` composes context, adapter and driver into a `MongoRuntime`, as the SQL runtime does. The caller creates the adapter with `stack.adapter.create(stack)`, and the runtime checks middleware against the target named by `context.contract.target`. The runtime has separate `query(plan)` and `execute(plan)` operations accepting `MongoQueryPlan` from `@internal/mongo-query-ast`. `query()` yields decoded rows, while `execute()` returns statement statistics. Both operations lower the plan with the adapter, and run the wire command on the driver; `query()` **optionally decodes** each row when `plan.resultShape` is present. - **Operation flow**: Row-producing work uses `query(plan)` and the query middleware lifecycle; statistics-producing work uses `execute(plan)` and the execute middleware lifecycle. Each operation allocates its own `CodecCallContext` and threads it through its codec dispatches. - **Lowering**: Happens in the adapter (`lower(plan)`), wrapped by the runtime's `lower` override into a `MongoExecutionPlan`. diff --git a/packages/2-mongo-family/7-runtime/src/exports/index.ts b/packages/2-mongo-family/7-runtime/src/exports/index.ts index b12404145cd5..78321dc57db9 100644 --- a/packages/2-mongo-family/7-runtime/src/exports/index.ts +++ b/packages/2-mongo-family/7-runtime/src/exports/index.ts @@ -1,7 +1,10 @@ export type { RuntimeTargetInstance } from '@internal/framework-components/execution'; -export type { MongoCodecLookup, MongoExecutionContext } from '@internal/mongo-query-ast/execution'; -export type { MongoEnumAccessors } from '../mongo-enums'; -export { buildMongoEnums } from '../mongo-enums'; +export type { + MongoCodecLookup, + MongoEnumAccessors, + MongoExecutionContext, +} from '@internal/mongo-query-ast/execution'; +export { buildMongoEnums } from '@internal/mongo-query-ast/execution'; export type { MongoExecutionPlan } from '../mongo-execution-plan'; export type { MongoExecutionStack, diff --git a/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts index 30b594fd6106..5a4c31524a3b 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts @@ -1,4 +1,3 @@ -import type { ContractExecutionSection } from '@internal/contract/types'; import { createExecutionStack, type RuntimeAdapterDescriptor, @@ -18,9 +17,9 @@ import { } from '@internal/framework-components/runtime'; import type { MongoCodec } from '@internal/mongo-codec'; import { type MongoCodecRegistry, newMongoCodecRegistry } from '@internal/mongo-codec'; +import type { MongoContract } from '@internal/mongo-contract'; import type { MongoAdapter } from '@internal/mongo-lowering'; import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; -import { blindCast } from '@internal/utils/casts'; /** * Mongo-specific static contributions a runtime descriptor declares. @@ -86,7 +85,7 @@ export function createMongoExecutionStack(op } export function createMongoExecutionContext< - TContract = unknown, + TContract extends MongoContract = MongoContract, TTargetId extends string = 'mongo', >(options: { readonly contract: TContract; @@ -102,7 +101,7 @@ export function createMongoExecutionContext< ]; const generators = collectMutationDefaultGenerators(contributors); - const execution = executionOf(options.contract); + const execution = options.contract.execution; assertMutationDefaultGeneratorsAvailable(execution, generators); for (const contributor of contributors) { @@ -129,13 +128,6 @@ export function createMongoExecutionContext< }); } -function executionOf(contract: unknown): ContractExecutionSection | undefined { - return blindCast< - { readonly execution?: ContractExecutionSection } | null | undefined, - 'the execution context receives a validated contract, whose execution section (when present) has the framework shape' - >(contract)?.execution; -} - function* iterateCodecs(registry: MongoCodecRegistry): Iterable> { yield* registry.values(); } diff --git a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts index 54eb43673706..232db0b56933 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts @@ -13,7 +13,6 @@ import { type StatementActivityRun, streamStatement, } from '@internal/framework-components/runtime'; -import type { MongoContract } from '@internal/mongo-contract'; import type { MongoAdapter, MongoDriver } from '@internal/mongo-lowering'; import type { MongoCodecLookup, @@ -71,7 +70,7 @@ function invalidStatisticsResult(commandKind: string, countField: string): Error * The runtime takes a {@link MongoExecutionContext} (built via `createMongoExecutionContext`), an adapter, and a driver, as the SQL runtime does. Codecs and the target come from the context. See ADR — Mongo result-shape as a structural plan field, § Codec registry: stack aggregation, not user threading. */ export interface MongoRuntimeOptions { - readonly context: MongoExecutionContext; + readonly context: MongoExecutionContext; readonly adapter: MongoAdapter; readonly driver: MongoDriver; readonly middleware?: readonly MongoMiddleware[]; diff --git a/packages/2-mongo-family/7-runtime/test/execution-context.test.ts b/packages/2-mongo-family/7-runtime/test/execution-context.test.ts index 156de3255bff..60cd53c351da 100644 --- a/packages/2-mongo-family/7-runtime/test/execution-context.test.ts +++ b/packages/2-mongo-family/7-runtime/test/execution-context.test.ts @@ -1,7 +1,8 @@ -import type { ExecutionMutationDefault } from '@internal/contract/types'; +import { type ExecutionMutationDefault, executionHash } from '@internal/contract/types'; import { type DataTypeValue, dataType, readJsonString } from '@internal/framework-components/codec'; import type { RuntimeMutationDefaultGenerator } from '@internal/framework-components/runtime'; import { mongoCodec, newMongoCodecRegistry } from '@internal/mongo-codec'; +import type { MongoContract } from '@internal/mongo-contract'; import { describe, expect, it } from 'vitest'; import { createMongoExecutionContext, @@ -10,6 +11,7 @@ import { type MongoRuntimeExtensionDescriptor, type MongoRuntimeTargetDescriptor, } from '../src/mongo-execution-stack'; +import { testContract } from './mongo-middleware-fixtures'; const NS = '__unbound__'; @@ -84,8 +86,11 @@ function counter(id: string, stability: RuntimeMutationDefaultGenerator['stabili return { id, stability, generate: () => ++next } satisfies RuntimeMutationDefaultGenerator; } -function contractWith(defaults: readonly ExecutionMutationDefault[]) { - return { execution: { executionHash: 'test', mutations: { defaults } } }; +function contractWith(defaults: readonly ExecutionMutationDefault[]): MongoContract { + return { + ...testContract, + execution: { executionHash: executionHash('test'), mutations: { defaults } }, + }; } function contextFor( @@ -124,7 +129,7 @@ describe('createMongoExecutionContext composition', () => { adapter: adapter(), extensions: [extension('test-extension', { codecIds: ['test/extension@1'] })], }); - const context = createMongoExecutionContext({ contract: {}, stack }); + const context = createMongoExecutionContext({ contract: testContract, stack }); for (const id of ['test/target@1', 'test/adapter@1', 'test/extension@1']) { expect(context.codecs.has(id)).toBe(true); } @@ -137,7 +142,7 @@ describe('createMongoExecutionContext composition', () => { adapter: adapter(), extensions: [extension('test-extension', { codecIds: ['test/adapter@1'] })], }); - expect(() => createMongoExecutionContext({ contract: {}, stack })).toThrow( + expect(() => createMongoExecutionContext({ contract: testContract, stack })).toThrow( expect.objectContaining({ code: 'RUNTIME.DUPLICATE_CODEC', details: expect.objectContaining({ @@ -154,7 +159,7 @@ describe('createMongoExecutionContext composition', () => { adapter: adapter([counter('clock', 'query')]), extensions: [extension('test-extension', { generators: [counter('clock', 'field')] })], }); - expect(() => createMongoExecutionContext({ contract: {}, stack })).toThrow( + expect(() => createMongoExecutionContext({ contract: testContract, stack })).toThrow( expect.objectContaining({ code: 'RUNTIME.DUPLICATE_MUTATION_DEFAULT_GENERATOR', details: expect.objectContaining({ @@ -189,7 +194,7 @@ describe('createMongoExecutionContext composition', () => { it('applies nothing for a contract without an execution section', () => { const stack = createMongoExecutionStack({ target: target(), adapter: adapter() }); - const context = createMongoExecutionContext({ contract: {}, stack }); + const context = createMongoExecutionContext({ contract: testContract, stack }); expect( context.applyMutationDefaults({ op: 'create', namespace: NS, entry: 'posts', values: {} }), ).toEqual([]); diff --git a/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts b/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts index 79994ce5aeef..8cd7ed214129 100644 --- a/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts +++ b/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts @@ -86,8 +86,7 @@ export default function mongo< const contract = validateMongoContract(options.contract); let binding = resolveOptionalMongoBinding(options); - const { context, stack, query, enums, enumsByNamespace, raw } = - buildMongoStaticContext(contract); + const { context, stack, query, enums, raw } = buildMongoStaticContext(contract); // Single source of truth for the lifecycle. `runtimePromise` is the in-flight // or settled build; `closed` is the terminal state set by `close()`. A failed @@ -152,8 +151,7 @@ export default function mongo< const orm = mongoOrm({ context, - executor: { query: queryRows, execute: executeStats }, - enums: enumsByNamespace, + runtime: { query: queryRows, execute: executeStats }, }); return { diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts index ff161318ade2..995726819a30 100644 --- a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts +++ b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts @@ -13,7 +13,6 @@ import { buildMongoEnums, createMongoExecutionContext, createMongoExecutionStack, - type MongoEnumAccessors, type MongoExecutionContext, type MongoExecutionStack, } from '@internal/mongo-runtime'; @@ -35,11 +34,10 @@ export interface MongoStaticContext< readonly raw: MongoRawClient; } -/** A {@link MongoStaticContext} with the enum accessors of every namespace, which `mongo()` hands its ORM. */ +/** A {@link MongoStaticContext} with the stack `mongo()` creates its adapter from. */ export interface MongoClientStaticContext< TContract extends MongoContractWithTypeMaps, > extends MongoStaticContext { - readonly enumsByNamespace: MongoEnumAccessors; readonly stack: MongoExecutionStack; } @@ -51,8 +49,7 @@ export function buildMongoStaticContext< adapter: mongoRuntimeAdapter, }); const context = createMongoExecutionContext({ contract, stack }); - const enumsByNamespace = buildMongoEnums(contract, context.codecs); - const unbound = enumsByNamespace[UNBOUND_NAMESPACE_ID]; + const unbound = buildMongoEnums(contract, context.codecs)[UNBOUND_NAMESPACE_ID]; assertDefined(unbound, 'the unbound namespace always exists on a mongo builder output'); const enums = blindCast< UnboundEnums, @@ -60,7 +57,7 @@ export function buildMongoStaticContext< >(unbound); const query = mongoQuery({ context }); const raw = mongoRaw({ context }); - return { context, contract, enums, enumsByNamespace, query, raw, stack }; + return { context, contract, enums, query, raw, stack }; } /** diff --git a/packages/3-targets/mongo/9-facade/test/collection-mutation-defaults.test.ts b/packages/3-targets/mongo/9-facade/test/collection-mutation-defaults.test.ts new file mode 100644 index 000000000000..9d67fc8b3a86 --- /dev/null +++ b/packages/3-targets/mongo/9-facade/test/collection-mutation-defaults.test.ts @@ -0,0 +1,58 @@ +import { AsyncIterableResult } from '@internal/framework-components/runtime'; +import { + createMongoCollection, + type MongoQueryExecutor, + type MongoQueryPlan, +} from '@internal/mongo-orm'; +import { describe, expect, it } from 'vitest'; +import { defineContract } from '../src/exports/contract-builder'; +import mongoStatic from '../src/static/mongo-static'; + +const contract = defineContract({}, ({ field, model, gen }) => ({ + models: { + Post: model('Post', { + collection: 'posts', + fields: { + _id: field.objectId(), + title: field.string(), + createdAt: field.date().onCreate(gen.now()), + }, + }), + }, +})); + +function recordingRuntime(): { runtime: MongoQueryExecutor; plans: MongoQueryPlan[] } { + const plans: MongoQueryPlan[] = []; + const runtime: MongoQueryExecutor = { + query: (plan: MongoQueryPlan) => { + plans.push(plan); + return new AsyncIterableResult( + (async function* () { + yield { insertedId: 'p1', document: { _id: 'p1' } } as Row; + })(), + ); + }, + execute: async (plan) => { + plans.push(plan); + return { affectedRows: 1 }; + }, + }; + return { runtime, plans }; +} + +describe('a collection created from a context', () => { + it('fills the generated fields of the context contract on create', async () => { + const { context } = mongoStatic({ contract }); + const { runtime, plans } = recordingRuntime(); + const posts = createMongoCollection({ context, runtime }, 'Post'); + + await posts.create({ title: 'Hello' }); + + expect(plans[0]?.command).toMatchObject({ + document: { + title: { value: 'Hello' }, + createdAt: { codecId: 'mongo/date@1', value: expect.any(Date) }, + }, + }); + }); +}); diff --git a/packages/3-targets/mongo/9-facade/test/contract-from-json.test.ts b/packages/3-targets/mongo/9-facade/test/contract-from-json.test.ts index 83b4ea35d322..f4d97947a5d9 100644 --- a/packages/3-targets/mongo/9-facade/test/contract-from-json.test.ts +++ b/packages/3-targets/mongo/9-facade/test/contract-from-json.test.ts @@ -53,6 +53,22 @@ describe('the query builder of a static context', () => { ); }); + it('rejects a malformed contract with the same code and message as mongo()', () => { + const malformed = { target: 'mongo' } as unknown as Contract; + const validationFailure = expect.objectContaining({ + code: 'CONTRACT.VALIDATION_FAILED', + message: [ + 'Contract structural validation failed: domain must be an object (was missing)', + 'roots must be an object (was missing)', + 'storage must be an object (was missing)', + 'targetFamily must be "mongo" (was missing)', + ].join('\n'), + }); + + expect(() => mongo({ contract: malformed })).toThrow(validationFailure); + expect(() => mongoStatic({ contract: malformed })).toThrow(validationFailure); + }); + it('builds queries over the contract of its context', () => { const { query, contract } = mongoStatic({ contract: authoredContract() }); const root = Object.keys(contract.roots)[0] as keyof typeof contract.roots & string; diff --git a/packages/3-targets/mongo/9-facade/test/enum-members.test.ts b/packages/3-targets/mongo/9-facade/test/enum-members.test.ts index ed3547a1b949..a886834d1a88 100644 --- a/packages/3-targets/mongo/9-facade/test/enum-members.test.ts +++ b/packages/3-targets/mongo/9-facade/test/enum-members.test.ts @@ -101,13 +101,9 @@ describe('db.enums member types', () => { }); }); -describe('the Mongo ORM checks a written enum value against the accessors db.enums holds', () => { +describe('the Mongo ORM checks a written enum value against the enums of its context', () => { it('passes a member to the database and refuses a value outside the enum', async () => { - const orm = mongoOrm({ - context, - executor: unreachableDatabase, - enums: buildMongoEnums(contract, context.codecs), - }); + const orm = mongoOrm({ context, runtime: unreachableDatabase }); await expect(orm.readings.create(reading)).rejects.toThrow('no database'); await expect(orm.readings.create({ ...reading, int32: 7 as never })).rejects.toMatchObject({ code: 'RUNTIME.ENCODE_FAILED', @@ -116,54 +112,10 @@ describe('the Mongo ORM checks a written enum value against the accessors db.enu }); }); - it('refuses accessors without an enum of the contract when it is built', () => { - const { DoubleLevel: _doubleLevel, ...withoutDouble } = Object.getOwnPropertyDescriptors( - buildMongoEnums(contract, context.codecs)['__unbound__'], - ); - const refusalFor = (enumName: string) => - expect.objectContaining({ - code: 'ORM.ARGUMENT_INVALID', - message: `mongoOrm() has no accessor for enum ${enumName} in namespace __unbound__, so it cannot check a value written to a field of that enum. Pass the contract's enum accessors: enums: buildMongoEnums(contract, context.codecs).`, - meta: { argument: 'enums', namespace: '__unbound__', enum: enumName }, - }); - expect(() => mongoOrm({ context, executor: unreachableDatabase, enums: {} })).toThrow( - refusalFor('TextLevel'), - ); - expect(() => - mongoOrm({ - context, - executor: unreachableDatabase, - enums: { __unbound__: Object.defineProperties({}, withoutDouble) }, - }), - ).toThrow(refusalFor('DoubleLevel')); - }); - - it('does not read an accessor when it is built', () => { - const unread = Object.defineProperties( - {}, - Object.fromEntries( - Object.keys(contract.domain.namespaces['__unbound__']?.enum ?? {}).map((name) => [ - name, - { - enumerable: true, - get: () => { - throw new Error(`read ${name}`); - }, - }, - ]), - ), - ); - expect(() => - mongoOrm({ context, executor: unreachableDatabase, enums: { __unbound__: unread } }), - ).not.toThrow(); - }); - - it('refuses a collection write when the collection was built without the accessor of the enum', async () => { - const readings = createMongoCollection(contract, 'Reading', unreachableDatabase, {}); - await expect(readings.create(reading)).rejects.toMatchObject({ - code: 'ORM.ARGUMENT_INVALID', - message: - "The ORM has no accessor for enum TextLevel, so it cannot check the value written to text in collection 'readings'. Pass the contract's enum accessors: enums: buildMongoEnums(contract, context.codecs).", + it('refuses a value outside the enum written through a collection', async () => { + const readings = createMongoCollection({ context, runtime: unreachableDatabase }, 'Reading'); + await expect(readings.create({ ...reading, text: 'medium' as never })).rejects.toMatchObject({ + code: 'RUNTIME.ENCODE_FAILED', }); }); diff --git a/packages/3-targets/mongo/9-facade/test/mongo.test.ts b/packages/3-targets/mongo/9-facade/test/mongo.test.ts index 63dc5d3a24f6..4bb5d3c46c2a 100644 --- a/packages/3-targets/mongo/9-facade/test/mongo.test.ts +++ b/packages/3-targets/mongo/9-facade/test/mongo.test.ts @@ -339,7 +339,7 @@ describe('mongo() facade', () => { 'the ORM %s on a closed client rejects instead of throwing at the call', async (member) => { const db = mongo({ contract: fakeContract, url: 'mongodb://localhost:27017/mydb' }); - const executor = mocks.mongoOrm.mock.calls.at(-1)?.[0].executor; + const executor = mocks.mongoOrm.mock.calls.at(-1)?.[0].runtime; await db.close(); let result: PromiseLike | undefined; diff --git a/test/integration/test/_harness/mongo.ts b/test/integration/test/_harness/mongo.ts index b6abf8e33bb6..041efa570ff6 100644 --- a/test/integration/test/_harness/mongo.ts +++ b/test/integration/test/_harness/mongo.ts @@ -18,7 +18,6 @@ import type { import type { MongoOrmClient } from '@internal/mongo-orm'; import { mongoOrm } from '@internal/mongo-orm'; import { - buildMongoEnums, createMongoExecutionContext, createMongoExecutionStack, createMongoRuntime, @@ -162,11 +161,7 @@ export async function withMongoPort< const runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); try { - const db = mongoOrm({ - context, - executor: runtime, - enums: buildMongoEnums(contract, context.codecs), - }); + const db = mongoOrm({ context, runtime: runtime }); const mongoDb = client.db(dbName); await fn({ db, client, mongoDb, contract }); } finally { diff --git a/test/integration/test/mongo/no-emit.test.ts b/test/integration/test/mongo/no-emit.test.ts index 5907d9c25e6a..c0628b85a798 100644 --- a/test/integration/test/mongo/no-emit.test.ts +++ b/test/integration/test/mongo/no-emit.test.ts @@ -1,5 +1,4 @@ import { mongoOrm } from '@internal/mongo-orm'; -import { buildMongoEnums } from '@internal/mongo-runtime'; import { ObjectId } from 'mongodb'; import { expect, expectTypeOf, it } from 'vitest'; import { contract } from './fixtures/contract'; @@ -28,11 +27,7 @@ describeWithMongoDB('Mongo no-emit integration', (ctx) => { }); const context = executionContextFor(contract); - const orm = mongoOrm({ - context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, context.codecs), - }); + const orm = mongoOrm({ context, runtime: ctx.runtime }); const tasks = await orm.tasks.include('assignee').all(); expect(tasks).toHaveLength(1); diff --git a/test/integration/test/mongo/orm.test.ts b/test/integration/test/mongo/orm.test.ts index 5c786be4141e..a6fb7e7060d9 100644 --- a/test/integration/test/mongo/orm.test.ts +++ b/test/integration/test/mongo/orm.test.ts @@ -1,7 +1,6 @@ import { MongoContractSerializer } from '@internal/family-mongo/ir'; import { mongoOrm } from '@internal/mongo-orm'; import { MongoFieldFilter } from '@internal/mongo-query-ast/execution'; -import { buildMongoEnums } from '@internal/mongo-runtime'; import { ObjectId } from 'mongodb'; import { expect, expectTypeOf, it } from 'vitest'; import type { Contract } from './fixtures/generated/contract'; @@ -57,11 +56,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { { name: 'Bob', email: 'bob@example.com', addresses: [] }, ]); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const results = await orm.users.orderBy({ name: 1 }).all(); expect(results).toHaveLength(2); @@ -76,11 +71,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { { name: 'Bob', email: 'bob@example.com', addresses: [] }, ]); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const results = await orm.users.where(MongoFieldFilter.eq('email', 'alice@example.com')).all(); expect(results).toHaveLength(1); @@ -104,11 +95,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { comments: [], }); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const results = await orm.tasks.include('assignee').all(); expect(results).toHaveLength(1); @@ -129,11 +116,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { ], }); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const users = await orm.users.all(); expect(users).toHaveLength(1); @@ -154,11 +137,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { comments: [{ _id: new ObjectId(), text: 'Found it!', createdAt: new Date('2025-01-01') }], }); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const tasks = await orm.tasks.all(); expect(tasks).toHaveLength(1); @@ -187,11 +166,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { }, ]); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const tasks = await orm.tasks.all(); expect(tasks).toHaveLength(2); @@ -215,11 +190,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { comments: [], }); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const results = await orm.tasks.all(); const r0 = results[0]!; @@ -241,11 +212,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { { name: 'Charlie', email: 'charlie@example.com', addresses: [] }, ]); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const results = await orm.users.where(MongoFieldFilter.eq('name', 'Alice').not()).all(); expect(results).toHaveLength(2); @@ -261,11 +228,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { { name: 'Bob', email: 'bob@example.com', addresses: [] }, ]); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const results = await orm.users.orderBy({ name: 1 }).all(); expect(results).toHaveLength(3); @@ -282,11 +245,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { { name: 'Charlie', email: 'charlie@example.com', addresses: [] }, ]); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const results = await orm.users.orderBy({ name: 1 }).offset(1).limit(1).all(); expect(results).toHaveLength(1); @@ -300,11 +259,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { { name: 'Bob', email: 'bob@example.com', addresses: [] }, ]); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const results = await orm.users.select('name').all(); expect(results).toHaveLength(2); @@ -335,11 +290,7 @@ describeWithMongoDB('mongoOrm integration', (ctx) => { comments: [{ _id: new ObjectId(), text: 'LGTM', createdAt: new Date() }], }); - const orm = mongoOrm({ - context: ctx.context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const orm = mongoOrm({ context: ctx.context, runtime: ctx.runtime }); const users = await orm.users.where(MongoFieldFilter.eq('name', 'Alice')).all(); expect(users).toHaveLength(1); diff --git a/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts b/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts index 47c0d0fe424b..9424cb529364 100644 --- a/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts +++ b/test/integration/test/mongo/runtime/mongo-execution-stack.test.ts @@ -10,6 +10,7 @@ import { } from '@internal/mongo-runtime'; import mongoRuntimeTarget from '@internal/target-mongo/runtime'; import { describe, expect, expectTypeOf, it } from 'vitest'; +import { fixtureContract } from '../setup'; const textType = dataType('test/text', { read: (json) => readJsonString('test/text', json) }); @@ -66,7 +67,7 @@ describe('createMongoExecutionContext', () => { target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - const context = createMongoExecutionContext({ contract: {}, stack }); + const context = createMongoExecutionContext({ contract: fixtureContract, stack }); for (const id of STANDARD_CODEC_IDS) { expect(context.codecs.get(id), `codec ${id} should be registered`).toBeDefined(); } @@ -98,7 +99,7 @@ describe('createMongoExecutionContext', () => { adapter: mongoRuntimeAdapter, extensions: [pack], }); - const context = createMongoExecutionContext({ contract: {}, stack }); + const context = createMongoExecutionContext({ contract: fixtureContract, stack }); expect(context.codecs.get('test/custom@1')).toBe(customCodec); expect(context.codecs.get('mongo/string@1')).toBeDefined(); }); @@ -132,7 +133,7 @@ describe('createMongoExecutionContext', () => { let caught: unknown; try { - createMongoExecutionContext({ contract: {}, stack }); + createMongoExecutionContext({ contract: fixtureContract, stack }); } catch (e) { caught = e; } @@ -154,10 +155,9 @@ describe('createMongoExecutionContext', () => { target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - const contract = { models: {} }; - const context = createMongoExecutionContext({ contract, stack }); + const context = createMongoExecutionContext({ contract: fixtureContract, stack }); expect(Object.isFrozen(context)).toBe(true); - expect(context.contract).toBe(contract); + expect(context.contract).toBe(fixtureContract); expect(Object.keys(context)).not.toContain('stack'); }); @@ -166,10 +166,9 @@ describe('createMongoExecutionContext', () => { target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - const contract = { target: 'mongo' as const, storageHash: 'test' }; - const context = createMongoExecutionContext({ contract, stack }); - expectTypeOf(context).toMatchTypeOf>(); - expectTypeOf(context.contract).toEqualTypeOf(); + const context = createMongoExecutionContext({ contract: fixtureContract, stack }); + expectTypeOf(context).toMatchTypeOf>(); + expectTypeOf(context.contract).toEqualTypeOf(); }); }); diff --git a/test/integration/test/mongo/runtime/mutation-default-generators.test.ts b/test/integration/test/mongo/runtime/mutation-default-generators.test.ts index 81b82ffd8390..128ae77d3c2b 100644 --- a/test/integration/test/mongo/runtime/mutation-default-generators.test.ts +++ b/test/integration/test/mongo/runtime/mutation-default-generators.test.ts @@ -1,7 +1,8 @@ import mongoRuntimeAdapter from '@internal/adapter-mongo/runtime'; -import type { ExecutionMutationDefault } from '@internal/contract/types'; +import { type ExecutionMutationDefault, executionHash } from '@internal/contract/types'; import type { RuntimeMutationDefaultGenerator } from '@internal/framework-components/runtime'; import { newMongoCodecRegistry } from '@internal/mongo-codec'; +import type { MongoContract } from '@internal/mongo-contract'; import { createMongoExecutionContext, createMongoExecutionStack, @@ -9,11 +10,15 @@ import { } from '@internal/mongo-runtime'; import mongoRuntimeTarget from '@internal/target-mongo/runtime'; import { describe, expect, it } from 'vitest'; +import { fixtureContract } from '../setup'; const NS = '__unbound__'; -function contractWith(defaults: readonly ExecutionMutationDefault[]) { - return { execution: { executionHash: 'test', mutations: { defaults } } }; +function contractWith(defaults: readonly ExecutionMutationDefault[]): MongoContract { + return { + ...fixtureContract, + execution: { executionHash: executionHash('test'), mutations: { defaults } }, + }; } function generator(id: string) { diff --git a/test/integration/test/mongo/setup.ts b/test/integration/test/mongo/setup.ts index cbd9fb6deaf2..fd5f93580e32 100644 --- a/test/integration/test/mongo/setup.ts +++ b/test/integration/test/mongo/setup.ts @@ -1,6 +1,7 @@ import mongoRuntimeAdapter from '@internal/adapter-mongo/runtime'; import { createMongoDriver } from '@internal/driver-mongo'; import { MongoContractSerializer } from '@internal/family-mongo/ir'; +import type { MongoContract } from '@internal/mongo-contract'; import { createMongoExecutionContext, createMongoExecutionStack, @@ -21,7 +22,7 @@ export const fixtureContract = new MongoContractSerializer().deserializeContract contractJson, ) as Contract; -export function executionContextFor( +export function executionContextFor( contract: TContract, ): MongoExecutionContext { const stack = createMongoExecutionStack({ diff --git a/test/integration/test/mongo/temporal-generators/temporal-generators.e2e.test.ts b/test/integration/test/mongo/temporal-generators/temporal-generators.e2e.test.ts index 8454f27b147f..9e1544c09e6a 100644 --- a/test/integration/test/mongo/temporal-generators/temporal-generators.e2e.test.ts +++ b/test/integration/test/mongo/temporal-generators/temporal-generators.e2e.test.ts @@ -246,7 +246,7 @@ describe('Mongo date generators end to end', () => { }); describe('Mongo mutation default generator registration', () => { - const contract = JSON.parse(JSON.stringify(contractJson)) as unknown; + const contract = contractFromJson(contractJson); it('throws RUNTIME.MUTATION_DEFAULT_GENERATOR_MISSING when no component provides timestampNow', () => { const { mutationDefaultGenerators: _, ...adapterWithoutGenerators } = mongoRuntimeAdapter; diff --git a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts index fbdc08656d60..c18716666e0e 100644 --- a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts +++ b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts @@ -57,12 +57,24 @@ describe.each(['app', 'extension'] as const)('the %s ORM and raw detection', (au ['a raw client built from a contract', 'const raw = mongoRaw({ contract });'], ['the mutationDefaults option', 'mongoOrm({ context, executor, mutationDefaults: context })'], ['the removed error code', "if (error.code === 'ORM.MUTATION_DEFAULTS_MISSING') {}"], + ['an ORM with an executor', 'mongoOrm({ context, executor: runtime })'], + [ + 'an ORM with enum accessors', + 'mongoOrm({\n context,\n runtime,\n enums: buildMongoEnums(contract, codecs),\n})', + ], + [ + 'a collection built from a contract', + "createMongoCollection(contract, 'User', executor, enums)", + ], + ['the removed enum accessor type', 'let enums: MongoOrmEnums;'], ])('finds %s', (_name, content) => { expect(detects(audience, content, change)).toBe(true); }); it.each([ - ['an ORM built from a context', 'mongoOrm({ context, executor, enums })'], + ['an ORM built from a context', 'mongoOrm({ context, runtime })'], + ['a collection built from a context', "createMongoCollection({ context, runtime }, 'User')"], + ['enum accessors read from a client', 'const enums = db.enums;'], ['a raw client built from a context', 'mongoRaw({ context })'], ['the static raw client', 'const { raw } = mongoStatic({ contract });'], ['a method of the same name', 'db.mongoOrm({ contract })'], @@ -87,6 +99,8 @@ describe.each(['app', 'extension'] as const)('the %s runtime adapter detection', 'a stack built with a driver', 'createMongoExecutionStack({\n target,\n adapter,\n driver: mongoDriver,\n})', ], + ['a context type over unknown', 'MongoExecutionContext'], + ['a context type over an object literal type', 'MongoExecutionContext<{ models: {} }>'], ])('finds %s', (_name, content) => { expect(detects(audience, content, change)).toBe(true); }); @@ -96,6 +110,7 @@ describe.each(['app', 'extension'] as const)('the %s runtime adapter detection', ['a method of the same name', 'factory.createMongoRuntime({ context })'], ['a stack on another object', 'options.stack.adapter'], ['a context type over one contract', 'MongoExecutionContext'], + ['a context type over a contract typeof', 'MongoExecutionContext'], ['a context type over a generic contract', 'MongoExecutionContext>'], ['a stack built without a driver', 'createMongoExecutionStack({ target, adapter })'], ])('skips %s', (_name, content) => { diff --git a/test/integration/test/value-objects/value-objects.e2e.test.ts b/test/integration/test/value-objects/value-objects.e2e.test.ts index 5291ff8f41da..9cab3de215ad 100644 --- a/test/integration/test/value-objects/value-objects.e2e.test.ts +++ b/test/integration/test/value-objects/value-objects.e2e.test.ts @@ -3,7 +3,6 @@ import postgresDriver from '@internal/driver-postgres/runtime'; import { MongoContractSerializer } from '@internal/family-mongo/ir'; import { instantiateExecutionStack } from '@internal/framework-components/execution'; import { mongoOrm } from '@internal/mongo-orm'; -import { buildMongoEnums } from '@internal/mongo-runtime'; import { PostgresRuntimeImpl } from '@internal/postgres/runtime'; import { orm as sqlOrm } from '@internal/sql-orm-client'; import { createExecutionContext, createSqlExecutionStack } from '@internal/sql-runtime'; @@ -25,11 +24,7 @@ describeWithMongoDB('value objects e2e: Mongo → real DB → typed ORM', (ctx) it('create and read value objects with correct types', async () => { const context = executionContextFor(contract); - const ormClient = mongoOrm({ - context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, context.codecs), - }); + const ormClient = mongoOrm({ context, runtime: ctx.runtime }); const shopCollection = ormClient['shop']!; const created = await shopCollection.create({ @@ -81,11 +76,7 @@ describeWithMongoDB('value objects e2e: Mongo → real DB → typed ORM', (ctx) it('non-null value object field roundtrips through update', async () => { const context = executionContextFor(contract); - const ormClient = mongoOrm({ - context, - executor: ctx.runtime, - enums: buildMongoEnums(contract, context.codecs), - }); + const ormClient = mongoOrm({ context, runtime: ctx.runtime }); const shopCollection = ormClient['shop']!; await shopCollection.create({ diff --git a/test/integration/test/value-objects/value-objects.integration.test.ts b/test/integration/test/value-objects/value-objects.integration.test.ts index e08dbef9bcdc..78d81ca27f30 100644 --- a/test/integration/test/value-objects/value-objects.integration.test.ts +++ b/test/integration/test/value-objects/value-objects.integration.test.ts @@ -17,7 +17,6 @@ import { } from '@internal/mongo-contract-psl'; import { mongoContextInput } from '@internal/mongo-contract-psl/test'; import { mongoOrm } from '@internal/mongo-orm'; -import { buildMongoEnums } from '@internal/mongo-runtime'; import { withSeedDiagnostics } from '@internal/psl-parser/interpret'; import { bindPslSchema } from '@internal/psl-parser/test'; import { interpretPslDocumentToSqlContract } from '@internal/sql-contract-psl'; @@ -214,11 +213,7 @@ describeWithMongoDB('value objects: end-to-end Mongo', (ctx) => { const validated = { contract: new MongoContractSerializer().deserializeContract(contract) }; const context = executionContextFor(validated.contract); - const orm = mongoOrm({ - context, - executor: ctx.runtime, - enums: buildMongoEnums(validated.contract, context.codecs), - }); + const orm = mongoOrm({ context, runtime: ctx.runtime }); const userCollection = orm['User']!; type CreateUser = Parameters[0]; @@ -250,11 +245,7 @@ describeWithMongoDB('value objects: end-to-end Mongo', (ctx) => { const validated = { contract: new MongoContractSerializer().deserializeContract(result.value) }; const context = executionContextFor(validated.contract); - const orm = mongoOrm({ - context, - executor: ctx.runtime, - enums: buildMongoEnums(validated.contract, context.codecs), - }); + const orm = mongoOrm({ context, runtime: ctx.runtime }); const userCollection = orm['User']!; type CreateUser = Parameters[0]; @@ -294,11 +285,7 @@ type Address { const validated = { contract: new MongoContractSerializer().deserializeContract(result.value) }; const context = executionContextFor(validated.contract); - const orm = mongoOrm({ - context, - executor: ctx.runtime, - enums: buildMongoEnums(validated.contract, context.codecs), - }); + const orm = mongoOrm({ context, runtime: ctx.runtime }); const userCollection = orm['User']!; await userCollection.create({ name: 'NoAddr', address: null } as unknown as Parameters< diff --git a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md index 898c7751947a..465352ad9a36 100644 --- a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md +++ b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md @@ -11,22 +11,26 @@ changes: - '(?()]*>)?\s*\(\s*\{\s*contract\b' + - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\b(?:executor|enums)\b' + - '(?()]*>)?\s*\(\s*(?!\{)' - '(?`. `MongoExecutionStack` no longer has a `driver` member, and `createMongoExecutionStack()` no longer takes a `driver` option. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments, or pass a driver to `createMongoExecutionStack`. + `createMongoRuntime()` takes `{ context, adapter, driver }`, as the SQL runtime does: create the adapter with `stack.adapter.create(stack)` and pass it beside the context. The runtime reads the target from `context.contract.target`, so build the context over the validated contract. `MongoExecutionContext` no longer has a `stack` member or a second type parameter, and its contract must be a MongoDB contract, as SQL's `ExecutionContext` requires a SQL contract: write `MongoExecutionContext`, and build it with `createMongoExecutionContext({ contract, stack })` over a validated contract. `MongoExecutionStack` no longer has a `driver` member, and `createMongoExecutionStack()` no longer takes a `driver` option. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments or a non-contract type argument, or pass a driver to `createMongoExecutionStack`. detection: glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" matches: - '(?]|<(?:[^<>]|<[^<>]*>)*>)*,' + - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\bdriver\b' --- @@ -54,9 +58,11 @@ Code that builds the ORM itself passes the context, which also fills generated f ```ts // Before const orm = mongoOrm({ contract, executor, mutationDefaults: context, enums }); +const users = createMongoCollection(contract, 'User', executor, enums, context); -// After -const orm = mongoOrm({ context, executor, enums }); +// After, as the SQL orm({ context, runtime }) +const orm = mongoOrm({ context, runtime: executor }); +const users = createMongoCollection({ context, runtime: executor }, 'User'); ``` The runtime takes an adapter beside the context, as the SQL runtime does, and reads the target from the context's contract: @@ -71,4 +77,4 @@ const context = createMongoExecutionContext({ contract, stack }); const runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); ``` -`MongoExecutionContext` takes one type parameter, the contract: `MongoExecutionContext`. +`MongoExecutionContext` takes one type parameter, a MongoDB contract: `MongoExecutionContext`. diff --git a/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md b/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md index 81af5e59424b..b5604577222f 100644 --- a/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md +++ b/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md @@ -11,22 +11,26 @@ changes: - '(?()]*>)?\s*\(\s*\{\s*contract\b' + - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\b(?:executor|enums)\b' + - '(?()]*>)?\s*\(\s*(?!\{)' - '(?`. `MongoExecutionContext` is declared in `@prisma/orm-family-mongo/query-ast/execution` (`@internal/mongo-query-ast/execution`), still exported from the runtime entry; it has no `stack` member and no second type parameter (`TTargetId`). `MongoExecutionStack` has no `driver` member, and `createMongoExecutionStack()` takes no `driver` option. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments, or pass a driver to `createMongoExecutionStack`. + `MongoRuntimeOptions` is `{ context, adapter, driver, middleware?, mode? }`, as the SQL runtime's options are: the caller creates the adapter with `stack.adapter.create(stack)`, and the runtime checks middleware against `context.contract.target`, so `context` is a `MongoExecutionContext`. `MongoExecutionContext` is declared in `@prisma/orm-family-mongo/query-ast/execution` (`@internal/mongo-query-ast/execution`), still exported from the runtime entry; it has no `stack` member and no second type parameter (`TTargetId`), and its type parameter is constrained as SQL's `ExecutionContext` is: `MongoExecutionContext`, and `createMongoExecutionContext` takes the same constraint. `buildMongoEnums` and `MongoEnumAccessors` are declared beside it in `@internal/mongo-query-ast/execution` and still exported from the runtime entry. `MongoExecutionStack` has no `driver` member, and `createMongoExecutionStack()` takes no `driver` option. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments or a non-contract type argument, or pass a driver to `createMongoExecutionStack`. detection: glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" matches: - '(?]|<(?:[^<>]|<[^<>]*>)*>)*,' + - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\bdriver\b' --- @@ -54,9 +58,11 @@ Code that builds the ORM itself passes the context, which also fills generated f ```ts // Before const orm = mongoOrm({ contract, executor, mutationDefaults: context, enums }); +const users = createMongoCollection(contract, 'User', executor, enums, context); -// After -const orm = mongoOrm({ context, executor, enums }); +// After, as the SQL orm({ context, runtime }) +const orm = mongoOrm({ context, runtime: executor }); +const users = createMongoCollection({ context, runtime: executor }, 'User'); ``` The runtime takes an adapter beside the context, as the SQL runtime does, and reads the target from the context's contract: @@ -71,4 +77,4 @@ const context = createMongoExecutionContext({ contract, stack }); const runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); ``` -`MongoExecutionContext` takes one type parameter, the contract: `MongoExecutionContext`. +`MongoExecutionContext` takes one type parameter, a MongoDB contract: `MongoExecutionContext`. From 61e3475139ad869595433bd400cd2047fbe0f79e Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 16:46:48 +0100 Subject: [PATCH 17/19] TML-3686: the Mongo context builds enum accessors once with buildNamespacedEnums createMongoExecutionContext builds the contract enum accessors with the framework buildNamespacedEnums, as Postgres does, and holds them as context.enums. db.enums and every ORM collection read those same objects. The copied buildMongoEnums and MongoEnumAccessors are removed, and mongoStatic needs no cast for its enums. Signed-off-by: willbot Signed-off-by: Will Madden --- docs/reference/error-reference.md | 2 +- .../4-query/query-ast/README.md | 2 +- .../query-ast/src/execution-context.ts | 5 +- .../query-ast/src/exports/execution.ts | 2 - .../4-query/query-ast/src/mongo-enums.ts | 33 -------- .../query-ast/test/mongo-enums.test.ts | 54 ------------- .../5-query-builders/orm/src/collection.ts | 27 +++---- .../orm/test/collection-context.ts | 2 + .../orm/test/mongo-orm-options.test-d.ts | 7 +- .../orm/test/mongo-raw.test.ts | 2 + .../test/fixtures/test-context.ts | 2 + packages/2-mongo-family/7-runtime/README.md | 2 +- .../7-runtime/src/exports/index.ts | 7 +- .../7-runtime/src/mongo-execution-stack.ts | 16 ++++ .../7-runtime/test/execution-context.test.ts | 55 ++++++++++++++ .../test/mongo-middleware-fixtures.ts | 12 ++- .../test/mongo-runtime-abort.test.ts | 7 +- .../test/mongo-runtime.finalization.test.ts | 1 + .../mongo/9-facade/src/static/mongo-static.ts | 10 +-- .../mongo/9-facade/test/enum-members.test.ts | 35 ++++++--- .../mongo/9-facade/test/mongo.test.ts | 75 +++++-------------- .../9-facade/test/structured-errors.test.ts | 9 ++- test/integration/test/mongo/setup.ts | 7 -- .../detection.test.ts | 3 + .../app/instructions.md | 3 +- .../extension/instructions.md | 3 +- 26 files changed, 171 insertions(+), 212 deletions(-) delete mode 100644 packages/2-mongo-family/4-query/query-ast/src/mongo-enums.ts delete mode 100644 packages/2-mongo-family/4-query/query-ast/test/mongo-enums.test.ts diff --git a/docs/reference/error-reference.md b/docs/reference/error-reference.md index 3e591037a46b..b15fe4c58338 100644 --- a/docs/reference/error-reference.md +++ b/docs/reference/error-reference.md @@ -1208,7 +1208,7 @@ A codec descriptor handed to a target codec-descriptor registry (Postgres, SQLit ### RUNTIME.CODEC_DESCRIPTOR_MISSING -A column, a domain enum, or an AST-carried CodecRef references a `codecId` for which no runtime component registered a codec descriptor: usually the extension pack that owns the codec is missing from the runtime stack. Surfaces at SQL context construction during the contract codec walk, when a SQL or Mongo client builds `db.enums` (for Mongo, in `buildMongoEnums`, which also builds the accessors `mongoOrm()` takes), or lazily when the AST codec resolver materializes a codec at query time. Payload: `codecId`; on the column path also `table`, `column`. +A column, a domain enum, or an AST-carried CodecRef references a `codecId` for which no runtime component registered a codec descriptor: usually the extension pack that owns the codec is missing from the runtime stack. Surfaces at SQL context construction during the contract codec walk, when a SQL or Mongo client builds `db.enums` (for Mongo, in `createMongoExecutionContext`, which builds the accessors `db.enums` and the ORM share), or lazily when the AST codec resolver materializes a codec at query time. Payload: `codecId`; on the column path also `table`, `column`. ### RUNTIME.CODEC_MISSING diff --git a/packages/2-mongo-family/4-query/query-ast/README.md b/packages/2-mongo-family/4-query/query-ast/README.md index e7ddaf0b9e2b..903452480625 100644 --- a/packages/2-mongo-family/4-query/query-ast/README.md +++ b/packages/2-mongo-family/4-query/query-ast/README.md @@ -9,6 +9,6 @@ Typed AST for MongoDB filters, pipeline stages, and command variants, plus the u - **Commands**: `AnyMongoCommand` and related command types (reads, writes, aggregates) carried on a plan - **Query plan**: `MongoQueryPlan` — a branded typed representation with shape `{ collection, command: AnyMongoCommand, meta: PlanMeta }` - **Visitors**: `MongoFilterVisitor`, `MongoFilterRewriter`, `MongoStageVisitor` interfaces for traversing and transforming AST nodes -- **Execution context**: `MongoExecutionContext` — the contract, a read-only codec lookup and the mutation defaults the query lanes build against, as SQL's `ExecutionContext` is; `buildMongoEnums(contract, codecs)` builds the contract's enum accessors from it +- **Execution context**: `MongoExecutionContext` — the contract, a read-only codec lookup, the contract's enum accessors and the mutation defaults the query lanes build against, as SQL's `ExecutionContext` is Lowering typed nodes to wire BSON is performed by `@internal/adapter-mongo` via `lower(plan)` on `MongoQueryPlan`, which dispatches on `command.kind` and uses internal helpers for filters and pipelines. diff --git a/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts index cb6930cc57e0..8148eae129b8 100644 --- a/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts +++ b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts @@ -1,3 +1,4 @@ +import type { NamespacedEnums } from '@internal/contract/enum-accessor'; import type { MutationDefaults } from '@internal/framework-components/runtime'; import type { MongoCodec } from '@internal/mongo-codec'; import type { MongoContract } from '@internal/mongo-contract'; @@ -13,7 +14,7 @@ export interface MongoCodecLookup { } /** - * The context the query lanes build against: the user's contract, a read-only lookup over the codec registry composed from every stack contributor, and `applyMutationDefaults`, which fills the contract's execution defaults from the composed generators. The runtime creates it with `createMongoExecutionContext`. + * The context the query lanes build against: the user's contract, a read-only lookup over the codec registry composed from every stack contributor, the contract's enum accessors built once through that lookup, and `applyMutationDefaults`, which fills the contract's execution defaults from the composed generators. The runtime creates it with `createMongoExecutionContext`. * * Mirrors SQL's `ExecutionContext`: it holds no runtime concerns such as the adapter or the stack; the runtime receives the adapter as its own option. */ @@ -21,4 +22,6 @@ export interface MongoExecutionContext; } diff --git a/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts b/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts index bb553ed3ed16..bcf76279755e 100644 --- a/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts +++ b/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts @@ -37,8 +37,6 @@ export { MongoNotExpr, MongoOrExpr, } from '../filter-expressions'; -export type { MongoEnumAccessors } from '../mongo-enums'; -export { buildMongoEnums } from '../mongo-enums'; export type { MongoQueryPlan } from '../query-plan'; export type { RawMongoCommand } from '../raw-commands'; export { diff --git a/packages/2-mongo-family/4-query/query-ast/src/mongo-enums.ts b/packages/2-mongo-family/4-query/query-ast/src/mongo-enums.ts deleted file mode 100644 index 12b6780887d5..000000000000 --- a/packages/2-mongo-family/4-query/query-ast/src/mongo-enums.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { buildEnumsMapForNamespace, type EnumAccessor } from '@internal/contract/enum-accessor'; -import type { Contract } from '@internal/contract/types'; -import { runtimeError } from '@internal/framework-components/runtime'; -import type { MongoCodecLookup } from './execution-context'; - -/** A contract's enum accessors by namespace and enum name. */ -export type MongoEnumAccessors = Readonly>>>; - -/** - * The contract's enum accessors by namespace, each reading its members through its codec in `codecs`: what `db.enums` holds, and what `mongoOrm()` checks a written enum value against. A contract whose enum codec `codecs` lacks is refused here, with `RUNTIME.CODEC_DESCRIPTOR_MISSING`; an enum's members are decoded when it is first read. - */ -export function buildMongoEnums( - contract: Pick, - codecs: MongoCodecLookup, -): MongoEnumAccessors { - const codecFor = (codecId: string) => { - const codec = codecs.get(codecId); - if (codec === undefined) { - throw runtimeError( - 'RUNTIME.CODEC_DESCRIPTOR_MISSING', - `No codec is registered for codecId '${codecId}', which a domain enum in the contract uses.`, - { codecId }, - ); - } - return codec; - }; - return Object.fromEntries( - Object.keys(contract.domain.namespaces).map((namespaceId) => [ - namespaceId, - buildEnumsMapForNamespace(contract.domain, namespaceId, codecFor), - ]), - ); -} diff --git a/packages/2-mongo-family/4-query/query-ast/test/mongo-enums.test.ts b/packages/2-mongo-family/4-query/query-ast/test/mongo-enums.test.ts deleted file mode 100644 index d5323acbc20a..000000000000 --- a/packages/2-mongo-family/4-query/query-ast/test/mongo-enums.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ContractEnum } from '@internal/contract/types'; -import { type DataTypeValue, dataType, readJsonString } from '@internal/framework-components/codec'; -import { describe, expect, it } from 'vitest'; -import type { MongoCodecLookup } from '../src/execution-context'; -import { buildMongoEnums } from '../src/mongo-enums'; - -const level: ContractEnum = { - codecId: 'test/level@1', - members: [ - { name: 'Low', value: '1' }, - { name: 'High', value: '10' }, - ], -}; - -const domain = { namespaces: { __unbound__: { enum: { Level: level } } } }; - -const levelType = dataType('test/level', { read: (json) => readJsonString('test/level', json) }); - -const levelCodecs: MongoCodecLookup = { - get: (id) => - id === 'test/level@1' - ? { - id, - dataType: levelType, - toWire: async (value: unknown) => value, - fromWire: async (wire: unknown) => wire, - toDataTypeValue: (value: unknown) => levelType.valueFrom(String(value), {}), - fromDataTypeValue: (value: DataTypeValue) => BigInt(String(value.value)), - } - : undefined, - has: (id) => id === 'test/level@1', -}; - -describe('buildMongoEnums', () => { - it("reads each enum's members through its codec, by namespace", () => { - const enums = buildMongoEnums({ domain } as never, levelCodecs); - expect({ - namespaces: Object.keys(enums), - members: enums['__unbound__']?.['Level']?.members, - }).toEqual({ namespaces: ['__unbound__'], members: { Low: 1n, High: 10n } }); - }); - - it('refuses a contract whose enum codec the lookup lacks', () => { - expect(() => - buildMongoEnums({ domain } as never, { get: () => undefined, has: () => false }), - ).toThrow( - expect.objectContaining({ - code: 'RUNTIME.CODEC_DESCRIPTOR_MISSING', - message: - "No codec is registered for codecId 'test/level@1', which a domain enum in the contract uses.", - }), - ); - }); -}); diff --git a/packages/2-mongo-family/5-query-builders/orm/src/collection.ts b/packages/2-mongo-family/5-query-builders/orm/src/collection.ts index 537f46a2cbeb..b7d6551676b9 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/collection.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/collection.ts @@ -1,3 +1,4 @@ +import type { EnumAccessor } from '@internal/contract/enum-accessor'; import { type ContractField, type ContractModel, @@ -29,7 +30,6 @@ import type { AnyMongoCommand, InsertManyResult, InsertOneResult, - MongoEnumAccessors, MongoExecutionContext, MongoFieldShape, MongoFilterExpr, @@ -37,7 +37,6 @@ import type { MongoResultShape, } from '@internal/mongo-query-ast/execution'; import { - buildMongoEnums, DeleteManyCommand, FindOneAndDeleteCommand, FindOneAndUpdateCommand, @@ -275,22 +274,16 @@ class MongoCollectionImpl< readonly #ctx: MongoCollectionContext; readonly #executor: MongoQueryExecutor; readonly #mutationDefaults: MutationDefaults; - readonly #enums: MongoEnumAccessors; #collectionName: string; #state: MongoCollectionState; #variantValue: string | undefined; - constructor( - ctx: MongoCollectionContext, - modelName: ModelName, - enums: MongoEnumAccessors, - ) { + constructor(ctx: MongoCollectionContext, modelName: ModelName) { this.#ctx = ctx; this.#contract = ctx.context.contract; this.#modelName = modelName; this.#executor = ctx.runtime; this.#mutationDefaults = ctx.context; - this.#enums = enums; this.#collectionName = resolveCollectionName(modelOf(this.#contract, modelName), modelName); this.#state = emptyCollectionState(); } @@ -1141,7 +1134,11 @@ class MongoCollectionImpl< if (valueSet === undefined || valueSet.entityKind !== 'enum' || value === null) return; const contractEnum = this.#contract.domain.namespaces[valueSet.namespaceId]?.enum?.[valueSet.entityName]; - const accessor = this.#enums[valueSet.namespaceId]?.[valueSet.entityName]; + const enums = blindCast< + Readonly> | undefined>>, + 'the accessors are keyed by the contract namespace ids and enum names a field value set names' + >(this.#ctx.context.enums); + const accessor = enums[valueSet.namespaceId]?.[valueSet.entityName]; if (contractEnum === undefined || accessor === undefined) return; const values: readonly unknown[] = field.many && Array.isArray(value) ? value : [value]; const outside = values.findIndex((entry) => entry !== null && !accessor.has(entry)); @@ -1441,7 +1438,6 @@ class MongoCollectionImpl< const instance = new MongoCollectionImpl( this.#ctx, this.#modelName, - this.#enums, ); instance.#state = { ...this.#state, ...overrides }; instance.#collectionName = this.#collectionName; @@ -1456,7 +1452,6 @@ class MongoCollectionImpl< const instance = new MongoCollectionImpl( this.#ctx, this.#modelName, - this.#enums, ); instance.#state = { ...this.#state, ...overrides }; instance.#collectionName = this.#collectionName; @@ -1465,7 +1460,7 @@ class MongoCollectionImpl< } } -/** A collection of `modelName` over the context's contract, which fills generated fields from the context's mutation defaults and checks written enum values against the contract's enums. */ +/** A collection of `modelName` over the context's contract, which fills generated fields from the context's mutation defaults and checks written enum values against the context's enum accessors. */ export function createMongoCollection< TContract extends MongoContractWithTypeMaps, ModelName extends string & keyof MongoModelsMap, @@ -1473,9 +1468,5 @@ export function createMongoCollection< ctx: MongoCollectionContext, modelName: ModelName, ): MongoCollection { - return new MongoCollectionImpl( - ctx, - modelName, - buildMongoEnums(ctx.context.contract, ctx.context.codecs), - ); + return new MongoCollectionImpl(ctx, modelName); } diff --git a/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts b/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts index 65c1e4d07ca8..a97b8b96f139 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts @@ -1,3 +1,4 @@ +import type { NamespacedEnums } from '@internal/contract/enum-accessor'; import type { MutationDefaults } from '@internal/framework-components/runtime'; import type { MongoContract } from '@internal/mongo-contract'; import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; @@ -15,6 +16,7 @@ export function contextOf( return { contract, codecs: noCodecs, + enums: {} as NamespacedEnums, applyMutationDefaults: (options) => mutationDefaults.applyMutationDefaults(options), }; } diff --git a/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts b/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts index ce36ba89be45..ffa347ccc77e 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts @@ -1,7 +1,4 @@ -import type { - MongoEnumAccessors, - MongoExecutionContext, -} from '@internal/mongo-query-ast/execution'; +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { expectTypeOf, test } from 'vitest'; import type { Contract } from '../../../1-foundation/mongo-contract/test/fixtures/orm-contract'; import { createMongoCollection } from '../src/collection'; @@ -12,7 +9,7 @@ import { mongoRaw } from '../src/mongo-raw'; declare const contract: Contract; declare const context: MongoExecutionContext; declare const runtime: MongoQueryExecutor; -declare const enums: MongoEnumAccessors; +declare const enums: MongoExecutionContext['enums']; test('mongoOrm() takes a context and a runtime, as the SQL orm() does', () => { mongoOrm({ context, runtime }); diff --git a/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts index 416309f38aa7..921aff0d0b8b 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts @@ -1,3 +1,4 @@ +import type { NamespacedEnums } from '@internal/contract/enum-accessor'; import type { MongoContract } from '@internal/mongo-contract'; import type { MongoExecutionContext, RawMongoCommand } from '@internal/mongo-query-ast/execution'; import { blindCast } from '@internal/utils/casts'; @@ -12,6 +13,7 @@ function contextOf( return { contract, codecs: { get: () => undefined, has: () => false }, + enums: {} as NamespacedEnums, applyMutationDefaults: () => [], }; } diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts index 937b9f7045cc..628c15a7ab0a 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts @@ -1,3 +1,4 @@ +import type { NamespacedEnums } from '@internal/contract/enum-accessor'; import type { MongoContract } from '@internal/mongo-contract'; import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; @@ -7,6 +8,7 @@ export function contextOf( return { contract, codecs: { get: () => undefined, has: () => false }, + enums: {} as NamespacedEnums, applyMutationDefaults: () => [], }; } diff --git a/packages/2-mongo-family/7-runtime/README.md b/packages/2-mongo-family/7-runtime/README.md index 1360d64ccbdf..a017dba3018d 100644 --- a/packages/2-mongo-family/7-runtime/README.md +++ b/packages/2-mongo-family/7-runtime/README.md @@ -48,7 +48,7 @@ Custom or third-party codecs (encryption, vendor scalars) are contributed via an ## Responsibilities - **Stack/context composition**: `createMongoExecutionStack` and `createMongoExecutionContext` mirror SQL's `createSqlExecutionStack` / `createExecutionContext`. The context aggregates codec contributions from `[stack.target, stack.adapter, ...stack.extensions]` into a single `MongoCodecRegistry`. The `MongoExecutionContext` type is declared in `@internal/mongo-query-ast`, as SQL's `ExecutionContext` is declared in relational-core, and holds no stack: the query lanes build against it, and the runtime receives the adapter as its own option. -- **Enum accessors**: `buildMongoEnums(contract, context.codecs)`, declared in `@internal/mongo-query-ast` and exported here, builds the contract's enum accessors by namespace: what `db.enums` holds and what the ORM checks a written enum value against. A contract whose enum codec the context lacks is refused here with `RUNTIME.CODEC_DESCRIPTOR_MISSING`. +- **Enum accessors**: `createMongoExecutionContext` builds the contract's enum accessors once, with the framework's `buildNamespacedEnums` over the stack's codecs, and holds them as `context.enums`: what `db.enums` holds and what the ORM checks a written enum value against. A contract whose enum codec no stack component contributes is refused with `RUNTIME.CODEC_DESCRIPTOR_MISSING`. - **Runtime executor**: `createMongoRuntime({ context, adapter, driver, ... })` composes context, adapter and driver into a `MongoRuntime`, as the SQL runtime does. The caller creates the adapter with `stack.adapter.create(stack)`, and the runtime checks middleware against the target named by `context.contract.target`. The runtime has separate `query(plan)` and `execute(plan)` operations accepting `MongoQueryPlan` from `@internal/mongo-query-ast`. `query()` yields decoded rows, while `execute()` returns statement statistics. Both operations lower the plan with the adapter, and run the wire command on the driver; `query()` **optionally decodes** each row when `plan.resultShape` is present. - **Operation flow**: Row-producing work uses `query(plan)` and the query middleware lifecycle; statistics-producing work uses `execute(plan)` and the execute middleware lifecycle. Each operation allocates its own `CodecCallContext` and threads it through its codec dispatches. - **Lowering**: Happens in the adapter (`lower(plan)`), wrapped by the runtime's `lower` override into a `MongoExecutionPlan`. diff --git a/packages/2-mongo-family/7-runtime/src/exports/index.ts b/packages/2-mongo-family/7-runtime/src/exports/index.ts index 78321dc57db9..bb93dc3a9ad5 100644 --- a/packages/2-mongo-family/7-runtime/src/exports/index.ts +++ b/packages/2-mongo-family/7-runtime/src/exports/index.ts @@ -1,10 +1,5 @@ export type { RuntimeTargetInstance } from '@internal/framework-components/execution'; -export type { - MongoCodecLookup, - MongoEnumAccessors, - MongoExecutionContext, -} from '@internal/mongo-query-ast/execution'; -export { buildMongoEnums } from '@internal/mongo-query-ast/execution'; +export type { MongoCodecLookup, MongoExecutionContext } from '@internal/mongo-query-ast/execution'; export type { MongoExecutionPlan } from '../mongo-execution-plan'; export type { MongoExecutionStack, diff --git a/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts index 5a4c31524a3b..1b75ffe027df 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts @@ -1,3 +1,4 @@ +import { buildNamespacedEnums } from '@internal/contract/enum-accessor'; import { createExecutionStack, type RuntimeAdapterDescriptor, @@ -120,9 +121,24 @@ export function createMongoExecutionContext< } } + const enums = Object.freeze( + buildNamespacedEnums(options.contract.domain, (codecId) => { + const codec = registry.get(codecId); + if (codec === undefined) { + throw runtimeError( + 'RUNTIME.CODEC_DESCRIPTOR_MISSING', + `No codec is registered for codecId '${codecId}', which a domain enum in the contract uses.`, + { codecId }, + ); + } + return codec; + }), + ); + return Object.freeze({ contract: options.contract, codecs: registry, + enums, applyMutationDefaults: (mutation: MutationDefaultsOptions) => applyMutationDefaults(execution, generators, mutation), }); diff --git a/packages/2-mongo-family/7-runtime/test/execution-context.test.ts b/packages/2-mongo-family/7-runtime/test/execution-context.test.ts index 60cd53c351da..5c50a3ecda7f 100644 --- a/packages/2-mongo-family/7-runtime/test/execution-context.test.ts +++ b/packages/2-mongo-family/7-runtime/test/execution-context.test.ts @@ -123,6 +123,61 @@ function fields( } describe('createMongoExecutionContext composition', () => { + it("builds the contract's enum accessors once, reading members through the stack's codecs", () => { + const stack = createMongoExecutionStack({ + target: target(), + adapter: adapter(), + extensions: [extension('levels', { codecIds: ['test/level@1'] })], + }); + const contract = { + ...testContract, + domain: { + namespaces: { + [NS]: { + enum: { + Level: { + codecId: 'test/level@1', + members: [ + { name: 'Low', value: 'low' }, + { name: 'High', value: 'high' }, + ], + }, + }, + }, + }, + }, + } as unknown as MongoContract; + + const context = createMongoExecutionContext({ contract, stack }); + const firstRead = context.enums[NS]?.['Level']; + + expect({ + namespaces: Object.keys(context.enums), + members: firstRead?.members, + }).toEqual({ namespaces: [NS], members: { Low: 'low', High: 'high' } }); + expect(context.enums[NS]?.['Level']).toBe(firstRead); + }); + + it('refuses a contract whose enum codec no stack component contributes', () => { + const stack = createMongoExecutionStack({ target: target(), adapter: adapter() }); + const contract = { + ...testContract, + domain: { + namespaces: { + [NS]: { enum: { Level: { codecId: 'test/level@1', members: [] } } }, + }, + }, + } as unknown as MongoContract; + + expect(() => createMongoExecutionContext({ contract, stack })).toThrow( + expect.objectContaining({ + code: 'RUNTIME.CODEC_DESCRIPTOR_MISSING', + message: + "No codec is registered for codecId 'test/level@1', which a domain enum in the contract uses.", + }), + ); + }); + it('folds the codecs of target, adapter and extensions into one lookup', () => { const stack = createMongoExecutionStack({ target: target(), diff --git a/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts b/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts index d84324f17b42..769cc7523f08 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts @@ -7,7 +7,10 @@ import type { MongoExecutionContext, MongoQueryPlan } from '@internal/mongo-quer import { AggregateWireCommand, type AnyMongoDmlWireCommand } from '@internal/mongo-wire'; import { vi } from 'vitest'; -export const testContract = { target: 'mongo' } as unknown as MongoContract; +export const testContract = { + target: 'mongo', + domain: { namespaces: {} }, +} as unknown as MongoContract; export function makeExecution( _adapter: MongoAdapter, @@ -26,7 +29,12 @@ export function makeExecution( resolveParams: vi.fn(async (_draft: MongoLoweredDraft, _ctx: CodecCallContext) => wireCommand), }; return { - context: Object.freeze({ contract: testContract, codecs, applyMutationDefaults: () => [] }), + context: Object.freeze({ + contract: testContract, + codecs, + enums: {}, + applyMutationDefaults: () => [], + }), adapter, }; } diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts index 8b5154a76f11..e976fe39656e 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts @@ -58,7 +58,12 @@ function makeExecution(adapter: MongoAdapter): { } { const codecs: MongoCodecRegistry = newMongoCodecRegistry(); return { - context: Object.freeze({ contract: testContract, codecs, applyMutationDefaults: () => [] }), + context: Object.freeze({ + contract: testContract, + codecs, + enums: {}, + applyMutationDefaults: () => [], + }), adapter, }; } diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts index 8dad785a0629..b51b3574b613 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts @@ -88,6 +88,7 @@ function makeExecution( context: { contract: testContract, codecs: codecsWithFailingDecode(), + enums: {}, applyMutationDefaults: () => [], }, adapter, diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts index 995726819a30..4622c5777916 100644 --- a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts +++ b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts @@ -10,7 +10,6 @@ import type { MongoRawClient } from '@internal/mongo-orm'; import { mongoRaw } from '@internal/mongo-orm'; import { mongoQuery } from '@internal/mongo-query-builder'; import { - buildMongoEnums, createMongoExecutionContext, createMongoExecutionStack, type MongoExecutionContext, @@ -18,7 +17,6 @@ import { } from '@internal/mongo-runtime'; import mongoRuntimeTarget from '@internal/target-mongo/runtime'; import { assertDefined } from '@internal/utils/assertions'; -import { blindCast } from '@internal/utils/casts'; import { validateMongoContract } from './contract-from-json'; type UnboundEnums> = @@ -49,12 +47,8 @@ export function buildMongoStaticContext< adapter: mongoRuntimeAdapter, }); const context = createMongoExecutionContext({ contract, stack }); - const unbound = buildMongoEnums(contract, context.codecs)[UNBOUND_NAMESPACE_ID]; - assertDefined(unbound, 'the unbound namespace always exists on a mongo builder output'); - const enums = blindCast< - UnboundEnums, - 'built from this contract domain; the mapped-type shape cannot be proven statically' - >(unbound); + const enums = context.enums[UNBOUND_NAMESPACE_ID]; + assertDefined(enums, 'the unbound namespace always exists on a mongo builder output'); const query = mongoQuery({ context }); const raw = mongoRaw({ context }); return { context, contract, enums, query, raw, stack }; diff --git a/packages/3-targets/mongo/9-facade/test/enum-members.test.ts b/packages/3-targets/mongo/9-facade/test/enum-members.test.ts index a886834d1a88..259393ad7694 100644 --- a/packages/3-targets/mongo/9-facade/test/enum-members.test.ts +++ b/packages/3-targets/mongo/9-facade/test/enum-members.test.ts @@ -1,7 +1,6 @@ import { AsyncIterableResult } from '@internal/framework-components/runtime'; import { createMongoCollection, type MongoQueryExecutor, mongoOrm } from '@internal/mongo-orm'; -import { buildMongoEnums } from '@internal/mongo-runtime'; -import { describe, expect, expectTypeOf, it } from 'vitest'; +import { describe, expect, expectTypeOf, it, vi } from 'vitest'; import { defineContract, enumType, field, member, model } from '../src/exports/contract-builder'; import mongoStatic from '../src/static/mongo-static'; @@ -119,13 +118,31 @@ describe('the Mongo ORM checks a written enum value against the enums of its con }); }); - it('refuses a contract whose enum codec the runtime lacks when the accessors are built', () => { - expect(() => buildMongoEnums(contract, { get: () => undefined, has: () => false })).toThrow( - expect.objectContaining({ - code: 'RUNTIME.CODEC_DESCRIPTOR_MISSING', - message: - "No codec is registered for codecId 'mongo/string@1', which a domain enum in the contract uses.", - }), + it('reads the accessors db.enums holds, the ones its context built once', async () => { + const int32Level = context.enums['__unbound__']?.['Int32Level']; + expect(enums.Int32Level).toBe(int32Level); + + const refuseEverything = vi.fn(() => false); + const sharedAccessors = { + __unbound__: { + TextLevel: { ...enums.TextLevel, has: refuseEverything }, + Int32Level: enums.Int32Level, + DoubleLevel: enums.DoubleLevel, + }, + }; + const sharedContext = { ...context, enums: sharedAccessors as unknown as typeof context.enums }; + const orm = mongoOrm({ context: sharedContext, runtime: unreachableDatabase }); + const readings = createMongoCollection( + { context: sharedContext, runtime: unreachableDatabase }, + 'Reading', ); + + await expect(orm.readings.create(reading)).rejects.toMatchObject({ + code: 'RUNTIME.ENCODE_FAILED', + }); + await expect(readings.create(reading)).rejects.toMatchObject({ + code: 'RUNTIME.ENCODE_FAILED', + }); + expect(refuseEverything).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/3-targets/mongo/9-facade/test/mongo.test.ts b/packages/3-targets/mongo/9-facade/test/mongo.test.ts index 4bb5d3c46c2a..bddd1b0aea30 100644 --- a/packages/3-targets/mongo/9-facade/test/mongo.test.ts +++ b/packages/3-targets/mongo/9-facade/test/mongo.test.ts @@ -4,7 +4,6 @@ import type { MongoContract, MongoContractWithTypeMaps, } from '@internal/mongo-contract'; -import type * as MongoRuntime from '@internal/mongo-runtime'; import { beforeEach, describe, expect, it, vi } from 'vitest'; type AnyMongoContract = MongoContractWithTypeMaps; @@ -32,8 +31,7 @@ vi.mock('@internal/target-mongo/runtime', () => ({ default: mocks.mongoRuntimeTarget, })); -vi.mock('@internal/mongo-runtime', async (importOriginal) => ({ - buildMongoEnums: (await importOriginal()).buildMongoEnums, +vi.mock('@internal/mongo-runtime', () => ({ createMongoExecutionStack: mocks.createMongoExecutionStack, createMongoExecutionContext: mocks.createMongoExecutionContext, createMongoRuntime: mocks.createMongoRuntime, @@ -77,6 +75,7 @@ const fakeOrm = { id: 'orm-instance' }; const fakeQuery = { id: 'query-instance' }; const fakeRaw = { id: 'raw-instance', collection: vi.fn() }; const fakeAdapter = { id: 'adapter-instance' }; +const fakeContext = { id: 'context-instance', enums: { __unbound__: {} } }; const fakeStack = { id: 'stack-instance', adapter: { create: () => fakeAdapter } }; describe('mongo() facade', () => { @@ -87,7 +86,7 @@ describe('mongo() facade', () => { mocks.deserializeContract.mockReturnValue(fakeContract); mocks.createMongoExecutionStack.mockReturnValue(fakeStack); - mocks.createMongoExecutionContext.mockReturnValue({ id: 'context-instance' }); + mocks.createMongoExecutionContext.mockReturnValue(fakeContext); mocks.driverFromConnection.mockResolvedValue({ id: 'driver-from-url', close: fakeDriverClose, @@ -144,7 +143,7 @@ describe('mongo() facade', () => { expect(mocks.createMongoRuntime).toHaveBeenCalledTimes(1); expect(mocks.createMongoRuntime).toHaveBeenCalledWith( expect.objectContaining({ - context: { id: 'context-instance' }, + context: fakeContext, adapter: fakeAdapter, }), ); @@ -434,65 +433,27 @@ describe('mongo() facade', () => { }); describe('db.enums (facade)', () => { - const roleEnum = { - codecId: 'mongo/string@1', - members: [ - { name: 'User', value: 'user' }, - { name: 'Admin', value: 'admin' }, - ], - } as const; - - const contractWithEnum = { - domain: { - namespaces: { - __unbound__: { models: {}, enum: { Role: roleEnum } }, - }, - }, - } as unknown as AnyMongoContract; - - const stringCodec = { - dataType: { fromContract: (json: unknown) => json, toContract: (value: unknown) => value }, - fromDataTypeValue: (value: unknown) => value, - toDataTypeValue: (value: unknown) => value, + const roleAccessor = { values: ['user', 'admin'] }; + const contextWithEnums = { + id: 'context-instance', + enums: { __unbound__: { Role: roleAccessor } }, }; beforeEach(() => { - mocks.deserializeContract.mockReturnValue(contractWithEnum); - mocks.createMongoExecutionContext.mockReturnValue({ - id: 'context-instance', - codecs: { get: (id: string) => (id === 'mongo/string@1' ? stringCodec : undefined) }, - }); + mocks.createMongoExecutionContext.mockReturnValue(contextWithEnums); }); - function roleAccessor() { - const db = mongo({ contract: contractWithEnum, url: 'mongodb://localhost:27017/mydb' }); - return db.enums['Role']!; - } - - it('refuses to build a client whose runtime has no codec for an enum', () => { - mocks.createMongoExecutionContext.mockReturnValue({ - id: 'context-instance', - codecs: { get: () => undefined }, - }); - expect(() => - mongo({ contract: contractWithEnum, url: 'mongodb://localhost:27017/mydb' }), - ).toThrow( - expect.objectContaining({ - code: 'RUNTIME.CODEC_DESCRIPTOR_MISSING', - message: - "No codec is registered for codecId 'mongo/string@1', which a domain enum in the contract uses.", - }), - ); - }); + it('holds the accessors of the execution context, the ones the ORM checks writes against', () => { + const db = mongo({ contract: fakeContract, url: 'mongodb://localhost:27017/mydb' }); - it('exposes the enum accessor at db.enums.Role', () => { - expect(roleAccessor().values).toEqual(['user', 'admin']); + expect(db.enums).toBe(contextWithEnums.enums.__unbound__); + expect(db.enums['Role']).toBe(roleAccessor); }); it('builds the enums surface eagerly, without connecting the driver', () => { - const db = mongo({ contract: contractWithEnum, url: 'mongodb://localhost:27017/mydb' }); + const db = mongo({ contract: fakeContract, url: 'mongodb://localhost:27017/mydb' }); - expect(db.enums['Role']!.values).toEqual(['user', 'admin']); + expect(db.enums['Role']).toBe(roleAccessor); expect(mocks.driverFromConnection).not.toHaveBeenCalled(); expect(mocks.createMongoRuntime).not.toHaveBeenCalled(); }); @@ -507,12 +468,12 @@ describe('mongo() facade', () => { it('context matches what createMongoExecutionContext returns', () => { const db = mongo({ contract: fakeContract, url: 'mongodb://localhost:27017/mydb' }); - expect(db.context).toEqual({ id: 'context-instance' }); + expect(db.context).toEqual(fakeContext); }); it('builds query, raw and orm from the one execution context', () => { mongo({ contract: fakeContract, url: 'mongodb://localhost:27017/mydb' }); - const context = { id: 'context-instance' }; + const context = fakeContext; expect(mocks.mongoQuery).toHaveBeenCalledWith({ context }); expect(mocks.mongoRaw).toHaveBeenCalledWith({ context }); expect(mocks.mongoOrm).toHaveBeenCalledWith(expect.objectContaining({ context })); @@ -538,7 +499,7 @@ describe('mongo() facade', () => { const db = mongo({ contract: fakeContract, url: 'mongodb://localhost:27017/mydb' }); await db.runtime(); expect(mocks.createMongoRuntime).toHaveBeenCalledWith( - expect.objectContaining({ context: { id: 'context-instance' } }), + expect.objectContaining({ context: fakeContext }), ); }); }); diff --git a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts index be9e8a9bfc27..74d8bef7a1bb 100644 --- a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts +++ b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts @@ -4,7 +4,6 @@ import type { MongoContract, MongoContractWithTypeMaps, } from '@internal/mongo-contract'; -import type * as MongoRuntime from '@internal/mongo-runtime'; import { isStructuredError } from '@internal/utils/structured-error'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -24,8 +23,7 @@ const mocks = vi.hoisted(() => ({ vi.mock('@internal/adapter-mongo/runtime', () => ({ default: { id: 'adapter' } })); vi.mock('@internal/target-mongo/runtime', () => ({ default: { id: 'target' } })); -vi.mock('@internal/mongo-runtime', async (importOriginal) => ({ - buildMongoEnums: (await importOriginal()).buildMongoEnums, +vi.mock('@internal/mongo-runtime', () => ({ createMongoExecutionStack: mocks.createMongoExecutionStack, createMongoExecutionContext: mocks.createMongoExecutionContext, createMongoRuntime: mocks.createMongoRuntime, @@ -186,7 +184,10 @@ describe('facade lifecycle structured codes', () => { id: 'stack', adapter: { create: () => ({ id: 'adapter' }) }, }); - mocks.createMongoExecutionContext.mockReturnValue({ id: 'context' }); + mocks.createMongoExecutionContext.mockReturnValue({ + id: 'context', + enums: { __unbound__: {} }, + }); mocks.driverFromConnection.mockResolvedValue({ id: 'driver', close: vi.fn().mockResolvedValue(undefined), diff --git a/test/integration/test/mongo/setup.ts b/test/integration/test/mongo/setup.ts index fd5f93580e32..47ff8af9df6e 100644 --- a/test/integration/test/mongo/setup.ts +++ b/test/integration/test/mongo/setup.ts @@ -6,7 +6,6 @@ import { createMongoExecutionContext, createMongoExecutionStack, createMongoRuntime, - type MongoCodecLookup, type MongoExecutionContext, type MongoRuntime, } from '@internal/mongo-runtime'; @@ -63,7 +62,6 @@ export async function withMongod(fn: (ctx: MongodContext) => Promise): Pro client, runtime, context, - codecs: context.codecs, }; return await fn(ctx); } finally { @@ -80,8 +78,6 @@ export interface MongodContext { readonly runtime: MongoRuntime; /** The execution context over the fixture contract, which `mongoOrm()` and `mongoRaw()` take. */ readonly context: MongoExecutionContext; - /** The execution context's codecs, which `buildMongoEnums()` reads enum members through. */ - readonly codecs: MongoCodecLookup; } export function describeWithMongoDB(name: string, fn: (ctx: MongodContext) => void): void { @@ -106,9 +102,6 @@ export function describeWithMongoDB(name: string, fn: (ctx: MongodContext) => vo get context() { return context; }, - get codecs() { - return context.codecs; - }, }; beforeAll(async () => { diff --git a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts index c18716666e0e..b24b55417924 100644 --- a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts +++ b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts @@ -100,6 +100,8 @@ describe.each(['app', 'extension'] as const)('the %s runtime adapter detection', 'createMongoExecutionStack({\n target,\n adapter,\n driver: mongoDriver,\n})', ], ['a context type over unknown', 'MongoExecutionContext'], + ['the removed enum helper', 'const enums = buildMongoEnums(contract, context.codecs);'], + ['the removed enum accessor type', 'let enums: MongoEnumAccessors;'], ['a context type over an object literal type', 'MongoExecutionContext<{ models: {} }>'], ])('finds %s', (_name, content) => { expect(detects(audience, content, change)).toBe(true); @@ -111,6 +113,7 @@ describe.each(['app', 'extension'] as const)('the %s runtime adapter detection', ['a stack on another object', 'options.stack.adapter'], ['a context type over one contract', 'MongoExecutionContext'], ['a context type over a contract typeof', 'MongoExecutionContext'], + ['the enum accessors of a context', 'const enums = context.enums;'], ['a context type over a generic contract', 'MongoExecutionContext>'], ['a stack built without a driver', 'createMongoExecutionStack({ target, adapter })'], ])('skips %s', (_name, content) => { diff --git a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md index 465352ad9a36..6087b7ca50e9 100644 --- a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md +++ b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md @@ -23,7 +23,7 @@ changes: - 'MUTATION_DEFAULTS_MISSING' - id: mongo-runtime-takes-adapter summary: | - `createMongoRuntime()` takes `{ context, adapter, driver }`, as the SQL runtime does: create the adapter with `stack.adapter.create(stack)` and pass it beside the context. The runtime reads the target from `context.contract.target`, so build the context over the validated contract. `MongoExecutionContext` no longer has a `stack` member or a second type parameter, and its contract must be a MongoDB contract, as SQL's `ExecutionContext` requires a SQL contract: write `MongoExecutionContext`, and build it with `createMongoExecutionContext({ contract, stack })` over a validated contract. `MongoExecutionStack` no longer has a `driver` member, and `createMongoExecutionStack()` no longer takes a `driver` option. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments or a non-contract type argument, or pass a driver to `createMongoExecutionStack`. + `createMongoRuntime()` takes `{ context, adapter, driver }`, as the SQL runtime does: create the adapter with `stack.adapter.create(stack)` and pass it beside the context. The runtime reads the target from `context.contract.target`, so build the context over the validated contract. `MongoExecutionContext` no longer has a `stack` member or a second type parameter, and its contract must be a MongoDB contract, as SQL's `ExecutionContext` requires a SQL contract: write `MongoExecutionContext`, and build it with `createMongoExecutionContext({ contract, stack })` over a validated contract. `MongoExecutionStack` no longer has a `driver` member, and `createMongoExecutionStack()` no longer takes a `driver` option. `buildMongoEnums()` is removed: read the contract's enum accessors from `db.enums`, or from `context.enums` on a context built with `createMongoExecutionContext()`. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments or a non-contract type argument, pass a driver to `createMongoExecutionStack`, or name `buildMongoEnums` or `MongoEnumAccessors`. detection: glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" matches: @@ -31,6 +31,7 @@ changes: - '(?]|<(?:[^<>]|<[^<>]*>)*>)*,' - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\bdriver\b' --- diff --git a/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md b/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md index b5604577222f..9bd32c46ede7 100644 --- a/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md +++ b/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md @@ -23,7 +23,7 @@ changes: - 'MUTATION_DEFAULTS_MISSING' - id: mongo-runtime-takes-adapter summary: | - `MongoRuntimeOptions` is `{ context, adapter, driver, middleware?, mode? }`, as the SQL runtime's options are: the caller creates the adapter with `stack.adapter.create(stack)`, and the runtime checks middleware against `context.contract.target`, so `context` is a `MongoExecutionContext`. `MongoExecutionContext` is declared in `@prisma/orm-family-mongo/query-ast/execution` (`@internal/mongo-query-ast/execution`), still exported from the runtime entry; it has no `stack` member and no second type parameter (`TTargetId`), and its type parameter is constrained as SQL's `ExecutionContext` is: `MongoExecutionContext`, and `createMongoExecutionContext` takes the same constraint. `buildMongoEnums` and `MongoEnumAccessors` are declared beside it in `@internal/mongo-query-ast/execution` and still exported from the runtime entry. `MongoExecutionStack` has no `driver` member, and `createMongoExecutionStack()` takes no `driver` option. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments or a non-contract type argument, or pass a driver to `createMongoExecutionStack`. + `MongoRuntimeOptions` is `{ context, adapter, driver, middleware?, mode? }`, as the SQL runtime's options are: the caller creates the adapter with `stack.adapter.create(stack)`, and the runtime checks middleware against `context.contract.target`, so `context` is a `MongoExecutionContext`. `MongoExecutionContext` is declared in `@prisma/orm-family-mongo/query-ast/execution` (`@internal/mongo-query-ast/execution`), still exported from the runtime entry; it has no `stack` member and no second type parameter (`TTargetId`), and its type parameter is constrained as SQL's `ExecutionContext` is: `MongoExecutionContext`, and `createMongoExecutionContext` takes the same constraint. `MongoExecutionStack` has no `driver` member, and `createMongoExecutionStack()` takes no `driver` option. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments or a non-co`buildMongoEnums()` and `MongoEnumAccessors` are removed: `createMongoExecutionContext()` builds the contract's enum accessors once, with the framework's `buildNamespacedEnums`, and holds them as `context.enums`; read them there or from `db.enums`. A hand-built `MongoExecutionContext` needs an `enums` member. ntract type argument, pass a driver to `createMongoExecutionStack`, or name `buildMongoEnums` or `MongoEnumAccessors`. detection: glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" matches: @@ -31,6 +31,7 @@ changes: - '(?]|<(?:[^<>]|<[^<>]*>)*>)*,' - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\bdriver\b' --- From 375428bcec92e208952b1c5a164579f3fb424e5f Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 16:55:23 +0100 Subject: [PATCH 18/19] TML-3686: db.enums is built by the static context, and the ORM builds its own accessors per client MongoExecutionContext has no enums, as SQL ExecutionContext has none. The static context builds db.enums with buildNamespacedEnums, as the Postgres static context does, with no cast. mongoOrm builds the accessors it checks written enum values against from the context once per client and hands them to every collection. Signed-off-by: willbot Signed-off-by: Will Madden --- docs/reference/error-reference.md | 2 +- .../4-query/query-ast/README.md | 2 +- .../query-ast/src/execution-context.ts | 5 +- .../5-query-builders/orm/README.md | 2 +- .../5-query-builders/orm/src/collection.ts | 42 +++++++++--- .../5-query-builders/orm/src/mongo-orm.ts | 6 +- .../orm/test/collection-context.ts | 2 - .../orm/test/mongo-orm-options.test-d.ts | 4 +- .../orm/test/mongo-raw.test.ts | 2 - .../test/fixtures/test-context.ts | 2 - packages/2-mongo-family/7-runtime/README.md | 1 - .../7-runtime/src/mongo-execution-stack.ts | 16 ----- .../7-runtime/test/execution-context.test.ts | 55 --------------- .../test/mongo-middleware-fixtures.ts | 1 - .../test/mongo-runtime-abort.test.ts | 1 - .../test/mongo-runtime.finalization.test.ts | 1 - .../mongo/9-facade/src/static/mongo-static.ts | 17 ++++- .../mongo/9-facade/test/enum-members.test.ts | 57 ++++++++++------ .../mongo/9-facade/test/mongo.test.ts | 68 +++++++++++++++---- .../9-facade/test/structured-errors.test.ts | 3 +- .../detection.test.ts | 2 +- .../app/instructions.md | 2 +- .../extension/instructions.md | 2 +- 23 files changed, 154 insertions(+), 141 deletions(-) diff --git a/docs/reference/error-reference.md b/docs/reference/error-reference.md index b15fe4c58338..936590986101 100644 --- a/docs/reference/error-reference.md +++ b/docs/reference/error-reference.md @@ -1208,7 +1208,7 @@ A codec descriptor handed to a target codec-descriptor registry (Postgres, SQLit ### RUNTIME.CODEC_DESCRIPTOR_MISSING -A column, a domain enum, or an AST-carried CodecRef references a `codecId` for which no runtime component registered a codec descriptor: usually the extension pack that owns the codec is missing from the runtime stack. Surfaces at SQL context construction during the contract codec walk, when a SQL or Mongo client builds `db.enums` (for Mongo, in `createMongoExecutionContext`, which builds the accessors `db.enums` and the ORM share), or lazily when the AST codec resolver materializes a codec at query time. Payload: `codecId`; on the column path also `table`, `column`. +A column, a domain enum, or an AST-carried CodecRef references a `codecId` for which no runtime component registered a codec descriptor: usually the extension pack that owns the codec is missing from the runtime stack. Surfaces at SQL context construction during the contract codec walk, when a SQL or Mongo client builds `db.enums` (for Mongo, also when `mongoOrm()` builds the accessors it checks written enum values against), or lazily when the AST codec resolver materializes a codec at query time. Payload: `codecId`; on the column path also `table`, `column`. ### RUNTIME.CODEC_MISSING diff --git a/packages/2-mongo-family/4-query/query-ast/README.md b/packages/2-mongo-family/4-query/query-ast/README.md index 903452480625..102ce10b0953 100644 --- a/packages/2-mongo-family/4-query/query-ast/README.md +++ b/packages/2-mongo-family/4-query/query-ast/README.md @@ -9,6 +9,6 @@ Typed AST for MongoDB filters, pipeline stages, and command variants, plus the u - **Commands**: `AnyMongoCommand` and related command types (reads, writes, aggregates) carried on a plan - **Query plan**: `MongoQueryPlan` — a branded typed representation with shape `{ collection, command: AnyMongoCommand, meta: PlanMeta }` - **Visitors**: `MongoFilterVisitor`, `MongoFilterRewriter`, `MongoStageVisitor` interfaces for traversing and transforming AST nodes -- **Execution context**: `MongoExecutionContext` — the contract, a read-only codec lookup, the contract's enum accessors and the mutation defaults the query lanes build against, as SQL's `ExecutionContext` is +- **Execution context**: `MongoExecutionContext` — the contract, a read-only codec lookup and the mutation defaults the query lanes build against, as SQL's `ExecutionContext` is Lowering typed nodes to wire BSON is performed by `@internal/adapter-mongo` via `lower(plan)` on `MongoQueryPlan`, which dispatches on `command.kind` and uses internal helpers for filters and pipelines. diff --git a/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts index 8148eae129b8..cb6930cc57e0 100644 --- a/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts +++ b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts @@ -1,4 +1,3 @@ -import type { NamespacedEnums } from '@internal/contract/enum-accessor'; import type { MutationDefaults } from '@internal/framework-components/runtime'; import type { MongoCodec } from '@internal/mongo-codec'; import type { MongoContract } from '@internal/mongo-contract'; @@ -14,7 +13,7 @@ export interface MongoCodecLookup { } /** - * The context the query lanes build against: the user's contract, a read-only lookup over the codec registry composed from every stack contributor, the contract's enum accessors built once through that lookup, and `applyMutationDefaults`, which fills the contract's execution defaults from the composed generators. The runtime creates it with `createMongoExecutionContext`. + * The context the query lanes build against: the user's contract, a read-only lookup over the codec registry composed from every stack contributor, and `applyMutationDefaults`, which fills the contract's execution defaults from the composed generators. The runtime creates it with `createMongoExecutionContext`. * * Mirrors SQL's `ExecutionContext`: it holds no runtime concerns such as the adapter or the stack; the runtime receives the adapter as its own option. */ @@ -22,6 +21,4 @@ export interface MongoExecutionContext; } diff --git a/packages/2-mongo-family/5-query-builders/orm/README.md b/packages/2-mongo-family/5-query-builders/orm/README.md index 079145fdf88b..e9b979fe8c49 100644 --- a/packages/2-mongo-family/5-query-builders/orm/README.md +++ b/packages/2-mongo-family/5-query-builders/orm/README.md @@ -4,7 +4,7 @@ MongoDB ORM client for Prisma 8. ## Responsibilities -- **ORM client factory**: `mongoOrm({ context, runtime })` creates a typed client with root-based collection accessors, as the SQL `orm({ context, runtime })` does. It fills generated fields through the context and refuses a written value outside a field's enum. `createMongoCollection({ context, runtime }, modelName)` creates one collection the same way +- **ORM client factory**: `mongoOrm({ context, runtime })` creates a typed client with root-based collection accessors, as the SQL `orm({ context, runtime })` does. It fills generated fields through the context and refuses a written value outside a field's enum, checking it against enum accessors it builds from the context once per client with the framework's `buildNamespacedEnums`. `createMongoCollection({ context, runtime }, modelName)` creates one collection the same way - **Typed queries**: `findMany` with equality filters (`MongoWhereFilter`) and reference includes (`MongoIncludeSpec`) - **Row type inference**: `InferFullRow` (scalar fields + embedded documents), `InferRootRow` (discriminated union for polymorphic roots), `IncludeResultFields` - **Polymorphic narrowing**: Discriminator field carries literal variant values, enabling TypeScript `switch`/`if` narrowing diff --git a/packages/2-mongo-family/5-query-builders/orm/src/collection.ts b/packages/2-mongo-family/5-query-builders/orm/src/collection.ts index b7d6551676b9..3b431f139c17 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/collection.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/collection.ts @@ -1,4 +1,4 @@ -import type { EnumAccessor } from '@internal/contract/enum-accessor'; +import { buildNamespacedEnums, type EnumAccessor } from '@internal/contract/enum-accessor'; import { type ContractField, type ContractModel, @@ -241,6 +241,26 @@ function topLevelUpdateFields( return fields; } +/** The contract's enum accessors by namespace and enum name, which a written enum value is checked against. */ +export type MongoEnumAccessors = Readonly< + Record> | undefined> +>; + +/** The context contract's enum accessors, each reading its members through its codec in the context. */ +export function buildEnumAccessors(context: MongoExecutionContext): MongoEnumAccessors { + return buildNamespacedEnums(context.contract.domain, (codecId) => { + const codec = context.codecs.get(codecId); + if (codec === undefined) { + throw runtimeError( + 'RUNTIME.CODEC_DESCRIPTOR_MISSING', + `No codec is registered for codecId '${codecId}', which a domain enum in the contract uses.`, + { codecId }, + ); + } + return codec; + }); +} + /** What a collection runs against: the execution context, whose contract, codecs and mutation defaults it uses, and the runtime that executes its plans. Mirrors the SQL ORM's `CollectionContext`. */ export interface MongoCollectionContext { readonly context: MongoExecutionContext; @@ -262,7 +282,7 @@ function isUnknownRecord(value: unknown): value is Record { const LIST_INDEX = /^\d+$/; -class MongoCollectionImpl< +export class MongoCollectionImpl< TContract extends MongoContractWithTypeMaps, ModelName extends string & keyof MongoModelsMap, TIncludes extends MongoIncludeSpec = NoIncludes, @@ -272,14 +292,20 @@ class MongoCollectionImpl< readonly #contract: TContract; readonly #modelName: ModelName; readonly #ctx: MongoCollectionContext; + readonly #enums: MongoEnumAccessors; readonly #executor: MongoQueryExecutor; readonly #mutationDefaults: MutationDefaults; #collectionName: string; #state: MongoCollectionState; #variantValue: string | undefined; - constructor(ctx: MongoCollectionContext, modelName: ModelName) { + constructor( + ctx: MongoCollectionContext, + modelName: ModelName, + enums: MongoEnumAccessors, + ) { this.#ctx = ctx; + this.#enums = enums; this.#contract = ctx.context.contract; this.#modelName = modelName; this.#executor = ctx.runtime; @@ -1134,11 +1160,7 @@ class MongoCollectionImpl< if (valueSet === undefined || valueSet.entityKind !== 'enum' || value === null) return; const contractEnum = this.#contract.domain.namespaces[valueSet.namespaceId]?.enum?.[valueSet.entityName]; - const enums = blindCast< - Readonly> | undefined>>, - 'the accessors are keyed by the contract namespace ids and enum names a field value set names' - >(this.#ctx.context.enums); - const accessor = enums[valueSet.namespaceId]?.[valueSet.entityName]; + const accessor = this.#enums[valueSet.namespaceId]?.[valueSet.entityName]; if (contractEnum === undefined || accessor === undefined) return; const values: readonly unknown[] = field.many && Array.isArray(value) ? value : [value]; const outside = values.findIndex((entry) => entry !== null && !accessor.has(entry)); @@ -1438,6 +1460,7 @@ class MongoCollectionImpl< const instance = new MongoCollectionImpl( this.#ctx, this.#modelName, + this.#enums, ); instance.#state = { ...this.#state, ...overrides }; instance.#collectionName = this.#collectionName; @@ -1452,6 +1475,7 @@ class MongoCollectionImpl< const instance = new MongoCollectionImpl( this.#ctx, this.#modelName, + this.#enums, ); instance.#state = { ...this.#state, ...overrides }; instance.#collectionName = this.#collectionName; @@ -1468,5 +1492,5 @@ export function createMongoCollection< ctx: MongoCollectionContext, modelName: ModelName, ): MongoCollection { - return new MongoCollectionImpl(ctx, modelName); + return new MongoCollectionImpl(ctx, modelName, buildEnumAccessors(ctx.context)); } diff --git a/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts b/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts index a1c4ba7cad3a..15bb1c7152c9 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts @@ -7,7 +7,7 @@ import type { import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { blindCast } from '@internal/utils/casts'; import type { MongoCollection } from './collection'; -import { createMongoCollection } from './collection'; +import { buildEnumAccessors, MongoCollectionImpl } from './collection'; import type { MongoQueryExecutor } from './executor'; export interface MongoOrmOptions { @@ -29,15 +29,17 @@ export function mongoOrm< TContract extends MongoContractWithTypeMaps, >(options: MongoOrmOptions): MongoOrmClient { const { context, runtime } = options; + const enums = buildEnumAccessors(context); const client: Record = {}; for (const [rootName, rootRef] of Object.entries(context.contract.roots)) { - client[rootName] = createMongoCollection( + client[rootName] = new MongoCollectionImpl( { context, runtime }, blindCast< RootModelName, 'roots entries are CrossReferences; rootRef.model is a valid RootModelName for this contract' >(rootRef.model), + enums, ); } diff --git a/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts b/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts index a97b8b96f139..65c1e4d07ca8 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/collection-context.ts @@ -1,4 +1,3 @@ -import type { NamespacedEnums } from '@internal/contract/enum-accessor'; import type { MutationDefaults } from '@internal/framework-components/runtime'; import type { MongoContract } from '@internal/mongo-contract'; import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; @@ -16,7 +15,6 @@ export function contextOf( return { contract, codecs: noCodecs, - enums: {} as NamespacedEnums, applyMutationDefaults: (options) => mutationDefaults.applyMutationDefaults(options), }; } diff --git a/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts b/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts index ffa347ccc77e..f9b69e6bfaa6 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/mongo-orm-options.test-d.ts @@ -1,7 +1,7 @@ import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { expectTypeOf, test } from 'vitest'; import type { Contract } from '../../../1-foundation/mongo-contract/test/fixtures/orm-contract'; -import { createMongoCollection } from '../src/collection'; +import { createMongoCollection, type MongoEnumAccessors } from '../src/collection'; import type { MongoQueryExecutor } from '../src/executor'; import { mongoOrm } from '../src/mongo-orm'; import { mongoRaw } from '../src/mongo-raw'; @@ -9,7 +9,7 @@ import { mongoRaw } from '../src/mongo-raw'; declare const contract: Contract; declare const context: MongoExecutionContext; declare const runtime: MongoQueryExecutor; -declare const enums: MongoExecutionContext['enums']; +declare const enums: MongoEnumAccessors; test('mongoOrm() takes a context and a runtime, as the SQL orm() does', () => { mongoOrm({ context, runtime }); diff --git a/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts index 921aff0d0b8b..416309f38aa7 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/mongo-raw.test.ts @@ -1,4 +1,3 @@ -import type { NamespacedEnums } from '@internal/contract/enum-accessor'; import type { MongoContract } from '@internal/mongo-contract'; import type { MongoExecutionContext, RawMongoCommand } from '@internal/mongo-query-ast/execution'; import { blindCast } from '@internal/utils/casts'; @@ -13,7 +12,6 @@ function contextOf( return { contract, codecs: { get: () => undefined, has: () => false }, - enums: {} as NamespacedEnums, applyMutationDefaults: () => [], }; } diff --git a/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts b/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts index 628c15a7ab0a..937b9f7045cc 100644 --- a/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts @@ -1,4 +1,3 @@ -import type { NamespacedEnums } from '@internal/contract/enum-accessor'; import type { MongoContract } from '@internal/mongo-contract'; import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; @@ -8,7 +7,6 @@ export function contextOf( return { contract, codecs: { get: () => undefined, has: () => false }, - enums: {} as NamespacedEnums, applyMutationDefaults: () => [], }; } diff --git a/packages/2-mongo-family/7-runtime/README.md b/packages/2-mongo-family/7-runtime/README.md index a017dba3018d..f50e71837c5b 100644 --- a/packages/2-mongo-family/7-runtime/README.md +++ b/packages/2-mongo-family/7-runtime/README.md @@ -48,7 +48,6 @@ Custom or third-party codecs (encryption, vendor scalars) are contributed via an ## Responsibilities - **Stack/context composition**: `createMongoExecutionStack` and `createMongoExecutionContext` mirror SQL's `createSqlExecutionStack` / `createExecutionContext`. The context aggregates codec contributions from `[stack.target, stack.adapter, ...stack.extensions]` into a single `MongoCodecRegistry`. The `MongoExecutionContext` type is declared in `@internal/mongo-query-ast`, as SQL's `ExecutionContext` is declared in relational-core, and holds no stack: the query lanes build against it, and the runtime receives the adapter as its own option. -- **Enum accessors**: `createMongoExecutionContext` builds the contract's enum accessors once, with the framework's `buildNamespacedEnums` over the stack's codecs, and holds them as `context.enums`: what `db.enums` holds and what the ORM checks a written enum value against. A contract whose enum codec no stack component contributes is refused with `RUNTIME.CODEC_DESCRIPTOR_MISSING`. - **Runtime executor**: `createMongoRuntime({ context, adapter, driver, ... })` composes context, adapter and driver into a `MongoRuntime`, as the SQL runtime does. The caller creates the adapter with `stack.adapter.create(stack)`, and the runtime checks middleware against the target named by `context.contract.target`. The runtime has separate `query(plan)` and `execute(plan)` operations accepting `MongoQueryPlan` from `@internal/mongo-query-ast`. `query()` yields decoded rows, while `execute()` returns statement statistics. Both operations lower the plan with the adapter, and run the wire command on the driver; `query()` **optionally decodes** each row when `plan.resultShape` is present. - **Operation flow**: Row-producing work uses `query(plan)` and the query middleware lifecycle; statistics-producing work uses `execute(plan)` and the execute middleware lifecycle. Each operation allocates its own `CodecCallContext` and threads it through its codec dispatches. - **Lowering**: Happens in the adapter (`lower(plan)`), wrapped by the runtime's `lower` override into a `MongoExecutionPlan`. diff --git a/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts index 1b75ffe027df..5a4c31524a3b 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts @@ -1,4 +1,3 @@ -import { buildNamespacedEnums } from '@internal/contract/enum-accessor'; import { createExecutionStack, type RuntimeAdapterDescriptor, @@ -121,24 +120,9 @@ export function createMongoExecutionContext< } } - const enums = Object.freeze( - buildNamespacedEnums(options.contract.domain, (codecId) => { - const codec = registry.get(codecId); - if (codec === undefined) { - throw runtimeError( - 'RUNTIME.CODEC_DESCRIPTOR_MISSING', - `No codec is registered for codecId '${codecId}', which a domain enum in the contract uses.`, - { codecId }, - ); - } - return codec; - }), - ); - return Object.freeze({ contract: options.contract, codecs: registry, - enums, applyMutationDefaults: (mutation: MutationDefaultsOptions) => applyMutationDefaults(execution, generators, mutation), }); diff --git a/packages/2-mongo-family/7-runtime/test/execution-context.test.ts b/packages/2-mongo-family/7-runtime/test/execution-context.test.ts index 5c50a3ecda7f..60cd53c351da 100644 --- a/packages/2-mongo-family/7-runtime/test/execution-context.test.ts +++ b/packages/2-mongo-family/7-runtime/test/execution-context.test.ts @@ -123,61 +123,6 @@ function fields( } describe('createMongoExecutionContext composition', () => { - it("builds the contract's enum accessors once, reading members through the stack's codecs", () => { - const stack = createMongoExecutionStack({ - target: target(), - adapter: adapter(), - extensions: [extension('levels', { codecIds: ['test/level@1'] })], - }); - const contract = { - ...testContract, - domain: { - namespaces: { - [NS]: { - enum: { - Level: { - codecId: 'test/level@1', - members: [ - { name: 'Low', value: 'low' }, - { name: 'High', value: 'high' }, - ], - }, - }, - }, - }, - }, - } as unknown as MongoContract; - - const context = createMongoExecutionContext({ contract, stack }); - const firstRead = context.enums[NS]?.['Level']; - - expect({ - namespaces: Object.keys(context.enums), - members: firstRead?.members, - }).toEqual({ namespaces: [NS], members: { Low: 'low', High: 'high' } }); - expect(context.enums[NS]?.['Level']).toBe(firstRead); - }); - - it('refuses a contract whose enum codec no stack component contributes', () => { - const stack = createMongoExecutionStack({ target: target(), adapter: adapter() }); - const contract = { - ...testContract, - domain: { - namespaces: { - [NS]: { enum: { Level: { codecId: 'test/level@1', members: [] } } }, - }, - }, - } as unknown as MongoContract; - - expect(() => createMongoExecutionContext({ contract, stack })).toThrow( - expect.objectContaining({ - code: 'RUNTIME.CODEC_DESCRIPTOR_MISSING', - message: - "No codec is registered for codecId 'test/level@1', which a domain enum in the contract uses.", - }), - ); - }); - it('folds the codecs of target, adapter and extensions into one lookup', () => { const stack = createMongoExecutionStack({ target: target(), diff --git a/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts b/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts index 769cc7523f08..14c8c2a09ca5 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts @@ -32,7 +32,6 @@ export function makeExecution( context: Object.freeze({ contract: testContract, codecs, - enums: {}, applyMutationDefaults: () => [], }), adapter, diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts index e976fe39656e..5a246d84debd 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts @@ -61,7 +61,6 @@ function makeExecution(adapter: MongoAdapter): { context: Object.freeze({ contract: testContract, codecs, - enums: {}, applyMutationDefaults: () => [], }), adapter, diff --git a/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts index b51b3574b613..8dad785a0629 100644 --- a/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts +++ b/packages/2-mongo-family/7-runtime/test/mongo-runtime.finalization.test.ts @@ -88,7 +88,6 @@ function makeExecution( context: { contract: testContract, codecs: codecsWithFailingDecode(), - enums: {}, applyMutationDefaults: () => [], }, adapter, diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts index 4622c5777916..858ee292f9be 100644 --- a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts +++ b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts @@ -1,6 +1,7 @@ import mongoRuntimeAdapter from '@internal/adapter-mongo/runtime'; -import type { NamespacedEnums } from '@internal/contract/enum-accessor'; +import { buildNamespacedEnums, type NamespacedEnums } from '@internal/contract/enum-accessor'; import { UNBOUND_NAMESPACE_ID } from '@internal/framework-components/ir'; +import { runtimeError } from '@internal/framework-components/runtime'; import type { AnyMongoTypeMaps, MongoContract, @@ -47,7 +48,19 @@ export function buildMongoStaticContext< adapter: mongoRuntimeAdapter, }); const context = createMongoExecutionContext({ contract, stack }); - const enums = context.enums[UNBOUND_NAMESPACE_ID]; + const enums = Object.freeze( + buildNamespacedEnums(context.contract.domain, (codecId) => { + const codec = context.codecs.get(codecId); + if (codec === undefined) { + throw runtimeError( + 'RUNTIME.CODEC_DESCRIPTOR_MISSING', + `No codec is registered for codecId '${codecId}', which a domain enum in the contract uses.`, + { codecId }, + ); + } + return codec; + }), + )[UNBOUND_NAMESPACE_ID]; assertDefined(enums, 'the unbound namespace always exists on a mongo builder output'); const query = mongoQuery({ context }); const raw = mongoRaw({ context }); diff --git a/packages/3-targets/mongo/9-facade/test/enum-members.test.ts b/packages/3-targets/mongo/9-facade/test/enum-members.test.ts index 259393ad7694..2bf73c7230cf 100644 --- a/packages/3-targets/mongo/9-facade/test/enum-members.test.ts +++ b/packages/3-targets/mongo/9-facade/test/enum-members.test.ts @@ -35,6 +35,10 @@ const contract = defineContract({ double: field.namedType(DoubleLevel), }, }), + Note: model('Note', { + collection: 'notes', + fields: { _id: field.objectId(), level: field.namedType(TextLevel) }, + }), }, }); @@ -118,31 +122,40 @@ describe('the Mongo ORM checks a written enum value against the enums of its con }); }); - it('reads the accessors db.enums holds, the ones its context built once', async () => { - const int32Level = context.enums['__unbound__']?.['Int32Level']; - expect(enums.Int32Level).toBe(int32Level); + it('builds db.enums once, so every read returns the same accessor', () => { + const db = mongoStatic({ contract }); + expect(db.enums.Int32Level).toBe(db.enums.Int32Level); + expect(db.enums.Int32Level).not.toBe(enums.Int32Level); + }); - const refuseEverything = vi.fn(() => false); - const sharedAccessors = { - __unbound__: { - TextLevel: { ...enums.TextLevel, has: refuseEverything }, - Int32Level: enums.Int32Level, - DoubleLevel: enums.DoubleLevel, - }, - }; - const sharedContext = { ...context, enums: sharedAccessors as unknown as typeof context.enums }; - const orm = mongoOrm({ context: sharedContext, runtime: unreachableDatabase }); - const readings = createMongoCollection( - { context: sharedContext, runtime: unreachableDatabase }, - 'Reading', - ); + it('builds its own accessors once per ORM client, which hold the values db.enums holds', async () => { + const codecLookups = vi.fn((codecId: string) => context.codecs.get(codecId)); + const countingContext = { ...context, codecs: { ...context.codecs, get: codecLookups } }; - await expect(orm.readings.create(reading)).rejects.toMatchObject({ - code: 'RUNTIME.ENCODE_FAILED', - }); - await expect(readings.create(reading)).rejects.toMatchObject({ + const orm = mongoOrm({ context: countingContext, runtime: unreachableDatabase }); + + expect(codecLookups).toHaveBeenCalledTimes(Object.keys(enums).length); + await expect(orm.readings.create(reading)).rejects.toThrow('no database'); + await expect(orm.notes.create({ level: enums.TextLevel.members.Low })).rejects.toThrow( + 'no database', + ); + await expect(orm.notes.create({ level: 'medium' as never })).rejects.toMatchObject({ code: 'RUNTIME.ENCODE_FAILED', }); - expect(refuseEverything).toHaveBeenCalledTimes(2); + }); + + it('refuses to build an ORM client whose context has no codec for an enum', () => { + expect(() => + mongoOrm({ + context: { ...context, codecs: { get: () => undefined, has: () => false } }, + runtime: unreachableDatabase, + }), + ).toThrow( + expect.objectContaining({ + code: 'RUNTIME.CODEC_DESCRIPTOR_MISSING', + message: + "No codec is registered for codecId 'mongo/string@1', which a domain enum in the contract uses.", + }), + ); }); }); diff --git a/packages/3-targets/mongo/9-facade/test/mongo.test.ts b/packages/3-targets/mongo/9-facade/test/mongo.test.ts index bddd1b0aea30..3256fad49ed2 100644 --- a/packages/3-targets/mongo/9-facade/test/mongo.test.ts +++ b/packages/3-targets/mongo/9-facade/test/mongo.test.ts @@ -75,7 +75,11 @@ const fakeOrm = { id: 'orm-instance' }; const fakeQuery = { id: 'query-instance' }; const fakeRaw = { id: 'raw-instance', collection: vi.fn() }; const fakeAdapter = { id: 'adapter-instance' }; -const fakeContext = { id: 'context-instance', enums: { __unbound__: {} } }; +const fakeContext = { + id: 'context-instance', + contract: fakeContract, + codecs: { get: () => undefined }, +}; const fakeStack = { id: 'stack-instance', adapter: { create: () => fakeAdapter } }; describe('mongo() facade', () => { @@ -433,27 +437,67 @@ describe('mongo() facade', () => { }); describe('db.enums (facade)', () => { - const roleAccessor = { values: ['user', 'admin'] }; - const contextWithEnums = { - id: 'context-instance', - enums: { __unbound__: { Role: roleAccessor } }, + const roleEnum = { + codecId: 'mongo/string@1', + members: [ + { name: 'User', value: 'user' }, + { name: 'Admin', value: 'admin' }, + ], + } as const; + + const contractWithEnum = { + domain: { + namespaces: { + __unbound__: { models: {}, enum: { Role: roleEnum } }, + }, + }, + } as unknown as AnyMongoContract; + + const stringCodec = { + dataType: { fromContract: (json: unknown) => json, toContract: (value: unknown) => value }, + fromDataTypeValue: (value: unknown) => value, + toDataTypeValue: (value: unknown) => value, }; beforeEach(() => { - mocks.createMongoExecutionContext.mockReturnValue(contextWithEnums); + mocks.deserializeContract.mockReturnValue(contractWithEnum); + mocks.createMongoExecutionContext.mockReturnValue({ + id: 'context-instance', + contract: contractWithEnum, + codecs: { get: (id: string) => (id === 'mongo/string@1' ? stringCodec : undefined) }, + }); }); - it('holds the accessors of the execution context, the ones the ORM checks writes against', () => { - const db = mongo({ contract: fakeContract, url: 'mongodb://localhost:27017/mydb' }); + function roleAccessor() { + const db = mongo({ contract: contractWithEnum, url: 'mongodb://localhost:27017/mydb' }); + return db.enums['Role']!; + } - expect(db.enums).toBe(contextWithEnums.enums.__unbound__); - expect(db.enums['Role']).toBe(roleAccessor); + it('refuses to build a client whose runtime has no codec for an enum', () => { + mocks.createMongoExecutionContext.mockReturnValue({ + id: 'context-instance', + contract: contractWithEnum, + codecs: { get: () => undefined }, + }); + expect(() => + mongo({ contract: contractWithEnum, url: 'mongodb://localhost:27017/mydb' }), + ).toThrow( + expect.objectContaining({ + code: 'RUNTIME.CODEC_DESCRIPTOR_MISSING', + message: + "No codec is registered for codecId 'mongo/string@1', which a domain enum in the contract uses.", + }), + ); + }); + + it('exposes the enum accessor at db.enums.Role', () => { + expect(roleAccessor().values).toEqual(['user', 'admin']); }); it('builds the enums surface eagerly, without connecting the driver', () => { - const db = mongo({ contract: fakeContract, url: 'mongodb://localhost:27017/mydb' }); + const db = mongo({ contract: contractWithEnum, url: 'mongodb://localhost:27017/mydb' }); - expect(db.enums['Role']).toBe(roleAccessor); + expect(db.enums['Role']!.values).toEqual(['user', 'admin']); expect(mocks.driverFromConnection).not.toHaveBeenCalled(); expect(mocks.createMongoRuntime).not.toHaveBeenCalled(); }); diff --git a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts index 74d8bef7a1bb..9cef2449e07c 100644 --- a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts +++ b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts @@ -186,7 +186,8 @@ describe('facade lifecycle structured codes', () => { }); mocks.createMongoExecutionContext.mockReturnValue({ id: 'context', - enums: { __unbound__: {} }, + contract: { domain: { namespaces: { __unbound__: {} } } }, + codecs: { get: () => undefined }, }); mocks.driverFromConnection.mockResolvedValue({ id: 'driver', diff --git a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts index b24b55417924..72fa1908dd70 100644 --- a/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts +++ b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts @@ -113,7 +113,7 @@ describe.each(['app', 'extension'] as const)('the %s runtime adapter detection', ['a stack on another object', 'options.stack.adapter'], ['a context type over one contract', 'MongoExecutionContext'], ['a context type over a contract typeof', 'MongoExecutionContext'], - ['the enum accessors of a context', 'const enums = context.enums;'], + ['the enum accessors of a client', 'const { enums } = mongoStatic({ contract });'], ['a context type over a generic contract', 'MongoExecutionContext>'], ['a stack built without a driver', 'createMongoExecutionStack({ target, adapter })'], ])('skips %s', (_name, content) => { diff --git a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md index 6087b7ca50e9..9760963d1ba2 100644 --- a/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md +++ b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md @@ -23,7 +23,7 @@ changes: - 'MUTATION_DEFAULTS_MISSING' - id: mongo-runtime-takes-adapter summary: | - `createMongoRuntime()` takes `{ context, adapter, driver }`, as the SQL runtime does: create the adapter with `stack.adapter.create(stack)` and pass it beside the context. The runtime reads the target from `context.contract.target`, so build the context over the validated contract. `MongoExecutionContext` no longer has a `stack` member or a second type parameter, and its contract must be a MongoDB contract, as SQL's `ExecutionContext` requires a SQL contract: write `MongoExecutionContext`, and build it with `createMongoExecutionContext({ contract, stack })` over a validated contract. `MongoExecutionStack` no longer has a `driver` member, and `createMongoExecutionStack()` no longer takes a `driver` option. `buildMongoEnums()` is removed: read the contract's enum accessors from `db.enums`, or from `context.enums` on a context built with `createMongoExecutionContext()`. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments or a non-contract type argument, pass a driver to `createMongoExecutionStack`, or name `buildMongoEnums` or `MongoEnumAccessors`. + `createMongoRuntime()` takes `{ context, adapter, driver }`, as the SQL runtime does: create the adapter with `stack.adapter.create(stack)` and pass it beside the context. The runtime reads the target from `context.contract.target`, so build the context over the validated contract. `MongoExecutionContext` no longer has a `stack` member or a second type parameter, and its contract must be a MongoDB contract, as SQL's `ExecutionContext` requires a SQL contract: write `MongoExecutionContext`, and build it with `createMongoExecutionContext({ contract, stack })` over a validated contract. `MongoExecutionStack` no longer has a `driver` member, and `createMongoExecutionStack()` no longer takes a `driver` option. `buildMongoEnums()` is removed: read the contract's enum accessors from `db.enums`, from `mongo({ contract })` or `mongoStatic({ contract })`. `MongoExecutionContext` has no `enums` member. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments or a non-contract type argument, pass a driver to `createMongoExecutionStack`, or name `buildMongoEnums` or `MongoEnumAccessors`. detection: glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" matches: diff --git a/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md b/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md index 9bd32c46ede7..2796a1697345 100644 --- a/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md +++ b/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md @@ -23,7 +23,7 @@ changes: - 'MUTATION_DEFAULTS_MISSING' - id: mongo-runtime-takes-adapter summary: | - `MongoRuntimeOptions` is `{ context, adapter, driver, middleware?, mode? }`, as the SQL runtime's options are: the caller creates the adapter with `stack.adapter.create(stack)`, and the runtime checks middleware against `context.contract.target`, so `context` is a `MongoExecutionContext`. `MongoExecutionContext` is declared in `@prisma/orm-family-mongo/query-ast/execution` (`@internal/mongo-query-ast/execution`), still exported from the runtime entry; it has no `stack` member and no second type parameter (`TTargetId`), and its type parameter is constrained as SQL's `ExecutionContext` is: `MongoExecutionContext`, and `createMongoExecutionContext` takes the same constraint. `MongoExecutionStack` has no `driver` member, and `createMongoExecutionStack()` takes no `driver` option. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments or a non-co`buildMongoEnums()` and `MongoEnumAccessors` are removed: `createMongoExecutionContext()` builds the contract's enum accessors once, with the framework's `buildNamespacedEnums`, and holds them as `context.enums`; read them there or from `db.enums`. A hand-built `MongoExecutionContext` needs an `enums` member. ntract type argument, pass a driver to `createMongoExecutionStack`, or name `buildMongoEnums` or `MongoEnumAccessors`. + `MongoRuntimeOptions` is `{ context, adapter, driver, middleware?, mode? }`, as the SQL runtime's options are: the caller creates the adapter with `stack.adapter.create(stack)`, and the runtime checks middleware against `context.contract.target`, so `context` is a `MongoExecutionContext`. `MongoExecutionContext` is declared in `@prisma/orm-family-mongo/query-ast/execution` (`@internal/mongo-query-ast/execution`), still exported from the runtime entry; it has no `stack` member and no second type parameter (`TTargetId`), and its type parameter is constrained as SQL's `ExecutionContext` is: `MongoExecutionContext`, and `createMongoExecutionContext` takes the same constraint. `MongoExecutionStack` has no `driver` member, and `createMongoExecutionStack()` takes no `driver` option. `buildMongoEnums()` and `MongoEnumAccessors` are removed, and `MongoExecutionContext` has no `enums` member, as SQL's `ExecutionContext` has none: the static context builds `db.enums` with the framework's `buildNamespacedEnums`, as `postgresStatic()` does, and the ORM builds the accessors it checks writes against from the context, once per client. The detection matches files that call `createMongoRuntime(`, read `.stack` from a context, give `MongoExecutionContext` two type arguments or a non-contract type argument, pass a driver to `createMongoExecutionStack`, or name `buildMongoEnums` or `MongoEnumAccessors`. detection: glob: "**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx}" matches: From 0b3daac6c87f0cee9cfaeb7f568c0dc2f9d8f958 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 17:00:41 +0100 Subject: [PATCH 19/19] TML-3686: one codec lookup builds Mongo enum accessors enumMemberCodecFor(codecs), beside MongoCodecLookup in query-ast, is the lookup buildNamespacedEnums reads enum members through. The static context and the ORM both use it instead of each writing its own. Signed-off-by: willbot Signed-off-by: Will Madden --- .../query-ast/src/execution-context.ts | 18 ++++++++++++- .../query-ast/src/exports/execution.ts | 1 + .../test/enum-member-codec-for.test.ts | 26 +++++++++++++++++++ .../5-query-builders/orm/src/collection.ts | 13 ++-------- .../7-runtime/src/exports/index.ts | 1 + .../mongo/9-facade/src/static/mongo-static.ts | 14 ++-------- .../mongo/9-facade/test/mongo.test.ts | 4 ++- .../9-facade/test/structured-errors.test.ts | 4 ++- 8 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 packages/2-mongo-family/4-query/query-ast/test/enum-member-codec-for.test.ts diff --git a/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts index cb6930cc57e0..7d97dc35b27b 100644 --- a/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts +++ b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts @@ -1,4 +1,5 @@ -import type { MutationDefaults } from '@internal/framework-components/runtime'; +import type { EnumMemberCodecFor } from '@internal/contract/enum-accessor'; +import { type MutationDefaults, runtimeError } from '@internal/framework-components/runtime'; import type { MongoCodec } from '@internal/mongo-codec'; import type { MongoContract } from '@internal/mongo-contract'; @@ -12,6 +13,21 @@ export interface MongoCodecLookup { has(id: string): boolean; } +/** The codec lookup `buildNamespacedEnums` reads enum members through: each enum's codec in `codecs`, refusing an enum whose codec `codecs` lacks with `RUNTIME.CODEC_DESCRIPTOR_MISSING`. */ +export function enumMemberCodecFor(codecs: MongoCodecLookup): EnumMemberCodecFor { + return (codecId) => { + const codec = codecs.get(codecId); + if (codec === undefined) { + throw runtimeError( + 'RUNTIME.CODEC_DESCRIPTOR_MISSING', + `No codec is registered for codecId '${codecId}', which a domain enum in the contract uses.`, + { codecId }, + ); + } + return codec; + }; +} + /** * The context the query lanes build against: the user's contract, a read-only lookup over the codec registry composed from every stack contributor, and `applyMutationDefaults`, which fills the contract's execution defaults from the composed generators. The runtime creates it with `createMongoExecutionContext`. * diff --git a/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts b/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts index bcf76279755e..6af154c87303 100644 --- a/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts +++ b/packages/2-mongo-family/4-query/query-ast/src/exports/execution.ts @@ -27,6 +27,7 @@ export { UpdateOneCommand, } from '../commands'; export type { MongoCodecLookup, MongoExecutionContext } from '../execution-context'; +export { enumMemberCodecFor } from '../execution-context'; export type { MongoFilterExpr } from '../filter-expressions'; export { isMongoFilterExpr, diff --git a/packages/2-mongo-family/4-query/query-ast/test/enum-member-codec-for.test.ts b/packages/2-mongo-family/4-query/query-ast/test/enum-member-codec-for.test.ts new file mode 100644 index 000000000000..c17ae1658ebc --- /dev/null +++ b/packages/2-mongo-family/4-query/query-ast/test/enum-member-codec-for.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { enumMemberCodecFor, type MongoCodecLookup } from '../src/execution-context'; + +describe('enumMemberCodecFor', () => { + it("returns an enum's codec from the lookup", () => { + const codec = { id: 'test/level@1' }; + const codecs = { + get: (id: string) => (id === 'test/level@1' ? codec : undefined), + has: (id: string) => id === 'test/level@1', + } as unknown as MongoCodecLookup; + + expect(enumMemberCodecFor(codecs)('test/level@1')).toBe(codec); + }); + + it('refuses an enum whose codec the lookup lacks', () => { + const codecs: MongoCodecLookup = { get: () => undefined, has: () => false }; + + expect(() => enumMemberCodecFor(codecs)('test/level@1')).toThrow( + expect.objectContaining({ + code: 'RUNTIME.CODEC_DESCRIPTOR_MISSING', + message: + "No codec is registered for codecId 'test/level@1', which a domain enum in the contract uses.", + }), + ); + }); +}); diff --git a/packages/2-mongo-family/5-query-builders/orm/src/collection.ts b/packages/2-mongo-family/5-query-builders/orm/src/collection.ts index 3b431f139c17..366dcfa2d289 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/collection.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/collection.ts @@ -38,6 +38,7 @@ import type { } from '@internal/mongo-query-ast/execution'; import { DeleteManyCommand, + enumMemberCodecFor, FindOneAndDeleteCommand, FindOneAndUpdateCommand, freezeMongoResultShape, @@ -248,17 +249,7 @@ export type MongoEnumAccessors = Readonly< /** The context contract's enum accessors, each reading its members through its codec in the context. */ export function buildEnumAccessors(context: MongoExecutionContext): MongoEnumAccessors { - return buildNamespacedEnums(context.contract.domain, (codecId) => { - const codec = context.codecs.get(codecId); - if (codec === undefined) { - throw runtimeError( - 'RUNTIME.CODEC_DESCRIPTOR_MISSING', - `No codec is registered for codecId '${codecId}', which a domain enum in the contract uses.`, - { codecId }, - ); - } - return codec; - }); + return buildNamespacedEnums(context.contract.domain, enumMemberCodecFor(context.codecs)); } /** What a collection runs against: the execution context, whose contract, codecs and mutation defaults it uses, and the runtime that executes its plans. Mirrors the SQL ORM's `CollectionContext`. */ diff --git a/packages/2-mongo-family/7-runtime/src/exports/index.ts b/packages/2-mongo-family/7-runtime/src/exports/index.ts index bb93dc3a9ad5..7c4526e30d41 100644 --- a/packages/2-mongo-family/7-runtime/src/exports/index.ts +++ b/packages/2-mongo-family/7-runtime/src/exports/index.ts @@ -1,5 +1,6 @@ export type { RuntimeTargetInstance } from '@internal/framework-components/execution'; export type { MongoCodecLookup, MongoExecutionContext } from '@internal/mongo-query-ast/execution'; +export { enumMemberCodecFor } from '@internal/mongo-query-ast/execution'; export type { MongoExecutionPlan } from '../mongo-execution-plan'; export type { MongoExecutionStack, diff --git a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts index 858ee292f9be..f35e229d4a34 100644 --- a/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts +++ b/packages/3-targets/mongo/9-facade/src/static/mongo-static.ts @@ -1,7 +1,6 @@ import mongoRuntimeAdapter from '@internal/adapter-mongo/runtime'; import { buildNamespacedEnums, type NamespacedEnums } from '@internal/contract/enum-accessor'; import { UNBOUND_NAMESPACE_ID } from '@internal/framework-components/ir'; -import { runtimeError } from '@internal/framework-components/runtime'; import type { AnyMongoTypeMaps, MongoContract, @@ -13,6 +12,7 @@ import { mongoQuery } from '@internal/mongo-query-builder'; import { createMongoExecutionContext, createMongoExecutionStack, + enumMemberCodecFor, type MongoExecutionContext, type MongoExecutionStack, } from '@internal/mongo-runtime'; @@ -49,17 +49,7 @@ export function buildMongoStaticContext< }); const context = createMongoExecutionContext({ contract, stack }); const enums = Object.freeze( - buildNamespacedEnums(context.contract.domain, (codecId) => { - const codec = context.codecs.get(codecId); - if (codec === undefined) { - throw runtimeError( - 'RUNTIME.CODEC_DESCRIPTOR_MISSING', - `No codec is registered for codecId '${codecId}', which a domain enum in the contract uses.`, - { codecId }, - ); - } - return codec; - }), + buildNamespacedEnums(context.contract.domain, enumMemberCodecFor(context.codecs)), )[UNBOUND_NAMESPACE_ID]; assertDefined(enums, 'the unbound namespace always exists on a mongo builder output'); const query = mongoQuery({ context }); diff --git a/packages/3-targets/mongo/9-facade/test/mongo.test.ts b/packages/3-targets/mongo/9-facade/test/mongo.test.ts index 3256fad49ed2..9c36693bc023 100644 --- a/packages/3-targets/mongo/9-facade/test/mongo.test.ts +++ b/packages/3-targets/mongo/9-facade/test/mongo.test.ts @@ -4,6 +4,7 @@ import type { MongoContract, MongoContractWithTypeMaps, } from '@internal/mongo-contract'; +import type * as MongoRuntime from '@internal/mongo-runtime'; import { beforeEach, describe, expect, it, vi } from 'vitest'; type AnyMongoContract = MongoContractWithTypeMaps; @@ -31,7 +32,8 @@ vi.mock('@internal/target-mongo/runtime', () => ({ default: mocks.mongoRuntimeTarget, })); -vi.mock('@internal/mongo-runtime', () => ({ +vi.mock('@internal/mongo-runtime', async (importOriginal) => ({ + enumMemberCodecFor: (await importOriginal()).enumMemberCodecFor, createMongoExecutionStack: mocks.createMongoExecutionStack, createMongoExecutionContext: mocks.createMongoExecutionContext, createMongoRuntime: mocks.createMongoRuntime, diff --git a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts index 9cef2449e07c..1562efcebf16 100644 --- a/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts +++ b/packages/3-targets/mongo/9-facade/test/structured-errors.test.ts @@ -4,6 +4,7 @@ import type { MongoContract, MongoContractWithTypeMaps, } from '@internal/mongo-contract'; +import type * as MongoRuntime from '@internal/mongo-runtime'; import { isStructuredError } from '@internal/utils/structured-error'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -23,7 +24,8 @@ const mocks = vi.hoisted(() => ({ vi.mock('@internal/adapter-mongo/runtime', () => ({ default: { id: 'adapter' } })); vi.mock('@internal/target-mongo/runtime', () => ({ default: { id: 'target' } })); -vi.mock('@internal/mongo-runtime', () => ({ +vi.mock('@internal/mongo-runtime', async (importOriginal) => ({ + enumMemberCodecFor: (await importOriginal()).enumMemberCodecFor, createMongoExecutionStack: mocks.createMongoExecutionStack, createMongoExecutionContext: mocks.createMongoExecutionContext, createMongoRuntime: mocks.createMongoRuntime,