|
I'm using v8 with TypeScript-based schemas. The documentation states the ts approach is useful when:
It also shows an example single-file 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.int32().primaryKey(),
name: field.text(),
},
});
const Post = model("Post", {
fields: {
id: field.int32().primaryKey(),
title: field.text(),
userId: field.int32(),
},
});
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",
}),
}),
},
};
});I want to split that into separate files and compose them in the main contract file, for example: I've 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. What is the official approach for this scenario? Thanks! |
Replies: 4 comments 3 replies
|
It is fully supported, but the challenge most developers hit when splitting into separate files is circular module imports ( In TypeScript, you can solve this cleanly by decoupling model field declarations from relation wiring. Here are the two standard patterns: Pattern 1: Factory Functions + Central Relation Wiring (Recommended)In this pattern, each domain file defines its own fields independently without needing to import sibling models. The reciprocal relations are wired together in
|
|
Splitting works today on the published packages. The thing that blocks it is the module loader, not the contract builder. Tested against 1. The blocker is the file extension
// prisma/orm @ main (1e8b6a6)
// packages/2-sql/2-authoring/contract-ts/src/config-types.ts:106 (typescriptContractFromPath)
const mod = await import(pathToFileURL(absolutePath).href);So Node's own TypeScript support runs import { userModel } from './models/user'; // ERR_MODULE_NOT_FOUND
import { userModel } from './models/user.ts'; // worksAdd (The CLI does also ship an esbuild-based loader at 2. Why the field sugar vanishes when you move to module scope
// packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts:2102
export const field = {
column: columnField,
generated: generatedField,
namedType: namedTypeField,
};Confirmed at runtime on rc.10: The sugar is composed per contract from the family pack plus the target pack plus your extensions, and only exists on the helpers object handed to the factory callback: // packages/2-sql/2-authoring/contract-ts/src/composed-authoring-helpers.ts:141
readonly field: CoreFieldHelpers & FieldHelpersFromNamespace<
ExtractFieldNamespaceFromPack<Family> &
ExtractFieldNamespaceFromPack<Target> &
MergeExtensionFieldNamespaces<Extensions>
>;built at
3. Working exampleFour files, relations colocated with each model, no
import type { defineContract } from '@prisma/orm-postgres/contract-builder';
export type Helpers = Parameters<NonNullable<Parameters<typeof defineContract>[1]>>[0];
import { rel } from '@prisma/orm-postgres/contract-builder';
import type { Helpers } from '../helpers.ts';
import type { postModel } from './post.ts';
export function userModel({ field, model }: Helpers) {
return model('User', {
fields: { id: field.int().id(), name: field.text() },
});
}
export function userRelations(
User: ReturnType<typeof userModel>,
Post: ReturnType<typeof postModel>,
) {
return User.relations({
posts: rel.hasMany(Post, { by: 'userId' }),
}).sql({ table: 'user' });
}
import { rel } from '@prisma/orm-postgres/contract-builder';
import type { Helpers } from '../helpers.ts';
import type { userModel } from './user.ts';
export function postModel({ field, model }: Helpers) {
return model('Post', {
fields: { id: field.int().id(), title: field.text(), userId: field.int() },
});
}
export function postRelations(
Post: ReturnType<typeof postModel>,
User: ReturnType<typeof userModel>,
) {
return Post.relations({
user: rel.belongsTo(User, { from: 'userId', to: 'id' }),
});
}
import { defineContract } from '@prisma/orm-postgres/contract-builder';
import { postModel, postRelations } from './models/post.ts';
import { userModel, userRelations } from './models/user.ts';
export default defineContract({}, (helpers) => {
const User = userModel(helpers);
const Post = postModel(helpers);
return {
models: {
User: userRelations(User, Post),
Post: postRelations(Post, User),
},
};
});The Measured results:
Counterfactual checks, so the above is not passing vacuously. Typo a helper name in If you would rather name the helpers type explicitly than derive it, import type sqlFamilyPack from '@prisma/orm-family-sql/family/pack';
import type { ComposedAuthoringHelpers } from '@prisma/orm-postgres/contract-builder';
import type postgresPack from '@prisma/orm-target-postgres/target/pack';
export type Helpers = ComposedAuthoringHelpers<typeof sqlFamilyPack, typeof postgresPack, undefined>;Both compile identically. The 4. Two module-scope variants, and why I am not recommending them
It emitted the same
The string form, Both work. Neither is as good as passing the model tokens. 5. On the two answers above@amasen02 is right on the substance: it is supported, and the fix is to inject the builder rather than have model files import each other. The posted code will not compile as written, though. @lonix1, your rc.12 emit failure was real and it is the extension rule in section 1, not a missing feature. The split itself works, including the reciprocal One unrelated note: the docs snippet you quoted uses |
I don't think this is officially/properly supported despite the claim in the docs, so added it as a feature request here.