1import type {
2 Contract,
3 ContractField,
4 ContractModel,
5} from "@prisma/orm-postgres/contract/types";
6import type { SqlStorage } from "@prisma/orm-postgres/family-contract/types";
7import type { DefaultModelRow } from "@prisma/orm-postgres/orm-client";
8import * as Data from "effect/Data";
9import * as Schema from "effect/Schema";
10
11/** An unsupported contract field requires an explicit codec mapping. */
12export class SchemaError extends Data.TaggedError("Prisma.SchemaError")<{
13 readonly field: string;
14 readonly message: string;
15}> {}
16
17/** A custom codec's row validator and its standalone TypeScript expression. */
18export interface CodecSchema {
19 readonly schema: Schema.Codec<unknown, unknown>;
20 /** Expression using `Schema` or names declared by `imports`. */
21 readonly expression: string;
22 /** Import declarations copied into standalone schema modules. */
23 readonly imports?: readonly string[];
24}
25
26export interface SchemaOptions {
27 readonly codecs?: Readonly<Record<string, CodecSchema>>;
28}
29
30export type ModelSchemas<C extends Contract<SqlStorage>> = {
31 readonly [Ns in keyof C["domain"]["namespaces"] & string]: {
32 readonly [
33 M in keyof C["domain"]["namespaces"][Ns]["models"] & string
34 ]: Schema.Codec<DefaultModelRow<C, M, Ns>, unknown>;
35 };
36};
37
38const builtin = (
39 schema: Schema.Codec<unknown, unknown>,
40 expression: string,
41): CodecSchema => ({ schema, expression });
42const text = builtin(Schema.String, "Schema.String");
43const int = builtin(Schema.Int, "Schema.Int");
44const float = builtin(Schema.Number, "Schema.Number");
45const bigint = builtin(Schema.BigInt, "Schema.BigInt");
46const codecs: Readonly<Record<string, CodecSchema>> = {
47 "pg/text@1": text,
48 "sql/text@1": text,
49 "pg/date-string@1": text,
50 "pg/timestamp-string@1": text,
51 "pg/timestamptz-string@1": text,
52 "pg/time-string@1": text,
53 "pg/char@1": text,
54 "pg/varchar@1": text,
55 "sql/char@1": text,
56 "sql/varchar@1": text,
57 "pg/inet@1": text,
58 "pg/interval@1": text,
59 "pg/timetz@1": text,
60 "pg/bit@1": text,
61 "pg/varbit@1": text,
62 "pg/int@1": int,
63 "sql/int@1": int,
64 "pg/int4@1": int,
65 "pg/int2@1": int,
66 "pg/int8number@1": int,
67 "pg/int8@1": bigint,
68 "pg/unboundedint@1": bigint,
69 "pg/float@1": float,
70 "sql/float@1": float,
71 "pg/float4@1": float,
72 "pg/float8@1": float,
73 "pg/bool@1": builtin(Schema.Boolean, "Schema.Boolean"),
74 "pg/bytea@1": builtin(Schema.Uint8Array, "Schema.Uint8Array"),
75 "pg/uuid@1": builtin(
76 Schema.String.check(Schema.isGUID()),
77 "Schema.String.check(Schema.isGUID())",
78 ),
79 "pg/numeric@1": builtin(
80 Schema.String.check(
81 Schema.isPattern(
82 /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$|^(?:NaN|Infinity|-Infinity)$/,
83 ),
84 ),
85 "Schema.String.check(Schema.isPattern(/^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$|^(?:NaN|Infinity|-Infinity)$/))",
86 ),
87 "pg/json@1": builtin(Schema.Json, "Schema.Json"),
88 "pg/jsonb@1": builtin(Schema.Json, "Schema.Json"),
89};
90
91function fieldSchema(
92 contract: Contract<SqlStorage>,
93 field: ContractField,
94 name: string,
95 options: SchemaOptions,
96): CodecSchema {
97 const fail = (message: string): never => {
98 throw new SchemaError({ field: name, message: `${name}: ${message}` });
99 };
100 if (field.type.kind !== "scalar")
101 return fail(
102 `unsupported ${field.type.kind} field; use an explicit application schema`,
103 );
104 const codec = field.type.codecId;
105 let value: CodecSchema | undefined =
106 options.codecs && Object.hasOwn(options.codecs, codec)
107 ? options.codecs[codec]
108 : undefined;
109 if (!value && field.valueSet) {
110 const ref = field.valueSet;
111 if (ref.spaceId || ref.entityKind !== "enum")
112 return fail("external value sets require an explicit codec mapping");
113 const enumeration =
114 contract.domain.namespaces[ref.namespaceId]?.enum?.[ref.entityName];
115 if (!enumeration)
116 return fail(`enum ${ref.namespaceId}.${ref.entityName} is missing`);
117 const literals = enumeration.members.map((member) => member.value);
118 if (
119 !literals.every(
120 (literal): literal is string | number | boolean =>
121 typeof literal === "string" ||
122 typeof literal === "number" ||
123 typeof literal === "boolean",
124 )
125 ) {
126 return fail(
127 "non-primitive enum values require an explicit codec mapping",
128 );
129 }
130 value = builtin(
131 Schema.Literals(literals),
132 `Schema.Literals(${JSON.stringify(literals)})`,
133 );
134 }
135 value ??= Object.hasOwn(codecs, codec) ? codecs[codec] : undefined;
136 if (!value)
137 return fail(
138 `unsupported codec ${codec}; supply options.codecs[${JSON.stringify(codec)}] with a schema and standalone expression`,
139 );
140 let { schema, expression } = value;
141 if (field.many) {
142 schema = Schema.Array(schema);
143 expression = `Schema.Array(${expression})`;
144 }
145 if (field.dict) {
146 schema = Schema.Record(Schema.String, schema);
147 expression = `Schema.Record(Schema.String, ${expression})`;
148 }
149 if (field.nullable) {
150 schema = Schema.NullOr(schema);
151 expression = `Schema.NullOr(${expression})`;
152 }
153 return { ...value, schema, expression };
154}
155
156function rowFields(model: ContractModel, name: string) {
157 if (model.base || model.variants || model.discriminator) {
158 throw new SchemaError({
159 field: name,
160 message: `${name}: variant and inherited models require an explicit application schema`,
161 });
162 }
163 return model.fields;
164}
165
166/**
167 * Derive scalar row validators without opening a database connection.
168 * Relations and mutation defaults are not row fields. These are database row
169 * schemas, not create/update inputs or public API response definitions.
170 */
171export function makeSchemas<C extends Contract<SqlStorage>>(
172 contract: C,
173 options: SchemaOptions = {},
174): ModelSchemas<C> {
175 return Object.fromEntries(
176 Object.entries(contract.domain.namespaces).map(([ns, namespace]) => [
177 ns,
178 Object.fromEntries(
179 Object.entries(namespace.models).map(([model, definition]) => [
180 model,
181 Schema.Struct(
182 Object.fromEntries(
183 Object.entries(rowFields(definition, `${ns}.${model}`)).map(
184 ([name, field]) => [
185 name,
186 fieldSchema(
187 contract,
188 field,
189 `${ns}.${model}.${name}`,
190 options,
191 ).schema,
192 ],
193 ),
194 ),
195 ),
196 ]),
197 ),
198 ]),
199 ) as unknown as ModelSchemas<C>;
200}
201
202const propertyKey = (name: string) =>
203 name === "__proto__" ? '["__proto__"]' : JSON.stringify(name);
204
205/** Emit a standalone module importing only Effect and explicit custom mappings. */
206export function emitSchemas(
207 contract: Contract<SqlStorage>,
208 options: SchemaOptions = {},
209): string {
210 const imports = new Set<string>();
211 const namespaces = Object.entries(contract.domain.namespaces)
212 .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
213 .map(([ns, namespace]) => {
214 const models = Object.entries(namespace.models)
215 .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
216 .map(([model, definition]) => {
217 const fields = Object.entries(rowFields(definition, `${ns}.${model}`))
218 .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
219 .map(([name, field]) => {
220 const mapped = fieldSchema(
221 contract,
222 field,
223 `${ns}.${model}.${name}`,
224 options,
225 );
226 for (const statement of mapped.imports ?? [])
227 imports.add(statement);
228 return ` ${propertyKey(name)}: ${mapped.expression},`;
229 });
230 return ` ${propertyKey(model)}: Schema.Struct({\n${fields.join("\n")}\n }),`;
231 });
232 return ` ${propertyKey(ns)}: {\n${models.join("\n")}\n },`;
233 });
234 return `// Generated by alchemy prisma generate.\nimport * as Schema from "effect/Schema";\n${[...imports].sort().join("\n")}\nexport const schemas = {\n${namespaces.join("\n")}\n} as const;\n`;
235}