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..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({ contract, runtime }); +const db = mongoOrm({ context, runtime }); // 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 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/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..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 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, 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 96c69e8a04fa..808f0f833259 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, 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({ contract, executor: runtime, enums: buildMongoEnums(contract, context.codecs) }); +const db = mongoOrm({ context, runtime }); class UserCollection extends Collection { admins() { return this.where({ kind: 'admin' }); } @@ -353,10 +353,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. +`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 '@internal/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/architecture docs/subsystems/4. Runtime & Middleware Framework.md b/docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md index d1d601f9bef3..a23bcb3ac614 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. + - `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/docs/reference/Mongo Pipeline Builder.md b/docs/reference/Mongo Pipeline Builder.md index aab8c6c63964..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 '@internal/mongo/static'; -import { mongoQuery } from '@internal/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') @@ -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/docs/reference/error-reference.md b/docs/reference/error-reference.md index 6dacb2ef1808..936590986101 100644 --- a/docs/reference/error-reference.md +++ b/docs/reference/error-reference.md @@ -1030,7 +1030,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 @@ -1088,10 +1088,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`. @@ -1212,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, 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/examples/mongo-demo/README.md b/examples/mongo-demo/README.md index 1d9bc7ca593f..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 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 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 b8ee866626aa..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, @@ -75,13 +74,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, driver, middleware: [cache] }); - const orm = mongoOrm({ - contract, - executor: runtime, - enums: buildMongoEnums(contract, context.codecs), + const runtime = createMongoRuntime({ + context, + adapter: stack.adapter.create(stack), + driver, + middleware: [cache], }); - const query = mongoQuery({ contract }); + const orm = mongoOrm({ context, runtime: runtime }); + const query = mongoQuery({ context }); return { runtime, orm, query, driver, driverExecuteSpy }; } 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 1c6dfdbebd55..0ce94336dc94 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 @@ -31,7 +31,7 @@ export interface ExecutionStack< > { readonly target: RuntimeTargetDescriptor; readonly adapter: RuntimeAdapterDescriptor; - readonly driver: + readonly driver?: | RuntimeDriverDescriptor | undefined; readonly extensions: readonly RuntimeExtensionDescriptor< 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/4-query/query-ast/README.md b/packages/2-mongo-family/4-query/query-ast/README.md index ed7444195ceb..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,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 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/package.json b/packages/2-mongo-family/4-query/query-ast/package.json index 09a52c02fe95..89726fcd85c3 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.21", "@internal/framework-components": "workspace:8.0.0-rc.21", + "@internal/mongo-codec": "workspace:8.0.0-rc.21", "@internal/mongo-contract": "workspace:8.0.0-rc.21", "@internal/mongo-value": "workspace:8.0.0-rc.21", "@internal/utils": "workspace:8.0.0-rc.21", 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..7d97dc35b27b --- /dev/null +++ b/packages/2-mongo-family/4-query/query-ast/src/execution-context.ts @@ -0,0 +1,40 @@ +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'; + +/** + * 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 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`. + * + * 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 { + 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..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 @@ -26,6 +26,8 @@ export { UpdateManyCommand, 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/README.md b/packages/2-mongo-family/5-query-builders/orm/README.md index 49084898573f..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()` 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, 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 e43392db04e1..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 @@ -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, @@ -30,6 +30,7 @@ import type { AnyMongoCommand, InsertManyResult, InsertOneResult, + MongoExecutionContext, MongoFieldShape, MongoFilterExpr, MongoQueryPlan, @@ -37,6 +38,7 @@ import type { } from '@internal/mongo-query-ast/execution'; import { DeleteManyCommand, + enumMemberCodecFor, FindOneAndDeleteCommand, FindOneAndUpdateCommand, freezeMongoResultShape, @@ -240,13 +242,22 @@ 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>>> +/** 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, 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`. */ +export interface MongoCollectionContext { + readonly context: MongoExecutionContext; + readonly runtime: MongoQueryExecutor; +} + function describeValue(value: unknown): string { if (typeof value === 'bigint') return `${value}n`; try { @@ -262,7 +273,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, @@ -271,26 +282,26 @@ class MongoCollectionImpl< { readonly #contract: TContract; readonly #modelName: ModelName; + readonly #ctx: MongoCollectionContext; + readonly #enums: MongoEnumAccessors; readonly #executor: MongoQueryExecutor; - readonly #mutationDefaults: MutationDefaults | undefined; - readonly #enums: MongoOrmEnums; + readonly #mutationDefaults: MutationDefaults; #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.#modelName = modelName; - this.#executor = executor; - this.#mutationDefaults = mutationDefaults; + this.#ctx = ctx; this.#enums = enums; - this.#collectionName = resolveCollectionName(modelOf(contract, modelName), modelName); + this.#contract = ctx.context.contract; + this.#modelName = modelName; + this.#executor = ctx.runtime; + this.#mutationDefaults = ctx.context; + this.#collectionName = resolveCollectionName(modelOf(this.#contract, modelName), modelName); this.#state = emptyCollectionState(); } @@ -1140,15 +1151,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; @@ -1271,7 +1275,7 @@ class MongoCollectionImpl< defaultValueCache: Map, ): Record { const applied = - this.#mutationDefaults?.applyMutationDefaults({ + this.#mutationDefaults.applyMutationDefaults({ op, namespace: UNBOUND_NAMESPACE_ID, entry: this.#collectionName, @@ -1445,10 +1449,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 }; @@ -1462,10 +1464,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 }; @@ -1475,15 +1475,13 @@ 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 context's enum accessors. */ 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, buildEnumAccessors(ctx.context)); } 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 3ecfc4052a66..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 @@ -1,23 +1,19 @@ -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'; +import type { MongoCollection } from './collection'; +import { buildEnumAccessors, MongoCollectionImpl } from './collection'; import type { MongoQueryExecutor } from './executor'; -import { ormError } from './orm-errors'; export interface MongoOrmOptions { - readonly contract: TContract; - 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; + /** The validated contract, its codecs, and the execution defaults filled on writes. */ + readonly context: MongoExecutionContext; + readonly runtime: MongoQueryExecutor; } export type MongoOrmClient< @@ -29,45 +25,21 @@ 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 { 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}`) } }, - ); - } - assertEnumsComplete(contract, enums); + const { context, runtime } = options; + const enums = buildEnumAccessors(context); const client: Record = {}; - for (const [rootName, rootRef] of Object.entries(contract.roots)) { - client[rootName] = createMongoCollection( - contract, + for (const [rootName, rootRef] of Object.entries(context.contract.roots)) { + client[rootName] = new MongoCollectionImpl( + { context, runtime }, blindCast< RootModelName, 'roots entries are CrossReferences; rootRef.model is a valid RootModelName for this contract' >(rootRef.model), - executor, enums, - mutationDefaults, ); } 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-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 4262ccb9ad5f..847abf308313 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,7 +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'; +import { collectionContext, contextOf } from './collection-context'; const contract = ormContractJson as unknown as Contract; @@ -105,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'); @@ -305,45 +305,13 @@ 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, - executor, - enums: noEnums, - mutationDefaults: fakeMutationDefaults(), + 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 cffb5b5caeb9..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 @@ -1,7 +1,7 @@ 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, @@ -14,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'; @@ -38,7 +37,7 @@ describe('ORM ergonomics integration', { let replSet: MongoMemoryReplSet; let client: MongoClient; let runtime: MongoRuntime; - let enums: MongoOrmEnums; + let context: MongoExecutionContext; const dbName = 'orm_ergonomics_test'; beforeAll(async () => { @@ -52,10 +51,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, driver }); - enums = buildMongoEnums(contract, context.codecs); + runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); }, timeouts.spinUpMongoMemoryServer); beforeEach(async () => { @@ -68,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({ contract, 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(); @@ -76,7 +74,7 @@ describe('ORM ergonomics integration', { }); it('createAll() returns decoded _ids', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const rows: Record[] = []; for await (const row of orm.users.createAll([ defaultUserData, @@ -91,7 +89,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, runtime }); const created = await orm.users.create(defaultUserData); const updated = await orm.users .where({ _id: created._id as string }) @@ -101,7 +99,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, runtime }); 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 +109,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, runtime }); 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 +117,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, runtime }); await orm.users.create(defaultUserData); const found = await orm.users.where({ name: 'Alice' }).first(); expect(found).not.toBeNull(); @@ -127,7 +125,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, 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(); @@ -142,7 +140,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, 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(); @@ -153,7 +151,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, runtime }); const user = await orm.users.create(defaultUserData); const updated = await orm.users .where({ _id: user._id as string }) @@ -166,7 +164,7 @@ describe('ORM ergonomics integration', { }); it('$pull removes element from array field', async () => { - const orm = mongoOrm({ contract, 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')]); @@ -176,7 +174,7 @@ describe('ORM ergonomics integration', { }); it('$inc increments numeric field', async () => { - const orm = mongoOrm({ contract, 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)]); @@ -186,7 +184,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, runtime }); const user = await orm.users.create({ ...defaultUserData, homeAddress: { city: 'SF', country: 'US' }, @@ -201,7 +199,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, runtime }); const user = await orm.users.create(defaultUserData); await orm.users .where({ _id: user._id as string }) @@ -214,7 +212,7 @@ describe('ORM ergonomics integration', { }); it('updateAll with callback updates multiple documents', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); await orm.users.create(defaultUserData); await orm.users.create({ ...defaultUserData, name: 'Bob', email: 'bob@test.com' }); @@ -235,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({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); await orm.users.create(defaultUserData); await expect( orm.users.where({ name: 'Alice' }).upsert({ @@ -248,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({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create(defaultUserData); await orm.tasks.create({ @@ -274,7 +272,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, runtime }); const user = await orm.users.create(defaultUserData); const result = await orm.users @@ -287,7 +285,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, runtime }); const user = await orm.users.create(defaultUserData); await orm.tasks.create({ title: 'Task 1', @@ -310,7 +308,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, 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 cdf8d8ea34fb..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 @@ -1,7 +1,7 @@ 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, @@ -14,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; @@ -25,7 +24,7 @@ describe('Mongo ORM polymorphism integration', { let replSet: MongoMemoryReplSet; let client: MongoClient; let runtime: MongoRuntime; - let enums: MongoOrmEnums; + let context: MongoExecutionContext; const dbName = 'polymorphism_test'; beforeAll(async () => { @@ -39,10 +38,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, driver }); - enums = buildMongoEnums(contract, context.codecs); + runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); }, timeouts.spinUpMongoMemoryServer); beforeEach(async () => { @@ -54,7 +52,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, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -81,7 +79,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, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -108,7 +106,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, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -135,7 +133,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, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -159,7 +157,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, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -190,7 +188,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, runtime }); await orm.users.createAll([ { @@ -216,7 +214,7 @@ describe('Mongo ORM polymorphism integration', { }); it('variant().first() returns narrowed result', async () => { - const orm = mongoOrm({ contract, executor: runtime, enums }); + const orm = mongoOrm({ context, runtime }); const user = await orm.users.create({ name: 'Alice', email: 'alice@test.com', @@ -238,7 +236,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, 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/integration/value-objects.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/integration/value-objects.test.ts index ec3bde065542..bfb22e8fb855 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/integration/value-objects.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/integration/value-objects.test.ts @@ -5,9 +5,9 @@ import type { MongoContract, MongoContractWithTypeMaps, } from '@internal/mongo-contract'; +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; import { MongoFieldFilter, type MongoFilterExpr } from '@internal/mongo-query-ast/execution'; import { - buildMongoEnums, createMongoExecutionContext, createMongoExecutionStack, createMongoRuntime, @@ -18,7 +18,6 @@ import { timeouts } from '@repo/test-utils'; import { MongoClient, ObjectId } from 'mongodb'; import { MongoMemoryReplSet } from 'mongodb-memory-server'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import type { MongoOrmEnums } from '../../src/collection'; import type { FieldExpression, FieldOperation } from '../../src/field-accessor'; import { mongoOrm } from '../../src/mongo-orm'; @@ -121,7 +120,7 @@ describe('value objects in the Mongo ORM', { timeout: timeouts.spinUpMongoMemory let replSet: MongoMemoryReplSet; let client: MongoClient; let runtime: MongoRuntime; - let enums: MongoOrmEnums; + let context: MongoExecutionContext; const dbName = 'orm_value_objects_test'; beforeAll(async () => { @@ -134,10 +133,9 @@ describe('value objects in the Mongo ORM', { timeout: timeouts.spinUpMongoMemory target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - const context = createMongoExecutionContext({ contract: {}, stack }); + context = createMongoExecutionContext({ contract, stack }); const driver = await createMongoDriver(replSet.getUri(), dbName); - runtime = createMongoRuntime({ context, driver }); - enums = buildMongoEnums(contract, context.codecs); + runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); }, timeouts.spinUpMongoMemoryServer); beforeEach(async () => { @@ -149,7 +147,7 @@ describe('value objects in the Mongo ORM', { timeout: timeouts.spinUpMongoMemory }, timeouts.spinUpMongoMemoryServer); it('writes and reads a nested and a list value object through the codecs of their members', async () => { - const places = placesOf(mongoOrm({ contract, executor: runtime, enums })); + const places = placesOf(mongoOrm({ context, runtime })); const landmarkId = new ObjectId().toHexString(); const stopId = new ObjectId().toHexString(); const created = await places.create({ @@ -178,7 +176,7 @@ describe('value objects in the Mongo ORM', { timeout: timeouts.spinUpMongoMemory }); it('filters and updates by a dot path into a nested value object', async () => { - const places = placesOf(mongoOrm({ contract, executor: runtime, enums })); + const places = placesOf(mongoOrm({ context, runtime })); const landmarkId = new ObjectId().toHexString(); await places.create({ address: { city: 'Lyon', landmark: { label: 'Fourvière', placeId: landmarkId } }, @@ -197,7 +195,7 @@ describe('value objects in the Mongo ORM', { timeout: timeouts.spinUpMongoMemory }); it('filters by a dot path through a list index into a list of value objects', async () => { - const places = placesOf(mongoOrm({ contract, executor: runtime, enums })); + const places = placesOf(mongoOrm({ context, runtime })); const firstId = new ObjectId().toHexString(); const secondId = new ObjectId().toHexString(); await places.create({ 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..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,15 +1,40 @@ -import { test } from 'vitest'; +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'; declare const contract: Contract; -declare const executor: MongoQueryExecutor; +declare const context: MongoExecutionContext; +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({ contract, 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', () => { + mongoRaw({ context }); + // @ts-expect-error a contract alone is not accepted; build the context with mongoStatic() or mongo() + 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/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/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/DEVELOPING.md b/packages/2-mongo-family/5-query-builders/query-builder/DEVELOPING.md index 8646b77f8a80..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 (`mongoQuery`) — validates root, creates 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 c440c2527dae..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,10 +4,13 @@ A typed CRUD query builder for MongoDB contracts. Reads, writes, pipeline aggreg ## Quick start +`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 { mongoQuery } from '@internal/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 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..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 `mongoQuery({ 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 00421d8322da..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'; @@ -29,10 +33,11 @@ export interface QueryRoot< rawCommand(command: C): MongoQueryPlan; } +/** 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: MongoExecutionContext }): QueryRoot { + const { contract } = options.context; return { from(rootName: K) { return createCollectionHandle(contract, rootName); 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..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({ 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 cf48983fa484..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; @@ -19,9 +25,19 @@ type OrderRow = { readonly tags: string[]; }; +describe('mongoQuery', () => { + it('takes an execution context, not a contract', () => { + 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({ contract }); + const p = mongoQuery({ context: contextOf(contract) }); const builder = p.from('orders'); builder.sort({ amount: -1 }); builder.sort({ status: 1 }); @@ -30,7 +46,7 @@ describe('builder shape tests', () => { }); it('group() replaces shape — previous fields inaccessible', () => { - const p = mongoQuery({ contract }); + const p = mongoQuery({ context: contextOf(contract) }); const grouped = p.from('orders').group((f) => ({ _id: f.customerId, total: acc.sum(f.amount), @@ -47,7 +63,7 @@ describe('builder shape tests', () => { }); it('addFields() extends shape', () => { - const p = mongoQuery({ contract }); + const p = mongoQuery({ context: contextOf(contract) }); const extended = p.from('orders').addFields((f) => ({ fullName: fn.concat(f.status, fn.literal(' ')), })); @@ -57,7 +73,7 @@ describe('builder shape tests', () => { }); it('project() inclusion narrows shape', () => { - const p = mongoQuery({ contract }); + const p = mongoQuery({ context: contextOf(contract) }); const projected = p.from('orders').project('status', 'amount'); projected.sort({ status: 1 }); @@ -68,7 +84,7 @@ describe('builder shape tests', () => { }); it('count() replaces shape with single field', () => { - const p = mongoQuery({ contract }); + const p = mongoQuery({ context: contextOf(contract) }); const counted = p.from('orders').count('total'); counted.sort({ total: 1 }); @@ -77,7 +93,7 @@ describe('builder shape tests', () => { }); it('lookup() adds array field and preserves existing fields', () => { - const p = mongoQuery({ contract }); + const p = mongoQuery({ context: contextOf(contract) }); const withLookup = p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -96,7 +112,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects bad local field at type level (AC-1 TC-1)', () => { - const p = mongoQuery({ contract }); + const p = mongoQuery({ context: contextOf(contract) }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -109,7 +125,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects bad foreign field at type level (AC-1 TC-2)', () => { - const p = mongoQuery({ contract }); + const p = mongoQuery({ context: contextOf(contract) }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -122,7 +138,7 @@ describe('builder shape tests', () => { }); it('lookup() rejects non-leaf returns from on() (AC-2 TC-3)', () => { - const p = mongoQuery({ contract }); + const p = mongoQuery({ context: contextOf(contract) }); p.from('orders').lookup((from) => from('users') .on((local, foreign) => ({ @@ -140,7 +156,7 @@ describe('builder shape tests', () => { // behaviour). See spec § Open Questions / Resolved decisions. it('replaceRoot() replaces entire shape', () => { - const p = mongoQuery({ contract }); + const p = mongoQuery({ context: contextOf(contract) }); type NewShape = { readonly x: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; }; @@ -155,12 +171,14 @@ 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 = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .build(); expectTypeOf>().toEqualTypeOf(); }); it('match() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .match((f) => f.status.eq('active') as MongoFilterExpr) .build(); @@ -168,17 +186,25 @@ describe('resolved row types', () => { }); it('sort() preserves row type', () => { - const plan = mongoQuery({ 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({ contract }).from('orders').limit(10).skip(5).sample(3).build(); + const plan = mongoQuery({ context: contextOf(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 = mongoQuery({ context: contextOf(contract) }) .from('orders') .addFields((f) => ({ fullName: fn.concat(f.status, fn.literal(' ')), @@ -198,7 +224,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 = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .project('status', 'amount') + .build(); expectTypeOf>().toEqualTypeOf<{ _id: string; status: string; @@ -207,7 +236,7 @@ describe('resolved row types', () => { }); it('project() computed includes expression fields at correct types', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .project((f) => ({ status: 1 as const, @@ -224,7 +253,7 @@ describe('resolved row types', () => { }); it('group() resolves _id at grouped-by field type, accumulators at correct types', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .group((f) => ({ _id: f.customerId, @@ -242,17 +271,23 @@ describe('resolved row types', () => { }); it('unwind() preserves row type', () => { - const plan = mongoQuery({ 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({ 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({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .sortByCount((f) => f.status) .build(); @@ -263,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({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .lookup((from) => from('users') @@ -291,7 +326,7 @@ describe('resolved row types', () => { }); it('lookup() resolves nested value-object fields on the foreign model recursively', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .lookup((from) => from('customers') @@ -330,7 +365,9 @@ 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 = mongoQuery({ context: contextOf(contract) }) + .from('customers') + .build(); expectTypeOf>().toEqualTypeOf<{ readonly _id: string; readonly name: string; @@ -351,7 +388,7 @@ describe('resolved row types', () => { }); it('addFields() preserves model-origin resolution and adds the new fields on top', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('customers') .addFields((f) => ({ upperName: fn.concat(f.name, fn.literal('!')), @@ -378,7 +415,7 @@ describe('resolved row types', () => { }); it('group() drops model-origin resolution (legitimate shape replacement)', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('customers') .group((f) => ({ _id: f.name, @@ -398,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({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .replaceRoot((f) => f.status) .build(); @@ -409,7 +446,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 = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .pipe(new MongoLimitStage(5)) + .build(); expectTypeOf>().toEqualTypeOf(); }); @@ -417,7 +457,7 @@ describe('resolved row types', () => { type NewShape = { readonly a: { readonly codecId: 'mongo/string@1'; readonly nullable: false }; }; - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .pipe(new MongoLimitStage(5)) .build(); @@ -425,14 +465,16 @@ describe('resolved row types', () => { }); it('nullable fields resolve to T | null', () => { - const plan = mongoQuery({ 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({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .match((f) => f.status.eq('active') as MongoFilterExpr) .group((f) => ({ @@ -451,7 +493,9 @@ describe('resolved row types', () => { }); it('execute() infers Row from build() plan type', () => { - const plan = mongoQuery({ 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(); @@ -460,7 +504,7 @@ describe('resolved row types', () => { describe('resolved row types — new stages', () => { it('redact() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .redact((f) => f.status) .build(); @@ -468,22 +512,29 @@ 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 = 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({ 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({ contract }).from('orders').unionWith('archived_orders').build(); + const plan = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .unionWith('archived_orders') + .build(); expectTypeOf>().toEqualTypeOf(); }); it('densify() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .densify({ field: 'amount', range: { step: 10, bounds: 'full' } }) .build(); @@ -491,7 +542,7 @@ describe('resolved row types — new stages', () => { }); it('fill() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .fill({ sortBy: { amount: 1 }, output: { notes: { method: 'linear' } } }) .build(); @@ -499,7 +550,7 @@ describe('resolved row types — new stages', () => { }); it('search() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .search({ text: { query: 'test', path: 'status' } }) .build(); @@ -507,7 +558,7 @@ describe('resolved row types — new stages', () => { }); it('vectorSearch() preserves row type', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .vectorSearch({ index: 'idx', @@ -521,7 +572,7 @@ describe('resolved row types — new stages', () => { }); it('bucket() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .bucket({ groupBy: MongoAggFieldRef.of('amount'), boundaries: [0, 100, 500] }) .build(); @@ -529,7 +580,7 @@ describe('resolved row types — new stages', () => { }); it('bucketAuto() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .bucketAuto({ groupBy: MongoAggFieldRef.of('amount'), buckets: 5 }) .build(); @@ -537,12 +588,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 = mongoQuery({ context: contextOf(contract) }) + .from('orders') + .facet({ counts: [], top: [] }) + .build(); expectTypeOf>().toEqualTypeOf>(); }); it('geoNear() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .geoNear({ near: [0, 0], distanceField: 'dist' }) .build(); @@ -550,7 +604,7 @@ describe('resolved row types — new stages', () => { }); it('graphLookup() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .graphLookup({ from: 'categories', @@ -564,7 +618,7 @@ describe('resolved row types — new stages', () => { }); it('setWindowFields() resets to untyped row', () => { - const plan = mongoQuery({ contract }) + const plan = mongoQuery({ context: contextOf(contract) }) .from('orders') .setWindowFields({ sortBy: { amount: 1 }, output: {} }) .build(); @@ -572,7 +626,7 @@ describe('resolved row types — new stages', () => { }); it('searchMeta() resets to untyped row', () => { - const plan = mongoQuery({ 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 56264ed24422..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({ contract: testContract }).from('orders'); + return mongoQuery({ context: contextOf(testContract) }).from('orders'); } function createCustomersBuilder() { - return mongoQuery({ 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({ 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({ 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 300034407878..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({ 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({ 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({ 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({ 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({ 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 2423123bbc75..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({ 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..937b9f7045cc --- /dev/null +++ b/packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-context.ts @@ -0,0 +1,12 @@ +import type { MongoContract } from '@internal/mongo-contract'; +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 ba8f3c08b733..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({ 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 bf8e975fcadd..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({ 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', () => { @@ -26,9 +27,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, - ), + mongoQuery({ + 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 0f9d145340f9..f3568ddb51cb 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 @@ -6,6 +6,7 @@ import { contractFieldToMongoFieldShape, contractModelToMongoResultShape, } from '../src/result-shape'; +import { contextOf } from './fixtures/test-context'; import { testContract } from './fixtures/test-contract'; function valueObjectRef(entityName: string, namespaceId = '__unbound__') { @@ -251,12 +252,16 @@ 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 = mongoQuery({ context: contextOf(testContract) }) + .from('orders') + .build(); expect(plan.resultShape?.kind).toBe('document'); }); it('decodes a value-object field through the value object storage holds', () => { - const plan = mongoQuery({ contract: testContract }).from('customers').build(); + const plan = mongoQuery({ context: contextOf(testContract) }) + .from('customers') + .build(); if (plan.resultShape?.kind !== 'document') throw new Error('expected document'); expect(plan.resultShape.fields['stats']).toEqual({ kind: 'document', @@ -269,7 +274,7 @@ describe('PipelineChain build resultShape', () => { }); it('project stage reifies a document shape retaining _id and the projected field', () => { - const plan = mongoQuery({ 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 3399dc27f0d0..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({ 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 bcb774871754..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({ contract }).from('orders'); + const handle = mongoQuery({ context: contextOf(contract) }).from('orders'); expectTypeOf(handle).toExtend>(); }); it('CollectionHandle.match(...) transitions to FilteredCollection', () => { - const filtered = mongoQuery({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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 456a97dd2ea0..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,12 +39,14 @@ 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 = mongoQuery({ context: contextOf(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 = 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 @@ -53,7 +56,7 @@ describe('stored names', () => { }); it('keys a looked-up foreign row by stored name', () => { - const plan = mongoQuery({ 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 472dda395a30..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({ 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 7520f3554432..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({ 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({ contract: testContract }); + const p = mongoQuery({ context: contextOf(testContract) }); expect(codeOf(() => p.from('nonexistent' as 'orders'))).toBe('ORM.MODEL_UNKNOWN'); }); @@ -63,12 +64,14 @@ describe('structured error codes', () => { orders: { $ref: '#/domain/models/Ghost', model: 'Ghost' }, }, }; - const p = mongoQuery({ 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({ contract: { ...testContract, storage: {} } as unknown as TContract }); + const p = mongoQuery({ + 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 29f606721e22..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({ 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..f50e71837c5b 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, + adapter: stack.adapter.create(stack), driver: await createMongoDriver(url, dbName), }); ``` @@ -46,9 +47,8 @@ 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`. -- **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. +- **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. +- **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/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..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,10 +1,8 @@ export type { RuntimeTargetInstance } from '@internal/framework-components/execution'; -export type { MongoEnumAccessors } from '../mongo-enums'; -export { buildMongoEnums } from '../mongo-enums'; +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 { - 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 deleted file mode 100644 index 2bfe288ede29..000000000000 --- a/packages/2-mongo-family/7-runtime/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 './mongo-execution-stack'; - -/** 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/7-runtime/src/mongo-execution-stack.ts b/packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts index 49ed4905be79..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,10 +1,7 @@ -import type { ContractExecutionSection } from '@internal/contract/types'; import { createExecutionStack, type RuntimeAdapterDescriptor, type RuntimeAdapterInstance, - type RuntimeDriverDescriptor, - type RuntimeDriverInstance, type RuntimeExtensionDescriptor, type RuntimeExtensionInstance, type RuntimeTargetDescriptor, @@ -15,14 +12,14 @@ 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 { MongoContract } from '@internal/mongo-contract'; import type { MongoAdapter } from '@internal/mongo-lowering'; -import { blindCast } from '@internal/utils/casts'; +import type { MongoExecutionContext } from '@internal/mongo-query-ast/execution'; /** * Mongo-specific static contributions a runtime descriptor declares. @@ -66,78 +63,34 @@ 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); -} - -/** - * 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; + return { target, adapter, extensions }; } export function createMongoExecutionContext< - TContract = unknown, + TContract extends MongoContract = MongoContract, TTargetId extends string = 'mongo', >(options: { readonly contract: TContract; readonly stack: MongoExecutionStack; -}): MongoExecutionContext { +}): MongoExecutionContext { const registry = newMongoCodecRegistry(); const owners = new Map(); @@ -148,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) { @@ -170,19 +123,11 @@ export function createMongoExecutionContext< return Object.freeze({ contract: options.contract, codecs: registry, - stack: options.stack, applyMutationDefaults: (mutation: MutationDefaultsOptions) => applyMutationDefaults(execution, generators, mutation), }); } -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 79d9d6c56696..232db0b56933 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts @@ -7,19 +7,23 @@ import { executeStatement, RuntimeCore, type RuntimeExecuteOptions, + type RuntimeMode, type RuntimeStatementStats, runtimeError, type StatementActivityRun, 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 { MongoMiddleware, MongoMiddlewareContext } from './mongo-middleware'; import { createMongoParamRefMutator, @@ -63,19 +67,14 @@ 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 - * 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 adapter: MongoAdapter; readonly driver: MongoDriver; readonly middleware?: readonly MongoMiddleware[]; - readonly mode?: 'strict' | 'permissive'; + readonly mode?: RuntimeMode; } export interface MongoRuntime { @@ -116,9 +115,8 @@ class MongoRuntimeImpl constructor(options: MongoRuntimeOptions) { const middleware = options.middleware ?? []; - const targetId = options.context.stack.target.targetId; for (const mw of middleware) { - checkMiddlewareCompatibility(mw, 'mongo', targetId); + checkMiddlewareCompatibility(mw, 'mongo', options.context.contract.target); } const ctx: ActivityContextFields = { @@ -144,9 +142,7 @@ class MongoRuntimeImpl super({ middleware, ctx }); this.#contextFields = ctx; - const adapterDescriptor = options.context.stack.adapter; - const adapterInstance = adapterDescriptor.create(options.context.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/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/2-mongo-family/7-runtime/test/mongo-enums.test.ts b/packages/2-mongo-family/7-runtime/test/mongo-enums.test.ts deleted file mode 100644 index 114771ac8024..000000000000 --- a/packages/2-mongo-family/7-runtime/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 { buildMongoEnums } from '../src/mongo-enums'; -import type { MongoCodecLookup } from '../src/mongo-execution-stack'; - -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/7-runtime/test/mongo-middleware-fixtures.ts b/packages/2-mongo-family/7-runtime/test/mongo-middleware-fixtures.ts index f596985f3ed5..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 @@ -1,29 +1,24 @@ 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 { 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 const testContract = { + target: 'mongo', + domain: { namespaces: {} }, +} as unknown as MongoContract; + +export function makeExecution( _adapter: MongoAdapter, wireCommand: AnyMongoDmlWireCommand = new AggregateWireCommand('users', []), -): MongoExecutionContext { +): { 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', @@ -33,31 +28,14 @@ export function makeContext( ), 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: testContract, + codecs, + applyMutationDefaults: () => [], + }), + adapter, }; - return Object.freeze({ contract: {}, codecs, stack, applyMutationDefaults: () => [] }); } 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..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 @@ -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' }]), }); @@ -76,13 +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 = makeContext(createMockAdapter(), new DeleteOneWireCommand('users', { id: 1 })); + const { context, adapter } = makeExecution( + createMockAdapter(), + new DeleteOneWireCommand('users', { id: 1 }), + ); const runtime = createMongoRuntime({ context, + adapter, driver, middleware: [ { @@ -157,7 +178,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: createMockDriver([]), middleware: [middleware], }); @@ -193,7 +214,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: failingDriver, middleware: [middleware], }); @@ -226,7 +247,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: failingDriver, middleware: [middleware], }); @@ -258,7 +279,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: failingDriver, middleware: [middleware], }); @@ -283,7 +304,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 +327,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: createMockDriver([]), middleware: [middleware], mode: 'permissive', @@ -334,7 +355,7 @@ describe('MongoRuntime middleware lifecycle', () => { const adapter = createMockAdapter(); const runtime = createMongoRuntime({ - context: makeContext(adapter), + ...makeExecution(adapter), driver: createMockDriver([]), middleware: [middleware], }); @@ -360,7 +381,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..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 @@ -1,18 +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 { 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, - MongoRuntimeTargetDescriptor, -} from '../src/mongo-execution-stack'; import { createMongoRuntime } from '../src/mongo-runtime'; +import { testContract } from './mongo-middleware-fixtures'; const baseMeta: PlanMeta = { target: 'mongo', @@ -57,40 +52,19 @@ function recordingAdapter(): RecordingAdapter { return { adapter, observed, resolveCallCount }; } -function makeContext(adapter: MongoAdapter): MongoExecutionContext { +function makeExecution(adapter: MongoAdapter): { + 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: testContract, + codecs, + applyMutationDefaults: () => [], + }), + adapter, }; - return Object.freeze({ contract: {}, codecs, stack, applyMutationDefaults: () => [] }); } function rowsDriver(rows: Record[] = []): MongoDriver { @@ -116,7 +90,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 +103,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 +117,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 +133,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 +159,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..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, 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 f8147b6afb28..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,17 +5,18 @@ 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 { 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 { 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) }); @@ -63,10 +64,10 @@ function codecsWithFailingDecode() { return registry; } -function makeContext( +function makeExecution( wireCommand: AnyMongoDmlWireCommand, onResolveParams: () => void = () => {}, -): MongoExecutionContext { +): { readonly context: MongoExecutionContext; readonly adapter: MongoAdapter } { const adapter = { familyId: 'mongo', targetId: 'mongo', @@ -84,32 +85,13 @@ function makeContext( }), } as unknown as MongoAdapter; return { - contract: {}, - codecs: codecsWithFailingDecode(), - 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: [], + context: { + contract: testContract, + codecs: codecsWithFailingDecode(), + applyMutationDefaults: () => [], }, - applyMutationDefaults: () => [], - } as unknown as MongoExecutionContext; + adapter, + }; } function createDriver(results: Record[], failure?: Error): MongoDriver { @@ -160,7 +142,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 +155,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 +168,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 +184,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 +199,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 +221,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 +234,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 +248,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 +273,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..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,18 +1,30 @@ import type { MongoCodecRegistry } from '@internal/mongo-codec'; -import { test } from 'vitest'; -import type { MongoExecutionContext, MongoRuntimeOptions } from '../src/exports/index'; +import type { MongoContract } from '@internal/mongo-contract'; +import type { MongoAdapter } from '@internal/mongo-lowering'; +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 ctx: MongoExecutionContext; +declare const adapter: MongoAdapter; +declare const stack: MongoExecutionStack; -test('runtime options accept only context, driver, middleware, and mode', () => { - createMongoRuntime({ - context: ctx, - driver: {} as never, - }); +test('the runtime takes the adapter beside the context, as the SQL runtime does', () => { + expectTypeOf().not.toHaveProperty('stack'); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().not.toHaveProperty('driver'); +}); + +test('runtime options accept only context, adapter, driver, middleware, and mode', () => { + createMongoRuntime({ context: ctx, adapter, driver: {} as never }); const validOptions = { context: ctx, + adapter, driver: {} as never, middleware: [ { @@ -28,6 +40,7 @@ test('runtime options accept only context, driver, middleware, and mode', () => createMongoRuntime({ context: ctx, + adapter, driver: {} as never, // @ts-expect-error `codecs` is not a property of MongoRuntimeOptions — // codecs are aggregated via `createMongoExecutionContext` and reached @@ -37,27 +50,28 @@ test('runtime options accept only context, driver, middleware, and mode', () => createMongoRuntime({ context: ctx, + adapter, driver: {} as never, - // @ts-expect-error `adapter` is not a property of MongoRuntimeOptions — - // the adapter is reached via `context.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, + adapter, 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 read from `context.contract.target`. targetId: 'mongo', }); createMongoRuntime({ context: ctx, + 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/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..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({ 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({ contract }).from('orders').build(); + const plan = mongoQuery({ context }).from('orders').build(); type Row = PlanRow; const runtime = {} as MongoRuntime; 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/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-context.ts b/packages/2-sql-family/5-runtime/src/sql-context.ts index 36df3cfd8607..35a88ce546d2 100644 --- a/packages/2-sql-family/5-runtime/src/sql-context.ts +++ b/packages/2-sql-family/5-runtime/src/sql-context.ts @@ -14,7 +14,6 @@ import { } from '@internal/framework-components/components'; import { createExecutionStack, - type ExecutionStack, type RuntimeAdapterDescriptor, type RuntimeAdapterInstance, type RuntimeDriverDescriptor, @@ -109,23 +108,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> - | undefined; - readonly extensions: readonly SqlRuntimeExtensionDescriptor[]; -}; +export type SqlExecutionStackWithDriver = + SqlExecutionStack & { + readonly driver: RuntimeDriverDescriptor< + 'sql', + TTargetId, + unknown, + SqlRuntimeDriverInstance + >; + }; export interface SqlRuntimeExtensionInstance extends RuntimeExtensionInstance<'sql', TTargetId> {} @@ -145,20 +136,34 @@ 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 = createExecutionStack({ target: options.target, adapter: options.adapter, 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/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/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.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(); 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/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/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/2-sql-family/9-family/README.md b/packages/2-sql-family/9-family/README.md index f02f9a54550f..ddc1c829ef1c 100644 --- a/packages/2-sql-family/9-family/README.md +++ b/packages/2-sql-family/9-family/README.md @@ -140,7 +140,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..0f10519a7052 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 pgvector from '@internal/extension-pgvector/runtime'; - -const stack = createSqlExecutionStack({ - target: postgresTarget, - adapter: postgresAdapter, - extensions: [pgvector], -}); -const context = createExecutionContext({ contract, stack }); -const stackInstance = instantiateExecutionStack(stack); +import postgres from '@prisma/orm-postgres/runtime'; +import pgvector from '@prisma/orm-extension-pgvector/runtime'; + +const db = postgres({ contract, url, extensions: [pgvector] }); ``` ### Query Usage diff --git a/packages/3-extensions/supabase/src/runtime/supabase.ts b/packages/3-extensions/supabase/src/runtime/supabase.ts index 612e5f292b39..4b0cc8a989bc 100644 --- a/packages/3-extensions/supabase/src/runtime/supabase.ts +++ b/packages/3-extensions/supabase/src/runtime/supabase.ts @@ -56,7 +56,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 type { Pool } from 'pg'; @@ -400,9 +399,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/mongo/9-facade/src/runtime/mongo.ts b/packages/3-targets/mongo/9-facade/src/runtime/mongo.ts index 1ad7f255a5c1..8cd7ed214129 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. */ @@ -86,8 +86,7 @@ export default function mongo< const contract = validateMongoContract(options.contract); let binding = resolveOptionalMongoBinding(options); - const { context, 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 @@ -106,6 +105,7 @@ export default function mongo< } return createMongoRuntime({ context, + adapter: stack.adapter.create(stack), driver, ...ifDefined('mode', options.mode), ...ifDefined('middleware', options.middleware), @@ -150,10 +150,8 @@ export default function mongo< } const orm = mongoOrm({ - contract, - executor: { query: queryRows, execute: executeStats }, - mutationDefaults: context, - enums: enumsByNamespace, + context, + 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 3260ac48a32b..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,5 +1,5 @@ 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 type { AnyMongoTypeMaps, @@ -10,15 +10,14 @@ 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 MongoEnumAccessors, + enumMemberCodecFor, type MongoExecutionContext, + type MongoExecutionStack, } 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> = @@ -34,11 +33,11 @@ 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; } export function buildMongoStaticContext< @@ -49,16 +48,13 @@ export function buildMongoStaticContext< adapter: mongoRuntimeAdapter, }); const context = createMongoExecutionContext({ contract, stack }); - const enumsByNamespace = buildMongoEnums(contract, context.codecs); - const unbound = enumsByNamespace[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 query = mongoQuery({ contract }); - const raw = mongoRaw({ contract }); - return { context, contract, enums, enumsByNamespace, query, raw }; + const enums = Object.freeze( + 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 }); + const raw = mongoRaw({ context }); + 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 bc7f12dfdd38..251b9f75944a 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,40 @@ 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('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; + + 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/enum-members.test.ts b/packages/3-targets/mongo/9-facade/test/enum-members.test.ts index 2daa677a9855..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 @@ -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'; @@ -36,6 +35,10 @@ const contract = defineContract({ double: field.namedType(DoubleLevel), }, }), + Note: model('Note', { + collection: 'notes', + fields: { _id: field.objectId(), level: field.namedType(TextLevel) }, + }), }, }); @@ -101,13 +104,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({ - contract, - 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,59 +115,42 @@ 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({ contract, executor: unreachableDatabase, enums: {} })).toThrow( - refusalFor('TextLevel'), - ); - expect(() => - mongoOrm({ - contract, - executor: unreachableDatabase, - enums: { __unbound__: Object.defineProperties({}, withoutDouble) }, - }), - ).toThrow(refusalFor('DoubleLevel')); + 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', + }); }); - 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({ contract, executor: unreachableDatabase, enums: { __unbound__: unread } }), - ).not.toThrow(); + 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); }); - 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('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 } }; + + 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', }); }); - it('refuses a contract whose enum codec the runtime lacks when the accessors are built', () => { - expect(() => buildMongoEnums(contract, { get: () => undefined, has: () => false })).toThrow( + 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: 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..9c36693bc023 100644 --- a/packages/3-targets/mongo/9-facade/test/mongo.test.ts +++ b/packages/3-targets/mongo/9-facade/test/mongo.test.ts @@ -33,7 +33,7 @@ vi.mock('@internal/target-mongo/runtime', () => ({ })); vi.mock('@internal/mongo-runtime', async (importOriginal) => ({ - buildMongoEnums: (await importOriginal()).buildMongoEnums, + enumMemberCodecFor: (await importOriginal()).enumMemberCodecFor, createMongoExecutionStack: mocks.createMongoExecutionStack, createMongoExecutionContext: mocks.createMongoExecutionContext, createMongoRuntime: mocks.createMongoRuntime, @@ -76,6 +76,13 @@ 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 fakeContext = { + id: 'context-instance', + contract: fakeContract, + codecs: { get: () => undefined }, +}; +const fakeStack = { id: 'stack-instance', adapter: { create: () => fakeAdapter } }; describe('mongo() facade', () => { beforeEach(() => { @@ -84,8 +91,8 @@ describe('mongo() facade', () => { } mocks.deserializeContract.mockReturnValue(fakeContract); - mocks.createMongoExecutionStack.mockReturnValue({ id: 'stack-instance' }); - mocks.createMongoExecutionContext.mockReturnValue({ id: 'context-instance' }); + mocks.createMongoExecutionStack.mockReturnValue(fakeStack); + mocks.createMongoExecutionContext.mockReturnValue(fakeContext); mocks.driverFromConnection.mockResolvedValue({ id: 'driver-from-url', close: fakeDriverClose, @@ -137,18 +144,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' }, + context: fakeContext, + 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'); }); @@ -336,7 +344,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; @@ -457,6 +465,7 @@ describe('mongo() facade', () => { mocks.deserializeContract.mockReturnValue(contractWithEnum); mocks.createMongoExecutionContext.mockReturnValue({ id: 'context-instance', + contract: contractWithEnum, codecs: { get: (id: string) => (id === 'mongo/string@1' ? stringCodec : undefined) }, }); }); @@ -469,6 +478,7 @@ describe('mongo() facade', () => { 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(() => @@ -504,7 +514,16 @@ 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 = fakeContext; + 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', () => { @@ -512,7 +531,7 @@ describe('mongo() facade', () => { expect(mocks.createMongoExecutionContext).toHaveBeenCalledTimes(1); expect(mocks.createMongoExecutionContext).toHaveBeenCalledWith({ contract: fakeContract, - stack: { id: 'stack-instance' }, + stack: fakeStack, }); }); @@ -526,7 +545,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 2a72216f5960..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 @@ -25,7 +25,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, + enumMemberCodecFor: (await importOriginal()).enumMemberCodecFor, createMongoExecutionStack: mocks.createMongoExecutionStack, createMongoExecutionContext: mocks.createMongoExecutionContext, createMongoRuntime: mocks.createMongoRuntime, @@ -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', () => ({ + mongoQuery: mocks.mongoQuery, +})); import { resolveMongoBinding, resolveOptionalMongoBinding } from '../src/runtime/binding'; import mongo from '../src/runtime/mongo'; @@ -180,8 +182,15 @@ describe('facade lifecycle structured codes', () => { fn.mockReset(); } mocks.deserializeContract.mockReturnValue(fakeContract); - mocks.createMongoExecutionStack.mockReturnValue({ id: 'stack' }); - mocks.createMongoExecutionContext.mockReturnValue({ id: 'context' }); + mocks.createMongoExecutionStack.mockReturnValue({ + id: 'stack', + adapter: { create: () => ({ id: 'adapter' }) }, + }); + mocks.createMongoExecutionContext.mockReturnValue({ + id: 'context', + contract: { domain: { namespaces: { __unbound__: {} } } }, + codecs: { get: () => undefined }, + }); mocks.driverFromConnection.mockResolvedValue({ id: 'driver', close: vi.fn().mockResolvedValue(undefined), diff --git a/packages/3-targets/mysql/9-facade/src/runtime/mysql.ts b/packages/3-targets/mysql/9-facade/src/runtime/mysql.ts index af58efd7d067..719331a9dc2f 100644 --- a/packages/3-targets/mysql/9-facade/src/runtime/mysql.ts +++ b/packages/3-targets/mysql/9-facade/src/runtime/mysql.ts @@ -285,9 +285,6 @@ export default function mysql< 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/mysql/9-facade/src/static/mysql-static.ts b/packages/3-targets/mysql/9-facade/src/static/mysql-static.ts index fa69414fe411..c6772952cb4c 100644 --- a/packages/3-targets/mysql/9-facade/src/static/mysql-static.ts +++ b/packages/3-targets/mysql/9-facade/src/static/mysql-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 MysqlStaticContext> exte export function buildMysqlStaticContext>( contract: TContract, context: ExecutionContext, - stack: SqlExecutionStackWithDriver, + stack: SqlExecutionStack, ): MysqlStaticContext { const { rawCodecInferer } = stack.adapter; const sqlNamespace = sql({ context, rawCodecInferer })[UNBOUND_NAMESPACE_ID]; diff --git a/packages/3-targets/postgres/9-facade/README.md b/packages/3-targets/postgres/9-facade/README.md index 13d6e146a907..7c83fed4ec37 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] 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 75cc5d49c2cc..b8fbe55cb341 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 @@ -195,9 +195,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 f3ed7e9e81b0..ff2028e692c2 100644 --- a/packages/3-targets/postgres/9-facade/src/runtime/postgres.ts +++ b/packages/3-targets/postgres/9-facade/src/runtime/postgres.ts @@ -189,9 +189,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]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42e3f88117fb..558fbb0827a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2171,6 +2171,9 @@ importers: '@internal/framework-components': specifier: workspace:8.0.0-rc.21 version: link:../../../1-framework/1-core/framework-components + '@internal/mongo-codec': + specifier: workspace:8.0.0-rc.21 + version: link:../../1-foundation/mongo-codec '@internal/mongo-contract': specifier: workspace:8.0.0-rc.21 version: link:../../1-foundation/mongo-contract diff --git a/test/integration/test/_harness/mongo.ts b/test/integration/test/_harness/mongo.ts index a32baede4fbb..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, @@ -159,15 +158,10 @@ export async function withMongoPort< }); const context = createMongoExecutionContext({ contract, stack }); const driver = await createMongoDriver(connectionUri, dbName); - const runtime = createMongoRuntime({ context, driver }); + const runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); try { - const db = mongoOrm({ - contract, - executor: runtime, - mutationDefaults: context, - 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/cross-package/cross-family-middleware.test.ts b/test/integration/test/cross-package/cross-family-middleware.test.ts index 16a528e80fc6..b8ea18678b7f 100644 --- a/test/integration/test/cross-package/cross-family-middleware.test.ts +++ b/test/integration/test/cross-package/cross-family-middleware.test.ts @@ -18,6 +18,7 @@ import { } 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 @@ -162,12 +163,18 @@ function mockAdapterDescriptor(adapter: MongoAdapter): MongoRuntimeAdapterDescri }; } -function makeMongoContext(adapter: MongoAdapter): MongoExecutionContext { +function makeMongoExecution(adapter: MongoAdapter): { + readonly context: MongoExecutionContext; + readonly adapter: MongoAdapter; +} { const stack = createMongoExecutionStack({ target: mongoRuntimeTarget, adapter: mockAdapterDescriptor(adapter), }); - return createMongoExecutionContext({ contract: {}, stack }); + return { + context: createMongoExecutionContext({ contract: fixtureContract, stack }), + adapter: stack.adapter.create(stack), + }; } function createMockMongoDriver(rows: Record[] = []): MongoDriver { @@ -246,7 +253,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 +288,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 +350,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 dca351265af8..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({ 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({ 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/no-emit.test.ts b/test/integration/test/mongo/no-emit.test.ts index 93c73f593727..c0628b85a798 100644 --- a/test/integration/test/mongo/no-emit.test.ts +++ b/test/integration/test/mongo/no-emit.test.ts @@ -1,9 +1,8 @@ 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'; -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,11 +26,8 @@ describeWithMongoDB('Mongo no-emit integration', (ctx) => { comments: [{ _id: commentId, text: 'Investigating', createdAt: new Date('2025-01-01') }], }); - const orm = mongoOrm({ - contract, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const context = executionContextFor(contract); + 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 2306e04a4953..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({ - contract, - 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({ - contract, - 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({ - contract, - 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({ - contract, - 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({ - contract, - 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({ - contract, - 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({ - contract, - 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({ - contract, - 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({ - contract, - 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({ - contract, - 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({ - contract, - 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({ - contract, - 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/query-builder.test.ts b/test/integration/test/mongo/query-builder.test.ts index 9a6b4a218e9d..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({ 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..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,6 +44,7 @@ describe('Mongo runtime content hash before param resolution', () => { } as unknown as MongoDriver; const runtime = createMongoRuntime({ context, + adapter: stack.adapter.create(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 94a1eac8bae8..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({ +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({ 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 fa3d0bbf4b4c..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({ 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..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 { @@ -15,6 +15,7 @@ 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'; @@ -25,12 +26,18 @@ const baseMeta: PlanMeta = { lane: 'orm', }; -function buildStackContext(): MongoExecutionContext { +function buildExecution(): { + readonly context: MongoExecutionContext; + readonly adapter: MongoAdapter; +} { const stack = createMongoExecutionStack({ target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter, }); - return createMongoExecutionContext({ contract: {}, stack }); + return { + context: createMongoExecutionContext({ contract: fixtureContract, stack }), + adapter: stack.adapter.create(stack), + }; } function recordingDriver(): { @@ -66,7 +73,7 @@ describe('MongoRuntime execute param-mutator wiring', () => { const { driver } = recordingDriver(); const runtime = createMongoRuntime({ - context: buildStackContext(), + ...buildExecution(), driver, middleware: [middleware], }); @@ -106,7 +113,7 @@ describe('MongoRuntime execute param-mutator wiring', () => { const { driver, commands } = recordingDriver(); const runtime = createMongoRuntime({ - context: buildStackContext(), + ...buildExecution(), driver, middleware: [middleware], }); @@ -136,8 +143,9 @@ 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 adapterInstance = mongoRuntimeAdapter.create( + createMongoExecutionStack({ target: mongoRuntimeTarget, adapter: mongoRuntimeAdapter }), + ); const structuralLowerSpy = vi.fn((plan) => { structuralDraft = adapterInstance.structuralLower(plan); return structuralDraft; @@ -158,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, driver }); + const runtime = createMongoRuntime({ + context: patchedContext, + adapter: patchedStack.adapter.create(patchedStack), + driver, + }); await runtime .query({ @@ -205,7 +220,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/fixtures/decode-fixture-contract.ts b/test/integration/test/mongo/runtime/fixtures/decode-fixture-contract.ts index b07e0afee258..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,7 +98,7 @@ 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 `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 `mongoQuery` for the type-level path. 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..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) }); @@ -30,14 +31,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([]); }); @@ -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; } @@ -149,16 +150,15 @@ 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, }); - 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.stack).toBe(stack); + expect(context.contract).toBe(fixtureContract); + expect(Object.keys(context)).not.toContain('stack'); }); it('preserves the TContract type on context.contract (no unknown widening)', () => { @@ -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/runtime/setup.ts b/test/integration/test/mongo/runtime/setup.ts index 1abf4c549a89..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, 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 4dc6bdb64015..47ff8af9df6e 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 type { MongoContract } from '@internal/mongo-contract'; 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,17 @@ 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, 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, + }; return await fn(ctx); } finally { await runtime?.close(); @@ -52,8 +76,8 @@ export interface MongodContext { readonly dbName: string; readonly client: MongoClient; readonly runtime: MongoRuntime; - /** The execution context's codecs, which `buildMongoEnums()` reads enum members through. */ - readonly codecs: MongoCodecLookup; + /** The execution context over the fixture contract, which `mongoOrm()` and `mongoRaw()` take. */ + readonly context: MongoExecutionContext; } export function describeWithMongoDB(name: string, fn: (ctx: MongodContext) => void): void { @@ -61,7 +85,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 +99,8 @@ export function describeWithMongoDB(name: string, fn: (ctx: MongodContext) => vo get runtime() { return runtime; }, - get codecs() { - return codecs; + get context() { + return context; }, }; @@ -94,10 +118,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, driver }); - codecs = context.codecs; + runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); }, timeouts.spinUpMongoMemoryServer); beforeEach(async () => { 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 new file mode 100644 index 000000000000..72fa1908dd70 --- /dev/null +++ b/test/integration/test/upgrade-instructions/mongo-query-takes-context/detection.test.ts @@ -0,0 +1,122 @@ +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, 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(`${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 ${change} 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); + }); +}); + +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') {}"], + ['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, 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 })'], + ])('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})', + ], + ['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); + }); + + 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'], + ['a context type over one contract', 'MongoExecutionContext'], + ['a context type over a contract typeof', 'MongoExecutionContext'], + ['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) => { + expect(detects(audience, content, change)).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 new file mode 100644 index 000000000000..07daffcd52de --- /dev/null +++ b/test/integration/test/upgrade-instructions/stack-driver-required/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 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();'], + ['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); + }); + + it.each([ + ['the stack with no driver', 'function build(stack: SqlExecutionStack) {}'], + ['a longer name', 'type MySqlExecutionStackWithDriverFactory = () => void;'], + ['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;'], + ['another member renamed to driver', 'const { adapter: driver } = stack;'], + ])('skips %s', (_name, content) => { + expect(detects(content)).toBe(false); + }); +}); 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; } 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..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'; @@ -11,7 +10,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,11 +23,8 @@ 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 ormClient = mongoOrm({ - contract, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const context = executionContextFor(contract); + const ormClient = mongoOrm({ context, runtime: ctx.runtime }); const shopCollection = ormClient['shop']!; const created = await shopCollection.create({ @@ -79,11 +75,8 @@ describeWithMongoDB('value objects e2e: Mongo → real DB → typed ORM', (ctx) }); it('non-null value object field roundtrips through update', async () => { - const ormClient = mongoOrm({ - contract, - executor: ctx.runtime, - enums: buildMongoEnums(contract, ctx.codecs), - }); + const context = executionContextFor(contract); + 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 db1a7a417e5f..95db6e0af8e8 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'; @@ -32,7 +31,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 { @@ -229,11 +228,8 @@ describeWithMongoDB('value objects: end-to-end Mongo', (ctx) => { contract: new MongoContractSerializer().deserializeContract(contract), }; - const orm = mongoOrm({ - contract: validated.contract, - executor: ctx.runtime, - enums: buildMongoEnums(validated.contract, ctx.codecs), - }); + const context = executionContextFor(validated.contract); + const orm = mongoOrm({ context, runtime: ctx.runtime }); const userCollection = orm['User']!; type CreateUser = Parameters[0]; @@ -263,14 +259,9 @@ describeWithMongoDB('value objects: end-to-end Mongo', (ctx) => { const result = interpretMongoPsl(mongoPsl); if (!result.ok) throw new Error(`Interpretation failed: ${result.failure.summary}`); - const validated = { - contract: new MongoContractSerializer().deserializeContract(result.value), - }; - const orm = mongoOrm({ - contract: validated.contract, - executor: ctx.runtime, - enums: buildMongoEnums(validated.contract, ctx.codecs), - }); + const validated = { contract: new MongoContractSerializer().deserializeContract(result.value) }; + const context = executionContextFor(validated.contract); + const orm = mongoOrm({ context, runtime: ctx.runtime }); const userCollection = orm['User']!; type CreateUser = Parameters[0]; @@ -308,14 +299,9 @@ type Address { const result = interpretMongoPsl(schema); if (!result.ok) throw new Error(`Interpretation failed: ${result.failure.summary}`); - const validated = { - contract: new MongoContractSerializer().deserializeContract(result.value), - }; - const orm = mongoOrm({ - contract: validated.contract, - executor: ctx.runtime, - enums: buildMongoEnums(validated.contract, ctx.codecs), - }); + const validated = { contract: new MongoContractSerializer().deserializeContract(result.value) }; + const context = executionContextFor(validated.contract); + 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 new file mode 100644 index 000000000000..9760963d1ba2 --- /dev/null +++ b/upgrade-instructions/pending/mongo-query-takes-context/app/instructions.md @@ -0,0 +1,81 @@ +--- +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' + - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\b(?:executor|enums)\b' + - '(?()]*>)?\s*\(\s*(?!\{)' + - '(?`, 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: + - '(?]|<(?:[^<>]|<[^<>]*>)*>)*,' + - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\bdriver\b' +--- + +# The MongoDB query surfaces and runtime take an execution context + +`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, raw } = mongoStatic({ contract }); + +// Or, with a client +const db = mongo({ contract, url }); +db.query.from('orders'); +``` + +Code that builds the ORM itself passes the context, which also fills generated fields: + +```ts +// Before +const orm = mongoOrm({ contract, executor, mutationDefaults: context, enums }); +const users = createMongoCollection(contract, 'User', executor, enums, context); + +// 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: + +```ts +// Before +const runtime = createMongoRuntime({ context, stack, driver }); + +// After +const stack = createMongoExecutionStack({ target, adapter }); +const context = createMongoExecutionContext({ contract, stack }); +const runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); +``` + +`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 new file mode 100644 index 000000000000..2796a1697345 --- /dev/null +++ b/upgrade-instructions/pending/mongo-query-takes-context/extension/instructions.md @@ -0,0 +1,81 @@ +--- +changes: + - id: mongo-query-takes-context + summary: | + `mongoQuery` (`@internal/mongo-query-builder`, published as `@prisma/orm-family-mongo/query-builder` and `@prisma/orm-mongo/query-builder`) takes `{ context }`, a `MongoExecutionContext`, instead of `{ contract }`, and reads the contract from it. Code that builds the builder itself passes the execution context it already has, for example `mongoQuery({ context })` after `createMongoExecutionContext({ contract, stack })`; code that has a client or a static context uses `db.query` or `mongoStatic({ contract }).query`. 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' + - '(?()]*>)?\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`), 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: + - '(?]|<(?:[^<>]|<[^<>]*>)*>)*,' + - '(?()]*>)?\s*\(\s*\{(?:[^{}]|\{[^{}]*\})*\bdriver\b' +--- + +# The MongoDB query surfaces and runtime take an execution context + +`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, raw } = mongoStatic({ contract }); + +// Or, with a client +const db = mongo({ contract, url }); +db.query.from('orders'); +``` + +Code that builds the ORM itself passes the context, which also fills generated fields: + +```ts +// Before +const orm = mongoOrm({ contract, executor, mutationDefaults: context, enums }); +const users = createMongoCollection(contract, 'User', executor, enums, context); + +// 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: + +```ts +// Before +const runtime = createMongoRuntime({ context, stack, driver }); + +// After +const stack = createMongoExecutionStack({ target, adapter }); +const context = createMongoExecutionContext({ contract, stack }); +const runtime = createMongoRuntime({ context, adapter: stack.adapter.create(stack), driver }); +``` + +`MongoExecutionContext` takes one type parameter, a MongoDB contract: `MongoExecutionContext`. 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..08b330ce800f --- /dev/null +++ b/upgrade-instructions/pending/stack-driver-required/extension/instructions.md @@ -0,0 +1,28 @@ +--- +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`, `clientStack.driver` or `const { driver } = createSqlExecutionStack(...)`. + 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(); +```