feat(emitter): generate FieldInputTypes and wire ORM consumption#335
feat(emitter): generate FieldInputTypes and wire ORM consumption#335
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR splits value-object types into input/output across the emitter and generated contracts, introduces per-field FieldInputTypes alongside FieldOutputTypes, updates query-builder types to consume both maps, updates generated contracts/examples/tests to the new types, removes many runtime coercions, and adds MONGOMS_VERSION to CI jobs. Changes
Sequence DiagramsequenceDiagram
participant Framework as Framework (emitter)
participant ContractDef as Generated Contract (.d.ts)
participant QueryBuilder as Query Builder / ORM
participant App as Application Code / Pages
rect rgba(120, 160, 255, 0.5)
note over Framework,App: Old Flow (single-direction types)
Framework->>ContractDef: Emit single value-object types (e.g., Address)
ContractDef->>QueryBuilder: Provide FieldOutputTypes
QueryBuilder->>App: Resolve model rows from FieldOutputTypes
App->>App: Use String()/Number() coercions as needed
end
rect rgba(120, 255, 160, 0.5)
note over Framework,App: New Flow (input/output split)
Framework->>ContractDef: Emit AddressOutput & AddressInput
ContractDef->>QueryBuilder: Provide FieldOutputTypes & FieldInputTypes
QueryBuilder->>App: Resolve ResolvedOutputRow/ResolvedInputRow for runtime and insert/update shapes
App->>App: Pass native IDs/numbers and input objects without coercion
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/emitter
@prisma-next/migration-tools
@prisma-next/vite-plugin-contract-emit
@prisma-next/runtime-executor
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-pipeline-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
b6e29a8 to
30a5699
Compare
MongoDB 7.0.12+ bundles mozjs with AVX2 instructions that crash on CI runners without AVX2 support. Pin to 7.0.11 (last pre-AVX2 release) across test, e2e, integration, and coverage jobs.
Design spec and execution plan for wiring FieldOutputTypes and FieldInputTypes from emitted contracts into the Mongo ORM row type resolution, eliminating ~60 type casts in the retail store.
Parameterize generateFieldResolvedType for input/output side so the
emitter can produce both FieldOutputTypes and FieldInputTypes maps.
Value object type aliases are now emitted as {Name}Output / {Name}Input
pairs (e.g. PriceOutput, PriceInput) instead of a single un-suffixed
alias. The Mongo emitter TypeMaps expression now includes
FieldInputTypes as a 4th type parameter.
Add 4th type parameter TFieldInputTypes to MongoTypeMaps with a default of Record<string, Record<string, unknown>> for backward compatibility. Add ExtractMongoFieldInputTypes extractor and export it. Existing contracts with 2- or 3-param MongoTypeMaps continue to compile.
Add ResolvedOutputRow and ResolvedInputRow helper types that prefer pre-resolved field types from the contract TypeMaps when available, falling back to InferModelRow for contracts without them. Wire DefaultModelRow, InferFullRow, EmbedRelationRowType, CreateInput, and VariantCreateInput to use the resolved helpers.
Both retail-store and mongo-demo contracts now include
FieldInputTypes maps alongside FieldOutputTypes, and value object
type aliases are emitted as {Name}Output / {Name}Input pairs.
With FieldOutputTypes and FieldInputTypes wired into the ORM, query results and mutation inputs are now properly typed. Remove all as-string, String(), and Number() coercions on ORM-produced values from app pages, API routes, seed data, and test files.
The emitter now generates {Name}Output / {Name}Input instead of
un-suffixed value object aliases — update the SQL emitter tests
to match.
Replace the side flag with resolveFieldType returning { input, output }
so callers pick the side they need from the result. This eliminates the
double iteration over models/fields, makes the renderOutputType
asymmetry explicit (output uses renderOutputType, input always falls
through to CodecTypes[...]['input']), and simplifies callers.
generateBothFieldTypesMaps produces both maps in a single pass.
Add NavItemInput self-reference assertion (F04), and type-level tests for InferFullRow with embedded relations, IncludedRow with references, and VariantCreateInput when FieldOutputTypes/FieldInputTypes are present in the contract (F03).
Add 5th type parameter TFieldInputTypes to SQL TypeMaps (mirroring the Mongo side), add FieldInputTypesOf extractor and ExtractFieldInputTypes convenience alias, and update the SQL emitter getTypeMapsExpression to include FieldInputTypes. All parameters have defaults for backward compatibility.
…ectural changes The SQL builder operates at the table/column (storage) level while FieldOutputTypes/FieldInputTypes are keyed by model/field (domain). Wiring them requires widening TableProxyContract, exporting internal relational-core helpers, and propagating model context through the scope system. The SQL builder also uses simple single-level indexed accesses (not the deep InferModelRow chain that caused Mongo opaque types), so this is not causing issues today.
SQL builder now consumes FieldOutputTypes and FieldInputTypes from the contract to resolve column types directly, bypassing opaque CodecTypes conditionals. This prevents the same opaque-type problem that motivated the Mongo ORM fix — parameterized codecs like Vector<1536> or Char<36> now resolve to concrete types. ResolveRow gains an optional PreResolved parameter, QueryContext carries resolvedColumnOutputTypes, and table-proxy computes per-table column resolution via table→model→column→field mapping. InsertValues/UpdateValues similarly prefer FieldInputTypes when available. Backward compatible: contracts without field type maps fall back to codec-based resolution.
4b87a7e to
e2a4ab5
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts (1)
897-997: Split this test file before adding more coverage.This file is already far past the repo’s 500-line test limit, and these new field-type suites add another separable concern. Please move the new
FieldInputTypes/FieldOutputTypes/resolveFieldTypecoverage into a dedicated test file so this one stays navigable.As per coding guidelines, "Keep test files under 500 lines to maintain readability and navigability. If a test file exceeds this limit, it should be split into multiple files."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts` around lines 897 - 997, Move the field-type related suites out of the large domain-type-generation.test.ts into a new test file (e.g., domain-field-types.test.ts): cut the describe blocks for generateFieldInputTypesMap, generateBothFieldTypesMaps and resolveFieldType (including their it tests) and paste them into the new file, keep the same imports/fixtures (stubCodecLookup, stubCodec, ContractModel, ContractField, generateFieldInputTypesMap, generateBothFieldTypesMaps, resolveFieldType) so tests compile unchanged, remove the moved suites from the original file, and ensure the test runner picks up the new file by following the same test naming/convention as other files in the suite.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/mongo-demo/src/contract.d.ts`:
- Line 5: The generated contract.d.ts file includes an unused import "Vector"
from '@prisma-next/adapter-mongo/codec-types'; remove that import from the
emitter output (delete the import line referencing Vector) and update the
emitter code that produces imports so it only emits imports for types actually
referenced in the contract (ensure the emission logic checks referenced symbols
before emitting entries for "Vector" or similar types).
In `@packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts`:
- Around line 351-368: resolveValueObjectType is calling resolveFieldType
without passing the new codecLookup argument, causing nested value-object
members to use generic CodecTypes[...] instead of codec-specific rendered types;
update resolveValueObjectType to accept and forward a codecLookup parameter into
resolveFieldType for each field, then propagate that parameter through
generateValueObjectType and generateValueObjectTypeAliases and update the
generate-contract-dts.ts call site to pass the codecLookup through so all nested
value-object alias generation uses the codec-aware rendering.
In `@packages/2-sql/4-lanes/sql-builder/src/types/table-proxy.ts`:
- Around line 87-101: The code is stripping null from pre-resolved field input
types by wrapping FieldInputs[...] in NonNullable; remove the NonNullable
wrapper so you use FieldInputs[ModelName][FieldName] verbatim (and still union
with null when Table['columns'][K]['nullable'] is true if desired). Update the
same pattern in the other occurrence used by ResolvedUpdateValues (the block
around lines 106-112) that references FindFieldForColumn, FieldInputs, ModelName
and CT so the codec/input types retain any intrinsic nullability instead of
being forcefully non-nullified.
In
`@packages/2-sql/4-lanes/sql-builder/test/playground/resolved-field-types.test-d.ts`:
- Around line 18-21: The test currently uses expectTypeOf<ExtractRow<typeof
result>>().toEqualTypeOf<...>() which does a structural comparison and can lose
brand information like Char<36>; replace this assertion with a strict identity
check using the Equal<> + Expect<> pattern to ensure branded types are preserved
— i.e. assert Expect<Equal<ExtractRow<typeof result>, { id: Char<36>; title:
string }>> so the brand on Char<36> is verified exactly; update the test in
resolved-field-types.test-d.ts and keep references to ExtractRow, result, Char,
Equal and Expect when making the change.
---
Nitpick comments:
In `@packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts`:
- Around line 897-997: Move the field-type related suites out of the large
domain-type-generation.test.ts into a new test file (e.g.,
domain-field-types.test.ts): cut the describe blocks for
generateFieldInputTypesMap, generateBothFieldTypesMaps and resolveFieldType
(including their it tests) and paste them into the new file, keep the same
imports/fixtures (stubCodecLookup, stubCodec, ContractModel, ContractField,
generateFieldInputTypesMap, generateBothFieldTypesMaps, resolveFieldType) so
tests compile unchanged, remove the moved suites from the original file, and
ensure the test runner picks up the new file by following the same test
naming/convention as other files in the suite.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 06903213-770b-4ac2-9068-5d174d515e55
⛔ Files ignored due to path filters (4)
packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.d.tsis excluded by!**/generated/**projects/orm-consolidation/plans/codec-output-types-plan.mdis excluded by!projects/**projects/orm-consolidation/specs/codec-output-types.spec.mdis excluded by!projects/**projects/orm-consolidation/specs/reviews/code-review.mdis excluded by!projects/**
📒 Files selected for processing (45)
.github/workflows/ci.ymlexamples/mongo-demo/src/contract.d.tsexamples/mongo-demo/src/contract.jsonexamples/retail-store/app/api/auth/signup/route.tsexamples/retail-store/app/api/orders/[id]/route.tsexamples/retail-store/app/cart/page.tsxexamples/retail-store/app/checkout/page.tsxexamples/retail-store/app/orders/[id]/page.tsxexamples/retail-store/app/orders/page.tsxexamples/retail-store/app/page.tsxexamples/retail-store/app/products/[id]/page.tsxexamples/retail-store/src/contract.d.tsexamples/retail-store/src/seed.tsexamples/retail-store/test/api-flows.test.tsexamples/retail-store/test/cart-lifecycle.test.tsexamples/retail-store/test/crud-lifecycle.test.tsexamples/retail-store/test/order-lifecycle.test.tsexamples/retail-store/test/relations.test.tsexamples/retail-store/test/update-operators.test.tspackages/1-framework/3-tooling/emitter/src/domain-type-generation.tspackages/1-framework/3-tooling/emitter/src/generate-contract-dts.tspackages/1-framework/3-tooling/emitter/test/domain-type-generation.test.tspackages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.tspackages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.tspackages/2-mongo-family/1-foundation/mongo-contract/test/contract-types.test-d.tspackages/2-mongo-family/3-tooling/emitter/src/index.tspackages/2-mongo-family/3-tooling/emitter/test/emitter-hook.e2e.test.tspackages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.tspackages/2-mongo-family/5-query-builders/orm/src/types.tspackages/2-mongo-family/5-query-builders/orm/test/value-object-inputs.test-d.tspackages/2-sql/1-core/contract/src/exports/types.tspackages/2-sql/1-core/contract/src/types.tspackages/2-sql/1-core/contract/test/contract-typemaps-shape.test.tspackages/2-sql/3-tooling/emitter/src/index.tspackages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.tspackages/2-sql/4-lanes/sql-builder/src/resolve.tspackages/2-sql/4-lanes/sql-builder/src/runtime/mutation-impl.tspackages/2-sql/4-lanes/sql-builder/src/runtime/query-impl.tspackages/2-sql/4-lanes/sql-builder/src/scope.tspackages/2-sql/4-lanes/sql-builder/src/types/mutation-query.tspackages/2-sql/4-lanes/sql-builder/src/types/shared.tspackages/2-sql/4-lanes/sql-builder/src/types/table-proxy.tspackages/2-sql/4-lanes/sql-builder/test/integration/mutation-defaults.test.tspackages/2-sql/4-lanes/sql-builder/test/playground/resolved-field-types.test-d.tspackages/2-sql/4-lanes/sql-builder/test/runtime/functions.test.ts
| // This file is automatically generated by 'prisma-next contract emit'. | ||
| // To regenerate, run: prisma-next contract emit | ||
| import type { CodecTypes as MongoCodecTypes } from '@prisma-next/adapter-mongo/codec-types'; | ||
| import type { Vector } from '@prisma-next/adapter-mongo/codec-types'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if Vector type is used in the mongo-demo contract
rg -n 'Vector' examples/mongo-demo/src/contract.d.tsRepository: prisma/prisma-next
Length of output: 133
🏁 Script executed:
wc -l examples/mongo-demo/src/contract.d.tsRepository: prisma/prisma-next
Length of output: 103
🏁 Script executed:
cat examples/mongo-demo/src/contract.d.tsRepository: prisma/prisma-next
Length of output: 11650
🏁 Script executed:
rg 'Vector' examples/mongo-demo/ -iRepository: prisma/prisma-next
Length of output: 169
Remove unused Vector import from emitter output.
The Vector type imported from @prisma-next/adapter-mongo/codec-types on line 5 is not used anywhere in the contract definition. Since this is a generated file, the emitter should be fixed to avoid generating stale imports.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/mongo-demo/src/contract.d.ts` at line 5, The generated contract.d.ts
file includes an unused import "Vector" from
'@prisma-next/adapter-mongo/codec-types'; remove that import from the emitter
output (delete the import line referencing Vector) and update the emitter code
that produces imports so it only emits imports for types actually referenced in
the contract (ensure the emission logic checks referenced symbols before
emitting entries for "Vector" or similar types).
| export function resolveValueObjectType( | ||
| _voName: string, | ||
| vo: ContractValueObject, | ||
| _valueObjects: Record<string, ContractValueObject>, | ||
| ): ResolvedFieldType { | ||
| const outputEntries: string[] = []; | ||
| const inputEntries: string[] = []; | ||
| for (const [fieldName, field] of Object.entries(vo.fields)) { | ||
| const tsType = generateFieldResolvedType(field); | ||
| fieldEntries.push(`readonly ${serializeObjectKey(fieldName)}: ${tsType}`); | ||
| const resolved = resolveFieldType(field); | ||
| const key = `readonly ${serializeObjectKey(fieldName)}`; | ||
| outputEntries.push(`${key}: ${resolved.output}`); | ||
| inputEntries.push(`${key}: ${resolved.input}`); | ||
| } | ||
| return fieldEntries.length > 0 ? `{ ${fieldEntries.join('; ')} }` : 'Record<string, never>'; | ||
| const empty = 'Record<string, never>'; | ||
| return { | ||
| output: outputEntries.length > 0 ? `{ ${outputEntries.join('; ')} }` : empty, | ||
| input: inputEntries.length > 0 ? `{ ${inputEntries.join('; ')} }` : empty, | ||
| }; |
There was a problem hiding this comment.
Thread codecLookup through value-object alias generation too.
resolveFieldType() now supports codec-specific output rendering, but resolveValueObjectType() still calls it without codecLookup. That means FooOutput aliases fall back to CodecTypes[...] for parameterized scalar members, while top-level field maps get the rendered concrete type. Nested value objects will lose the precision this PR is trying to add.
Suggested direction
export function resolveValueObjectType(
_voName: string,
vo: ContractValueObject,
_valueObjects: Record<string, ContractValueObject>,
+ codecLookup?: CodecLookup,
): ResolvedFieldType {
const outputEntries: string[] = [];
const inputEntries: string[] = [];
for (const [fieldName, field] of Object.entries(vo.fields)) {
- const resolved = resolveFieldType(field);
+ const resolved = resolveFieldType(field, codecLookup);
const key = `readonly ${serializeObjectKey(fieldName)}`;
outputEntries.push(`${key}: ${resolved.output}`);
inputEntries.push(`${key}: ${resolved.input}`);
}
const empty = 'Record<string, never>';
return {
output: outputEntries.length > 0 ? `{ ${outputEntries.join('; ')} }` : empty,
input: inputEntries.length > 0 ? `{ ${inputEntries.join('; ')} }` : empty,
};
}You'll also want to pass that lookup through generateValueObjectType() / generateValueObjectTypeAliases() and their generate-contract-dts.ts call site.
Also applies to: 421-423
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts` around
lines 351 - 368, resolveValueObjectType is calling resolveFieldType without
passing the new codecLookup argument, causing nested value-object members to use
generic CodecTypes[...] instead of codec-specific rendered types; update
resolveValueObjectType to accept and forward a codecLookup parameter into
resolveFieldType for each field, then propagate that parameter through
generateValueObjectType and generateValueObjectTypeAliases and update the
generate-contract-dts.ts call site to pass the codecLookup through so all nested
value-object alias generation uses the codec-aware rendering.
| [K in keyof Table['columns']]?: FindFieldForColumn< | ||
| C, | ||
| ModelName, | ||
| K & string | ||
| > extends infer FieldName extends string | ||
| ? FieldName extends keyof FieldInputs[ModelName] | ||
| ? Table['columns'][K]['nullable'] extends true | ||
| ? NonNullable<FieldInputs[ModelName][FieldName]> | null | ||
| : NonNullable<FieldInputs[ModelName][FieldName]> | ||
| : Table['columns'][K]['codecId'] extends keyof CT | ||
| ? CT[Table['columns'][K]['codecId']]['input'] | ||
| : unknown | ||
| : Table['columns'][K]['codecId'] extends keyof CT | ||
| ? CT[Table['columns'][K]['codecId']]['input'] | ||
| : unknown; |
There was a problem hiding this comment.
Don't strip null out of pre-resolved field input types.
FieldInputTypes already encode the field’s real input shape. Re-wrapping them as NonNullable<...> (and then re-adding | null from the storage column) will incorrectly reject codecs whose own input type includes null on a non-nullable column. ResolvedUpdateValues inherits the same bug via Line 112.
Suggested fix
type ResolvedInsertValues<
C,
Table extends StorageTable,
TableName extends string,
CT extends Record<string, { readonly input: unknown }>,
FieldInputs extends Record<string, Record<string, unknown>>,
> = string extends keyof FieldInputs
? InsertValues<Table, CT>
: FindModelForTable<C, TableName> extends infer ModelName extends string
? ModelName extends keyof FieldInputs
? {
[K in keyof Table['columns']]?: FindFieldForColumn<
C,
ModelName,
K & string
> extends infer FieldName extends string
? FieldName extends keyof FieldInputs[ModelName]
- ? Table['columns'][K]['nullable'] extends true
- ? NonNullable<FieldInputs[ModelName][FieldName]> | null
- : NonNullable<FieldInputs[ModelName][FieldName]>
+ ? FieldInputs[ModelName][FieldName]
: Table['columns'][K]['codecId'] extends keyof CT
? CT[Table['columns'][K]['codecId']]['input']
: unknown
: Table['columns'][K]['codecId'] extends keyof CT
? CT[Table['columns'][K]['codecId']]['input']
: unknown;
}
: InsertValues<Table, CT>
: InsertValues<Table, CT>;Also applies to: 106-112
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/2-sql/4-lanes/sql-builder/src/types/table-proxy.ts` around lines 87
- 101, The code is stripping null from pre-resolved field input types by
wrapping FieldInputs[...] in NonNullable; remove the NonNullable wrapper so you
use FieldInputs[ModelName][FieldName] verbatim (and still union with null when
Table['columns'][K]['nullable'] is true if desired). Update the same pattern in
the other occurrence used by ResolvedUpdateValues (the block around lines
106-112) that references FindFieldForColumn, FieldInputs, ModelName and CT so
the codec/input types retain any intrinsic nullability instead of being
forcefully non-nullified.
| expectTypeOf<ExtractRow<typeof result>>().toEqualTypeOf<{ | ||
| id: Char<36>; | ||
| title: string; | ||
| }>(); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify current branded-type assertion style in this test file.
rg -n "expectTypeOf|type Equal|type Expect" packages/2-sql/4-lanes/sql-builder/test/playground/resolved-field-types.test-d.ts -C2Repository: prisma/prisma-next
Length of output: 583
Use strict identity checks for branded type assertions in .test-d.ts.
For branded types like Char<36>, expectTypeOf().toEqualTypeOf() performs structural comparison which can erase brand information and produce false positives. Use the Equal<A, B> identity pattern with Expect<T extends true> instead, which leverages TypeScript's distributive conditional types to check strict type identity including brands.
♻️ Suggested refactor
+type Equal<A, B> =
+ (<T>() => T extends A ? 1 : 2) extends
+ (<T>() => T extends B ? 1 : 2) ? true : false;
+type Expect<T extends true> = T;
+
test('SELECT resolves parameterized Char column to concrete type via FieldOutputTypes', () => {
const result = db.articles.select('id', 'title').build();
- expectTypeOf<ExtractRow<typeof result>>().toEqualTypeOf<{
- id: Char<36>;
- title: string;
- }>();
+ type _row = ExtractRow<typeof result>;
+ type _expected = { id: Char<36>; title: string };
+ type _assert = Expect<Equal<_row, _expected>>;
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/2-sql/4-lanes/sql-builder/test/playground/resolved-field-types.test-d.ts`
around lines 18 - 21, The test currently uses expectTypeOf<ExtractRow<typeof
result>>().toEqualTypeOf<...>() which does a structural comparison and can lose
brand information like Char<36>; replace this assertion with a strict identity
check using the Equal<> + Expect<> pattern to ensure branded types are preserved
— i.e. assert Expect<Equal<ExtractRow<typeof result>, { id: Char<36>; title:
string }>> so the brand on Char<36> is verified exactly; update the test in
resolved-field-types.test-d.ts and keep references to ExtractRow, result, Char,
Equal and Expect when making the change.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/integration/test/contract-builder.types.test-d.ts`:
- Around line 413-414: Replace the value-argument form of the Vitest type
assertion with the explicit generic never form: change the uses of
expectTypeOf(Row['payload']).toEqualTypeOf(undefined as never) and
expectTypeOf(Row['meta']).toEqualTypeOf(undefined as never) to use the generic
API expectTypeOf<Row['payload']>().toEqualTypeOf<never>() and
expectTypeOf<Row['meta']>().toEqualTypeOf<never>(); this makes the intent
explicit and follows Vitest guidance while keeping the same exact-type assertion
semantics.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: df00974c-2ee8-4007-a929-cc1c7d8555b9
📒 Files selected for processing (1)
test/integration/test/contract-builder.types.test-d.ts
| expectTypeOf<Row['payload']>().toEqualTypeOf(undefined as never); | ||
| expectTypeOf<Row['meta']>().toEqualTypeOf(undefined as never); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Vitest expectTypeOf, for exact type assertions, is expectTypeOf().toEqualTypeOf()preferred overexpectTypeOf().toEqualTypeOf(undefined as never)? Does the value-argument form introduce weaker or inference-sensitive checks?
💡 Result:
Yes, expectTypeOf.toEqualTypeOf is preferred over expectTypeOf.toEqualTypeOf(undefined as never) for exact type assertions in Vitest's expectTypeOf. The official Vitest documentation provides examples using the generic type parameter form for toEqualTypeOf, such as expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>, explicitly recommending this syntax for precise type equality checks. The expect-type library (underlying Vitest's implementation) documentation emphasizes using generic type arguments over concrete values for better error messages and reliability. Concrete values require TypeScript inference of the type argument T in toEqualTypeOf(value), which produces less informative errors on failure due to reliance on a generic Mismatch type. For never specifically, expectTypeOf.toBeNever is shown in docs, but for general exact matches via toEqualTypeOf, the generic directly specifies the bottom type without inference. The value form undefined as never forces never via type assertion but still relies on inference for the matcher parameter, potentially introducing sensitivity to context or less strict structural checks compared to the direct generic, which uses TypeScript's internal identicalness operator (StrictEqualUsingTSInternalIdenticalToOperator) for exact structural equality. No sources indicate the value form introduces "weaker" checks explicitly, but inference can lead to unexpected passes or poorer diagnostics, making the generic form more robust and preferred for exact assertions.
Citations:
- 1: https://vitest.dev/api/expect-typeof
- 2: https://v0.vitest.dev/api/expect-typeof
- 3: https://vitest.dev/api/expect-typeof.html
- 4: https://vitest.dev/guide/testing-types
- 5: https://github.com/mmkal/expect-type
- 6: https://github.com/mmkal/expect-type/blob/master/src/index.ts
- 7: https://github.com/mmkal/expect-type/blob/HEAD/test/usage.test.ts
- 8: https://github.com/mmkal/expect-type/blob/HEAD/src/index.ts
🏁 Script executed:
sed -n '410,420p' test/integration/test/contract-builder.types.test-d.tsRepository: prisma/prisma-next
Length of output: 591
Use explicit generic never form for exact type assertions.
The value-argument form toEqualTypeOf(undefined as never) is less explicit and relies on inference. Use the generic form toEqualTypeOf<never>() for clearer, more robust type-equality checks as recommended by Vitest's official documentation.
Suggested patch
- expectTypeOf<Row['payload']>().toEqualTypeOf(undefined as never);
- expectTypeOf<Row['meta']>().toEqualTypeOf(undefined as never);
+ expectTypeOf<Row['payload']>().toEqualTypeOf<never>();
+ expectTypeOf<Row['meta']>().toEqualTypeOf<never>();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| expectTypeOf<Row['payload']>().toEqualTypeOf(undefined as never); | |
| expectTypeOf<Row['meta']>().toEqualTypeOf(undefined as never); | |
| expectTypeOf<Row['payload']>().toEqualTypeOf<never>(); | |
| expectTypeOf<Row['meta']>().toEqualTypeOf<never>(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/integration/test/contract-builder.types.test-d.ts` around lines 413 -
414, Replace the value-argument form of the Vitest type assertion with the
explicit generic never form: change the uses of
expectTypeOf(Row['payload']).toEqualTypeOf(undefined as never) and
expectTypeOf(Row['meta']).toEqualTypeOf(undefined as never) to use the generic
API expectTypeOf<Row['payload']>().toEqualTypeOf<never>() and
expectTypeOf<Row['meta']>().toEqualTypeOf<never>(); this makes the intent
explicit and follows Vitest guidance while keeping the same exact-type assertion
semantics.
17c7a2d to
11af80f
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/prisma-next-demo/src/orm-client/create-user-with-address.ts`:
- Line 3: Remove the ".d" extension from the TypeScript import in
create-user-with-address.ts: update the import that currently references
'../prisma/contract.d' so it imports AddressInput and Contract from
'../prisma/contract' (extensionless) to follow the repo guideline "Never add
file extensions to imports in TypeScript".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: c9dc41a2-ff2e-41e2-9e9d-2d669ae536bf
⛔ Files ignored due to path filters (1)
test/e2e/framework/test/fixtures/generated/contract.d.tsis excluded by!**/generated/**
📒 Files selected for processing (3)
examples/prisma-next-demo/src/orm-client/create-user-with-address.tstest/integration/package.jsontest/integration/test/contract-builder.types.test-d.ts
✅ Files skipped from review due to trivial changes (1)
- test/integration/test/contract-builder.types.test-d.ts
| import type { DefaultModelRow } from '@prisma-next/sql-orm-client'; | ||
| import type { Runtime } from '@prisma-next/sql-runtime'; | ||
| import type { Address, Contract } from '../prisma/contract.d'; | ||
| import type { AddressInput, Contract } from '../prisma/contract.d'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Read-only check: find TS/TSX imports that include explicit file extensions.
rg -nP --type=ts --type=tsx "from ['\"][^'\"]+\\.(d|ts|tsx|js|jsx)['\"]"Repository: prisma/prisma-next
Length of output: 89
🏁 Script executed:
#!/bin/bash
# Read the target file to confirm the import
cat -n examples/prisma-next-demo/src/orm-client/create-user-with-address.ts | head -10Repository: prisma/prisma-next
Length of output: 524
🏁 Script executed:
#!/bin/bash
# Check if contract.d.ts exists in the expected location
find examples/prisma-next-demo -name "contract.d.ts" -o -name "contract.ts"Repository: prisma/prisma-next
Length of output: 157
🏁 Script executed:
#!/bin/bash
# Corrected check: find TS imports with explicit file extensions (no tsx type)
rg -nP --type=ts "from ['\"][^'\"]+\\.(d|ts|js|jsx)['\"]" examples/Repository: prisma/prisma-next
Length of output: 1991
Drop the .d extension from the TypeScript import path.
Line 3 should use an extensionless module specifier per the repo guideline "Never add file extensions to imports in TypeScript".
Suggested patch
-import type { AddressInput, Contract } from '../prisma/contract.d';
+import type { AddressInput, Contract } from '../prisma/contract';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import type { AddressInput, Contract } from '../prisma/contract.d'; | |
| import type { AddressInput, Contract } from '../prisma/contract'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/prisma-next-demo/src/orm-client/create-user-with-address.ts` at line
3, Remove the ".d" extension from the TypeScript import in
create-user-with-address.ts: update the import that currently references
'../prisma/contract.d' so it imports AddressInput and Contract from
'../prisma/contract' (extensionless) to follow the repo guideline "Never add
file extensions to imports in TypeScript".
closes TML-2245
Key snippet
New — single-pass field type resolution
The resolution function returns both input and output type expressions in one call.
renderOutputTypeis explicitly output-only — the input side always falls through to the generic codec accessor.New — ORM fallback heuristic
When
FieldOutputTypesis a concrete map (not the defaultedRecord<string, Record<string, unknown>>), the ORM indexes directly into it for the model's row type — bypassing the deepInferModelRowconditional chain that TypeScript can't fully evaluate.Intent
Wire the pre-resolved
FieldOutputTypes(and a newFieldInputTypes) from emitted contracts into both the Mongo ORM's and SQL query builder's type resolution, so query results and mutation inputs resolve to concrete primitives (string,number,Date) instead of opaque conditional types. This eliminates ~100 explicit type casts in the retail store example.Change map
resolveFieldType,generateBothFieldTypesMaps,resolveValueObjectType, split VO aliasesThe story
Add single-pass field type resolution: Introduce
resolveFieldTypereturning{ input, output }in one call, andgenerateBothFieldTypesMapswhich iterates over models/fields once to produce both maps.renderOutputTypeis explicitly output-only — the input side always uses the generic codec accessor. Value object aliases are split from{Name}into{Name}Input/{Name}Output.Extend
MongoTypeMapsto carryFieldInputTypes: Add a 4th type parameter (defaulted) alongside the existingFieldOutputTypesparameter. Add a parallel extractorExtractMongoFieldInputTypes. The defaults ensure backward compatibility for contracts emitted before this change.ORM prefers pre-resolved types over
InferModelRow: IntroduceResolvedOutputRowandResolvedInputRowtype helpers that check whether the contract carries concrete field type maps. If yes, index directly into them (strippingreadonly). If no, fall back toInferModelRow. Wire these intoDefaultModelRow,InferFullRow,EmbedRelationRowType,CreateInput, andVariantCreateInput.Re-emit contracts and remove casts: Re-emit both the retail store and mongo-demo
contract.d.tswith the new 4-parameterMongoTypeMapsand split value object aliases. Remove all ~100 codec-related casts from the retail store.Extend SQL
TypeMapsand wire query builders: AddTFieldInputTypesas a 5th type parameter to the SQL family'sTypeMaps(with a default for backward compat). AddFieldInputTypesOfextractor andExtractFieldInputTypesconvenience alias. Update the SQL emitter'sgetTypeMapsExpression()to passFieldInputTypes.SQL query builders consume pre-resolved field type maps: Introduce
FindModelForTable,FindFieldForColumn, andResolvedColumnTypestype helpers intable-proxy.tsthat map table columns back to model fields via the contract's storage mapping — enabling the SQL builder to consumeFieldOutputTypes/FieldInputTypeswithout any new imports fromrelational-core.ResolveRowgains an optionalPreResolvedparameter; when pre-resolved types are available for a column, it uses them directly (applying nullability), falling back to the existing codec-based resolution otherwise.ContractToQCis parameterized by table name and computesresolvedColumnOutputTypesper-table.insert()/update()call sites useResolvedInsertValues/ResolvedUpdateValuesfor the input side.Behavior changes & evidence
Emitter generates
FieldInputTypesalongsideFieldOutputTypesin a single pass:resolveFieldTypereturns both sides per field;generateBothFieldTypesMapsaccumulates both maps in one iteration.renderOutputTypeasymmetry explicit.Value object aliases split into
{Name}Input/{Name}Output: Before:export type Price = { ... }. After:export type PriceOutput = { ... }; export type PriceInput = { ... }. The un-suffixed alias is removed.FieldOutputTypesreferences{Name}Outputaliases andFieldInputTypesreferences{Name}Inputaliases, maintaining consistent naming.MongoTypeMapsgains 4th type parameterTFieldInputTypes: Before: 3 parameters. After: 4 parameters, all with defaults.ORM
DefaultModelRowresolves to concrete primitives: Before:DefaultModelRow=InferModelRow(deep conditional chain, opaque). After:DefaultModelRow=ResolvedOutputRow(direct indexed access when present).string,number, etc.SQL
TypeMapsgains 5th type parameterTFieldInputTypes: Before: 4 parameters (CodecTypes,OperationTypes,QueryOperationTypes,FieldOutputTypes). After: 5 parameters addingFieldInputTypes, all with defaults.FieldInputTypesfor all targets, and the SQL contract type system must carry it so the SQL query builders can consume it.SQL query builders resolve to concrete types via pre-resolved field type maps: Before:
ResolveRowusedCodecTypes[codecId]['output']conditionals;InsertValues/UpdateValuesusedCodecTypes[codecId]['input']conditionals. After:ResolveRowgains aPreResolvedparameter that bypasses codec lookups when pre-resolved types are available;ResolvedInsertValues/ResolvedUpdateValuespreferFieldInputTypesat theTableProxycall sites.Retail store compiles with zero codec-related casts: ~100 casts removed across app pages, data layer, seed, and tests.
examples/retail-store/Compatibility / migration / risk
Price,Address) are replaced withPriceOutput/PriceInput. Any user code importing the un-suffixed alias must update.MongoTypeMapsbackward-compatible: All new type parameters have defaults. Older contracts continue to compile and the ORM falls back toInferModelRow.Follow-ups / open questions
renderInputTypewill need to be added to codecs andresolveFieldTypeupdated to use it for the input side.VariantCreateInputasymmetry: Variant fields use the output type path viaVariantModelRow, not the input type path. Documented with a TODO; harmless today but will need fixing when input/output types diverge.Non-goals / intentionally out of scope
renderInputTypeon codecs (deferred to TML-2229)Summary by CodeRabbit
Bug Fixes
Data Integrity
Documentation
Chores