@prisma-next/sql-contract-ts
Status: Current SQL TypeScript contract authoring surface
This package owns the SQL TypeScript authoring API for Prisma Next.
Package Classification
- Domain: sql
- Layer: authoring
- Plane: migration
Overview
This package is part of the SQL family namespace (packages/2-sql/2-authoring/contract-ts) and provides:
- the SQL contract DSL centered on
defineContract(...)
- the base structural helpers exported from
./contract-builder: field.column(...), field.generated(...), field.namedType(...), plus model(...) and rel.*
- an optional callback overload that exposes pack-composed helpers — namespaced helpers like
field.id.uuidv7(), field.text(), field.temporal.createdAt(), field.temporal.updatedAt(), plus pack-contributed entity-type helpers at top level alongside the built-in model / rel (e.g. enum({ name, values }))
- lowering from authored model definitions into the canonical SQL
Contract
Responsibilities
- SQL contract authoring: Build SQL contracts programmatically with type safety
- Pack-composed helper vocabulary: Merge family, target, and extension authoring contributions into the callback helper namespaces
- Lowering pipeline: Turn authored model definitions into the canonical SQL contract artifacts consumed by the rest of the stack
- Config helper: Provide
typescriptContract(...) for prisma-next.config.ts
- Schema validation: Contract JSON validation flows through the per-target descriptor SPI (
descriptor.contractSerializer.deserializeContract(json))
Package Status
This is the current SQL TypeScript authoring implementation. Shared descriptor types live in @prisma-next/contract-authoring. Contract validation flows through the per-target descriptor's contractSerializer SPI (e.g. postgresTarget.contractSerializer.deserializeContract), or via the canonical façade (postgres<Contract>(...)).
Architecture
- Base DSL:
./contract-builder exports the stable structural DSL (defineContract, field, model, rel)
- Composed helper namespaces:
defineContract(config, (helpers) => ...) synthesizes helpers.field.* and helpers.type.* from the selected family, target, and extension packs
- SQL resolution and contract generation: internal resolution normalizes names, relations, indexes, and FK materialization before producing the canonical SQL contract artifacts
- Shared descriptor layer:
@prisma-next/contract-authoring provides the target-neutral descriptor types used by the DSL and by authoring-adjacent packs
Contributor-facing lowering notes and detailed warning semantics live in DEVELOPING.md.
flowchart LR
builderInput[TypeScript contract input] --> sqlContractTs[@prisma-next/sql-contract-ts]
sqlContractTs --> authoringCore[@prisma-next/contract-authoring]
sqlContractTs --> sqlTypes[@prisma-next/sql-contract/types]
sqlContractTs --> contract[SQL Contract]
Exports
./contract-builder - SQL contract DSL (defineContract, field, model, rel)
./config-types - typescriptContract(...) config helper
Usage
Direct Structural DSL
Direct imports expose the base structural helpers. Use this surface when you want to author with explicit column descriptors, explicit generators, or named storage types.
Built-in ID helpers from @prisma-next/ids already return the generated-field spec accepted by field.generated(...), so field.generated(uuidv4()) is a valid structural DSL call.
import { textColumn, timestamptzColumn } from '@prisma-next/adapter-postgres/column-types';
import sqlFamily from '@prisma-next/family-sql/pack';
import { uuidv4 } from '@prisma-next/ids';
import { defineContract, field, model, rel } from '@prisma-next/sql-contract-ts/contract-builder';
import postgresPack from '@prisma-next/target-postgres/pack';
const User = model('User', {
fields: {
id: field.generated(uuidv4()).id(),
email: field.column(textColumn).unique(),
createdAt: field.column(timestamptzColumn).defaultSql('now()'),
},
})
.relations({
posts: rel.hasMany('Post', { by: 'userId' }),
})
.sql({
table: 'app_user',
});
const Post = model('Post', {
fields: {
id: field.generated(uuidv4()).id(),
userId: field.column(textColumn),
title: field.column(textColumn),
},
})
.relations({
user: rel.belongsTo(User, { from: 'userId', to: 'id' }),
})
.sql({
table: 'blog_post',
});
export const contract = defineContract({
family: sqlFamily,
target: postgresPack,
naming: { tables: 'snake_case', columns: 'snake_case' },
models: {
User,
Post,
},
});
Callback Helper Vocabulary
Pack-provided helper presets are available through the callback overload. This is the surface that exposes field.id.*, field.text(), field.temporal.createdAt(), field.temporal.updatedAt(), type.sql.String(...), and extension helpers such as type.pgvector.Vector(...).
import pgvector from '@prisma-next/extension-pgvector/pack';
import sqlFamily from '@prisma-next/family-sql/pack';
import { defineContract } from '@prisma-next/sql-contract-ts/contract-builder';
import postgresPack from '@prisma-next/target-postgres/pack';
export const contract = defineContract(
{
family: sqlFamily,
target: postgresPack,
extensionPacks: { pgvector },
},
({ type, enum: enumEntity, field, model, rel }) => {
const types = {
ShortName: type.sql.String(35),
Role: enumEntity({ name: 'role', values: ['USER', 'ADMIN'] as const }),
Embedding1536: type.pgvector.Vector(1536),
} as const;
const User = model('User', {
fields: {
id: field.id.uuidv7().sql({ id: { name: 'user_pkey' } }),
shortName: field.namedType(types.ShortName),
role: field.namedType(types.Role),
embedding: field.namedType(types.Embedding1536).optional(),
createdAt: field.temporal.createdAt(),
updatedAt: field.temporal.updatedAt(),
},
});
const Post = model('Post', {
fields: {
id: field.id.uuidv7(),
authorId: field.uuid(),
title: field.text(),
},
});
return {
types,
models: {
User: User.relations({
posts: rel.hasMany(Post, { by: 'authorId' }),
}).sql({
table: 'user',
}),
Post: Post.relations({
author: rel.belongsTo(User, { from: 'authorId', to: 'id' }),
}).sql({
table: 'post',
}),
},
};
},
);
Constraint Placement
Single-field constraints are usually most readable inline on the field, while compound constraints live in .attributes(...) or model-level .sql(...).
const Membership = model('Membership', {
fields: {
orgId: field.column(textColumn).column('org_id'),
userId: field.column(textColumn).column('user_id'),
role: field.column(textColumn),
},
})
.attributes(({ fields, constraints }) => ({
id: constraints.id([fields.orgId, fields.userId], { name: 'membership_pkey' }),
uniques: [
constraints.unique([fields.orgId, fields.role], {
name: 'membership_org_role_key',
}),
],
}))
.sql({ table: 'membership' });
Helper Notes
- Structural helpers:
field.column(...), field.generated(...), field.namedType(...), plus model(...) and rel.*
- Callback helper presets:
field.id.uuidv4(), field.id.uuidv7(), field.id.nanoid({ size }), field.uuid(), field.text(), field.timestamp(), field.temporal.createdAt(), field.temporal.updatedAt(), and type.*
- Timestamp helpers mirror PSL semantics:
field.temporal.createdAt() lowers to a target storage now() default, while field.temporal.updatedAt() lowers to the target-owned timestampNow execution default for create and non-empty update mutations.
- Keep field-local and FK-local storage overrides next to the authoring site with
field.sql(...) and rel.belongsTo(...).sql({ fk })
- Prefer typed local refs such as
field.namedType(types.Role), User.refs.id, and User.ref('id') when those tokens are available
- See API.md for generated-field spec semantics, validation rules, and typed-reference warning behavior
Foreign Key Defaults
Use the root-level foreignKeyDefaults option when a contract wants non-default FK materialization:
const contract = defineContract({
family: sqlFamily,
target: postgresPack,
foreignKeyDefaults: { constraint: true, index: false },
models: {
},
});
Per-FK overrides still live next to the FK authoring site, either via constraints.foreignKey(...) inside model .sql(...) or via rel.belongsTo(...).sql({ fk: ... }). See ADR 161.
Validating Contracts
Contract JSON validation flows through the per-target descriptor's
contractSerializer SPI; this package focuses on authoring and lowering.
End-user app code typically goes through the canonical façade
(postgres<Contract>(...)), which threads the descriptor SPI internally.
import postgres from '@prisma-next/postgres/runtime';
import contractJson from './contract.json' with { type: 'json' };
import type { Contract, TypeMaps } from './contract.d';
const db = postgres<Contract, TypeMaps>({
contractJson,
url: process.env['DATABASE_URL']!,
});
For advanced consumers that need direct access to the SPI:
import postgresTarget from '@prisma-next/target-postgres/control';
import type { Contract } from './contract.d';
const contract = postgresTarget.contractSerializer.deserializeContract(
contractJson,
) as Contract;
Config Helper
Use typescriptContract from this package when wiring TS-authored contracts in prisma-next.config.ts.
import { defineConfig } from '@prisma-next/cli/config-types';
import { typescriptContract } from '@prisma-next/sql-contract-ts/config-types';
import { contract } from './src/prisma/contract';
export default defineConfig({
contract: typescriptContract(contract, 'src/prisma/contract.json'),
});
Optional third argument (options bag) for typescriptContract / typescriptContractFromPath, and defaultControlPolicy on emptyContract:
typescriptContract(contract, 'src/prisma/contract.json', { defaultControlPolicy: 'external' });
The specifier value applies only when the loaded contract omits defaultControlPolicy (a value authored on the contract module wins).
Dependencies
@prisma-next/config - ContractConfig types used by typescriptContract(...)
@prisma-next/contract-authoring - Shared descriptor types
@prisma-next/framework-components - Pack refs, authoring contributions, and codec lookup types
@prisma-next/sql-contract - SQL contract types and validation target
Testing
Unit tests for the authoring DSL live in this package. Broader integration tests that span authoring, emission, CLI, and runtime packages live in @prisma-next/integration-tests.
Migration Notes
- Direct imports give you the structural DSL
- The callback overload gives you pack-composed helper vocabularies
- Import authoring helpers directly from
@prisma-next/sql-contract-ts
- Reach contract validation through the per-target descriptor's
contractSerializer.deserializeContract(...) (or via the canonical façade)
See Also
@prisma-next/contract-authoring - Shared target-neutral authoring descriptor types
@prisma-next/sql-contract-psl - PSL parser-output to SQL contract interpreter
@prisma-next/sql-contract-psl/provider - SQL PSL-first prismaContract() helper