@effectify/prisma 1.0.1 → 1.1.1
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.
- package/README.md +33 -38
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +15 -14
- package/dist/src/commands/init.d.ts +1 -1
- package/dist/src/commands/init.js +36 -47
- package/dist/src/commands/prisma.d.ts +4 -4
- package/dist/src/commands/prisma.js +12 -12
- package/dist/src/generators/sql-schema-generator.js +15 -16
- package/dist/src/runtime/index.d.ts +303 -0
- package/dist/src/runtime/index.js +216 -0
- package/dist/src/schema-generator/effect/enum.d.ts +12 -0
- package/dist/src/schema-generator/effect/enum.js +18 -0
- package/dist/src/schema-generator/effect/generator.d.ts +16 -0
- package/dist/src/schema-generator/effect/generator.js +42 -0
- package/dist/src/schema-generator/effect/join-table.d.ts +12 -0
- package/dist/src/schema-generator/effect/join-table.js +28 -0
- package/dist/src/schema-generator/effect/type.d.ts +18 -0
- package/dist/src/schema-generator/effect/type.js +82 -0
- package/dist/src/schema-generator/index.d.ts +11 -0
- package/dist/src/schema-generator/index.js +83 -0
- package/dist/src/schema-generator/kysely/generator.d.ts +11 -0
- package/dist/src/schema-generator/kysely/generator.js +7 -0
- package/dist/src/schema-generator/kysely/type.d.ts +14 -0
- package/dist/src/schema-generator/kysely/type.js +44 -0
- package/dist/src/schema-generator/prisma/enum.d.ts +19 -0
- package/dist/src/schema-generator/prisma/enum.js +19 -0
- package/dist/src/schema-generator/prisma/generator.d.ts +53 -0
- package/dist/src/schema-generator/prisma/generator.js +29 -0
- package/dist/src/schema-generator/prisma/relation.d.ts +83 -0
- package/dist/src/schema-generator/prisma/relation.js +165 -0
- package/dist/src/schema-generator/prisma/type.d.ts +108 -0
- package/dist/src/schema-generator/prisma/type.js +85 -0
- package/dist/src/schema-generator/utils/annotations.d.ts +32 -0
- package/dist/src/schema-generator/utils/annotations.js +79 -0
- package/dist/src/schema-generator/utils/codegen.d.ts +9 -0
- package/dist/src/schema-generator/utils/codegen.js +14 -0
- package/dist/src/schema-generator/utils/naming.d.ts +29 -0
- package/dist/src/schema-generator/utils/naming.js +68 -0
- package/dist/src/schema-generator/utils/type-mappings.d.ts +62 -0
- package/dist/src/schema-generator/utils/type-mappings.js +70 -0
- package/dist/src/services/formatter-service.d.ts +10 -0
- package/dist/src/services/formatter-service.js +18 -0
- package/dist/src/services/generator-context.d.ts +2 -2
- package/dist/src/services/generator-context.js +2 -2
- package/dist/src/services/generator-service.d.ts +9 -8
- package/dist/src/services/generator-service.js +39 -43
- package/dist/src/services/render-service.d.ts +3 -3
- package/dist/src/services/render-service.js +8 -8
- package/dist/src/templates/effect-branded-id.eta +2 -0
- package/dist/src/templates/effect-enums.eta +9 -0
- package/dist/src/templates/effect-index.eta +4 -0
- package/dist/src/templates/effect-join-table.eta +8 -0
- package/dist/src/templates/effect-model.eta +6 -0
- package/dist/src/templates/effect-types-header.eta +7 -0
- package/dist/src/templates/index-default.eta +2 -1
- package/dist/src/templates/kysely-db-interface.eta +6 -0
- package/dist/src/templates/prisma-repository.eta +57 -32
- 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;
|