Skip to content

prismaContract() / mongoContract() only accept a file path, not in-memory text #30115

Description

@WhyAsh5114

What problem are you trying to solve?

prismaContract(schemaPath, options) (@prisma/orm-family-sql, packages/2-sql/2-authoring/contract-psl/src/provider.ts:65-113) and mongoContract() (@prisma/orm-family-mongo) always readFile() their first argument — there's no way to hand them schema text directly:

export function prismaContract(schemaPath: string, options: PrismaContractOptions): ContractConfig {
const source: PslInterpretCapable = {
format: 'psl',
inputs: [schemaPath],
interpret(input, context) {
const scalarColumnDescriptors: ReadonlyMap<string, ColumnDescriptor> =
collectScalarTypeConstructors(context.authoringContributions.type);
return interpretPslDocumentToSqlContract({
symbolTable: input.symbolTable,
sourceFile: input.sourceFile,
sourceId: input.sourceId,
seedDiagnostics: [],
target: options.target,
authoringContributions: context.authoringContributions,
scalarColumnDescriptors,
...ifDefined(
'composedExtensions',
context.composedExtensions.length > 0 ? [...context.composedExtensions] : undefined,
),
composedExtensionContracts: context.composedExtensionContracts,
...ifDefined(
'composedExtensionPackRefs',
options.composedExtensionPackRefs?.length ? options.composedExtensionPackRefs : undefined,
),
controlMutationDefaults: context.controlMutationDefaults,
createNamespace: options.createNamespace,
capabilities: context.capabilities,
codecLookup: context.codecLookup,
...ifDefined('enumInferenceCodecs', options.enumInferenceCodecs),
});
},
async load(context) {
const [absoluteSchemaPath] = context.resolvedInputs;
if (absoluteSchemaPath === undefined) {
throw new InternalError(
'prismaContract: context.resolvedInputs is empty. The CLI config loader should populate it positional-matched with source.inputs.',
);
}
let schema: string;
try {
schema = await readFile(absoluteSchemaPath, 'utf-8');
} catch (error) {
const message = String(error);
return notOk({
summary: `Failed to read Prisma schema at "${schemaPath}"`,
diagnostics: [
{
code: 'PSL_SCHEMA_READ_FAILED',
message,

I maintain prisma-next-idb's sync layer, which needs to strip an IDB-only attribute (@idb.exclude) and append a synthetic Changelog model to a consumer's schema before handing it to defineConfig. Since prismaContract can't take that composed text directly, I have to write it to a generated file on disk just so there's something to readFile():

export default defineConfig({
  contract: writeSqlSchemaWithSync('src/schema.prisma', 'src/schema.postgres.generated.prisma'),
  // ...
});

Proposed solution

Accept schema text directly, e.g.:

prismaContract({ path } | { text }, options)
// or
prismaContract(schemaPath, { ...options, injectSchemaText?: (schema: string) => string })

Prior art: I already ship the injectSchemaText shape in my own IDB-family provider (prismaIdbContract()), where it runs on the raw text right before parse():

readonly injectSchemaText?: (schema: string) => string;

It's in production use today and the injected text flows through the normal parse → interpret → hash pipeline, so storageHash correctness isn't a new concern — it's the SQL/Mongo providers that are missing this, not a novel design. Happy to port it over as a PR.

Alternatives considered

Before filing, I checked whether the public builder DSL (typescriptContract/defineContract) could replace this — it can, but only for a schema authored entirely in TS. My case needs to keep an arbitrary, PSL-authored consumer schema untouched and splice in one extra model; there's no supported way to merge a builder-built Contract into a separately-parsed one, and prismaContract's extension-pack options (composedExtensionPackRefs) are for registering new PSL vocabulary, not injecting a plain model — using them here would be more machinery than the temp-file workaround it'd replace. The temp-file approach (above) is the only thing that currently works.

Scope and impact

  • Touches contract-psl in both the SQL and Mongo family packages (near-identical implementations).
  • Additive only — no breaking change to the existing signature.
  • No target-specific (Postgres/SQLite/Mongo) implications.

Happy to open a PR if this is worth landing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions