What problem are you trying to solve?
I'm using v8 with TypeScript schemas.
The docs page shows an example schema; here it is, but simplified:
import { defineContract, rel } from "@prisma/orm-postgres/contract-builder";
export default defineContract({}, ({ field, model }) => {
const User = model("User", {
fields: {
id: field.bigint(),
name: field.text(),
},
});
const Post = model("Post", {
fields: {
id: field.bigint(),
title: field.text(),
userId: field.bigint(),
},
});
return {
models: {
User: User.relations({
posts: rel.hasMany(Post, { by: "userId" }),
}).sql({ table: "user" }),
Post: Post.relations({
user: rel.belongsTo(User, {
from: "userId",
to: "id",
}),
}),
},
};
});
The documentation states the ts approach is useful when:
model definitions must be split, composed, or reused across ordinary TypeScript modules or packages
I want to do exactly that: split the overall schema into separate files and compose them in the main contract file, for example:
prisma/
contract.ts
user.ts
post.ts
I struggled to get that to work, as there's no guidance for that scenario (other than stating it's possible), and the changes between versions makes it hard to find the correct syntax.
With the help of AI I found two approaches, and both are non-ideal.
Approach 1
prisma/
contract.ts
helpers.ts
modelRegistry.ts
models/
user.ts
post.ts
// helpers.ts
import { defineContract } from "@prisma/orm-postgres/contract-builder";
export type ContractHelpers = Parameters<Parameters<typeof defineContract>[1]>[0];
// modelRegistry.ts
import type * as schemaUser from "./models/user";
import type * as schemaPost from "./models/post";
export interface ModelRegistry {
User: ReturnType<typeof schemaUser.define>;
Post: ReturnType<typeof schemaPost.define>;
}
// models/user.ts
import type { ContractHelpers } from "../helpers";
import type { ModelRegistry } from "../modelRegistry";
export function define({ field, model }: ContractHelpers) {
return model("User", {
fields: {
id: field.bigint(),
name: field.text(),
},
});
}
export function configure(
{ User, Post }: ModelRegistry,
{ rel }: ContractHelpers,
) {
return User.relations({
posts: rel.hasMany(Post, { by: "userId" }),
}).sql({ table: "user" });
}
// models/post.ts
import type { ContractHelpers } from "../helpers";
import type { ModelRegistry } from "../modelRegistry";
export function define({ field, model }: ContractHelpers) {
return model("Post", {
fields: {
id: field.bigint(),
title: field.text(),
userId: field.bigint(),
},
});
}
export function configure(
{ User, Post }: ModelRegistry,
{ rel }: ContractHelpers,
) {
return Post.relations({
user: rel.belongsTo(User, { from: "userId", to: "id" }),
});
}
// contract.ts
import { defineContract } from "@prisma/orm-postgres/contract-builder";
import * as schemaUser from "./models/user";
import * as schemaPost from "./models/post";
import type { ModelRegistry } from "./modelRegistry";
export default defineContract({}, (helpers) => {
const base: ModelRegistry = {
User: schemaUser.define(helpers),
Post: schemaPost.define(helpers),
};
return {
models: {
User: schemaUser.configure(base, helpers),
Post: schemaPost.configure(base, helpers),
},
};
});
This approach is verbose, and requires the helpers.ts and modelRegistry.ts utilities, the former of which is coupled to an internal implementation detail.
Approach 2
prisma/
contract.ts
models/
user.ts
post.ts
// models/user.ts
import { field, model, rel } from "@prisma/orm-postgres/contract-builder";
import { PostBase } from "./post";
export const UserBase = model("User", {
fields: {
id: field.column({ codecId: "pg/int4@1", nativeType: "int4" }),
name: field.column({ codecId: "pg/text@1", nativeType: "text" }),
},
});
export const User = UserBase.relations({
posts: rel.hasMany(() => PostBase, { by: "userId" }),
});
// models/post.ts
import { field, model, rel } from "@prisma/orm-postgres/contract-builder";
import { UserBase } from "./user";
export const PostBase = model("Post", {
fields: {
id: field.column({ codecId: "pg/int4@1", nativeType: "int4" }),
title: field.column({ codecId: "pg/text@1", nativeType: "text" }),
userId: field.column({ codecId: "pg/int4@1", nativeType: "int4" }),
},
});
export const Post = PostBase.relations({
user: rel.belongsTo(() => UserBase, { from: "userId", to: "id" }),
});
// contract.ts
import { defineContract } from "@prisma/orm-postgres/contract-builder";
import { User } from "./models/user";
import { Post } from "./models/post";
export default defineContract({}, () => ({
models: { User, Post },
}));
This approach is simpler and cleaner, but lacks the syntax sugar for column definitions; it's too low-level and requires hardcoding magic strings like "pg/int4@1" etc.
Proposed solution
This use case is supposedly supported, but currently there is no official way to actually implement it. Both approaches above are workarounds, not solutions. Please provide an officially supported way to split a contract across files that:
- uses only exported, public types (no reliance on the library's implementation details)
- keeps the column-definition syntax sugar (
field.text(), field.int(), etc.) rather than requiring error-prone raw codecId/nativeType magic strings
- doesn't require
any
Thank you!
Alternatives considered
No response
Scope and impact
No response
What problem are you trying to solve?
I'm using v8 with TypeScript schemas.
The docs page shows an example schema; here it is, but simplified:
The documentation states the ts approach is useful when:
I want to do exactly that: split the overall schema into separate files and compose them in the main contract file, for example:
I struggled to get that to work, as there's no guidance for that scenario (other than stating it's possible), and the changes between versions makes it hard to find the correct syntax.
With the help of AI I found two approaches, and both are non-ideal.
Approach 1
This approach is verbose, and requires the
helpers.tsandmodelRegistry.tsutilities, the former of which is coupled to an internal implementation detail.Approach 2
This approach is simpler and cleaner, but lacks the syntax sugar for column definitions; it's too low-level and requires hardcoding magic strings like
"pg/int4@1"etc.Proposed solution
This use case is supposedly supported, but currently there is no official way to actually implement it. Both approaches above are workarounds, not solutions. Please provide an officially supported way to split a contract across files that:
field.text(),field.int(), etc.) rather than requiring error-prone rawcodecId/nativeTypemagic stringsanyThank you!
Alternatives considered
No response
Scope and impact
No response