TypeScript-authored contracts lose metadata when passed directly to postgres({ contract }) in @prisma/orm-postgres@8.0.0-rc.11. This affects scalar results and filters, relation cardinality, aggregate availability, and prepared scalar parameters. The reproduction below imports only Prisma; it does not use Alchemy, Effect, emitted contract declarations, or a database connection.
Reproduction
Install @prisma/orm-postgres@8.0.0-rc.11 and typescript@5.9.3 in an ESM package, save this as repro.ts, and run:
npx tsc --noEmit --strict --module nodenext --target esnext --skipLibCheck repro.ts
import { defineContract } from "@prisma/orm-postgres/contract-builder";
import postgres from "@prisma/orm-postgres/runtime";
const contract = defineContract({ extensions: {} }, ({ field, model, rel }) => {
const User = model("User", {
fields: {
id: field.int().id(),
email: field.text(),
name: field.text().optional(),
},
});
const Post = model("Post", {
fields: { id: field.int().id(), authorId: field.int() },
relations: {
author: rel.belongsTo(User, { from: "authorId", to: "id" }),
},
});
return {
models: {
User: User.relations({ posts: rel.hasMany(Post, { by: "authorId" }) }),
Post,
},
};
});
export async function check(db: ReturnType<typeof postgres<typeof contract>>) {
const user = await db.orm.public.User.first();
if (user) {
const id: number = user.id;
const email: string = user.email;
const name: string | null = user.name;
}
const post = await db.orm.public.Post.include("author").first();
if (post) {
const email: string = post.author.email;
}
await db.prepare({ email: "pg/text@1" }, (sql, params) =>
sql.public.User.select("id", "email")
.where((fields, fns) => fns.eq(fields.email, params.email))
.build(),
);
await db.orm.public.User.aggregate((aggregate) => ({
total: aggregate.count(),
}));
// @ts-expect-error a numeric field must reject a string filter
db.orm.public.User.where({ id: "not-a-number" });
}
Expected
- Scalar row properties are
number, string, and string | null.
- The required
belongsTo relation produces one author, not an array.
aggregate.count() is available for the Postgres target.
- A scalar text column can be compared with a scalar prepared text parameter.
- The incorrect numeric filter consumes
@ts-expect-error.
Actual
- Scalar row properties are
unknown.
- The author is typed as
DefaultModelRow<...>[], so post.author.email fails.
- Aggregation receives
AggregateOperationsUnavailable.
- The prepared comparison reports incompatible scalar/list expressions (
many: true versus many?: undefined).
- The incorrect numeric filter is accepted (
Unused '@ts-expect-error' directive).
Declaration details
The published SQL contract-builder declarations appear to contribute several independent losses:
BuiltModels maps every relation to the broad ContractRelation instead of retaining the authored target, cardinality, and nullability.
SqlContractResult.domain.namespaces is Readonly<Record<string, BuiltDomainNamespace<Definition>>>, losing literal namespace keys.
- The returned
TypeMaps omits its aggregate metadata argument, defaulting to Record<string, never>.
FieldManyOf checks FieldState extends { readonly many?: true }. Scalar state has many?: undefined, which also satisfies that check when exactOptionalPropertyTypes is disabled. Scalar field maps consequently acquire an array dimension. This should distinguish the value true from an absent/undefined flag.
There are related authoring metadata losses: .default()/.defaultSql() return the same state type; model namespace literals are widened to string; preset execution defaults are omitted from field state. These complicate correctly inferred create inputs and non-public namespaces even after fixing the query-facing metadata.
We are adding a local opt-in declaration adapter in Alchemy while retaining Prisma's actual functions and builders at runtime. We would prefer to remove that compatibility layer once native contracts preserve this metadata. Generated declarations remain useful as a parity oracle, but requiring generated application imports would defeat our direct TypeScript-contract integration.
TypeScript-authored contracts lose metadata when passed directly to
postgres({ contract })in@prisma/orm-postgres@8.0.0-rc.11. This affects scalar results and filters, relation cardinality, aggregate availability, and prepared scalar parameters. The reproduction below imports only Prisma; it does not use Alchemy, Effect, emitted contract declarations, or a database connection.Reproduction
Install
@prisma/orm-postgres@8.0.0-rc.11andtypescript@5.9.3in an ESM package, save this asrepro.ts, and run:Expected
number,string, andstring | null.belongsTorelation produces one author, not an array.aggregate.count()is available for the Postgres target.@ts-expect-error.Actual
unknown.DefaultModelRow<...>[], sopost.author.emailfails.AggregateOperationsUnavailable.many: trueversusmany?: undefined).Unused '@ts-expect-error' directive).Declaration details
The published SQL contract-builder declarations appear to contribute several independent losses:
BuiltModelsmaps every relation to the broadContractRelationinstead of retaining the authored target, cardinality, and nullability.SqlContractResult.domain.namespacesisReadonly<Record<string, BuiltDomainNamespace<Definition>>>, losing literal namespace keys.TypeMapsomits its aggregate metadata argument, defaulting toRecord<string, never>.FieldManyOfchecksFieldState extends { readonly many?: true }. Scalar state hasmany?: undefined, which also satisfies that check whenexactOptionalPropertyTypesis disabled. Scalar field maps consequently acquire an array dimension. This should distinguish the valuetruefrom an absent/undefined flag.There are related authoring metadata losses:
.default()/.defaultSql()return the same state type; model namespace literals are widened tostring; preset execution defaults are omitted from field state. These complicate correctly inferred create inputs and non-public namespaces even after fixing the query-facing metadata.We are adding a local opt-in declaration adapter in Alchemy while retaining Prisma's actual functions and builders at runtime. We would prefer to remove that compatibility layer once native contracts preserve this metadata. Generated declarations remain useful as a parity oracle, but requiring generated application imports would defeat our direct TypeScript-contract integration.