| TML-3578: Mongo fields and SQL value-object fields are typed by the shared field type generic, and a Mongo defineContract contract with enums exports declarations (#30747) Closes TML-3578. An emitted Mongo `contract.d.ts` now computes its field types from the contract with the same generic that SQL and both `defineContract` builders use, instead of spelling every field out as text: ```ts // examples/mongo-demo/src/contract.d.ts, before export type FieldOutputTypes = { readonly __unbound__: { readonly User: { readonly bio: CodecTypes['mongo/string@1']['output'] | null; readonly role: 'admin' | 'author' | 'reader'; // … every field of every model, written out }; }; }; // after type FieldTypeOverrides = { readonly __unbound__: { readonly models: { readonly User: { readonly role: { readonly output: 'admin' | 'author' | 'reader'; readonly input: 'admin' | 'author' | 'reader' } }; }; }; }; export type FieldOutputTypes = FieldTypesFromDomain<ContractBase['domain']['namespaces'], CodecTypes, ParameterizedOutputTypeFunctions, FieldTypeOverrides, 'output'>; ``` The emitter writes only what a type cannot compute from the contract: the member union of a value set and text a codec renders itself (`arktype/json@1`). ## The decision There is now one copy of the field type rules for every family and both authoring paths. #30709 did this for SQL tables. Three copies remained, and this PR deletes them: 1. **The framework emitter's text path** (`resolveFieldType`, `applyModifiers`, `codecOutputType` and helpers). It typed every Mongo field and every SQL value-object field. The `EmissionSpi.fieldTypesExpression` hook goes with it. 2. **Mongo `defineContract`'s own copy** (`BuilderFieldChannelType`). 3. **The Mongo ORM's update accessors** (`ResolveFieldType` in `field-accessor.ts`), which typed `set()` and `push()` straight from the codec types, so an enum field took any `string`. What moved: - `FieldTypesFromDomain` moved from `@internal/sql-contract/types` to `@internal/contract/types`, with `ValueObjectTypesFromDomain` and `StoredValueType` beside it. It types value-object fields (nested and recursive) and union fields itself. `ColumnTypesFromStorage` stays in SQL. - `FieldTypeOverrides` is one map keyed like the contract's domain: `[namespace]['models' | 'valueObjects'][owner][field]`. - Mongo's vector codec declares `Vector<N>` with a type function, as pgvector does, instead of `renderOutputType`. - One framework check, `assertDataTypeParams`, refuses invalid type parameters for both families with `CONTRACT.TYPE_PARAMS_INVALID`. Mongo had lost its only check when `renderOutputType` went, so a vector `length` of `0`, `-1` or `2.5` was accepted. Mongo PSL reports it as a `PSL_INVALID_ATTRIBUTE_ARGUMENT` diagnostic. - The family hook that reads a value set from storage is renamed `resolveStorageValueSet`. The framework reads domain enums itself. ## Behaviour changes users see - **Exporting a Mongo `defineContract` contract with enums works.** With `declaration: true` it failed with TS4023, and then TS2742, also on `main`. - **Mongo `defineContract(scaffold, factory)` no longer types everything `never`.** On `main` every field, row and create input of the factory form was `never`. The existing type tests passed anyway, because a check against `never` accepts any expected type. - **Enum fields inside value objects type as their members on both paths.** On `main`, the emitted path gave the codec type (`string`) and Mongo `defineContract` gave the members. Now both give `'reader' | 'editor'`. A value-object field whose codec renders its own type gets that type back. - **Mongo ORM `set()`, `push()` and dot paths take the field's input type.** An enum field takes its members. A list of numbers no longer offers `inc` or `mul`. - **Mongo `defineContract` rows are `readonly`**, matching the emitted contract. Each has an entry in `upgrade-instructions/pending/shared-field-type-generic/`. A SQL `contract.d.ts` that is not re-emitted stops compiling, because the SQL family entry no longer exports `FieldTypesFromDomain`; re-emitting fixes it. ## Type-check cost The shared generic costs more type instantiations than the written-out text on `main`. Measured with `tsc --extendedDiagnostics --incremental false`: | Example | `main` | This branch | Increase | | --- | --- | --- | --- | | `mongo-demo` | 28,991 | 31,862 | +9.9% | | `retail-store` | 99,597 | 108,478 | +8.9% | This is above the roughly 5% bar set for SQL in #30709, and it is accepted. Mongo field types now cost about 250 to 300 instantiations per field, against about 600 to 1,100 for Postgres contracts on the same generic. A Postgres example of `mongo-demo`'s size (`prisma7-adoption`, 4 models and 16 fields) costs 455,487 instantiations for the whole app. Getting under 5% would mean writing Mongo field types out as text again, which is the second copy of the rules this PR removes. Type-check cost in the SQL ORM and query builder is being looked at separately. ## How it is proven - **Same meaning for every emitted contract.** A script compared all 291 changed committed `contract.d.ts` files with `main`, field by field, by exact type equality (so `readonly` and `any` count). All are equal. A planted change to one value set was caught. The script is in the review notes, not the repo, because it needs `main`'s copies side by side. - **A lasting guard:** a new Mongo fixture in `test/integration/test/contract-parity/` compares every type map of a Mongo `defineContract` contract with its emitted twin. Every key is compared or excluded with a written reason. - **Type tests for every rule of the generic** in `packages/1-framework/0-foundation/contract/test/field-value-types.types.test-d.ts`: value set, parameterized refinement, codec output and input, list, dictionary, nullability (including a missing `nullable` key), value objects, unions. - **Declaration emit:** a Mongo source in `test/e2e/framework/test/declaration-emit/`. - **ORM accessors:** `test/integration/test/mongo/field-accessor-types.test-d.ts`, against both the built and the emitted contract. - Each behaviour fix has a test that failed before it. ## Alternatives considered - **Keep Mongo field types as written-out text, to stay under the 5% cost bar.** Rejected: it keeps a second copy of the rules, which is how the value-object enum and factory-form `never` defects arose. - **Defer enum members in value objects.** It changes emitted types (from `string` to the members), but widening to `string` undoes what this project set out to do: contract values equal what the database stores. - **Keep exporting `FieldTypesFromDomain` from the SQL family entry, so old SQL `contract.d.ts` files keep compiling.** Rejected: the repo does not keep compatibility exports unless asked, and re-emitting is the normal step. Agent: ishtar-12 --------- Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> | 2 天前 |