@effectify/prisma 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +33 -38
  2. package/dist/src/cli.d.ts +1 -1
  3. package/dist/src/cli.js +15 -14
  4. package/dist/src/commands/init.d.ts +1 -1
  5. package/dist/src/commands/init.js +36 -47
  6. package/dist/src/commands/prisma.d.ts +4 -4
  7. package/dist/src/commands/prisma.js +12 -12
  8. package/dist/src/generators/sql-schema-generator.js +15 -16
  9. package/dist/src/runtime/index.d.ts +303 -0
  10. package/dist/src/runtime/index.js +216 -0
  11. package/dist/src/schema-generator/effect/enum.d.ts +12 -0
  12. package/dist/src/schema-generator/effect/enum.js +18 -0
  13. package/dist/src/schema-generator/effect/generator.d.ts +16 -0
  14. package/dist/src/schema-generator/effect/generator.js +42 -0
  15. package/dist/src/schema-generator/effect/join-table.d.ts +12 -0
  16. package/dist/src/schema-generator/effect/join-table.js +28 -0
  17. package/dist/src/schema-generator/effect/type.d.ts +18 -0
  18. package/dist/src/schema-generator/effect/type.js +82 -0
  19. package/dist/src/schema-generator/index.d.ts +11 -0
  20. package/dist/src/schema-generator/index.js +83 -0
  21. package/dist/src/schema-generator/kysely/generator.d.ts +11 -0
  22. package/dist/src/schema-generator/kysely/generator.js +7 -0
  23. package/dist/src/schema-generator/kysely/type.d.ts +14 -0
  24. package/dist/src/schema-generator/kysely/type.js +44 -0
  25. package/dist/src/schema-generator/prisma/enum.d.ts +19 -0
  26. package/dist/src/schema-generator/prisma/enum.js +19 -0
  27. package/dist/src/schema-generator/prisma/generator.d.ts +53 -0
  28. package/dist/src/schema-generator/prisma/generator.js +29 -0
  29. package/dist/src/schema-generator/prisma/relation.d.ts +83 -0
  30. package/dist/src/schema-generator/prisma/relation.js +165 -0
  31. package/dist/src/schema-generator/prisma/type.d.ts +108 -0
  32. package/dist/src/schema-generator/prisma/type.js +85 -0
  33. package/dist/src/schema-generator/utils/annotations.d.ts +32 -0
  34. package/dist/src/schema-generator/utils/annotations.js +79 -0
  35. package/dist/src/schema-generator/utils/codegen.d.ts +9 -0
  36. package/dist/src/schema-generator/utils/codegen.js +14 -0
  37. package/dist/src/schema-generator/utils/naming.d.ts +29 -0
  38. package/dist/src/schema-generator/utils/naming.js +68 -0
  39. package/dist/src/schema-generator/utils/type-mappings.d.ts +62 -0
  40. package/dist/src/schema-generator/utils/type-mappings.js +70 -0
  41. package/dist/src/services/formatter-service.d.ts +10 -0
  42. package/dist/src/services/formatter-service.js +18 -0
  43. package/dist/src/services/generator-context.d.ts +2 -2
  44. package/dist/src/services/generator-context.js +2 -2
  45. package/dist/src/services/generator-service.d.ts +9 -8
  46. package/dist/src/services/generator-service.js +39 -43
  47. package/dist/src/services/render-service.d.ts +3 -3
  48. package/dist/src/services/render-service.js +8 -8
  49. package/dist/src/templates/effect-branded-id.eta +2 -0
  50. package/dist/src/templates/effect-enums.eta +9 -0
  51. package/dist/src/templates/effect-index.eta +4 -0
  52. package/dist/src/templates/effect-join-table.eta +8 -0
  53. package/dist/src/templates/effect-model.eta +6 -0
  54. package/dist/src/templates/effect-types-header.eta +7 -0
  55. package/dist/src/templates/index-default.eta +2 -1
  56. package/dist/src/templates/kysely-db-interface.eta +6 -0
  57. package/dist/src/templates/prisma-repository.eta +57 -32
  58. package/package.json +11 -6
@@ -0,0 +1,303 @@
1
+ import { Schema } from "effect";
2
+ import type { ColumnType as KyselyColumnType, Generated as KyselyGenerated, Insertable as KyselyInsertable, Selectable as KyselySelectable, Updateable as KyselyUpdateable } from "kysely";
3
+ /**
4
+ * Runtime helpers for Kysely schema integration
5
+ * These are imported by generated code
6
+ *
7
+ * ## Type Extraction Patterns
8
+ *
9
+ * For Effect Schemas (recommended - full type safety):
10
+ * ```typescript
11
+ * import { Selectable, Insertable, Updateable } from '@effectify/prisma';
12
+ * import { User } from './generated/types';
13
+ *
14
+ * type UserSelect = Selectable<User>;
15
+ * type UserInsert = Insertable<User>;
16
+ * type UserUpdate = Updateable<User>;
17
+ * ```
18
+ *
19
+ * Note: This package exports branded versions of ColumnType and Generated that
20
+ * are compatible with Effect Schema's type inference. These extend the base
21
+ * select type (S) while carrying phantom insert/update type information.
22
+ */
23
+ export type { KyselyColumnType, KyselyGenerated, KyselyInsertable, KyselySelectable, KyselyUpdateable };
24
+ export declare const ColumnTypeId: unique symbol;
25
+ export declare const GeneratedId: unique symbol;
26
+ /**
27
+ * Symbol for VariantMarker - used in mapped type pattern that survives declaration emit.
28
+ */
29
+ export declare const VariantTypeId: unique symbol;
30
+ export type VariantTypeId = typeof VariantTypeId;
31
+ /**
32
+ * Variant marker using mapped type pattern from Effect's Brand.
33
+ *
34
+ * TypeScript cannot simplify mapped types that depend on generic parameters.
35
+ * This ensures the variant information survives declaration emit (.d.ts generation).
36
+ *
37
+ * Pattern derived from Effect's Brand<K>:
38
+ * ```typescript
39
+ * readonly [BrandTypeId]: { readonly [k in K]: K } // Mapped type - cannot be simplified!
40
+ * ```
41
+ *
42
+ * Our pattern uses a conditional type within the mapped type to encode both I and U:
43
+ * ```typescript
44
+ * readonly [VariantTypeId]: { readonly [K in "insert" | "update"]: K extends "insert" ? I : U }
45
+ * ```
46
+ */
47
+ export interface VariantMarker<in out I, in out U> {
48
+ readonly [VariantTypeId]: {
49
+ readonly [K in "insert" | "update"]: K extends "insert" ? I : U;
50
+ };
51
+ }
52
+ /**
53
+ * Branded ColumnType that extends S while carrying phantom insert/update type information.
54
+ *
55
+ * This replaces Kysely's ColumnType because:
56
+ * 1. Kysely's ColumnType<S,I,U> = { __select__: S, __insert__: I, __update__: U } is NOT a subtype of S
57
+ * 2. Schema.make<KyselyColumnType<...>>(ast) doesn't work because AST represents S, not the struct
58
+ * 3. Our ColumnType<S,I,U> = S & Brand IS a subtype of S, so Schema.make works correctly
59
+ *
60
+ * Includes Kysely's phantom properties (__select__, __insert__, __update__) so that:
61
+ * 1. Kysely recognizes this as a ColumnType for INSERT/UPDATE operations
62
+ * 2. WHERE clauses work with plain S values (not branded)
63
+ *
64
+ * Uses VariantMarker with mapped types to survive TypeScript declaration emit.
65
+ *
66
+ * Usage is identical to Kysely's ColumnType:
67
+ * ```typescript
68
+ * type IdField = ColumnType<string, never, never>; // Read-only ID
69
+ * type CreatedAt = ColumnType<Date, Date | undefined, Date>; // Optional on insert
70
+ * ```
71
+ */
72
+ export type ColumnType<S, I = S, U = S> = S & VariantMarker<I, U> & {
73
+ /** Kysely extracts this type for SELECT and WHERE */
74
+ readonly __select__: S;
75
+ /** Kysely uses this for INSERT */
76
+ readonly __insert__: I;
77
+ /** Kysely uses this for UPDATE */
78
+ readonly __update__: U;
79
+ };
80
+ /**
81
+ * Base Generated brand without Kysely phantom properties.
82
+ * Used as the __select__ return type to preserve branding on SELECT.
83
+ *
84
+ * Uses VariantMarker<T | undefined, T> so that Generated fields are:
85
+ * - Optional on insert (T | undefined) - can be provided or omitted
86
+ * - Required on update (T) - must provide value if updating
87
+ *
88
+ * This differs from ColumnType<S, never, never> which completely excludes
89
+ * the field from insert (used for auto-generated IDs).
90
+ */
91
+ type GeneratedBrand<T> = T & VariantMarker<T | undefined, T> & {
92
+ readonly [GeneratedId]: true;
93
+ };
94
+ /**
95
+ * Branded Generated type for database-generated fields.
96
+ *
97
+ * Follows @effect/sql Model.Generated pattern - the field is:
98
+ * - Required on select (T) - Kysely returns the base type
99
+ * - Optional on insert (T | undefined) - Kysely recognizes this
100
+ * - Allowed on update (T)
101
+ *
102
+ * Includes Kysely's phantom properties (__select__, __insert__, __update__) so that:
103
+ * 1. Kysely recognizes this as a ColumnType and makes it optional on INSERT
104
+ * 2. WHERE clauses work with plain T values (not branded)
105
+ *
106
+ * The Selectable<T> type utility preserves the full Generated<T> type for schema alignment.
107
+ * Kysely operations work with the underlying T type.
108
+ *
109
+ * Uses VariantMarker with mapped types to survive TypeScript declaration emit.
110
+ */
111
+ export type Generated<T> = GeneratedBrand<T> & {
112
+ /** Kysely extracts this type for SELECT and WHERE - base type for compatibility */
113
+ readonly __select__: T;
114
+ /** Kysely uses this for INSERT - optional */
115
+ readonly __insert__: T | undefined;
116
+ /** Kysely uses this for UPDATE */
117
+ readonly __update__: T;
118
+ };
119
+ /**
120
+ * Interface for ColumnType schema - preserves type parameters in declaration emit.
121
+ *
122
+ * Named interfaces with type parameters are preserved by TypeScript in declaration files,
123
+ * unlike anonymous intersection types which may be simplified.
124
+ *
125
+ * This follows the Schema.brand pattern from Effect which returns a named interface.
126
+ */
127
+ export interface ColumnTypeSchema<S extends Schema.Schema.All, IType, UType> extends Schema.Schema<ColumnType<Schema.Schema.Type<S>, IType, UType>, ColumnType<Schema.Schema.Encoded<S>, IType, UType>, Schema.Schema.Context<S>> {
128
+ /** The original select schema */
129
+ readonly selectSchema: S;
130
+ }
131
+ /**
132
+ * Mark a field as having different types for select/insert/update
133
+ * Used for ID fields with @default (read-only)
134
+ *
135
+ * The insert/update schemas are stored in annotations and used at runtime
136
+ * to determine which fields to include in Insertable/Updateable schemas.
137
+ *
138
+ * Returns a ColumnTypeSchema which:
139
+ * 1. Is a named interface (preserved in declaration emit)
140
+ * 2. Contains the ColumnType<S, I, U> brand with Kysely phantom properties
141
+ * 3. Includes the original schema via `selectSchema` property
142
+ *
143
+ * This enables Kysely to recognize fields with `__insert__: never` and omit them from INSERT.
144
+ */
145
+ export declare const columnType: <SType, SEncoded, SR, IType, IEncoded, IR, UType, UEncoded, UR>(selectSchema: Schema.Schema<SType, SEncoded, SR>, insertSchema: Schema.Schema<IType, IEncoded, IR>, updateSchema: Schema.Schema<UType, UEncoded, UR>) => ColumnTypeSchema<Schema.Schema<SType, SEncoded, SR>, IType, UType>;
146
+ /**
147
+ * Interface for Generated schema - preserves type parameter in declaration emit.
148
+ *
149
+ * Named interfaces with type parameters are preserved by TypeScript in declaration files,
150
+ * unlike anonymous intersection types which may be simplified.
151
+ *
152
+ * This follows the Schema.brand pattern from Effect which returns a named interface.
153
+ */
154
+ export interface GeneratedSchema<S extends Schema.Schema.All> extends Schema.Schema<Generated<Schema.Schema.Type<S>>, Generated<Schema.Schema.Encoded<S>>, Schema.Schema.Context<S>> {
155
+ /** The original schema before Generated wrapper */
156
+ readonly from: S;
157
+ }
158
+ /**
159
+ * Mark a field as database-generated (omitted from insert)
160
+ * Used for fields with @default
161
+ *
162
+ * Follows @effect/sql Model.Generated pattern:
163
+ * - Present in select and update schemas
164
+ * - OMITTED from insert schema (not optional, completely absent)
165
+ *
166
+ * Returns a GeneratedSchema<S> which:
167
+ * 1. Is a named interface (preserved in declaration emit)
168
+ * 2. Contains the Generated<T> brand using VariantMarker (mapped types survive emit)
169
+ * 3. Includes the original schema via `from` property
170
+ *
171
+ * This enables CustomInsertable to filter out generated fields at compile time.
172
+ */
173
+ export declare const generated: <S extends Schema.Schema.All>(schema: S) => GeneratedSchema<S>;
174
+ /**
175
+ * Extract the insert type from a field using the __insert__ phantom property:
176
+ * - ColumnType<S, I, U> -> I (via __insert__)
177
+ * - Generated<T> -> T | undefined (via __insert__)
178
+ * - Other types -> as-is
179
+ *
180
+ * Uses the __insert__ property which is always present on ColumnType and Generated.
181
+ * This approach is more reliable across module boundaries than using VariantMarker
182
+ * with unique symbols, which can cause type matching failures when TypeScript
183
+ * compiles from source files with different symbol references.
184
+ */
185
+ type ExtractInsertType<T> = T extends {
186
+ readonly __insert__: infer I;
187
+ } ? I : T extends {
188
+ [VariantTypeId]: {
189
+ insert: infer I;
190
+ };
191
+ } ? I : T;
192
+ /**
193
+ * Check if a type is nullable (includes null or undefined).
194
+ * Matches Kysely's IfNullable behavior:
195
+ * type IfNullable<T, K> = undefined extends T ? K : null extends T ? K : never;
196
+ *
197
+ * A field is optional for insert if its InsertType can be null or undefined.
198
+ */
199
+ type IsOptionalInsert<T> = undefined extends ExtractInsertType<T> ? true : null extends ExtractInsertType<T> ? true : false;
200
+ /**
201
+ * Extract the base type without null/undefined for optional fields.
202
+ * Keeps the type as-is (including null) for the property type,
203
+ * since the optionality is expressed via `?` not the type itself.
204
+ */
205
+ type ExtractInsertBaseType<T> = ExtractInsertType<T>;
206
+ /**
207
+ * Extract the update type from a field using the __update__ phantom property:
208
+ * - ColumnType<S, I, U> -> U (via __update__)
209
+ * - Generated<T> -> T (via __update__)
210
+ * - Other types -> as-is
211
+ *
212
+ * Uses the __update__ property which is always present on ColumnType and Generated.
213
+ * This approach is more reliable across module boundaries than using VariantMarker
214
+ * with unique symbols, which can cause type matching failures when TypeScript
215
+ * compiles from source files with different symbol references.
216
+ */
217
+ type ExtractUpdateType<T> = T extends {
218
+ readonly __update__: infer U;
219
+ } ? U : T extends {
220
+ [VariantTypeId]: {
221
+ update: infer U;
222
+ };
223
+ } ? U : T;
224
+ /**
225
+ * Custom Insertable type that:
226
+ * - Omits fields with `never` insert type (read-only IDs)
227
+ * - Makes fields with `T | undefined` insert type optional with type T
228
+ * - Keeps other fields required
229
+ */
230
+ type CustomInsertable<T> = {
231
+ [K in keyof T as ExtractInsertType<T[K]> extends never ? never : IsOptionalInsert<T[K]> extends true ? never : K]: ExtractInsertType<T[K]>;
232
+ } & {
233
+ [K in keyof T as ExtractInsertType<T[K]> extends never ? never : IsOptionalInsert<T[K]> extends true ? K : never]?: ExtractInsertBaseType<T[K]>;
234
+ };
235
+ /**
236
+ * Custom Updateable type that properly omits fields with `never` update types.
237
+ */
238
+ type CustomUpdateable<T> = {
239
+ [K in keyof T as ExtractUpdateType<T[K]> extends never ? never : K]?: ExtractUpdateType<T[K]>;
240
+ };
241
+ type MutableInsert<Type> = CustomInsertable<Type>;
242
+ /**
243
+ * Strip Generated<T> wrapper, returning the underlying type T.
244
+ * For non-Generated types, returns as-is.
245
+ * Preserves branded foreign keys (UserId, ProductId, etc.).
246
+ */
247
+ type StripGeneratedWrapper<T> = T extends GeneratedBrand<infer U> ? U : T;
248
+ /**
249
+ * Strip ColumnType wrapper, extracting the select type S.
250
+ * Must check AFTER Generated because Generated<T> also has __select__.
251
+ * Uses __insert__ existence to differentiate ColumnType from other types.
252
+ */
253
+ type StripColumnTypeWrapper<T> = T extends {
254
+ readonly __select__: infer S;
255
+ readonly __insert__: unknown;
256
+ } ? S : T;
257
+ /**
258
+ * Strip all Kysely wrappers (Generated, ColumnType) from a field type.
259
+ * Order matters: check Generated first, then ColumnType.
260
+ * Preserves branded foreign keys (UserId, ProductId, etc.).
261
+ */
262
+ type StripKyselyWrapper<T> = StripColumnTypeWrapper<StripGeneratedWrapper<T>>;
263
+ /**
264
+ * Strip Kysely wrappers from all fields in a type.
265
+ * Preserves branded foreign keys (UserId, ProductId, etc.).
266
+ */
267
+ type StripKyselyWrappersFromObject<T> = {
268
+ readonly [K in keyof T]: StripKyselyWrapper<T[K]>;
269
+ };
270
+ export declare function Selectable<Type, Encoded>(schema: Schema.Schema<Type, Encoded>): Schema.Schema<StripKyselyWrappersFromObject<Type>, StripKyselyWrappersFromObject<Encoded>, never>;
271
+ /**
272
+ * Create Insertable schema from base schema
273
+ * Generated fields (@default) are made optional, not excluded
274
+ */
275
+ export declare function Insertable<Type, Encoded>(schema: Schema.Schema<Type, Encoded>): Schema.Schema<MutableInsert<Type>, MutableInsert<Encoded>, never>;
276
+ /**
277
+ * Create Updateable schema from base schema
278
+ */
279
+ export declare function Updateable<Type, Encoded>(schema: Schema.Schema<Type, Encoded>): Schema.Schema<CustomUpdateable<Type>, CustomUpdateable<Encoded>, never>;
280
+ /**
281
+ * Extract SELECT type from schema.
282
+ * - Preserves branded foreign keys (UserId, ProductId, etc.)
283
+ * - Strips Generated<T> and ColumnType<S,I,U> wrappers to match what Kysely returns
284
+ *
285
+ * Kysely extracts __select__ for SELECT results.
286
+ * Generated<T>/ColumnType remain in the DB interface for INSERT recognition,
287
+ * but Selectable<T> gives you the clean type matching query results.
288
+ *
289
+ * @example type UserSelect = Selectable<User>;
290
+ */
291
+ export type Selectable<T> = StripKyselyWrappersFromObject<T>;
292
+ /**
293
+ * Extract INSERT type from schema.
294
+ * Omits fields with `never` insert type (read-only IDs, generated fields).
295
+ * @example type UserInsert = Insertable<User>;
296
+ */
297
+ export type Insertable<T> = CustomInsertable<T>;
298
+ /**
299
+ * Extract UPDATE type from schema.
300
+ * Omits fields with `never` update type, makes all fields optional.
301
+ * @example type UserUpdate = Updateable<User>;
302
+ */
303
+ export type Updateable<T> = CustomUpdateable<T>;
@@ -0,0 +1,216 @@
1
+ import { Schema } from "effect";
2
+ import * as AST from "effect/SchemaAST";
3
+ export const ColumnTypeId = Symbol.for("/ColumnTypeId");
4
+ export const GeneratedId = Symbol.for("/GeneratedId");
5
+ /**
6
+ * Symbol for VariantMarker - used in mapped type pattern that survives declaration emit.
7
+ */
8
+ export const VariantTypeId = Symbol.for("@effectify/prisma/VariantType");
9
+ /**
10
+ * Mark a field as having different types for select/insert/update
11
+ * Used for ID fields with @default (read-only)
12
+ *
13
+ * The insert/update schemas are stored in annotations and used at runtime
14
+ * to determine which fields to include in Insertable/Updateable schemas.
15
+ *
16
+ * Returns a ColumnTypeSchema which:
17
+ * 1. Is a named interface (preserved in declaration emit)
18
+ * 2. Contains the ColumnType<S, I, U> brand with Kysely phantom properties
19
+ * 3. Includes the original schema via `selectSchema` property
20
+ *
21
+ * This enables Kysely to recognize fields with `__insert__: never` and omit them from INSERT.
22
+ */
23
+ export const columnType = (selectSchema, insertSchema, updateSchema) => {
24
+ const schemas = {
25
+ selectSchema,
26
+ insertSchema,
27
+ updateSchema,
28
+ };
29
+ // Return annotated schema with ColumnType brand at type level
30
+ // The runtime annotation enables filtering in Insertable() function
31
+ // The type-level brand enables Kysely to recognize INSERT/UPDATE constraints
32
+ const annotated = selectSchema.annotations({ [ColumnTypeId]: schemas });
33
+ return Object.assign(annotated, { selectSchema });
34
+ };
35
+ /**
36
+ * Mark a field as database-generated (omitted from insert)
37
+ * Used for fields with @default
38
+ *
39
+ * Follows @effect/sql Model.Generated pattern:
40
+ * - Present in select and update schemas
41
+ * - OMITTED from insert schema (not optional, completely absent)
42
+ *
43
+ * Returns a GeneratedSchema<S> which:
44
+ * 1. Is a named interface (preserved in declaration emit)
45
+ * 2. Contains the Generated<T> brand using VariantMarker (mapped types survive emit)
46
+ * 3. Includes the original schema via `from` property
47
+ *
48
+ * This enables CustomInsertable to filter out generated fields at compile time.
49
+ */
50
+ export const generated = (schema) => {
51
+ // Return annotated schema with Generated brand at type level
52
+ // The runtime annotation enables filtering in Insertable() function
53
+ // The type-level brand enables filtering in CustomInsertable type utility
54
+ const annotated = schema.annotations({ [GeneratedId]: true });
55
+ return Object.assign(annotated, { from: schema });
56
+ };
57
+ /**
58
+ * Schema for validating column type annotations structure
59
+ */
60
+ const ColumnTypeSchemasValidator = Schema.Struct({
61
+ selectSchema: Schema.Any,
62
+ insertSchema: Schema.Any,
63
+ updateSchema: Schema.Any,
64
+ });
65
+ /**
66
+ * Extract and validate column type schemas from AST annotations
67
+ * Returns null if not a column type or validation fails
68
+ */
69
+ function getColumnTypeSchemas(ast) {
70
+ if (!(ColumnTypeId in ast.annotations)) {
71
+ return null;
72
+ }
73
+ const annotation = ast.annotations[ColumnTypeId];
74
+ const decoded = Schema.decodeUnknownOption(ColumnTypeSchemasValidator)(annotation);
75
+ if (decoded._tag === "None") {
76
+ return null;
77
+ }
78
+ // The decoded value has the correct structure, and the annotation
79
+ // was created by columnType() which ensures proper Schema types
80
+ return annotation;
81
+ }
82
+ const isGeneratedType = (ast) => GeneratedId in ast.annotations;
83
+ const isOptionalType = (ast) => {
84
+ // Check for Union(T, Undefined) or Union(T, null) patterns
85
+ // These are optional on insert because omitting = NULL in DB
86
+ if (!AST.isUnion(ast)) {
87
+ return false;
88
+ }
89
+ return (ast.types.some((t) => AST.isUndefinedKeyword(t)) ||
90
+ ast.types.some((t) => isNullType(t)));
91
+ };
92
+ const isNullType = (ast) => AST.isLiteral(ast) &&
93
+ Object.entries(ast.annotations).find(([sym, value]) => sym === AST.IdentifierAnnotationId.toString() && value === "null");
94
+ /**
95
+ * Strip null from a union type for Insertable fields.
96
+ * For INSERT operations, omitting a field = null in DB, so we don't need explicit null.
97
+ * Returns the non-null type if it's a union with null, otherwise returns the original type.
98
+ */
99
+ const stripNullFromUnion = (ast) => {
100
+ if (!AST.isUnion(ast)) {
101
+ return ast;
102
+ }
103
+ // Filter out null types from the union
104
+ const nonNullTypes = ast.types.filter((t) => !isNullType(t));
105
+ // If only one type remains, return it directly (unwrap single-element union)
106
+ if (nonNullTypes.length === 1) {
107
+ return nonNullTypes[0];
108
+ }
109
+ // If multiple types remain, create a new union without null
110
+ if (nonNullTypes.length > 1) {
111
+ return AST.Union.make(nonNullTypes);
112
+ }
113
+ // Edge case: all types were null (shouldn't happen in practice)
114
+ return ast;
115
+ };
116
+ const extractParametersFromTypeLiteral = (ast, schemaType) => {
117
+ return ast.propertySignatures
118
+ .map((prop) => {
119
+ const columnSchemas = getColumnTypeSchemas(prop.type);
120
+ if (columnSchemas !== null) {
121
+ const targetSchema = columnSchemas[schemaType];
122
+ // Check for Schema.Never BEFORE mutable transformation
123
+ // Schema.mutable() wraps in Transformation node, changing _tag
124
+ if (AST.isNeverKeyword(targetSchema.ast)) {
125
+ return null; // Will be filtered out
126
+ }
127
+ // Use Schema.mutable() for insert/update schema to make arrays mutable
128
+ // Kysely expects mutable T[] for insert/update operations
129
+ const shouldBeMutable = schemaType === "updateSchema" || schemaType === "insertSchema";
130
+ return new AST.PropertySignature(prop.name, shouldBeMutable ? Schema.mutable(targetSchema).ast : targetSchema.ast, prop.isOptional, prop.isReadonly, prop.annotations);
131
+ }
132
+ // Handle Generated fields for Selectable - need to unwrap the base type
133
+ // Generated<T> annotates the schema but we want plain T for select
134
+ if (schemaType === "selectSchema" && isGeneratedType(prop.type)) {
135
+ // Generated fields have the base schema stored in annotations
136
+ // The AST is the annotated version of the base schema, so just strip annotations
137
+ // Get the underlying type by removing the Generated annotation
138
+ const baseAst = AST.annotations(prop.type, {
139
+ ...prop.type.annotations,
140
+ [GeneratedId]: undefined,
141
+ [ColumnTypeId]: undefined,
142
+ });
143
+ return new AST.PropertySignature(prop.name, baseAst, prop.isOptional, prop.isReadonly, prop.annotations);
144
+ }
145
+ // Apply Schema.mutable() to regular fields for insert/updateSchema to make arrays mutable
146
+ // Safe for all types - no-op for non-arrays
147
+ if (schemaType === "updateSchema" || schemaType === "insertSchema") {
148
+ return new AST.PropertySignature(prop.name, Schema.mutable(Schema.asSchema(Schema.make(prop.type))).ast, prop.isOptional, prop.isReadonly, prop.annotations);
149
+ }
150
+ // Regular fields - return as-is
151
+ return prop;
152
+ })
153
+ .filter((prop) => prop !== null);
154
+ };
155
+ // ============================================================================
156
+ // Schema Functions
157
+ // ============================================================================
158
+ export function Selectable(schema) {
159
+ // Strip Generated/ColumnType wrappers to match what Kysely returns from queries
160
+ // Branded foreign keys (UserId, ProductId) are preserved
161
+ const { ast } = schema;
162
+ if (!AST.isTypeLiteral(ast)) {
163
+ // Non-struct schemas: use as identity
164
+ // Internal cast needed because Schema.make(ast) returns unknown types
165
+ // The return type annotation is what TypeScript uses for declaration emit
166
+ return Schema.asSchema(Schema.make(ast));
167
+ }
168
+ // Extract select schemas from annotated fields (strips wrappers at runtime)
169
+ return Schema.asSchema(Schema.make(new AST.TypeLiteral(extractParametersFromTypeLiteral(ast, "selectSchema"), ast.indexSignatures, ast.annotations)));
170
+ }
171
+ /**
172
+ * Create Insertable schema from base schema
173
+ * Generated fields (@default) are made optional, not excluded
174
+ */
175
+ export function Insertable(schema) {
176
+ const { ast } = schema;
177
+ if (!AST.isTypeLiteral(ast)) {
178
+ // Internal cast - return type annotation is what TypeScript uses for declaration emit
179
+ return Schema.asSchema(Schema.make(ast));
180
+ }
181
+ const extracted = extractParametersFromTypeLiteral(ast, "insertSchema");
182
+ const fields = extracted.map((prop) => {
183
+ // Check if this is a Generated field - make it optional
184
+ const isGenerated = isGeneratedType(prop.type);
185
+ // Make Union(T, null) fields optional and strip null from the type
186
+ // For INSERT, omitting a field = null in DB, so explicit null is unnecessary
187
+ const isOptional = isOptionalType(prop.type) || isGenerated;
188
+ // For generated fields, unwrap the base type from the Generated annotation
189
+ let fieldType = prop.type;
190
+ if (isGenerated) {
191
+ // Strip the Generated annotation to get the base type
192
+ fieldType = AST.annotations(prop.type, {
193
+ ...prop.type.annotations,
194
+ [GeneratedId]: undefined,
195
+ });
196
+ }
197
+ else if (isOptionalType(prop.type)) {
198
+ fieldType = stripNullFromUnion(prop.type);
199
+ }
200
+ return new AST.PropertySignature(prop.name, fieldType, isOptional, prop.isReadonly, prop.annotations);
201
+ });
202
+ return Schema.asSchema(Schema.make(new AST.TypeLiteral(fields, ast.indexSignatures, ast.annotations)));
203
+ }
204
+ /**
205
+ * Create Updateable schema from base schema
206
+ */
207
+ export function Updateable(schema) {
208
+ const { ast } = schema;
209
+ if (!AST.isTypeLiteral(ast)) {
210
+ // Internal cast - return type annotation is what TypeScript uses for declaration emit
211
+ return Schema.asSchema(Schema.make(ast));
212
+ }
213
+ const extracted = extractParametersFromTypeLiteral(ast, "updateSchema");
214
+ const res = new AST.TypeLiteral(extracted.map((prop) => new AST.PropertySignature(prop.name, AST.Union.make([prop.type, new AST.UndefinedKeyword()]), true, prop.isReadonly, prop.annotations)), ast.indexSignatures, ast.annotations);
215
+ return Schema.asSchema(Schema.make(res));
216
+ }
@@ -0,0 +1,12 @@
1
+ import type { DMMF } from "@prisma/generator-helper";
2
+ export declare function prepareEnumData(enumDef: DMMF.DatamodelEnum): {
3
+ name: string;
4
+ values: string;
5
+ };
6
+ export declare function prepareEnumsData(enums: readonly DMMF.DatamodelEnum[]): {
7
+ header: string;
8
+ enums: {
9
+ name: string;
10
+ values: string;
11
+ }[];
12
+ } | null;
@@ -0,0 +1,18 @@
1
+ import { getEnumValueDbName } from "../prisma/enum.js";
2
+ import { generateFileHeader } from "../utils/codegen.js";
3
+ import { toPascalCase } from "../utils/naming.js";
4
+ export function prepareEnumData(enumDef) {
5
+ const schemaName = toPascalCase(enumDef.name);
6
+ const values = enumDef.values
7
+ .map((v) => `"${getEnumValueDbName(v)}"`)
8
+ .join(", ");
9
+ return { name: schemaName, values };
10
+ }
11
+ export function prepareEnumsData(enums) {
12
+ if (enums.length === 0) {
13
+ return null;
14
+ }
15
+ const header = generateFileHeader();
16
+ const enumItems = enums.map(prepareEnumData);
17
+ return { header, enums: enumItems };
18
+ }
@@ -0,0 +1,16 @@
1
+ import type { DMMF } from "@prisma/generator-helper";
2
+ export declare const prepareBrandedIdSchemaData: (model: DMMF.Model, fields: readonly DMMF.Field[]) => {
3
+ name: string;
4
+ baseType: string;
5
+ } | null;
6
+ export declare const prepareModelSchemaData: (dmmf: DMMF.Document, model: DMMF.Model, fields: readonly DMMF.Field[]) => {
7
+ name: string;
8
+ fields: {
9
+ name: string;
10
+ type: string;
11
+ }[];
12
+ };
13
+ export declare const prepareTypesHeaderData: (dmmf: DMMF.Document, hasEnums: boolean) => {
14
+ header: string;
15
+ enumImports: string | null;
16
+ };
@@ -0,0 +1,42 @@
1
+ import { buildKyselyFieldType } from "../kysely/type.js";
2
+ import { buildForeignKeyMap } from "../prisma/relation.js";
3
+ import { isUuidField } from "../prisma/type.js";
4
+ import { generateFileHeader } from "../utils/codegen.js";
5
+ import { toPascalCase } from "../utils/naming.js";
6
+ import { buildFieldType } from "./type.js";
7
+ export const prepareBrandedIdSchemaData = (model, fields) => {
8
+ const idField = fields.find((f) => f.isId);
9
+ if (!idField) {
10
+ return null;
11
+ }
12
+ const name = toPascalCase(model.name);
13
+ const isUuid = isUuidField(idField);
14
+ let baseType;
15
+ if (isUuid) {
16
+ baseType = "Schema.UUID";
17
+ }
18
+ else if (idField.type === "Int") {
19
+ baseType = "Schema.Number.pipe(Schema.positive())";
20
+ }
21
+ else {
22
+ baseType = "Schema.String";
23
+ }
24
+ return { name, baseType };
25
+ };
26
+ export const prepareModelSchemaData = (dmmf, model, fields) => {
27
+ const fkMap = buildForeignKeyMap(model, dmmf.datamodel.models);
28
+ const name = toPascalCase(model.name);
29
+ const fieldDefinitions = fields.map((field) => {
30
+ const baseType = buildFieldType(field, dmmf, fkMap);
31
+ const fieldType = buildKyselyFieldType(baseType, field, dmmf, model.name);
32
+ return { name: field.name, type: fieldType };
33
+ });
34
+ return { name, fields: fieldDefinitions };
35
+ };
36
+ export const prepareTypesHeaderData = (dmmf, hasEnums) => {
37
+ const header = generateFileHeader();
38
+ const enumImports = hasEnums
39
+ ? dmmf.datamodel.enums.map((e) => toPascalCase(e.name)).join(", ")
40
+ : null;
41
+ return { header, enumImports };
42
+ };
@@ -0,0 +1,12 @@
1
+ import type { DMMF } from "@prisma/generator-helper";
2
+ import type { JoinTableInfo } from "../prisma/relation.js";
3
+ export declare function prepareJoinTableData(joinTable: JoinTableInfo, dmmf: DMMF.Document): {
4
+ tableName: string;
5
+ modelA: string;
6
+ modelB: string;
7
+ columnAFieldName: string;
8
+ columnBFieldName: string;
9
+ name: string;
10
+ columnAField: string;
11
+ columnBField: string;
12
+ };
@@ -0,0 +1,28 @@
1
+ import { isUuidField } from "../prisma/type.js";
2
+ import { toPascalCase, toSnakeCase } from "../utils/naming.js";
3
+ export function prepareJoinTableData(joinTable, dmmf) {
4
+ const { tableName, relationName, modelA, modelB } = joinTable;
5
+ const columnAFieldName = `${toSnakeCase(modelA)}_id`;
6
+ const columnBFieldName = `${toSnakeCase(modelB)}_id`;
7
+ const modelADef = dmmf.datamodel.models.find((m) => m.name === modelA);
8
+ const modelBDef = dmmf.datamodel.models.find((m) => m.name === modelB);
9
+ const modelAIdField = modelADef?.fields.find((f) => f.isId);
10
+ const modelBIdField = modelBDef?.fields.find((f) => f.isId);
11
+ const modelABaseType = modelAIdField && isUuidField(modelAIdField) ? "Schema.UUID" : "Schema.String";
12
+ const modelBBaseType = modelBIdField && isUuidField(modelBIdField) ? "Schema.UUID" : "Schema.String";
13
+ const modelASchemaType = modelAIdField?.type === "Int" ? "Schema.Number" : modelABaseType;
14
+ const modelBSchemaType = modelBIdField?.type === "Int" ? "Schema.Number" : modelBBaseType;
15
+ const columnAField = ` ${columnAFieldName}: Schema.propertySignature(columnType(${modelASchemaType}, Schema.Never, Schema.Never)).pipe(Schema.fromKey("A"))`;
16
+ const columnBField = ` ${columnBFieldName}: Schema.propertySignature(columnType(${modelBSchemaType}, Schema.Never, Schema.Never)).pipe(Schema.fromKey("B"))`;
17
+ const pascalName = toPascalCase(relationName);
18
+ return {
19
+ tableName,
20
+ modelA,
21
+ modelB,
22
+ columnAFieldName,
23
+ columnBFieldName,
24
+ name: pascalName,
25
+ columnAField,
26
+ columnBField,
27
+ };
28
+ }
@@ -0,0 +1,18 @@
1
+ import type { DMMF } from "@prisma/generator-helper";
2
+ /**
3
+ * Map Prisma field type to Effect Schema type
4
+ * Priority order: annotation → FK branded → UUID → scalar → enum → unknown fallback
5
+ *
6
+ * @param field - The Prisma field to map
7
+ * @param dmmf - The full DMMF document for enum lookups
8
+ * @param fkMap - Optional FK field → target model mapping for branded FK types
9
+ */
10
+ export declare function mapFieldToEffectType(field: DMMF.Field, dmmf: DMMF.Document, fkMap?: Map<string, string>): string;
11
+ /**
12
+ * Build complete field type with array and optional wrapping
13
+ *
14
+ * @param field - The Prisma field to build type for
15
+ * @param dmmf - The full DMMF document for enum lookups
16
+ * @param fkMap - Optional FK field → target model mapping for branded FK types
17
+ */
18
+ export declare function buildFieldType(field: DMMF.Field, dmmf: DMMF.Document, fkMap?: Map<string, string>): string;