@effectify/prisma 0.1.1 → 0.1.2
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/package.json +14 -11
- package/prisma/dev.db +0 -0
- package/prisma/generated/edge.js +7 -7
- package/prisma/generated/effect/index.ts +2 -7
- package/prisma/generated/effect/models/Todo.ts +6 -0
- package/prisma/generated/effect/prisma-repository.ts +18 -18
- package/prisma/generated/effect/schemas/enums.ts +1 -1
- package/prisma/generated/effect/schemas/types.ts +1 -1
- package/prisma/generated/index-browser.js +4 -4
- package/prisma/generated/index.d.ts +25 -9
- package/prisma/generated/index.js +7 -7
- package/prisma/generated/package.json +3 -3
- package/prisma/generated/schema.prisma +1 -1
- package/prisma/schema.prisma +1 -1
- package/src/cli.ts +22 -0
- package/src/commands/init.ts +0 -2
- package/src/commands/prisma.ts +50 -0
- package/src/generators/sql-schema-generator.ts +0 -9
- package/src/services/generator-context.ts +4 -0
- package/src/services/generator-service.ts +178 -0
- package/src/services/render-service.ts +32 -0
- package/src/templates/index-custom-error.eta +190 -0
- package/src/templates/index-default.eta +363 -0
- package/src/templates/model.eta +6 -0
- package/src/templates/prisma-raw-sql.eta +31 -0
- package/src/{effect-prisma.ts → templates/prisma-repository.eta} +18 -890
- package/src/templates/prisma-schema.eta +94 -0
- package/vitest.config.ts +1 -0
- package/src/cli.tsx +0 -23
- package/src/commands/generate-effect.ts +0 -109
- package/src/commands/generate-sql-schema.ts +0 -109
- package/src/generators/prisma-effect-generator.ts +0 -496
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import * as FileSystem from '@effect/platform/FileSystem'
|
|
2
|
+
import * as Path from '@effect/platform/Path'
|
|
3
|
+
import type { DMMF, GeneratorOptions } from '@prisma/generator-helper'
|
|
4
|
+
import * as Context from 'effect/Context'
|
|
5
|
+
import * as Effect from 'effect/Effect'
|
|
6
|
+
import * as Layer from 'effect/Layer'
|
|
7
|
+
import { GeneratorContext } from './generator-context.js'
|
|
8
|
+
import { RenderService } from './render-service.js'
|
|
9
|
+
|
|
10
|
+
export class GeneratorService extends Context.Tag('GeneratorService')<
|
|
11
|
+
GeneratorService,
|
|
12
|
+
{
|
|
13
|
+
readonly generate: Effect.Effect<void, Error, GeneratorContext>
|
|
14
|
+
}
|
|
15
|
+
>() {
|
|
16
|
+
static Live = Layer.effect(
|
|
17
|
+
GeneratorService,
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const fs = yield* FileSystem.FileSystem
|
|
20
|
+
const path = yield* Path.Path
|
|
21
|
+
const { render } = yield* RenderService
|
|
22
|
+
|
|
23
|
+
const parseErrorImportPath = (
|
|
24
|
+
errorImportPath: string | undefined,
|
|
25
|
+
): { path: string; className: string } | null => {
|
|
26
|
+
if (!errorImportPath) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
const [modulePath, className] = errorImportPath.split('#')
|
|
30
|
+
if (!(modulePath && className)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Invalid errorImportPath format: "${errorImportPath}". Expected "path/to/module#ErrorClassName"`,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
return { path: modulePath, className }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const addExtension = (filePath: string, extension: string): string => {
|
|
39
|
+
if (!extension) {
|
|
40
|
+
return filePath
|
|
41
|
+
}
|
|
42
|
+
const ext = path.extname(filePath)
|
|
43
|
+
if (ext) {
|
|
44
|
+
return filePath
|
|
45
|
+
}
|
|
46
|
+
return `${filePath}.${extension}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fixSchemaImports = (outputDir: string) =>
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
const schemasDir = path.join(outputDir, 'schemas')
|
|
52
|
+
const indexFile = path.join(schemasDir, 'index.ts')
|
|
53
|
+
|
|
54
|
+
const exists = yield* fs.exists(indexFile)
|
|
55
|
+
if (!exists) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const content = yield* fs.readFileString(indexFile)
|
|
60
|
+
const fixedContent = content
|
|
61
|
+
.replace(/export \* from '\.\/enums'/g, "export * from './enums.js'")
|
|
62
|
+
.replace(/export \* from '\.\/types'/g, "export * from './types.js'")
|
|
63
|
+
|
|
64
|
+
if (content !== fixedContent) {
|
|
65
|
+
yield* fs.writeFileString(indexFile, fixedContent)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const getClientImportPath = (config: GeneratorOptions['generator']['config']) =>
|
|
70
|
+
Array.isArray(config.clientImportPath)
|
|
71
|
+
? config.clientImportPath[0]
|
|
72
|
+
: (config.clientImportPath ?? '@prisma/client')
|
|
73
|
+
|
|
74
|
+
const getErrorImportPath = (config: GeneratorOptions['generator']['config']) =>
|
|
75
|
+
Array.isArray(config.errorImportPath) ? config.errorImportPath[0] : config.errorImportPath
|
|
76
|
+
|
|
77
|
+
const getImportFileExtension = (config: GeneratorOptions['generator']['config']) =>
|
|
78
|
+
Array.isArray(config.importFileExtension) ? config.importFileExtension[0] : (config.importFileExtension ?? '')
|
|
79
|
+
|
|
80
|
+
const getCustomError = (
|
|
81
|
+
config: GeneratorOptions['generator']['config'],
|
|
82
|
+
options: GeneratorOptions,
|
|
83
|
+
schemaDir: string,
|
|
84
|
+
) => {
|
|
85
|
+
const errorImportPathRaw = getErrorImportPath(config)
|
|
86
|
+
const importFileExtension = getImportFileExtension(config)
|
|
87
|
+
|
|
88
|
+
let customError = parseErrorImportPath(errorImportPathRaw)
|
|
89
|
+
|
|
90
|
+
if (customError?.path.startsWith('.')) {
|
|
91
|
+
const outputDir = options.generator.output?.value
|
|
92
|
+
if (outputDir) {
|
|
93
|
+
const absoluteErrorPath = path.resolve(schemaDir, customError.path)
|
|
94
|
+
const relativeToOutput = path.relative(outputDir, absoluteErrorPath)
|
|
95
|
+
const normalizedPath = relativeToOutput.startsWith('.') ? relativeToOutput : `./${relativeToOutput}`
|
|
96
|
+
const pathWithExtension = addExtension(normalizedPath, importFileExtension)
|
|
97
|
+
customError = { ...customError, path: pathWithExtension }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return customError
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const getGeneratorConfig = (options: GeneratorOptions, schemaDir: string) => {
|
|
104
|
+
const { config } = options.generator
|
|
105
|
+
const clientImportPath = getClientImportPath(config)
|
|
106
|
+
const customError = getCustomError(config, options, schemaDir)
|
|
107
|
+
|
|
108
|
+
return { clientImportPath, customError }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const generatePrismaSchema = (outputDir: string) =>
|
|
112
|
+
Effect.gen(function* () {
|
|
113
|
+
const content = yield* render('prisma-schema', {})
|
|
114
|
+
yield* fs.writeFileString(path.join(outputDir, 'prisma-schema.ts'), content)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const generatePrismaRepository = (outputDir: string, clientImportPath: string) =>
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
const content = yield* render('prisma-repository', { clientImportPath })
|
|
120
|
+
yield* fs.writeFileString(path.join(outputDir, 'prisma-repository.ts'), content)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const generateModels = (outputDir: string, models: readonly DMMF.Model[]) =>
|
|
124
|
+
Effect.gen(function* () {
|
|
125
|
+
yield* fs.makeDirectory(path.join(outputDir, 'models'), { recursive: true })
|
|
126
|
+
for (const model of models) {
|
|
127
|
+
const content = yield* render('model', { model })
|
|
128
|
+
yield* fs.writeFileString(path.join(outputDir, 'models', `${model.name}.ts`), content)
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const generateIndex = (
|
|
133
|
+
outputDir: string,
|
|
134
|
+
models: readonly DMMF.Model[],
|
|
135
|
+
clientImportPath: string,
|
|
136
|
+
customError: { path: string; className: string } | null,
|
|
137
|
+
) =>
|
|
138
|
+
Effect.gen(function* () {
|
|
139
|
+
const errorType = customError ? customError.className : 'PrismaError'
|
|
140
|
+
const rawSqlOperations = yield* render('prisma-raw-sql', { errorType })
|
|
141
|
+
const modelExports = models.map((m) => `export * from "./models/${m.name}.js"`).join('\n')
|
|
142
|
+
|
|
143
|
+
const templateName = customError ? 'index-custom-error' : 'index-default'
|
|
144
|
+
const content = yield* render(templateName, {
|
|
145
|
+
clientImportPath,
|
|
146
|
+
customError,
|
|
147
|
+
rawSqlOperations,
|
|
148
|
+
modelExports,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
yield* fs.writeFileString(path.join(outputDir, 'index.ts'), content)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const generate = Effect.gen(function* () {
|
|
155
|
+
const options = yield* GeneratorContext
|
|
156
|
+
const models = options.dmmf.datamodel.models
|
|
157
|
+
const outputDir = options.generator.output?.value
|
|
158
|
+
const schemaDir = path.dirname(options.schemaPath)
|
|
159
|
+
|
|
160
|
+
if (!outputDir) {
|
|
161
|
+
return yield* Effect.fail(new Error('No output directory specified'))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { clientImportPath, customError } = getGeneratorConfig(options, schemaDir)
|
|
165
|
+
|
|
166
|
+
yield* fs.makeDirectory(outputDir, { recursive: true })
|
|
167
|
+
|
|
168
|
+
yield* generatePrismaSchema(outputDir)
|
|
169
|
+
yield* generatePrismaRepository(outputDir, clientImportPath)
|
|
170
|
+
yield* generateModels(outputDir, models)
|
|
171
|
+
yield* generateIndex(outputDir, models, clientImportPath, customError)
|
|
172
|
+
yield* fixSchemaImports(outputDir)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return { generate }
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import * as Context from 'effect/Context'
|
|
4
|
+
import * as Effect from 'effect/Effect'
|
|
5
|
+
import * as Layer from 'effect/Layer'
|
|
6
|
+
import { Eta } from 'eta'
|
|
7
|
+
|
|
8
|
+
export class RenderService extends Context.Tag('RenderService')<
|
|
9
|
+
RenderService,
|
|
10
|
+
{
|
|
11
|
+
readonly render: (templateName: string, data: Record<string, unknown>) => Effect.Effect<string, Error>
|
|
12
|
+
}
|
|
13
|
+
>() {
|
|
14
|
+
static Live = Layer.sync(RenderService, () => {
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
16
|
+
const __dirname = path.dirname(__filename)
|
|
17
|
+
const templatesDir = path.resolve(__dirname, '../templates')
|
|
18
|
+
|
|
19
|
+
const eta = new Eta({
|
|
20
|
+
views: templatesDir,
|
|
21
|
+
autoEscape: false,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
render: (templateName: string, data: Record<string, unknown>) =>
|
|
26
|
+
Effect.try({
|
|
27
|
+
try: () => eta.render(templateName, data),
|
|
28
|
+
catch: (error) => new Error(`Failed to render template ${templateName}: ${error}`),
|
|
29
|
+
}),
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// This file was generated by prisma-effect-generator, do not edit manually.
|
|
2
|
+
|
|
3
|
+
import { Context, Effect, Exit, Layer } from "effect"
|
|
4
|
+
import { Service } from "effect/Effect"
|
|
5
|
+
import { Prisma as PrismaNamespace, PrismaClient as BasePrismaClient } from "<%= it.clientImportPath %>"
|
|
6
|
+
import { <%= it.customError.className %>, mapPrismaError } from "<%= it.customError.path %>"
|
|
7
|
+
import * as Model from "./prisma-repository.js"
|
|
8
|
+
|
|
9
|
+
// Symbol used to identify intentional rollbacks vs actual errors
|
|
10
|
+
const ROLLBACK = Symbol.for("prisma.effect.rollback")
|
|
11
|
+
|
|
12
|
+
// Type for the flat transaction client with commit/rollback control
|
|
13
|
+
type FlatTransactionClient = PrismaNamespace.TransactionClient & {
|
|
14
|
+
$commit: () => Promise<void>
|
|
15
|
+
$rollback: () => Promise<void>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Transaction options for $transaction and $isolatedTransaction */
|
|
19
|
+
type TransactionOptions = {
|
|
20
|
+
maxWait?: number
|
|
21
|
+
timeout?: number
|
|
22
|
+
isolationLevel?: PrismaNamespace.TransactionIsolationLevel
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Context tag for the Prisma client instance.
|
|
27
|
+
* Holds the transaction client (tx) and root client.
|
|
28
|
+
*/
|
|
29
|
+
export class PrismaClient extends Context.Tag("PrismaClient")<
|
|
30
|
+
PrismaClient,
|
|
31
|
+
{
|
|
32
|
+
tx: BasePrismaClient | PrismaNamespace.TransactionClient
|
|
33
|
+
client: BasePrismaClient
|
|
34
|
+
}
|
|
35
|
+
>() {
|
|
36
|
+
static layer = (
|
|
37
|
+
...args: ConstructorParameters<typeof BasePrismaClient>
|
|
38
|
+
) => Layer.scoped(
|
|
39
|
+
PrismaClient,
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const prisma = new BasePrismaClient(...args)
|
|
42
|
+
yield* Effect.addFinalizer(() => Effect.promise(() => prisma.$disconnect()))
|
|
43
|
+
return { tx: prisma, client: prisma }
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
static layerEffect = <R, E>(
|
|
48
|
+
optionsEffect: Effect.Effect<ConstructorParameters<typeof BasePrismaClient>[0], E, R>
|
|
49
|
+
) => Layer.scoped(
|
|
50
|
+
PrismaClient,
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const options = yield* optionsEffect
|
|
53
|
+
const prisma = new BasePrismaClient(options)
|
|
54
|
+
yield* Effect.addFinalizer(() => Effect.promise(() => prisma.$disconnect()))
|
|
55
|
+
return { tx: prisma, client: prisma }
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Re-export the custom error type for convenience
|
|
61
|
+
export { <%= it.customError.className %> }
|
|
62
|
+
|
|
63
|
+
// Use the user-provided error mapper
|
|
64
|
+
const mapError = mapPrismaError
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Internal helper to begin a callback-free interactive transaction.
|
|
68
|
+
*/
|
|
69
|
+
const $begin = (
|
|
70
|
+
client: BasePrismaClient,
|
|
71
|
+
options?: {
|
|
72
|
+
maxWait?: number
|
|
73
|
+
timeout?: number
|
|
74
|
+
isolationLevel?: PrismaNamespace.TransactionIsolationLevel
|
|
75
|
+
}
|
|
76
|
+
): Effect.Effect<FlatTransactionClient, <%= it.customError.className %>> =>
|
|
77
|
+
Effect.async<FlatTransactionClient, <%= it.customError.className %>>((resume) => {
|
|
78
|
+
let setTxClient: (txClient: PrismaNamespace.TransactionClient) => void
|
|
79
|
+
let commit: () => void
|
|
80
|
+
let rollback: () => void
|
|
81
|
+
|
|
82
|
+
const txClientPromise = new Promise<PrismaNamespace.TransactionClient>((res) => {
|
|
83
|
+
setTxClient = res
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const txPromise = new Promise<void>((_res, _rej) => {
|
|
87
|
+
commit = () => _res(undefined)
|
|
88
|
+
rollback = () => _rej(ROLLBACK)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const tx = client.$transaction((txClient) => {
|
|
92
|
+
setTxClient(txClient)
|
|
93
|
+
return txPromise
|
|
94
|
+
}, options).catch((e) => {
|
|
95
|
+
if (e === ROLLBACK) return
|
|
96
|
+
throw e
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
txClientPromise.then((innerTx) => {
|
|
100
|
+
const proxy = new Proxy(innerTx, {
|
|
101
|
+
get(target, prop) {
|
|
102
|
+
if (prop === "$commit") return () => { commit(); return tx }
|
|
103
|
+
if (prop === "$rollback") return () => { rollback(); return tx }
|
|
104
|
+
return target[prop as keyof typeof target]
|
|
105
|
+
},
|
|
106
|
+
}) as FlatTransactionClient
|
|
107
|
+
resume(Effect.succeed(proxy))
|
|
108
|
+
}).catch((error) => {
|
|
109
|
+
resume(Effect.fail(mapError(error, "$transaction", "Prisma")))
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The main Prisma service with all database operations.
|
|
115
|
+
* Provides type-safe, effectful access to your Prisma models.
|
|
116
|
+
*/
|
|
117
|
+
export class Prisma extends Service<Prisma>()("Prisma", {
|
|
118
|
+
effect: Effect.gen(function* () {
|
|
119
|
+
return {
|
|
120
|
+
$transaction: <R, E, A>(
|
|
121
|
+
effect: Effect.Effect<A, E, R>,
|
|
122
|
+
options?: TransactionOptions
|
|
123
|
+
) =>
|
|
124
|
+
Effect.flatMap(
|
|
125
|
+
PrismaClient,
|
|
126
|
+
({ client, tx }): Effect.Effect<A, E | <%= it.customError.className %>, R> => {
|
|
127
|
+
const isRootClient = "$transaction" in tx
|
|
128
|
+
if (!isRootClient) {
|
|
129
|
+
return effect
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Effect.acquireUseRelease(
|
|
133
|
+
$begin(client, options),
|
|
134
|
+
(txClient) =>
|
|
135
|
+
effect.pipe(
|
|
136
|
+
Effect.provideService(PrismaClient, { tx: txClient, client })
|
|
137
|
+
),
|
|
138
|
+
(txClient, exit) =>
|
|
139
|
+
Exit.isSuccess(exit)
|
|
140
|
+
? Effect.promise(() => txClient.$commit())
|
|
141
|
+
: Effect.promise(() => txClient.$rollback())
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
),
|
|
145
|
+
|
|
146
|
+
$isolatedTransaction: <R, E, A>(
|
|
147
|
+
effect: Effect.Effect<A, E, R>,
|
|
148
|
+
options?: TransactionOptions
|
|
149
|
+
) =>
|
|
150
|
+
Effect.flatMap(
|
|
151
|
+
PrismaClient,
|
|
152
|
+
({ client }): Effect.Effect<A, E | <%= it.customError.className %>, R> => {
|
|
153
|
+
return Effect.acquireUseRelease(
|
|
154
|
+
$begin(client, options),
|
|
155
|
+
(txClient) =>
|
|
156
|
+
effect.pipe(
|
|
157
|
+
Effect.provideService(PrismaClient, { tx: txClient, client })
|
|
158
|
+
),
|
|
159
|
+
(txClient, exit) =>
|
|
160
|
+
Exit.isSuccess(exit)
|
|
161
|
+
? Effect.promise(() => txClient.$commit())
|
|
162
|
+
: Effect.promise(() => txClient.$rollback())
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
),
|
|
166
|
+
<%~ it.rawSqlOperations %>
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}) {
|
|
170
|
+
static layer = (
|
|
171
|
+
...args: ConstructorParameters<typeof BasePrismaClient>
|
|
172
|
+
) => Layer.merge(PrismaClient.layer(...args), Prisma.Default)
|
|
173
|
+
|
|
174
|
+
static layerEffect = <R, E>(
|
|
175
|
+
optionsEffect: Effect.Effect<ConstructorParameters<typeof BasePrismaClient>[0], E, R>
|
|
176
|
+
) => Layer.merge(PrismaClient.layerEffect(optionsEffect), Prisma.Default)
|
|
177
|
+
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Deprecated aliases for backward compatibility
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
export const PrismaClientService = PrismaClient
|
|
185
|
+
export const PrismaService = Prisma
|
|
186
|
+
export const makePrismaLayer = PrismaClient.layer
|
|
187
|
+
export const makePrismaLayerEffect = PrismaClient.layerEffect
|
|
188
|
+
|
|
189
|
+
<%~ it.modelExports %>
|
|
190
|
+
|