prisma-generator-effect 0.0.4
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/LICENSE +21 -0
- package/README.md +607 -0
- package/dist/index.js +1493 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const generator_helper_1 = require("@prisma/generator-helper");
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const header = `// This file was generated by prisma-generator-effect, do not edit manually.\n`;
|
|
11
|
+
// Utility function to convert PascalCase to camelCase
|
|
12
|
+
function toCamelCase(str) {
|
|
13
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
14
|
+
}
|
|
15
|
+
(0, generator_helper_1.generatorHandler)({
|
|
16
|
+
onManifest() {
|
|
17
|
+
return {
|
|
18
|
+
defaultOutput: "../generated/effect",
|
|
19
|
+
prettyName: "Prisma Effect Generator",
|
|
20
|
+
// No engines required - we only read the DMMF schema
|
|
21
|
+
requiresEngines: [],
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
async onGenerate(options) {
|
|
25
|
+
const models = options.dmmf.datamodel.models;
|
|
26
|
+
const outputDir = options.generator.output?.value;
|
|
27
|
+
const schemaDir = node_path_1.default.dirname(options.schemaPath);
|
|
28
|
+
const configPath = options.generator.config.clientImportPath;
|
|
29
|
+
const clientImportPath = Array.isArray(configPath)
|
|
30
|
+
? configPath[0]
|
|
31
|
+
: configPath ?? "@prisma/client";
|
|
32
|
+
// Get datasource provider (e.g., "sqlite", "postgresql", "mysql", etc.)
|
|
33
|
+
const datasources = options.datasources;
|
|
34
|
+
const provider = datasources[0]?.provider; // Usually there's one datasource, but could be multiple
|
|
35
|
+
// Custom error configuration: "path/to/module#ErrorClassName"
|
|
36
|
+
// Path is relative to schema.prisma, e.g., "./errors#PrismaError"
|
|
37
|
+
// The module must export:
|
|
38
|
+
// - The error class (e.g., `export class PrismaError extends ...`)
|
|
39
|
+
// - A mapper function named `mapPrismaError` with signature:
|
|
40
|
+
// `(error: unknown, operation: string, model: string) => YourErrorType`
|
|
41
|
+
const errorConfigRaw = options.generator.config.errorImportPath;
|
|
42
|
+
const errorImportPathRaw = Array.isArray(errorConfigRaw)
|
|
43
|
+
? errorConfigRaw[0]
|
|
44
|
+
: errorConfigRaw;
|
|
45
|
+
// Import file extension for generated imports (e.g., "js", "ts", or "" for no extension)
|
|
46
|
+
// Useful for ESM compatibility where imports need explicit extensions
|
|
47
|
+
const importExtConfigRaw = options.generator.config.importFileExtension;
|
|
48
|
+
const importFileExtension = Array.isArray(importExtConfigRaw)
|
|
49
|
+
? importExtConfigRaw[0]
|
|
50
|
+
: importExtConfigRaw ?? "";
|
|
51
|
+
if (!outputDir) {
|
|
52
|
+
throw new Error("No output directory specified");
|
|
53
|
+
}
|
|
54
|
+
// Helper to add file extension to a path if configured
|
|
55
|
+
const addExtension = (filePath) => {
|
|
56
|
+
if (!importFileExtension)
|
|
57
|
+
return filePath;
|
|
58
|
+
// Don't add extension if path already has one
|
|
59
|
+
const ext = node_path_1.default.extname(filePath);
|
|
60
|
+
if (ext)
|
|
61
|
+
return filePath;
|
|
62
|
+
return `${filePath}.${importFileExtension}`;
|
|
63
|
+
};
|
|
64
|
+
// Convert errorImportPath from schema-relative to output-relative
|
|
65
|
+
let errorImportPath;
|
|
66
|
+
if (errorImportPathRaw) {
|
|
67
|
+
const [modulePath, className] = errorImportPathRaw.split("#");
|
|
68
|
+
if (!modulePath || !className) {
|
|
69
|
+
throw new Error(`Invalid errorImportPath format: "${errorImportPathRaw}". Expected "path/to/module#ErrorClassName"`);
|
|
70
|
+
}
|
|
71
|
+
// If it's a relative path, convert from schema-relative to output-relative
|
|
72
|
+
if (modulePath.startsWith(".")) {
|
|
73
|
+
const absoluteErrorPath = node_path_1.default.resolve(schemaDir, modulePath);
|
|
74
|
+
const relativeToOutput = node_path_1.default.relative(outputDir, absoluteErrorPath);
|
|
75
|
+
// Ensure it starts with ./ or ../
|
|
76
|
+
const normalizedPath = relativeToOutput.startsWith(".")
|
|
77
|
+
? relativeToOutput
|
|
78
|
+
: `./${relativeToOutput}`;
|
|
79
|
+
// Add file extension if configured
|
|
80
|
+
const pathWithExtension = addExtension(normalizedPath);
|
|
81
|
+
errorImportPath = `${pathWithExtension}#${className}`;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Package import (e.g., "@myorg/errors#PrismaError"), use as-is
|
|
85
|
+
errorImportPath = errorImportPathRaw;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Clean output directory
|
|
89
|
+
await promises_1.default.rm(outputDir, { recursive: true, force: true });
|
|
90
|
+
await promises_1.default.mkdir(outputDir, { recursive: true });
|
|
91
|
+
// Generate unified index file with PrismaService
|
|
92
|
+
await generateUnifiedService([...models], outputDir, clientImportPath, errorImportPath, provider);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
function generateRawSqlOperations(customError) {
|
|
96
|
+
// With custom error, use mapError which maps to user's error type
|
|
97
|
+
// Without custom error, use mapError which maps to PrismaError union
|
|
98
|
+
const errorType = customError ? customError.className : "PrismaError";
|
|
99
|
+
return `
|
|
100
|
+
$executeRaw: (args: PrismaNamespace.Sql | [PrismaNamespace.Sql, ...any[]]): Effect.Effect<number, ${errorType}, PrismaClient> =>
|
|
101
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
102
|
+
Effect.tryPromise({
|
|
103
|
+
try: () => (Array.isArray(args) ? client.$executeRaw(args[0], ...args.slice(1)) : client.$executeRaw(args)),
|
|
104
|
+
catch: (error) => mapError(error, "$executeRaw", "Prisma")
|
|
105
|
+
})
|
|
106
|
+
),
|
|
107
|
+
|
|
108
|
+
$executeRawUnsafe: (query: string, ...values: any[]): Effect.Effect<number, ${errorType}, PrismaClient> =>
|
|
109
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
110
|
+
Effect.tryPromise({
|
|
111
|
+
try: () => client.$executeRawUnsafe(query, ...values),
|
|
112
|
+
catch: (error) => mapError(error, "$executeRawUnsafe", "Prisma")
|
|
113
|
+
})
|
|
114
|
+
),
|
|
115
|
+
|
|
116
|
+
$queryRaw: <T = unknown>(args: PrismaNamespace.Sql | [PrismaNamespace.Sql, ...any[]]): Effect.Effect<T, ${errorType}, PrismaClient> =>
|
|
117
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
118
|
+
Effect.tryPromise({
|
|
119
|
+
try: () => (Array.isArray(args) ? client.$queryRaw(args[0], ...args.slice(1)) : client.$queryRaw(args)) as Promise<T>,
|
|
120
|
+
catch: (error) => mapError(error, "$queryRaw", "Prisma")
|
|
121
|
+
})
|
|
122
|
+
),
|
|
123
|
+
|
|
124
|
+
$queryRawUnsafe: <T = unknown>(query: string, ...values: any[]): Effect.Effect<T, ${errorType}, PrismaClient> =>
|
|
125
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
126
|
+
Effect.tryPromise({
|
|
127
|
+
try: () => client.$queryRawUnsafe(query, ...values) as Promise<T>,
|
|
128
|
+
catch: (error) => mapError(error, "$queryRawUnsafe", "Prisma")
|
|
129
|
+
})
|
|
130
|
+
),`;
|
|
131
|
+
}
|
|
132
|
+
function generateModelOperations(models, customError, provider) {
|
|
133
|
+
// Check if provider supports createManyAndReturn and updateManyAndReturn
|
|
134
|
+
const supportsManyAndReturn = provider === "postgresql" ||
|
|
135
|
+
provider === "postgres" ||
|
|
136
|
+
provider === "prisma+postgres" ||
|
|
137
|
+
provider === "cockroachdb" ||
|
|
138
|
+
provider === "sqlite";
|
|
139
|
+
return models
|
|
140
|
+
.map((model) => {
|
|
141
|
+
const modelName = model.name;
|
|
142
|
+
const modelNameCamel = toCamelCase(modelName);
|
|
143
|
+
// Type alias for the model delegate (e.g., BasePrismaClient['user'])
|
|
144
|
+
const delegate = `BasePrismaClient['${modelNameCamel}']`;
|
|
145
|
+
// Cast Promise results to ensure consistent typing across Prisma versions
|
|
146
|
+
// This handles Prisma 7's GlobalOmitConfig and works fine with Prisma 6 too
|
|
147
|
+
const promiseCast = (op, nullable = false) => {
|
|
148
|
+
const resultType = `PrismaNamespace.Result<${delegate}, A, '${op}'>`;
|
|
149
|
+
const fullType = nullable ? `${resultType} | null` : resultType;
|
|
150
|
+
return ` as Promise<${fullType}>`;
|
|
151
|
+
};
|
|
152
|
+
// Aggregate/groupBy use complex internal types that need stronger casts
|
|
153
|
+
const strongPromiseCast = (op) => {
|
|
154
|
+
const resultType = `PrismaNamespace.Result<${delegate}, A, '${op}'>`;
|
|
155
|
+
return ` as unknown as Promise<${resultType}>`;
|
|
156
|
+
};
|
|
157
|
+
const resultType = (op, nullable = false) => {
|
|
158
|
+
const baseType = `PrismaNamespace.Result<${delegate}, A, '${op}'>`;
|
|
159
|
+
return nullable ? `${baseType} | null` : baseType;
|
|
160
|
+
};
|
|
161
|
+
// With custom error: all operations use single error type and mapError
|
|
162
|
+
// Without custom error: use per-operation error types and mappers
|
|
163
|
+
const errorType = (opErrorType) => customError ? customError.className : opErrorType;
|
|
164
|
+
const mapperFn = (defaultMapper) => customError ? "mapError" : defaultMapper;
|
|
165
|
+
return ` ${modelNameCamel}: {
|
|
166
|
+
findUnique: <A extends PrismaNamespace.Args<${delegate}, 'findUnique'>>(
|
|
167
|
+
args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'findUnique'>>
|
|
168
|
+
): Effect.Effect<${resultType("findUnique", true)}, ${errorType("PrismaFindError")}, PrismaClient> =>
|
|
169
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
170
|
+
Effect.tryPromise({
|
|
171
|
+
try: () => client.${modelNameCamel}.findUnique(args as any)${promiseCast("findUnique", true)},
|
|
172
|
+
catch: (error) => ${mapperFn("mapFindError")}(error, "findUnique", "${modelName}")
|
|
173
|
+
})
|
|
174
|
+
),
|
|
175
|
+
|
|
176
|
+
findUniqueOrThrow: <A extends PrismaNamespace.Args<${delegate}, 'findUniqueOrThrow'>>(
|
|
177
|
+
args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'findUniqueOrThrow'>>
|
|
178
|
+
): Effect.Effect<${resultType("findUniqueOrThrow")}, ${errorType("PrismaFindOrThrowError")}, PrismaClient> =>
|
|
179
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
180
|
+
Effect.tryPromise({
|
|
181
|
+
try: () => client.${modelNameCamel}.findUniqueOrThrow(args as any)${promiseCast("findUniqueOrThrow")},
|
|
182
|
+
catch: (error) => ${mapperFn("mapFindOrThrowError")}(error, "findUniqueOrThrow", "${modelName}")
|
|
183
|
+
})
|
|
184
|
+
),
|
|
185
|
+
|
|
186
|
+
findFirst: <A extends PrismaNamespace.Args<${delegate}, 'findFirst'> = {}>(
|
|
187
|
+
args?: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'findFirst'>>
|
|
188
|
+
): Effect.Effect<${resultType("findFirst", true)}, ${errorType("PrismaFindError")}, PrismaClient> =>
|
|
189
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
190
|
+
Effect.tryPromise({
|
|
191
|
+
try: () => client.${modelNameCamel}.findFirst(args as any)${promiseCast("findFirst", true)},
|
|
192
|
+
catch: (error) => ${mapperFn("mapFindError")}(error, "findFirst", "${modelName}")
|
|
193
|
+
})
|
|
194
|
+
),
|
|
195
|
+
|
|
196
|
+
findFirstOrThrow: <A extends PrismaNamespace.Args<${delegate}, 'findFirstOrThrow'> = {}>(
|
|
197
|
+
args?: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'findFirstOrThrow'>>
|
|
198
|
+
): Effect.Effect<${resultType("findFirstOrThrow")}, ${errorType("PrismaFindOrThrowError")}, PrismaClient> =>
|
|
199
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
200
|
+
Effect.tryPromise({
|
|
201
|
+
try: () => client.${modelNameCamel}.findFirstOrThrow(args as any)${promiseCast("findFirstOrThrow")},
|
|
202
|
+
catch: (error) => ${mapperFn("mapFindOrThrowError")}(error, "findFirstOrThrow", "${modelName}")
|
|
203
|
+
})
|
|
204
|
+
),
|
|
205
|
+
|
|
206
|
+
findMany: <A extends PrismaNamespace.Args<${delegate}, 'findMany'> = {}>(
|
|
207
|
+
args?: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'findMany'>>
|
|
208
|
+
): Effect.Effect<${resultType("findMany")}, ${errorType("PrismaFindError")}, PrismaClient> =>
|
|
209
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
210
|
+
Effect.tryPromise({
|
|
211
|
+
try: () => client.${modelNameCamel}.findMany(args as any)${promiseCast("findMany")},
|
|
212
|
+
catch: (error) => ${mapperFn("mapFindError")}(error, "findMany", "${modelName}")
|
|
213
|
+
})
|
|
214
|
+
),
|
|
215
|
+
|
|
216
|
+
create: <A extends PrismaNamespace.Args<${delegate}, 'create'>>(
|
|
217
|
+
args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'create'>>
|
|
218
|
+
): Effect.Effect<${resultType("create")}, ${errorType("PrismaCreateError")}, PrismaClient> =>
|
|
219
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
220
|
+
Effect.tryPromise({
|
|
221
|
+
try: () => client.${modelNameCamel}.create(args as any)${promiseCast("create")},
|
|
222
|
+
catch: (error) => ${mapperFn("mapCreateError")}(error, "create", "${modelName}")
|
|
223
|
+
})
|
|
224
|
+
),
|
|
225
|
+
|
|
226
|
+
createMany: (
|
|
227
|
+
args?: PrismaNamespace.Args<${delegate}, 'createMany'>
|
|
228
|
+
): Effect.Effect<PrismaNamespace.BatchPayload, ${errorType("PrismaCreateError")}, PrismaClient> =>
|
|
229
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
230
|
+
Effect.tryPromise({
|
|
231
|
+
try: () => client.${modelNameCamel}.createMany(args as any),
|
|
232
|
+
catch: (error) => ${mapperFn("mapCreateError")}(error, "createMany", "${modelName}")
|
|
233
|
+
})
|
|
234
|
+
),${supportsManyAndReturn
|
|
235
|
+
? `
|
|
236
|
+
|
|
237
|
+
createManyAndReturn: <A extends PrismaNamespace.Args<${delegate}, 'createManyAndReturn'>>(
|
|
238
|
+
args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'createManyAndReturn'>>
|
|
239
|
+
): Effect.Effect<${resultType("createManyAndReturn")}, ${errorType("PrismaCreateError")}, PrismaClient> =>
|
|
240
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
241
|
+
Effect.tryPromise({
|
|
242
|
+
try: () => client.${modelNameCamel}.createManyAndReturn(args as any)${promiseCast("createManyAndReturn")},
|
|
243
|
+
catch: (error) => ${mapperFn("mapCreateError")}(error, "createManyAndReturn", "${modelName}")
|
|
244
|
+
})
|
|
245
|
+
),`
|
|
246
|
+
: ""}
|
|
247
|
+
|
|
248
|
+
delete: <A extends PrismaNamespace.Args<${delegate}, 'delete'>>(
|
|
249
|
+
args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'delete'>>
|
|
250
|
+
): Effect.Effect<${resultType("delete")}, ${errorType("PrismaDeleteError")}, PrismaClient> =>
|
|
251
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
252
|
+
Effect.tryPromise({
|
|
253
|
+
try: () => client.${modelNameCamel}.delete(args as any)${promiseCast("delete")},
|
|
254
|
+
catch: (error) => ${mapperFn("mapDeleteError")}(error, "delete", "${modelName}")
|
|
255
|
+
})
|
|
256
|
+
),
|
|
257
|
+
|
|
258
|
+
update: <A extends PrismaNamespace.Args<${delegate}, 'update'>>(
|
|
259
|
+
args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'update'>>
|
|
260
|
+
): Effect.Effect<${resultType("update")}, ${errorType("PrismaUpdateError")}, PrismaClient> =>
|
|
261
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
262
|
+
Effect.tryPromise({
|
|
263
|
+
try: () => client.${modelNameCamel}.update(args as any)${promiseCast("update")},
|
|
264
|
+
catch: (error) => ${mapperFn("mapUpdateError")}(error, "update", "${modelName}")
|
|
265
|
+
})
|
|
266
|
+
),
|
|
267
|
+
|
|
268
|
+
deleteMany: (
|
|
269
|
+
args?: PrismaNamespace.Args<${delegate}, 'deleteMany'>
|
|
270
|
+
): Effect.Effect<PrismaNamespace.BatchPayload, ${errorType("PrismaDeleteManyError")}, PrismaClient> =>
|
|
271
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
272
|
+
Effect.tryPromise({
|
|
273
|
+
try: () => client.${modelNameCamel}.deleteMany(args as any),
|
|
274
|
+
catch: (error) => ${mapperFn("mapDeleteManyError")}(error, "deleteMany", "${modelName}")
|
|
275
|
+
})
|
|
276
|
+
),
|
|
277
|
+
|
|
278
|
+
updateMany: (
|
|
279
|
+
args: PrismaNamespace.Args<${delegate}, 'updateMany'>
|
|
280
|
+
): Effect.Effect<PrismaNamespace.BatchPayload, ${errorType("PrismaUpdateManyError")}, PrismaClient> =>
|
|
281
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
282
|
+
Effect.tryPromise({
|
|
283
|
+
try: () => client.${modelNameCamel}.updateMany(args as any),
|
|
284
|
+
catch: (error) => ${mapperFn("mapUpdateManyError")}(error, "updateMany", "${modelName}")
|
|
285
|
+
})
|
|
286
|
+
),${supportsManyAndReturn
|
|
287
|
+
? `
|
|
288
|
+
|
|
289
|
+
updateManyAndReturn: <A extends PrismaNamespace.Args<${delegate}, 'updateManyAndReturn'>>(
|
|
290
|
+
args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'updateManyAndReturn'>>
|
|
291
|
+
): Effect.Effect<${resultType("updateManyAndReturn")}, ${errorType("PrismaUpdateManyError")}, PrismaClient> =>
|
|
292
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
293
|
+
Effect.tryPromise({
|
|
294
|
+
try: () => client.${modelNameCamel}.updateManyAndReturn(args as any)${promiseCast("updateManyAndReturn")},
|
|
295
|
+
catch: (error) => ${mapperFn("mapUpdateManyError")}(error, "updateManyAndReturn", "${modelName}")
|
|
296
|
+
})
|
|
297
|
+
),`
|
|
298
|
+
: ""}
|
|
299
|
+
|
|
300
|
+
upsert: <A extends PrismaNamespace.Args<${delegate}, 'upsert'>>(
|
|
301
|
+
args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'upsert'>>
|
|
302
|
+
): Effect.Effect<${resultType("upsert")}, ${errorType("PrismaCreateError")}, PrismaClient> =>
|
|
303
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
304
|
+
Effect.tryPromise({
|
|
305
|
+
try: () => client.${modelNameCamel}.upsert(args as any)${promiseCast("upsert")},
|
|
306
|
+
catch: (error) => ${mapperFn("mapCreateError")}(error, "upsert", "${modelName}")
|
|
307
|
+
})
|
|
308
|
+
),
|
|
309
|
+
|
|
310
|
+
// Aggregation operations
|
|
311
|
+
count: <A extends PrismaNamespace.Args<${delegate}, 'count'> = {}>(
|
|
312
|
+
args?: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'count'>>
|
|
313
|
+
): Effect.Effect<${resultType("count")}, ${errorType("PrismaFindError")}, PrismaClient> =>
|
|
314
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
315
|
+
Effect.tryPromise({
|
|
316
|
+
try: () => client.${modelNameCamel}.count(args as any)${promiseCast("count")},
|
|
317
|
+
catch: (error) => ${mapperFn("mapFindError")}(error, "count", "${modelName}")
|
|
318
|
+
})
|
|
319
|
+
),
|
|
320
|
+
|
|
321
|
+
aggregate: <A extends PrismaNamespace.Args<${delegate}, 'aggregate'>>(
|
|
322
|
+
args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'aggregate'>>
|
|
323
|
+
): Effect.Effect<${resultType("aggregate")}, ${errorType("PrismaFindError")}, PrismaClient> =>
|
|
324
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
325
|
+
Effect.tryPromise({
|
|
326
|
+
try: () => client.${modelNameCamel}.aggregate(args as any)${strongPromiseCast("aggregate")},
|
|
327
|
+
catch: (error) => ${mapperFn("mapFindError")}(error, "aggregate", "${modelName}")
|
|
328
|
+
})
|
|
329
|
+
),
|
|
330
|
+
|
|
331
|
+
groupBy: <A extends PrismaNamespace.Args<${delegate}, 'groupBy'>>(
|
|
332
|
+
args: PrismaNamespace.Exact<A, PrismaNamespace.Args<${delegate}, 'groupBy'>>
|
|
333
|
+
): Effect.Effect<${resultType("groupBy")}, ${errorType("PrismaFindError")}, PrismaClient> =>
|
|
334
|
+
Effect.flatMap(PrismaClient, ({ tx: client }) =>
|
|
335
|
+
Effect.tryPromise({
|
|
336
|
+
try: () => client.${modelNameCamel}.groupBy(args as any)${strongPromiseCast("groupBy")},
|
|
337
|
+
catch: (error) => ${mapperFn("mapFindError")}(error, "groupBy", "${modelName}")
|
|
338
|
+
})
|
|
339
|
+
)
|
|
340
|
+
}`;
|
|
341
|
+
})
|
|
342
|
+
.join(",\n\n");
|
|
343
|
+
}
|
|
344
|
+
// Parse error import path like "./errors#PrismaError" into { path, className }
|
|
345
|
+
function parseErrorImportPath(errorImportPath) {
|
|
346
|
+
if (!errorImportPath)
|
|
347
|
+
return null;
|
|
348
|
+
const [path, className] = errorImportPath.split("#");
|
|
349
|
+
if (!path || !className) {
|
|
350
|
+
throw new Error(`Invalid errorImportPath format: "${errorImportPath}". Expected "path/to/module#ErrorClassName"`);
|
|
351
|
+
}
|
|
352
|
+
return { path, className };
|
|
353
|
+
}
|
|
354
|
+
async function generateUnifiedService(models, outputDir, clientImportPath, errorImportPath, provider) {
|
|
355
|
+
const customError = parseErrorImportPath(errorImportPath);
|
|
356
|
+
const rawSqlOperations = generateRawSqlOperations(customError);
|
|
357
|
+
const modelOperations = generateModelOperations(models, customError, provider);
|
|
358
|
+
// Generate different content based on whether custom error is configured
|
|
359
|
+
const serviceContent = customError
|
|
360
|
+
? generateCustomErrorService(customError, clientImportPath, rawSqlOperations, modelOperations)
|
|
361
|
+
: generateDefaultErrorService(clientImportPath, rawSqlOperations, modelOperations);
|
|
362
|
+
await promises_1.default.writeFile(node_path_1.default.join(outputDir, "index.ts"), serviceContent);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Generate service with custom user-provided error class.
|
|
366
|
+
* All operations use a single error type and a simple mapError function.
|
|
367
|
+
*/
|
|
368
|
+
function generateCustomErrorService(customError, clientImportPath, rawSqlOperations, modelOperations) {
|
|
369
|
+
return `${header}
|
|
370
|
+
import { Context, Effect, Exit, Layer } from "effect"
|
|
371
|
+
import { Service } from "effect/Effect"
|
|
372
|
+
import { Prisma as PrismaNamespace, PrismaClient as BasePrismaClient } from "${clientImportPath}"
|
|
373
|
+
import { ${customError.className}, mapPrismaError } from "${customError.path}"
|
|
374
|
+
|
|
375
|
+
// Symbol used to identify intentional rollbacks vs actual errors
|
|
376
|
+
const ROLLBACK = Symbol.for("prisma.effect.rollback")
|
|
377
|
+
|
|
378
|
+
// Type for the flat transaction client with commit/rollback control
|
|
379
|
+
type FlatTransactionClient = PrismaNamespace.TransactionClient & {
|
|
380
|
+
$commit: () => Promise<void>
|
|
381
|
+
$rollback: () => Promise<void>
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Transaction options for $transaction and $isolatedTransaction */
|
|
385
|
+
type TransactionOptions = {
|
|
386
|
+
maxWait?: number
|
|
387
|
+
timeout?: number
|
|
388
|
+
isolationLevel?: PrismaNamespace.TransactionIsolationLevel
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Context tag for the Prisma client instance.
|
|
393
|
+
* Holds the transaction client (tx) and root client.
|
|
394
|
+
*
|
|
395
|
+
* Use \`PrismaClient.layer()\` or \`PrismaClient.layerEffect()\` to create a layer.
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* // Prisma 6 - all options are optional
|
|
399
|
+
* const layer = PrismaClient.layer({ datasourceUrl: "..." })
|
|
400
|
+
*
|
|
401
|
+
* // Prisma 7 - adapter or accelerateUrl is required
|
|
402
|
+
* const layer = PrismaClient.layer({ adapter: myAdapter })
|
|
403
|
+
*
|
|
404
|
+
* // With transaction options (Prisma uses these as defaults for $transaction)
|
|
405
|
+
* const layer = PrismaClient.layer({
|
|
406
|
+
* adapter: myAdapter, // or datasourceUrl for Prisma 6
|
|
407
|
+
* transactionOptions: { isolationLevel: "Serializable", timeout: 10000 }
|
|
408
|
+
* })
|
|
409
|
+
*/
|
|
410
|
+
export class PrismaClient extends Context.Tag("PrismaClient")<
|
|
411
|
+
PrismaClient,
|
|
412
|
+
{
|
|
413
|
+
tx: BasePrismaClient | PrismaNamespace.TransactionClient
|
|
414
|
+
client: BasePrismaClient
|
|
415
|
+
}
|
|
416
|
+
>() {
|
|
417
|
+
/**
|
|
418
|
+
* Create a PrismaClient layer with the given options.
|
|
419
|
+
* The client will be automatically disconnected when the layer scope ends.
|
|
420
|
+
*
|
|
421
|
+
* Pass options directly - the signature matches PrismaClient's constructor.
|
|
422
|
+
* Prisma 6: all options are optional
|
|
423
|
+
* Prisma 7: requires either \`adapter\` or \`accelerateUrl\`
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* // Prisma 6
|
|
427
|
+
* const layer = PrismaClient.layer({ datasourceUrl: process.env.DATABASE_URL })
|
|
428
|
+
*
|
|
429
|
+
* // Prisma 7 with adapter
|
|
430
|
+
* const layer = PrismaClient.layer({ adapter: myAdapter })
|
|
431
|
+
*
|
|
432
|
+
* // With transaction options
|
|
433
|
+
* const layer = PrismaClient.layer({
|
|
434
|
+
* adapter: myAdapter,
|
|
435
|
+
* transactionOptions: { isolationLevel: "Serializable" }
|
|
436
|
+
* })
|
|
437
|
+
*/
|
|
438
|
+
static layer = (
|
|
439
|
+
...args: ConstructorParameters<typeof BasePrismaClient>
|
|
440
|
+
) => Layer.scoped(
|
|
441
|
+
PrismaClient,
|
|
442
|
+
Effect.gen(function* () {
|
|
443
|
+
const prisma = new BasePrismaClient(...args)
|
|
444
|
+
yield* Effect.addFinalizer(() => Effect.promise(() => prisma.$disconnect()))
|
|
445
|
+
return { tx: prisma, client: prisma }
|
|
446
|
+
})
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Create a PrismaClient layer where options are computed via an Effect.
|
|
451
|
+
* Useful when you need to fetch configuration from environment, config service, or create adapters.
|
|
452
|
+
* The client will be automatically disconnected when the layer scope ends.
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* // Get config from a service
|
|
456
|
+
* const layer = PrismaClient.layerEffect(
|
|
457
|
+
* Effect.gen(function* () {
|
|
458
|
+
* const config = yield* ConfigService
|
|
459
|
+
* return { adapter: createAdapter(config.databaseUrl) }
|
|
460
|
+
* })
|
|
461
|
+
* )
|
|
462
|
+
*
|
|
463
|
+
* // With transaction options
|
|
464
|
+
* const layer = PrismaClient.layerEffect(
|
|
465
|
+
* Effect.gen(function* () {
|
|
466
|
+
* return {
|
|
467
|
+
* adapter: myAdapter,
|
|
468
|
+
* transactionOptions: { isolationLevel: "Serializable" }
|
|
469
|
+
* }
|
|
470
|
+
* })
|
|
471
|
+
* )
|
|
472
|
+
*/
|
|
473
|
+
static layerEffect = <R, E>(
|
|
474
|
+
optionsEffect: Effect.Effect<ConstructorParameters<typeof BasePrismaClient>[0], E, R>
|
|
475
|
+
) => Layer.scoped(
|
|
476
|
+
PrismaClient,
|
|
477
|
+
Effect.gen(function* () {
|
|
478
|
+
const options = yield* optionsEffect
|
|
479
|
+
const prisma = new BasePrismaClient(options)
|
|
480
|
+
yield* Effect.addFinalizer(() => Effect.promise(() => prisma.$disconnect()))
|
|
481
|
+
return { tx: prisma, client: prisma }
|
|
482
|
+
})
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Re-export the custom error type for convenience
|
|
487
|
+
export { ${customError.className} }
|
|
488
|
+
|
|
489
|
+
// Use the user-provided error mapper
|
|
490
|
+
const mapError = mapPrismaError
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Internal helper to begin a callback-free interactive transaction.
|
|
494
|
+
* Returns a transaction client with $commit and $rollback methods.
|
|
495
|
+
* This allows transactions to run in the same fiber as the parent effect.
|
|
496
|
+
*/
|
|
497
|
+
const $begin = (
|
|
498
|
+
client: BasePrismaClient,
|
|
499
|
+
options?: {
|
|
500
|
+
maxWait?: number
|
|
501
|
+
timeout?: number
|
|
502
|
+
isolationLevel?: PrismaNamespace.TransactionIsolationLevel
|
|
503
|
+
}
|
|
504
|
+
): Effect.Effect<FlatTransactionClient, ${customError.className}> =>
|
|
505
|
+
Effect.async<FlatTransactionClient, ${customError.className}>((resume) => {
|
|
506
|
+
let setTxClient: (txClient: PrismaNamespace.TransactionClient) => void
|
|
507
|
+
let commit: () => void
|
|
508
|
+
let rollback: () => void
|
|
509
|
+
|
|
510
|
+
// Promise that resolves when we get the transaction client
|
|
511
|
+
const txClientPromise = new Promise<PrismaNamespace.TransactionClient>((res) => {
|
|
512
|
+
setTxClient = res
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
// Promise that controls when the transaction commits/rolls back
|
|
516
|
+
const txPromise = new Promise<void>((_res, _rej) => {
|
|
517
|
+
commit = () => _res(undefined)
|
|
518
|
+
rollback = () => _rej(ROLLBACK)
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
// Start the transaction - Prisma will wait on txPromise before committing
|
|
522
|
+
const tx = client.$transaction((txClient) => {
|
|
523
|
+
setTxClient(txClient)
|
|
524
|
+
return txPromise
|
|
525
|
+
}, options).catch((e) => {
|
|
526
|
+
// Swallow intentional rollbacks, rethrow actual errors
|
|
527
|
+
if (e === ROLLBACK) return
|
|
528
|
+
throw e
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
// Once we have the transaction client, wrap it with commit/rollback methods
|
|
532
|
+
txClientPromise.then((innerTx) => {
|
|
533
|
+
const proxy = new Proxy(innerTx, {
|
|
534
|
+
get(target, prop) {
|
|
535
|
+
if (prop === "$commit") return () => { commit(); return tx }
|
|
536
|
+
if (prop === "$rollback") return () => { rollback(); return tx }
|
|
537
|
+
return target[prop as keyof typeof target]
|
|
538
|
+
},
|
|
539
|
+
}) as FlatTransactionClient
|
|
540
|
+
resume(Effect.succeed(proxy))
|
|
541
|
+
}).catch((error) => {
|
|
542
|
+
resume(Effect.fail(mapError(error, "$transaction", "Prisma")))
|
|
543
|
+
})
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* The main Prisma service with all database operations.
|
|
548
|
+
* Provides type-safe, effectful access to your Prisma models.
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* const program = Effect.gen(function* () {
|
|
552
|
+
* const prisma = yield* Prisma
|
|
553
|
+
* const user = yield* prisma.user.create({ data: { name: "Alice" } })
|
|
554
|
+
* return user
|
|
555
|
+
* })
|
|
556
|
+
*
|
|
557
|
+
* // Run with default layer (Prisma 6)
|
|
558
|
+
* Effect.runPromise(program.pipe(Effect.provide(Prisma.Live)))
|
|
559
|
+
*
|
|
560
|
+
* // Or with custom options
|
|
561
|
+
* Effect.runPromise(program.pipe(Effect.provide(Prisma.layer({ datasourceUrl: "..." }))))
|
|
562
|
+
*/
|
|
563
|
+
export class Prisma extends Service<Prisma>()("Prisma", {
|
|
564
|
+
effect: Effect.gen(function* () {
|
|
565
|
+
return {
|
|
566
|
+
/**
|
|
567
|
+
* Execute an effect within a database transaction.
|
|
568
|
+
* All operations within the effect will be atomic - they either all succeed or all fail.
|
|
569
|
+
*
|
|
570
|
+
* This implementation uses a callback-free transaction pattern that keeps the effect
|
|
571
|
+
* running in the same fiber as the parent, preserving Ref, FiberRef, and Context access.
|
|
572
|
+
*
|
|
573
|
+
* Options passed here override any defaults set via TransactionConfig layer.
|
|
574
|
+
*
|
|
575
|
+
* @example
|
|
576
|
+
* const result = yield* prisma.$transaction(
|
|
577
|
+
* Effect.gen(function* () {
|
|
578
|
+
* const user = yield* prisma.user.create({ data: { name: "Alice" } })
|
|
579
|
+
* yield* prisma.post.create({ data: { title: "Hello", authorId: user.id } })
|
|
580
|
+
* return user
|
|
581
|
+
* })
|
|
582
|
+
* )
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* // Override default isolation level for this transaction
|
|
586
|
+
* const result = yield* prisma.$transaction(myEffect, {
|
|
587
|
+
* isolationLevel: "ReadCommitted"
|
|
588
|
+
* })
|
|
589
|
+
*/
|
|
590
|
+
$transaction: <R, E, A>(
|
|
591
|
+
effect: Effect.Effect<A, E, R>,
|
|
592
|
+
options?: TransactionOptions
|
|
593
|
+
) =>
|
|
594
|
+
Effect.flatMap(
|
|
595
|
+
PrismaClient,
|
|
596
|
+
({ client, tx }): Effect.Effect<A, E | ${customError.className}, R> => {
|
|
597
|
+
// If we're already in a transaction, just run the effect directly (no nesting)
|
|
598
|
+
const isRootClient = "$transaction" in tx
|
|
599
|
+
if (!isRootClient) {
|
|
600
|
+
return effect
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Use acquireUseRelease to manage the transaction lifecycle
|
|
604
|
+
// This keeps everything in the same fiber, preserving Ref/FiberRef/Context
|
|
605
|
+
return Effect.acquireUseRelease(
|
|
606
|
+
// Acquire: begin a new transaction
|
|
607
|
+
// Prisma merges per-call options with constructor defaults internally
|
|
608
|
+
$begin(client, options),
|
|
609
|
+
|
|
610
|
+
// Use: run the effect with the transaction client injected
|
|
611
|
+
(txClient) =>
|
|
612
|
+
effect.pipe(
|
|
613
|
+
Effect.provideService(PrismaClient, { tx: txClient, client })
|
|
614
|
+
),
|
|
615
|
+
|
|
616
|
+
// Release: commit on success, rollback on failure/interruption
|
|
617
|
+
(txClient, exit) =>
|
|
618
|
+
Exit.isSuccess(exit)
|
|
619
|
+
? Effect.promise(() => txClient.$commit())
|
|
620
|
+
: Effect.promise(() => txClient.$rollback())
|
|
621
|
+
)
|
|
622
|
+
}
|
|
623
|
+
),
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Execute an effect in a NEW transaction, even if already inside a transaction.
|
|
627
|
+
* Unlike \`$transaction\`, this always creates a fresh, independent transaction.
|
|
628
|
+
*
|
|
629
|
+
* Use this for operations that should NOT be rolled back with the parent:
|
|
630
|
+
* - Audit logging that must persist even if main operation fails
|
|
631
|
+
* - Saga pattern where each step has independent commit/rollback
|
|
632
|
+
* - Background job queuing that should commit immediately
|
|
633
|
+
*
|
|
634
|
+
* ⚠️ WARNING: The isolated transaction can commit while the parent rolls back,
|
|
635
|
+
* or vice versa. Use carefully to avoid data inconsistencies.
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* yield* prisma.$transaction(
|
|
639
|
+
* Effect.gen(function* () {
|
|
640
|
+
* // This audit log commits independently - survives parent rollback
|
|
641
|
+
* yield* prisma.$isolatedTransaction(
|
|
642
|
+
* prisma.auditLog.create({ data: { action: "attempt", userId } })
|
|
643
|
+
* )
|
|
644
|
+
* // Main operation - if this fails, audit log is still committed
|
|
645
|
+
* yield* prisma.user.delete({ where: { id: userId } })
|
|
646
|
+
* })
|
|
647
|
+
* )
|
|
648
|
+
*/
|
|
649
|
+
$isolatedTransaction: <R, E, A>(
|
|
650
|
+
effect: Effect.Effect<A, E, R>,
|
|
651
|
+
options?: TransactionOptions
|
|
652
|
+
) =>
|
|
653
|
+
Effect.flatMap(
|
|
654
|
+
PrismaClient,
|
|
655
|
+
({ client }): Effect.Effect<A, E | ${customError.className}, R> => {
|
|
656
|
+
// Always use the root client to create a fresh transaction
|
|
657
|
+
return Effect.acquireUseRelease(
|
|
658
|
+
$begin(client, options),
|
|
659
|
+
(txClient) =>
|
|
660
|
+
effect.pipe(
|
|
661
|
+
Effect.provideService(PrismaClient, { tx: txClient, client })
|
|
662
|
+
),
|
|
663
|
+
(txClient, exit) =>
|
|
664
|
+
Exit.isSuccess(exit)
|
|
665
|
+
? Effect.promise(() => txClient.$commit())
|
|
666
|
+
: Effect.promise(() => txClient.$rollback())
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
),
|
|
670
|
+
${rawSqlOperations}
|
|
671
|
+
|
|
672
|
+
${modelOperations}
|
|
673
|
+
}
|
|
674
|
+
})
|
|
675
|
+
}) {
|
|
676
|
+
/**
|
|
677
|
+
* Create a complete Prisma layer with the given PrismaClient options.
|
|
678
|
+
* This is the recommended way to create a Prisma layer - it bundles both
|
|
679
|
+
* PrismaClient and Prisma service together.
|
|
680
|
+
*
|
|
681
|
+
* Pass options directly - the signature matches PrismaClient's constructor.
|
|
682
|
+
* Prisma 6: all options are optional
|
|
683
|
+
* Prisma 7: requires either \`adapter\` or \`accelerateUrl\`
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* // Prisma 6
|
|
687
|
+
* const MainLayer = Prisma.layer({ datasourceUrl: process.env.DATABASE_URL })
|
|
688
|
+
*
|
|
689
|
+
* // Prisma 7 with adapter
|
|
690
|
+
* const MainLayer = Prisma.layer({ adapter: myAdapter })
|
|
691
|
+
*
|
|
692
|
+
* // With transaction options
|
|
693
|
+
* const MainLayer = Prisma.layer({
|
|
694
|
+
* adapter: myAdapter,
|
|
695
|
+
* transactionOptions: { isolationLevel: "Serializable" }
|
|
696
|
+
* })
|
|
697
|
+
*
|
|
698
|
+
* // Use it
|
|
699
|
+
* Effect.runPromise(program.pipe(Effect.provide(MainLayer)))
|
|
700
|
+
*/
|
|
701
|
+
static layer = (
|
|
702
|
+
...args: ConstructorParameters<typeof BasePrismaClient>
|
|
703
|
+
) => Layer.merge(PrismaClient.layer(...args), Prisma.Default)
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Create a complete Prisma layer where PrismaClient options are computed via an Effect.
|
|
707
|
+
* This is useful when you need to fetch configuration or create adapters using Effect.
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* // Get config from a service
|
|
711
|
+
* const MainLayer = Prisma.layerEffect(
|
|
712
|
+
* Effect.gen(function* () {
|
|
713
|
+
* const config = yield* ConfigService
|
|
714
|
+
* return { adapter: createAdapter(config.databaseUrl) }
|
|
715
|
+
* })
|
|
716
|
+
* )
|
|
717
|
+
*
|
|
718
|
+
* // With transaction options
|
|
719
|
+
* const MainLayer = Prisma.layerEffect(
|
|
720
|
+
* Effect.gen(function* () {
|
|
721
|
+
* return {
|
|
722
|
+
* adapter: myAdapter,
|
|
723
|
+
* transactionOptions: { isolationLevel: "Serializable" }
|
|
724
|
+
* }
|
|
725
|
+
* })
|
|
726
|
+
* )
|
|
727
|
+
*/
|
|
728
|
+
static layerEffect = <R, E>(
|
|
729
|
+
optionsEffect: Effect.Effect<ConstructorParameters<typeof BasePrismaClient>[0], E, R>
|
|
730
|
+
) => Layer.merge(PrismaClient.layerEffect(optionsEffect), Prisma.Default)
|
|
731
|
+
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ============================================================================
|
|
735
|
+
// Deprecated aliases for backward compatibility
|
|
736
|
+
// ============================================================================
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* @deprecated Use \`PrismaClient\` instead. Will be removed in next major version.
|
|
740
|
+
*/
|
|
741
|
+
export const PrismaClientService = PrismaClient
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* @deprecated Use \`Prisma\` instead. Will be removed in next major version.
|
|
745
|
+
*/
|
|
746
|
+
export const PrismaService = Prisma
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* @deprecated Use \`PrismaClient.layer()\` instead. Will be removed in next major version.
|
|
750
|
+
*/
|
|
751
|
+
export const makePrismaLayer = PrismaClient.layer
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* @deprecated Use \`PrismaClient.layerEffect()\` instead. Will be removed in next major version.
|
|
755
|
+
*/
|
|
756
|
+
export const makePrismaLayerEffect = PrismaClient.layerEffect
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
`;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Generate service with default tagged error classes.
|
|
763
|
+
* Operations have per-operation error types for fine-grained error handling.
|
|
764
|
+
*/
|
|
765
|
+
function generateDefaultErrorService(clientImportPath, rawSqlOperations, modelOperations) {
|
|
766
|
+
return `${header}
|
|
767
|
+
import { Context, Data, Effect, Exit, Layer } from "effect"
|
|
768
|
+
import { Service } from "effect/Effect"
|
|
769
|
+
import { Prisma as PrismaNamespace, PrismaClient as BasePrismaClient } from "${clientImportPath}"
|
|
770
|
+
|
|
771
|
+
// Symbol used to identify intentional rollbacks vs actual errors
|
|
772
|
+
const ROLLBACK = Symbol.for("prisma.effect.rollback")
|
|
773
|
+
|
|
774
|
+
// Type for the flat transaction client with commit/rollback control
|
|
775
|
+
type FlatTransactionClient = PrismaNamespace.TransactionClient & {
|
|
776
|
+
$commit: () => Promise<void>
|
|
777
|
+
$rollback: () => Promise<void>
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** Transaction options for $transaction and $isolatedTransaction */
|
|
781
|
+
type TransactionOptions = {
|
|
782
|
+
maxWait?: number
|
|
783
|
+
timeout?: number
|
|
784
|
+
isolationLevel?: PrismaNamespace.TransactionIsolationLevel
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Context tag for the Prisma client instance.
|
|
789
|
+
* Holds the transaction client (tx) and root client.
|
|
790
|
+
*
|
|
791
|
+
* Use \`PrismaClient.layer()\` or \`PrismaClient.layerEffect()\` to create a layer.
|
|
792
|
+
*
|
|
793
|
+
* @example
|
|
794
|
+
* // Prisma 6 - all options are optional
|
|
795
|
+
* const layer = PrismaClient.layer({ datasourceUrl: "..." })
|
|
796
|
+
*
|
|
797
|
+
* // Prisma 7 - adapter or accelerateUrl is required
|
|
798
|
+
* const layer = PrismaClient.layer({ adapter: myAdapter })
|
|
799
|
+
*
|
|
800
|
+
* // With transaction options (Prisma uses these as defaults for $transaction)
|
|
801
|
+
* const layer = PrismaClient.layer({
|
|
802
|
+
* adapter: myAdapter, // or datasourceUrl for Prisma 6
|
|
803
|
+
* transactionOptions: { isolationLevel: "Serializable", timeout: 10000 }
|
|
804
|
+
* })
|
|
805
|
+
*/
|
|
806
|
+
export class PrismaClient extends Context.Tag("PrismaClient")<
|
|
807
|
+
PrismaClient,
|
|
808
|
+
{
|
|
809
|
+
tx: BasePrismaClient | PrismaNamespace.TransactionClient
|
|
810
|
+
client: BasePrismaClient
|
|
811
|
+
}
|
|
812
|
+
>() {
|
|
813
|
+
/**
|
|
814
|
+
* Create a PrismaClient layer with the given options.
|
|
815
|
+
* The client will be automatically disconnected when the layer scope ends.
|
|
816
|
+
*
|
|
817
|
+
* Pass options directly - the signature matches PrismaClient's constructor.
|
|
818
|
+
* Prisma 6: all options are optional
|
|
819
|
+
* Prisma 7: requires either \`adapter\` or \`accelerateUrl\`
|
|
820
|
+
*
|
|
821
|
+
* @example
|
|
822
|
+
* // Prisma 6
|
|
823
|
+
* const layer = PrismaClient.layer({ datasourceUrl: process.env.DATABASE_URL })
|
|
824
|
+
*
|
|
825
|
+
* // Prisma 7 with adapter
|
|
826
|
+
* const layer = PrismaClient.layer({ adapter: myAdapter })
|
|
827
|
+
*
|
|
828
|
+
* // With transaction options
|
|
829
|
+
* const layer = PrismaClient.layer({
|
|
830
|
+
* adapter: myAdapter,
|
|
831
|
+
* transactionOptions: { isolationLevel: "Serializable" }
|
|
832
|
+
* })
|
|
833
|
+
*/
|
|
834
|
+
static layer = (
|
|
835
|
+
...args: ConstructorParameters<typeof BasePrismaClient>
|
|
836
|
+
) => Layer.scoped(
|
|
837
|
+
PrismaClient,
|
|
838
|
+
Effect.gen(function* () {
|
|
839
|
+
const prisma = new BasePrismaClient(...args)
|
|
840
|
+
yield* Effect.addFinalizer(() => Effect.promise(() => prisma.$disconnect()))
|
|
841
|
+
return { tx: prisma, client: prisma }
|
|
842
|
+
})
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Create a PrismaClient layer where options are computed via an Effect.
|
|
847
|
+
* Useful when you need to fetch configuration from environment, config service, or create adapters.
|
|
848
|
+
* The client will be automatically disconnected when the layer scope ends.
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* // Get config from a service
|
|
852
|
+
* const layer = PrismaClient.layerEffect(
|
|
853
|
+
* Effect.gen(function* () {
|
|
854
|
+
* const config = yield* ConfigService
|
|
855
|
+
* return { adapter: createAdapter(config.databaseUrl) }
|
|
856
|
+
* })
|
|
857
|
+
* )
|
|
858
|
+
*
|
|
859
|
+
* // With transaction options
|
|
860
|
+
* const layer = PrismaClient.layerEffect(
|
|
861
|
+
* Effect.gen(function* () {
|
|
862
|
+
* return {
|
|
863
|
+
* adapter: myAdapter,
|
|
864
|
+
* transactionOptions: { isolationLevel: "Serializable" }
|
|
865
|
+
* }
|
|
866
|
+
* })
|
|
867
|
+
* )
|
|
868
|
+
*/
|
|
869
|
+
static layerEffect = <R, E>(
|
|
870
|
+
optionsEffect: Effect.Effect<ConstructorParameters<typeof BasePrismaClient>[0], E, R>
|
|
871
|
+
) => Layer.scoped(
|
|
872
|
+
PrismaClient,
|
|
873
|
+
Effect.gen(function* () {
|
|
874
|
+
const options = yield* optionsEffect
|
|
875
|
+
const prisma = new BasePrismaClient(options)
|
|
876
|
+
yield* Effect.addFinalizer(() => Effect.promise(() => prisma.$disconnect()))
|
|
877
|
+
return { tx: prisma, client: prisma }
|
|
878
|
+
})
|
|
879
|
+
)
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export class PrismaUniqueConstraintError extends Data.TaggedError("PrismaUniqueConstraintError")<{
|
|
883
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
884
|
+
operation: string
|
|
885
|
+
model: string
|
|
886
|
+
}> {}
|
|
887
|
+
|
|
888
|
+
export class PrismaForeignKeyConstraintError extends Data.TaggedError("PrismaForeignKeyConstraintError")<{
|
|
889
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
890
|
+
operation: string
|
|
891
|
+
model: string
|
|
892
|
+
}> {}
|
|
893
|
+
|
|
894
|
+
export class PrismaRecordNotFoundError extends Data.TaggedError("PrismaRecordNotFoundError")<{
|
|
895
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
896
|
+
operation: string
|
|
897
|
+
model: string
|
|
898
|
+
}> {}
|
|
899
|
+
|
|
900
|
+
export class PrismaRelationViolationError extends Data.TaggedError("PrismaRelationViolationError")<{
|
|
901
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
902
|
+
operation: string
|
|
903
|
+
model: string
|
|
904
|
+
}> {}
|
|
905
|
+
|
|
906
|
+
export class PrismaRelatedRecordNotFoundError extends Data.TaggedError("PrismaRelatedRecordNotFoundError")<{
|
|
907
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
908
|
+
operation: string
|
|
909
|
+
model: string
|
|
910
|
+
}> {}
|
|
911
|
+
|
|
912
|
+
export class PrismaTransactionConflictError extends Data.TaggedError("PrismaTransactionConflictError")<{
|
|
913
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
914
|
+
operation: string
|
|
915
|
+
model: string
|
|
916
|
+
}> {}
|
|
917
|
+
|
|
918
|
+
export class PrismaValueTooLongError extends Data.TaggedError("PrismaValueTooLongError")<{
|
|
919
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
920
|
+
operation: string
|
|
921
|
+
model: string
|
|
922
|
+
}> {}
|
|
923
|
+
|
|
924
|
+
export class PrismaValueOutOfRangeError extends Data.TaggedError("PrismaValueOutOfRangeError")<{
|
|
925
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
926
|
+
operation: string
|
|
927
|
+
model: string
|
|
928
|
+
}> {}
|
|
929
|
+
|
|
930
|
+
export class PrismaDbConstraintError extends Data.TaggedError("PrismaDbConstraintError")<{
|
|
931
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
932
|
+
operation: string
|
|
933
|
+
model: string
|
|
934
|
+
}> {}
|
|
935
|
+
|
|
936
|
+
export class PrismaConnectionError extends Data.TaggedError("PrismaConnectionError")<{
|
|
937
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
938
|
+
operation: string
|
|
939
|
+
model: string
|
|
940
|
+
}> {}
|
|
941
|
+
|
|
942
|
+
export class PrismaMissingRequiredValueError extends Data.TaggedError("PrismaMissingRequiredValueError")<{
|
|
943
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
944
|
+
operation: string
|
|
945
|
+
model: string
|
|
946
|
+
}> {}
|
|
947
|
+
|
|
948
|
+
export class PrismaInputValidationError extends Data.TaggedError("PrismaInputValidationError")<{
|
|
949
|
+
cause: PrismaNamespace.PrismaClientKnownRequestError
|
|
950
|
+
operation: string
|
|
951
|
+
model: string
|
|
952
|
+
}> {}
|
|
953
|
+
|
|
954
|
+
export type PrismaCreateError =
|
|
955
|
+
| PrismaValueTooLongError
|
|
956
|
+
| PrismaUniqueConstraintError
|
|
957
|
+
| PrismaForeignKeyConstraintError
|
|
958
|
+
| PrismaDbConstraintError
|
|
959
|
+
| PrismaInputValidationError
|
|
960
|
+
| PrismaMissingRequiredValueError
|
|
961
|
+
| PrismaRelatedRecordNotFoundError
|
|
962
|
+
| PrismaValueOutOfRangeError
|
|
963
|
+
| PrismaConnectionError
|
|
964
|
+
| PrismaTransactionConflictError
|
|
965
|
+
|
|
966
|
+
export type PrismaUpdateError =
|
|
967
|
+
| PrismaValueTooLongError
|
|
968
|
+
| PrismaUniqueConstraintError
|
|
969
|
+
| PrismaForeignKeyConstraintError
|
|
970
|
+
| PrismaDbConstraintError
|
|
971
|
+
| PrismaInputValidationError
|
|
972
|
+
| PrismaMissingRequiredValueError
|
|
973
|
+
| PrismaRelationViolationError
|
|
974
|
+
| PrismaRelatedRecordNotFoundError
|
|
975
|
+
| PrismaValueOutOfRangeError
|
|
976
|
+
| PrismaConnectionError
|
|
977
|
+
| PrismaRecordNotFoundError
|
|
978
|
+
| PrismaTransactionConflictError
|
|
979
|
+
|
|
980
|
+
export type PrismaDeleteError =
|
|
981
|
+
| PrismaForeignKeyConstraintError
|
|
982
|
+
| PrismaRelationViolationError
|
|
983
|
+
| PrismaConnectionError
|
|
984
|
+
| PrismaRecordNotFoundError
|
|
985
|
+
| PrismaTransactionConflictError
|
|
986
|
+
|
|
987
|
+
export type PrismaFindOrThrowError =
|
|
988
|
+
| PrismaConnectionError
|
|
989
|
+
| PrismaRecordNotFoundError
|
|
990
|
+
|
|
991
|
+
export type PrismaFindError =
|
|
992
|
+
| PrismaConnectionError
|
|
993
|
+
|
|
994
|
+
export type PrismaDeleteManyError =
|
|
995
|
+
| PrismaForeignKeyConstraintError
|
|
996
|
+
| PrismaRelationViolationError
|
|
997
|
+
| PrismaConnectionError
|
|
998
|
+
| PrismaTransactionConflictError
|
|
999
|
+
|
|
1000
|
+
export type PrismaUpdateManyError =
|
|
1001
|
+
| PrismaValueTooLongError
|
|
1002
|
+
| PrismaUniqueConstraintError
|
|
1003
|
+
| PrismaForeignKeyConstraintError
|
|
1004
|
+
| PrismaDbConstraintError
|
|
1005
|
+
| PrismaInputValidationError
|
|
1006
|
+
| PrismaMissingRequiredValueError
|
|
1007
|
+
| PrismaValueOutOfRangeError
|
|
1008
|
+
| PrismaConnectionError
|
|
1009
|
+
| PrismaTransactionConflictError
|
|
1010
|
+
|
|
1011
|
+
export type PrismaError =
|
|
1012
|
+
| PrismaValueTooLongError
|
|
1013
|
+
| PrismaUniqueConstraintError
|
|
1014
|
+
| PrismaForeignKeyConstraintError
|
|
1015
|
+
| PrismaDbConstraintError
|
|
1016
|
+
| PrismaInputValidationError
|
|
1017
|
+
| PrismaMissingRequiredValueError
|
|
1018
|
+
| PrismaRelationViolationError
|
|
1019
|
+
| PrismaRelatedRecordNotFoundError
|
|
1020
|
+
| PrismaValueOutOfRangeError
|
|
1021
|
+
| PrismaConnectionError
|
|
1022
|
+
| PrismaRecordNotFoundError
|
|
1023
|
+
| PrismaTransactionConflictError
|
|
1024
|
+
|
|
1025
|
+
// Generic mapper for raw operations and fallback
|
|
1026
|
+
const mapError = (error: unknown, operation: string, model: string): PrismaError => {
|
|
1027
|
+
if (error instanceof PrismaNamespace.PrismaClientKnownRequestError) {
|
|
1028
|
+
switch (error.code) {
|
|
1029
|
+
case "P2000":
|
|
1030
|
+
return new PrismaValueTooLongError({ cause: error, operation, model });
|
|
1031
|
+
case "P2002":
|
|
1032
|
+
return new PrismaUniqueConstraintError({ cause: error, operation, model });
|
|
1033
|
+
case "P2003":
|
|
1034
|
+
return new PrismaForeignKeyConstraintError({ cause: error, operation, model });
|
|
1035
|
+
case "P2004":
|
|
1036
|
+
return new PrismaDbConstraintError({ cause: error, operation, model });
|
|
1037
|
+
case "P2005":
|
|
1038
|
+
case "P2006":
|
|
1039
|
+
case "P2019":
|
|
1040
|
+
return new PrismaInputValidationError({ cause: error, operation, model });
|
|
1041
|
+
case "P2011":
|
|
1042
|
+
case "P2012":
|
|
1043
|
+
return new PrismaMissingRequiredValueError({ cause: error, operation, model });
|
|
1044
|
+
case "P2014":
|
|
1045
|
+
return new PrismaRelationViolationError({ cause: error, operation, model });
|
|
1046
|
+
case "P2015":
|
|
1047
|
+
case "P2018":
|
|
1048
|
+
return new PrismaRelatedRecordNotFoundError({ cause: error, operation, model });
|
|
1049
|
+
case "P2020":
|
|
1050
|
+
return new PrismaValueOutOfRangeError({ cause: error, operation, model });
|
|
1051
|
+
case "P2024":
|
|
1052
|
+
return new PrismaConnectionError({ cause: error, operation, model });
|
|
1053
|
+
case "P2025":
|
|
1054
|
+
return new PrismaRecordNotFoundError({ cause: error, operation, model });
|
|
1055
|
+
case "P2034":
|
|
1056
|
+
return new PrismaTransactionConflictError({ cause: error, operation, model });
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
// Unknown errors are not handled and will be treated as defects
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Specific mappers to narrow error types per operation
|
|
1064
|
+
|
|
1065
|
+
// Create, Upsert
|
|
1066
|
+
const mapCreateError = (error: unknown, operation: string, model: string): PrismaCreateError => {
|
|
1067
|
+
if (error instanceof PrismaNamespace.PrismaClientKnownRequestError) {
|
|
1068
|
+
switch (error.code) {
|
|
1069
|
+
case "P2000":
|
|
1070
|
+
return new PrismaValueTooLongError({ cause: error, operation, model });
|
|
1071
|
+
case "P2002":
|
|
1072
|
+
return new PrismaUniqueConstraintError({ cause: error, operation, model });
|
|
1073
|
+
case "P2003":
|
|
1074
|
+
return new PrismaForeignKeyConstraintError({ cause: error, operation, model });
|
|
1075
|
+
case "P2004":
|
|
1076
|
+
return new PrismaDbConstraintError({ cause: error, operation, model });
|
|
1077
|
+
case "P2005":
|
|
1078
|
+
case "P2006":
|
|
1079
|
+
case "P2019":
|
|
1080
|
+
return new PrismaInputValidationError({ cause: error, operation, model });
|
|
1081
|
+
case "P2011":
|
|
1082
|
+
case "P2012":
|
|
1083
|
+
return new PrismaMissingRequiredValueError({ cause: error, operation, model });
|
|
1084
|
+
case "P2015":
|
|
1085
|
+
case "P2018":
|
|
1086
|
+
return new PrismaRelatedRecordNotFoundError({ cause: error, operation, model });
|
|
1087
|
+
case "P2020":
|
|
1088
|
+
return new PrismaValueOutOfRangeError({ cause: error, operation, model });
|
|
1089
|
+
case "P2024":
|
|
1090
|
+
return new PrismaConnectionError({ cause: error, operation, model });
|
|
1091
|
+
case "P2034":
|
|
1092
|
+
return new PrismaTransactionConflictError({ cause: error, operation, model });
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Update
|
|
1099
|
+
const mapUpdateError = (error: unknown, operation: string, model: string): PrismaUpdateError => {
|
|
1100
|
+
if (error instanceof PrismaNamespace.PrismaClientKnownRequestError) {
|
|
1101
|
+
switch (error.code) {
|
|
1102
|
+
case "P2000":
|
|
1103
|
+
return new PrismaValueTooLongError({ cause: error, operation, model });
|
|
1104
|
+
case "P2002":
|
|
1105
|
+
return new PrismaUniqueConstraintError({ cause: error, operation, model });
|
|
1106
|
+
case "P2003":
|
|
1107
|
+
return new PrismaForeignKeyConstraintError({ cause: error, operation, model });
|
|
1108
|
+
case "P2004":
|
|
1109
|
+
return new PrismaDbConstraintError({ cause: error, operation, model });
|
|
1110
|
+
case "P2005":
|
|
1111
|
+
case "P2006":
|
|
1112
|
+
case "P2019":
|
|
1113
|
+
return new PrismaInputValidationError({ cause: error, operation, model });
|
|
1114
|
+
case "P2011":
|
|
1115
|
+
case "P2012":
|
|
1116
|
+
return new PrismaMissingRequiredValueError({ cause: error, operation, model });
|
|
1117
|
+
case "P2014":
|
|
1118
|
+
return new PrismaRelationViolationError({ cause: error, operation, model });
|
|
1119
|
+
case "P2015":
|
|
1120
|
+
case "P2018":
|
|
1121
|
+
return new PrismaRelatedRecordNotFoundError({ cause: error, operation, model });
|
|
1122
|
+
case "P2020":
|
|
1123
|
+
return new PrismaValueOutOfRangeError({ cause: error, operation, model });
|
|
1124
|
+
case "P2024":
|
|
1125
|
+
return new PrismaConnectionError({ cause: error, operation, model });
|
|
1126
|
+
case "P2025":
|
|
1127
|
+
return new PrismaRecordNotFoundError({ cause: error, operation, model });
|
|
1128
|
+
case "P2034":
|
|
1129
|
+
return new PrismaTransactionConflictError({ cause: error, operation, model });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
throw error;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Delete
|
|
1136
|
+
const mapDeleteError = (error: unknown, operation: string, model: string): PrismaDeleteError => {
|
|
1137
|
+
if (error instanceof PrismaNamespace.PrismaClientKnownRequestError) {
|
|
1138
|
+
switch (error.code) {
|
|
1139
|
+
case "P2003":
|
|
1140
|
+
return new PrismaForeignKeyConstraintError({ cause: error, operation, model });
|
|
1141
|
+
case "P2014":
|
|
1142
|
+
return new PrismaRelationViolationError({ cause: error, operation, model });
|
|
1143
|
+
case "P2024":
|
|
1144
|
+
return new PrismaConnectionError({ cause: error, operation, model });
|
|
1145
|
+
case "P2025":
|
|
1146
|
+
return new PrismaRecordNotFoundError({ cause: error, operation, model });
|
|
1147
|
+
case "P2034":
|
|
1148
|
+
return new PrismaTransactionConflictError({ cause: error, operation, model });
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
throw error;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// FindOrThrow
|
|
1155
|
+
const mapFindOrThrowError = (error: unknown, operation: string, model: string): PrismaFindOrThrowError => {
|
|
1156
|
+
if (error instanceof PrismaNamespace.PrismaClientKnownRequestError) {
|
|
1157
|
+
switch (error.code) {
|
|
1158
|
+
case "P2024":
|
|
1159
|
+
return new PrismaConnectionError({ cause: error, operation, model });
|
|
1160
|
+
case "P2025":
|
|
1161
|
+
return new PrismaRecordNotFoundError({ cause: error, operation, model });
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
throw error;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Find
|
|
1168
|
+
const mapFindError = (error: unknown, operation: string, model: string): PrismaFindError => {
|
|
1169
|
+
if (error instanceof PrismaNamespace.PrismaClientKnownRequestError) {
|
|
1170
|
+
switch (error.code) {
|
|
1171
|
+
case "P2024":
|
|
1172
|
+
return new PrismaConnectionError({ cause: error, operation, model });
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
throw error;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// DeleteMany
|
|
1179
|
+
const mapDeleteManyError = (error: unknown, operation: string, model: string): PrismaDeleteManyError => {
|
|
1180
|
+
if (error instanceof PrismaNamespace.PrismaClientKnownRequestError) {
|
|
1181
|
+
switch (error.code) {
|
|
1182
|
+
case "P2003":
|
|
1183
|
+
return new PrismaForeignKeyConstraintError({ cause: error, operation, model });
|
|
1184
|
+
case "P2014":
|
|
1185
|
+
return new PrismaRelationViolationError({ cause: error, operation, model });
|
|
1186
|
+
case "P2024":
|
|
1187
|
+
return new PrismaConnectionError({ cause: error, operation, model });
|
|
1188
|
+
case "P2034":
|
|
1189
|
+
return new PrismaTransactionConflictError({ cause: error, operation, model });
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
throw error;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// UpdateMany
|
|
1196
|
+
const mapUpdateManyError = (error: unknown, operation: string, model: string): PrismaUpdateManyError => {
|
|
1197
|
+
if (error instanceof PrismaNamespace.PrismaClientKnownRequestError) {
|
|
1198
|
+
switch (error.code) {
|
|
1199
|
+
case "P2000":
|
|
1200
|
+
return new PrismaValueTooLongError({ cause: error, operation, model });
|
|
1201
|
+
case "P2002":
|
|
1202
|
+
return new PrismaUniqueConstraintError({ cause: error, operation, model });
|
|
1203
|
+
case "P2003":
|
|
1204
|
+
return new PrismaForeignKeyConstraintError({ cause: error, operation, model });
|
|
1205
|
+
case "P2004":
|
|
1206
|
+
return new PrismaDbConstraintError({ cause: error, operation, model });
|
|
1207
|
+
case "P2005":
|
|
1208
|
+
case "P2006":
|
|
1209
|
+
case "P2019":
|
|
1210
|
+
return new PrismaInputValidationError({ cause: error, operation, model });
|
|
1211
|
+
case "P2011":
|
|
1212
|
+
case "P2012":
|
|
1213
|
+
return new PrismaMissingRequiredValueError({ cause: error, operation, model });
|
|
1214
|
+
case "P2020":
|
|
1215
|
+
return new PrismaValueOutOfRangeError({ cause: error, operation, model });
|
|
1216
|
+
case "P2024":
|
|
1217
|
+
return new PrismaConnectionError({ cause: error, operation, model });
|
|
1218
|
+
case "P2034":
|
|
1219
|
+
return new PrismaTransactionConflictError({ cause: error, operation, model });
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
throw error;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Internal helper to begin a callback-free interactive transaction.
|
|
1227
|
+
* Returns a transaction client with $commit and $rollback methods.
|
|
1228
|
+
* This allows transactions to run in the same fiber as the parent effect.
|
|
1229
|
+
*/
|
|
1230
|
+
const $begin = (
|
|
1231
|
+
client: BasePrismaClient,
|
|
1232
|
+
options?: {
|
|
1233
|
+
maxWait?: number
|
|
1234
|
+
timeout?: number
|
|
1235
|
+
isolationLevel?: PrismaNamespace.TransactionIsolationLevel
|
|
1236
|
+
}
|
|
1237
|
+
): Effect.Effect<FlatTransactionClient, PrismaError> =>
|
|
1238
|
+
Effect.async<FlatTransactionClient, PrismaError>((resume) => {
|
|
1239
|
+
let setTxClient: (txClient: PrismaNamespace.TransactionClient) => void
|
|
1240
|
+
let commit: () => void
|
|
1241
|
+
let rollback: () => void
|
|
1242
|
+
|
|
1243
|
+
// Promise that resolves when we get the transaction client
|
|
1244
|
+
const txClientPromise = new Promise<PrismaNamespace.TransactionClient>((res) => {
|
|
1245
|
+
setTxClient = res
|
|
1246
|
+
})
|
|
1247
|
+
|
|
1248
|
+
// Promise that controls when the transaction commits/rolls back
|
|
1249
|
+
const txPromise = new Promise<void>((_res, _rej) => {
|
|
1250
|
+
commit = () => _res(undefined)
|
|
1251
|
+
rollback = () => _rej(ROLLBACK)
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
// Start the transaction - Prisma will wait on txPromise before committing
|
|
1255
|
+
const tx = client.$transaction((txClient) => {
|
|
1256
|
+
setTxClient(txClient)
|
|
1257
|
+
return txPromise
|
|
1258
|
+
}, options).catch((e) => {
|
|
1259
|
+
// Swallow intentional rollbacks, rethrow actual errors
|
|
1260
|
+
if (e === ROLLBACK) return
|
|
1261
|
+
throw e
|
|
1262
|
+
})
|
|
1263
|
+
|
|
1264
|
+
// Once we have the transaction client, wrap it with commit/rollback methods
|
|
1265
|
+
txClientPromise.then((innerTx) => {
|
|
1266
|
+
const proxy = new Proxy(innerTx, {
|
|
1267
|
+
get(target, prop) {
|
|
1268
|
+
if (prop === "$commit") return () => { commit(); return tx }
|
|
1269
|
+
if (prop === "$rollback") return () => { rollback(); return tx }
|
|
1270
|
+
return target[prop as keyof typeof target]
|
|
1271
|
+
},
|
|
1272
|
+
}) as FlatTransactionClient
|
|
1273
|
+
resume(Effect.succeed(proxy))
|
|
1274
|
+
}).catch((error) => {
|
|
1275
|
+
resume(Effect.fail(mapError(error, "$transaction", "Prisma")))
|
|
1276
|
+
})
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* The main Prisma service with all database operations.
|
|
1281
|
+
* Provides type-safe, effectful access to your Prisma models.
|
|
1282
|
+
*
|
|
1283
|
+
* @example
|
|
1284
|
+
* const program = Effect.gen(function* () {
|
|
1285
|
+
* const prisma = yield* Prisma
|
|
1286
|
+
* const user = yield* prisma.user.create({ data: { name: "Alice" } })
|
|
1287
|
+
* return user
|
|
1288
|
+
* })
|
|
1289
|
+
*
|
|
1290
|
+
* // Run with default layer (Prisma 6)
|
|
1291
|
+
* Effect.runPromise(program.pipe(Effect.provide(Prisma.Live)))
|
|
1292
|
+
*
|
|
1293
|
+
* // Or with custom options
|
|
1294
|
+
* Effect.runPromise(program.pipe(Effect.provide(Prisma.layer({ datasourceUrl: "..." }))))
|
|
1295
|
+
*/
|
|
1296
|
+
export class Prisma extends Service<Prisma>()("Prisma", {
|
|
1297
|
+
effect: Effect.gen(function* () {
|
|
1298
|
+
return {
|
|
1299
|
+
/**
|
|
1300
|
+
* Execute an effect within a database transaction.
|
|
1301
|
+
* All operations within the effect will be atomic - they either all succeed or all fail.
|
|
1302
|
+
*
|
|
1303
|
+
* This implementation uses a callback-free transaction pattern that keeps the effect
|
|
1304
|
+
* running in the same fiber as the parent, preserving Ref, FiberRef, and Context access.
|
|
1305
|
+
*
|
|
1306
|
+
* Options passed here override any defaults set via transactionOptions in the layer.
|
|
1307
|
+
*
|
|
1308
|
+
* @example
|
|
1309
|
+
* const result = yield* prisma.$transaction(
|
|
1310
|
+
* Effect.gen(function* () {
|
|
1311
|
+
* const user = yield* prisma.user.create({ data: { name: "Alice" } })
|
|
1312
|
+
* yield* prisma.post.create({ data: { title: "Hello", authorId: user.id } })
|
|
1313
|
+
* return user
|
|
1314
|
+
* })
|
|
1315
|
+
* )
|
|
1316
|
+
*
|
|
1317
|
+
* @example
|
|
1318
|
+
* // Override default isolation level for this transaction
|
|
1319
|
+
* const result = yield* prisma.$transaction(myEffect, {
|
|
1320
|
+
* isolationLevel: "ReadCommitted"
|
|
1321
|
+
* })
|
|
1322
|
+
*/
|
|
1323
|
+
$transaction: <R, E, A>(
|
|
1324
|
+
effect: Effect.Effect<A, E, R>,
|
|
1325
|
+
options?: TransactionOptions
|
|
1326
|
+
) =>
|
|
1327
|
+
Effect.flatMap(
|
|
1328
|
+
PrismaClient,
|
|
1329
|
+
({ client, tx }): Effect.Effect<A, E | PrismaError, R> => {
|
|
1330
|
+
// If we're already in a transaction, just run the effect directly (no nesting)
|
|
1331
|
+
const isRootClient = "$transaction" in tx
|
|
1332
|
+
if (!isRootClient) {
|
|
1333
|
+
return effect
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Use acquireUseRelease to manage the transaction lifecycle
|
|
1337
|
+
// This keeps everything in the same fiber, preserving Ref/FiberRef/Context
|
|
1338
|
+
return Effect.acquireUseRelease(
|
|
1339
|
+
// Acquire: begin a new transaction
|
|
1340
|
+
// Prisma merges per-call options with constructor defaults internally
|
|
1341
|
+
$begin(client, options),
|
|
1342
|
+
|
|
1343
|
+
// Use: run the effect with the transaction client injected
|
|
1344
|
+
(txClient) =>
|
|
1345
|
+
effect.pipe(
|
|
1346
|
+
Effect.provideService(PrismaClient, { tx: txClient, client })
|
|
1347
|
+
),
|
|
1348
|
+
|
|
1349
|
+
// Release: commit on success, rollback on failure/interruption
|
|
1350
|
+
(txClient, exit) =>
|
|
1351
|
+
Exit.isSuccess(exit)
|
|
1352
|
+
? Effect.promise(() => txClient.$commit())
|
|
1353
|
+
: Effect.promise(() => txClient.$rollback())
|
|
1354
|
+
)
|
|
1355
|
+
}
|
|
1356
|
+
),
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* Execute an effect in a NEW transaction, even if already inside a transaction.
|
|
1360
|
+
* Unlike \`$transaction\`, this always creates a fresh, independent transaction.
|
|
1361
|
+
*
|
|
1362
|
+
* Use this for operations that should NOT be rolled back with the parent:
|
|
1363
|
+
* - Audit logging that must persist even if main operation fails
|
|
1364
|
+
* - Saga pattern where each step has independent commit/rollback
|
|
1365
|
+
* - Background job queuing that should commit immediately
|
|
1366
|
+
*
|
|
1367
|
+
* ⚠️ WARNING: The isolated transaction can commit while the parent rolls back,
|
|
1368
|
+
* or vice versa. Use carefully to avoid data inconsistencies.
|
|
1369
|
+
*
|
|
1370
|
+
* @example
|
|
1371
|
+
* yield* prisma.$transaction(
|
|
1372
|
+
* Effect.gen(function* () {
|
|
1373
|
+
* // This audit log commits independently - survives parent rollback
|
|
1374
|
+
* yield* prisma.$isolatedTransaction(
|
|
1375
|
+
* prisma.auditLog.create({ data: { action: "attempt", userId } })
|
|
1376
|
+
* )
|
|
1377
|
+
* // Main operation - if this fails, audit log is still committed
|
|
1378
|
+
* yield* prisma.user.delete({ where: { id: userId } })
|
|
1379
|
+
* })
|
|
1380
|
+
* )
|
|
1381
|
+
*/
|
|
1382
|
+
$isolatedTransaction: <R, E, A>(
|
|
1383
|
+
effect: Effect.Effect<A, E, R>,
|
|
1384
|
+
options?: TransactionOptions
|
|
1385
|
+
) =>
|
|
1386
|
+
Effect.flatMap(
|
|
1387
|
+
PrismaClient,
|
|
1388
|
+
({ client }): Effect.Effect<A, E | PrismaError, R> => {
|
|
1389
|
+
// Always use the root client to create a fresh transaction
|
|
1390
|
+
return Effect.acquireUseRelease(
|
|
1391
|
+
$begin(client, options),
|
|
1392
|
+
(txClient) =>
|
|
1393
|
+
effect.pipe(
|
|
1394
|
+
Effect.provideService(PrismaClient, { tx: txClient, client })
|
|
1395
|
+
),
|
|
1396
|
+
(txClient, exit) =>
|
|
1397
|
+
Exit.isSuccess(exit)
|
|
1398
|
+
? Effect.promise(() => txClient.$commit())
|
|
1399
|
+
: Effect.promise(() => txClient.$rollback())
|
|
1400
|
+
)
|
|
1401
|
+
}
|
|
1402
|
+
),
|
|
1403
|
+
${rawSqlOperations}
|
|
1404
|
+
|
|
1405
|
+
${modelOperations}
|
|
1406
|
+
}
|
|
1407
|
+
})
|
|
1408
|
+
}) {
|
|
1409
|
+
/**
|
|
1410
|
+
* Create a complete Prisma layer with the given PrismaClient options.
|
|
1411
|
+
* This is the recommended way to create a Prisma layer - it bundles both
|
|
1412
|
+
* PrismaClient and Prisma service together.
|
|
1413
|
+
*
|
|
1414
|
+
* Pass options directly - the signature matches PrismaClient's constructor.
|
|
1415
|
+
* Prisma 6: all options are optional
|
|
1416
|
+
* Prisma 7: requires either \`adapter\` or \`accelerateUrl\`
|
|
1417
|
+
*
|
|
1418
|
+
* @example
|
|
1419
|
+
* // Prisma 6
|
|
1420
|
+
* const MainLayer = Prisma.layer({ datasourceUrl: process.env.DATABASE_URL })
|
|
1421
|
+
*
|
|
1422
|
+
* // Prisma 7 with adapter
|
|
1423
|
+
* const MainLayer = Prisma.layer({ adapter: myAdapter })
|
|
1424
|
+
*
|
|
1425
|
+
* // With transaction options
|
|
1426
|
+
* const MainLayer = Prisma.layer({
|
|
1427
|
+
* adapter: myAdapter,
|
|
1428
|
+
* transactionOptions: { isolationLevel: "Serializable" }
|
|
1429
|
+
* })
|
|
1430
|
+
*
|
|
1431
|
+
* // Use it
|
|
1432
|
+
* Effect.runPromise(program.pipe(Effect.provide(MainLayer)))
|
|
1433
|
+
*/
|
|
1434
|
+
static layer = (
|
|
1435
|
+
...args: ConstructorParameters<typeof BasePrismaClient>
|
|
1436
|
+
) => Layer.merge(PrismaClient.layer(...args), Prisma.Default)
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Create a complete Prisma layer where PrismaClient options are computed via an Effect.
|
|
1440
|
+
* This is useful when you need to fetch configuration or create adapters using Effect.
|
|
1441
|
+
*
|
|
1442
|
+
* @example
|
|
1443
|
+
* // Get config from a service
|
|
1444
|
+
* const MainLayer = Prisma.layerEffect(
|
|
1445
|
+
* Effect.gen(function* () {
|
|
1446
|
+
* const config = yield* ConfigService
|
|
1447
|
+
* return { adapter: createAdapter(config.databaseUrl) }
|
|
1448
|
+
* })
|
|
1449
|
+
* )
|
|
1450
|
+
*
|
|
1451
|
+
* // With transaction options
|
|
1452
|
+
* const MainLayer = Prisma.layerEffect(
|
|
1453
|
+
* Effect.gen(function* () {
|
|
1454
|
+
* return {
|
|
1455
|
+
* adapter: myAdapter,
|
|
1456
|
+
* transactionOptions: { isolationLevel: "Serializable" }
|
|
1457
|
+
* }
|
|
1458
|
+
* })
|
|
1459
|
+
* )
|
|
1460
|
+
*/
|
|
1461
|
+
static layerEffect = <R, E>(
|
|
1462
|
+
optionsEffect: Effect.Effect<ConstructorParameters<typeof BasePrismaClient>[0], E, R>
|
|
1463
|
+
) => Layer.merge(PrismaClient.layerEffect(optionsEffect), Prisma.Default)
|
|
1464
|
+
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// ============================================================================
|
|
1468
|
+
// Deprecated aliases for backward compatibility
|
|
1469
|
+
// ============================================================================
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* @deprecated Use \`PrismaClient\` instead. Will be removed in next major version.
|
|
1473
|
+
*/
|
|
1474
|
+
export const PrismaClientService = PrismaClient
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* @deprecated Use \`Prisma\` instead. Will be removed in next major version.
|
|
1478
|
+
*/
|
|
1479
|
+
export const PrismaService = Prisma
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* @deprecated Use \`PrismaClient.layer()\` instead. Will be removed in next major version.
|
|
1483
|
+
*/
|
|
1484
|
+
export const makePrismaLayer = PrismaClient.layer
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* @deprecated Use \`PrismaClient.layerEffect()\` instead. Will be removed in next major version.
|
|
1488
|
+
*/
|
|
1489
|
+
export const makePrismaLayerEffect = PrismaClient.layerEffect
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
`;
|
|
1493
|
+
}
|