prisma-effect-schema 0.1.4 → 0.1.5
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 +266 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
# prisma-effect-schema
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/prisma-effect-schema)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A Prisma generator that creates type-safe [Effect Schema](https://effect.website/docs/schema/introduction) definitions from your Prisma models.
|
|
7
|
+
|
|
8
|
+
> **Disclaimer:** This is a community project and is **not maintained by the Effect team**. It may contain bugs or have incomplete coverage of edge cases. Use at your own discretion and please report any issues you encounter.
|
|
9
|
+
|
|
10
|
+
## Why prisma-effect-schema?
|
|
11
|
+
|
|
12
|
+
When using Prisma with Effect, you need runtime validation schemas that match your database models. Writing these by hand is tedious and error-prone. This generator:
|
|
13
|
+
|
|
14
|
+
- **Keeps schemas in sync** with your Prisma models automatically
|
|
15
|
+
- **Provides type-safe IDs** through branded types (no more mixing up `UserId` and `PostId`)
|
|
16
|
+
- **Handles complex types** like JSON, enums, and relations out of the box
|
|
17
|
+
- **Produces deterministic output** to minimize git diffs
|
|
4
18
|
|
|
5
19
|
## Features
|
|
6
20
|
|
|
7
21
|
- Generates Effect Schemas for all Prisma models and enums
|
|
8
22
|
- Creates branded ID types for type-safe entity references
|
|
9
23
|
- Handles all Prisma scalar types (String, Int, Float, Boolean, DateTime, Json, Bytes, BigInt, Decimal)
|
|
24
|
+
- Intelligent foreign key resolution using Prisma relation metadata
|
|
25
|
+
- Optional relation schemas with `Schema.suspend()` for circular references
|
|
10
26
|
- Deterministic output (sorted fields/models) to minimize git diffs
|
|
11
27
|
- No timestamps in generated files to avoid unnecessary churn
|
|
12
|
-
-
|
|
28
|
+
- Fully configurable via Prisma schema
|
|
13
29
|
|
|
14
30
|
## Installation
|
|
15
31
|
|
|
16
32
|
```bash
|
|
17
|
-
npm install prisma-effect-schema
|
|
33
|
+
npm install prisma-effect-schema effect
|
|
34
|
+
# or
|
|
35
|
+
pnpm add prisma-effect-schema effect
|
|
18
36
|
# or
|
|
19
|
-
|
|
37
|
+
yarn add prisma-effect-schema effect
|
|
20
38
|
```
|
|
21
39
|
|
|
22
|
-
|
|
40
|
+
> **Note:** `effect` is a peer dependency and must be installed separately.
|
|
23
41
|
|
|
24
|
-
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### 1. Add the generator to your `schema.prisma`
|
|
25
45
|
|
|
26
46
|
```prisma
|
|
27
47
|
generator client {
|
|
@@ -30,7 +50,7 @@ generator client {
|
|
|
30
50
|
|
|
31
51
|
generator effectSchema {
|
|
32
52
|
provider = "prisma-effect-schema"
|
|
33
|
-
output = "./generated/
|
|
53
|
+
output = "./generated/schemas.ts"
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
datasource db {
|
|
@@ -56,121 +76,299 @@ model Post {
|
|
|
56
76
|
}
|
|
57
77
|
```
|
|
58
78
|
|
|
59
|
-
|
|
79
|
+
### 2. Run the generator
|
|
60
80
|
|
|
61
81
|
```bash
|
|
62
82
|
npx prisma generate
|
|
63
83
|
```
|
|
64
84
|
|
|
65
|
-
|
|
85
|
+
### 3. Use the generated schemas
|
|
66
86
|
|
|
67
87
|
```typescript
|
|
68
88
|
import { Schema } from "effect";
|
|
89
|
+
import { User, UserId, Post, PostId } from "./generated/schemas";
|
|
90
|
+
|
|
91
|
+
// Validate data
|
|
92
|
+
const parseUser = Schema.decodeUnknownSync(User);
|
|
93
|
+
const user = parseUser({
|
|
94
|
+
id: "clx123...",
|
|
95
|
+
email: "alice@example.com",
|
|
96
|
+
name: "Alice",
|
|
97
|
+
createdAt: new Date(),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Type-safe IDs prevent mixing up different entity IDs
|
|
101
|
+
const getUserById = (id: UserId) => { /* ... */ };
|
|
102
|
+
const postId: PostId = "post_123" as PostId;
|
|
103
|
+
// getUserById(postId); // Type error! Can't use PostId where UserId is expected
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Generated Output
|
|
107
|
+
|
|
108
|
+
The generator produces clean, readable TypeScript:
|
|
69
109
|
|
|
110
|
+
```typescript
|
|
111
|
+
import { Schema } from "effect"
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
70
114
|
// Branded IDs
|
|
71
|
-
|
|
72
|
-
export
|
|
115
|
+
// ============================================================================
|
|
116
|
+
export const UserId = Schema.String.pipe(Schema.brand("UserId"))
|
|
117
|
+
export type UserId = typeof UserId.Type
|
|
73
118
|
|
|
74
|
-
export const PostId = Schema.String.pipe(Schema.brand("PostId"))
|
|
75
|
-
export type PostId = typeof PostId.Type
|
|
119
|
+
export const PostId = Schema.String.pipe(Schema.brand("PostId"))
|
|
120
|
+
export type PostId = typeof PostId.Type
|
|
76
121
|
|
|
77
|
-
//
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Models (scalar fields only)
|
|
124
|
+
// ============================================================================
|
|
78
125
|
export const User = Schema.Struct({
|
|
79
|
-
|
|
126
|
+
createdAt: Schema.Date,
|
|
80
127
|
email: Schema.String,
|
|
128
|
+
id: UserId,
|
|
81
129
|
name: Schema.NullOr(Schema.String),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
export type User = typeof User.Type;
|
|
130
|
+
})
|
|
131
|
+
export type User = typeof User.Type
|
|
85
132
|
|
|
86
133
|
export const Post = Schema.Struct({
|
|
87
|
-
|
|
88
|
-
title: Schema.String,
|
|
134
|
+
authorId: UserId, // Automatically references User's branded ID
|
|
89
135
|
content: Schema.NullOr(Schema.String),
|
|
136
|
+
id: PostId,
|
|
90
137
|
published: Schema.Boolean,
|
|
91
|
-
|
|
92
|
-
})
|
|
93
|
-
export type Post = typeof Post.Type
|
|
138
|
+
title: Schema.String,
|
|
139
|
+
})
|
|
140
|
+
export type Post = typeof Post.Type
|
|
94
141
|
```
|
|
95
142
|
|
|
96
143
|
## Configuration Options
|
|
97
144
|
|
|
145
|
+
All options are configured in your `schema.prisma`:
|
|
146
|
+
|
|
98
147
|
```prisma
|
|
99
148
|
generator effectSchema {
|
|
100
|
-
provider
|
|
101
|
-
output
|
|
149
|
+
provider = "prisma-effect-schema"
|
|
150
|
+
output = "./generated/schemas.ts"
|
|
151
|
+
|
|
152
|
+
// Generate branded ID types (UserId, PostId, etc.)
|
|
153
|
+
// Default: true
|
|
154
|
+
useBrandedIds = "true"
|
|
102
155
|
|
|
103
156
|
// Include relation fields (uses Schema.suspend for circular refs)
|
|
157
|
+
// When true, generates both `Model` and `ModelWithRelations` schemas
|
|
104
158
|
// Default: false
|
|
105
159
|
includeRelations = "true"
|
|
106
160
|
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
161
|
+
// How to handle DateTime fields
|
|
162
|
+
// - "Date": Schema.Date (for Prisma results - Date objects)
|
|
163
|
+
// - "DateTimeString": Schema.DateTimeUtc (for API validation - ISO strings)
|
|
164
|
+
// Default: "Date"
|
|
165
|
+
dateTimeHandling = "Date"
|
|
110
166
|
|
|
111
167
|
// Sort fields alphabetically for deterministic output
|
|
112
168
|
// Default: true
|
|
113
|
-
sortFields
|
|
169
|
+
sortFields = "true"
|
|
170
|
+
|
|
171
|
+
// Custom header for the generated file (replaces default header)
|
|
172
|
+
// customHeader = "// My custom header\nimport { Schema } from 'effect'"
|
|
114
173
|
}
|
|
115
174
|
```
|
|
116
175
|
|
|
176
|
+
### Option Details
|
|
177
|
+
|
|
178
|
+
#### `useBrandedIds`
|
|
179
|
+
|
|
180
|
+
When enabled (default), generates branded ID types for models with String primary keys:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// With useBrandedIds = true
|
|
184
|
+
export const UserId = Schema.String.pipe(Schema.brand("UserId"))
|
|
185
|
+
export const User = Schema.Struct({
|
|
186
|
+
id: UserId, // Branded type
|
|
187
|
+
// ...
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// With useBrandedIds = false
|
|
191
|
+
export const User = Schema.Struct({
|
|
192
|
+
id: Schema.String, // Plain string
|
|
193
|
+
// ...
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The branded ID name uses the primary key field name:
|
|
198
|
+
- `User.id` (String @id) -> `UserId`
|
|
199
|
+
- `Course.slug` (String @id) -> `CourseSlug`
|
|
200
|
+
|
|
201
|
+
Foreign keys automatically reference their target model's branded ID:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
export const Post = Schema.Struct({
|
|
205
|
+
authorId: UserId, // Automatically uses UserId, not plain String
|
|
206
|
+
// ...
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### `includeRelations`
|
|
211
|
+
|
|
212
|
+
When enabled, generates additional `*WithRelations` schemas:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// Base schema (always generated)
|
|
216
|
+
export const Post = Schema.Struct({
|
|
217
|
+
id: PostId,
|
|
218
|
+
title: Schema.String,
|
|
219
|
+
authorId: UserId,
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// With relations (only when includeRelations = true)
|
|
223
|
+
export const PostWithRelations = Schema.Struct({
|
|
224
|
+
id: PostId,
|
|
225
|
+
title: Schema.String,
|
|
226
|
+
authorId: UserId,
|
|
227
|
+
author: Schema.suspend(() => User), // Handles circular refs
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### `dateTimeHandling`
|
|
232
|
+
|
|
233
|
+
Choose how DateTime fields are handled:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// dateTimeHandling = "Date" (default)
|
|
237
|
+
// For validating Prisma query results (Date objects)
|
|
238
|
+
createdAt: Schema.Date
|
|
239
|
+
|
|
240
|
+
// dateTimeHandling = "DateTimeString"
|
|
241
|
+
// For validating API input (ISO 8601 strings)
|
|
242
|
+
createdAt: Schema.DateTimeUtc
|
|
243
|
+
```
|
|
244
|
+
|
|
117
245
|
## Type Mapping
|
|
118
246
|
|
|
119
|
-
| Prisma Type | Effect Schema |
|
|
120
|
-
| -------------- | ----------------------------- |
|
|
121
|
-
| `String` | `Schema.String` |
|
|
122
|
-
| `Int` | `Schema.Int` |
|
|
123
|
-
| `Float` | `Schema.Number` |
|
|
124
|
-
| `Boolean` | `Schema.Boolean` |
|
|
125
|
-
| `DateTime` | `Schema.Date` |
|
|
126
|
-
| `Json` | `JsonValueSchema`
|
|
127
|
-
| `Bytes` | `Schema.Uint8Array` |
|
|
128
|
-
| `BigInt` | `Schema.BigInt` |
|
|
129
|
-
| `Decimal` | `Schema.
|
|
130
|
-
| `Enum` | `Schema.Literal(...)` |
|
|
131
|
-
| Optional (`?`) | `Schema.NullOr(...)` |
|
|
132
|
-
| List (`[]`) | `Schema.Array(...)` |
|
|
247
|
+
| Prisma Type | Effect Schema | Notes |
|
|
248
|
+
| -------------- | ----------------------------- | ------------------------------- |
|
|
249
|
+
| `String` | `Schema.String` | Or branded ID if PK/FK |
|
|
250
|
+
| `Int` | `Schema.Int` | |
|
|
251
|
+
| `Float` | `Schema.Number` | |
|
|
252
|
+
| `Boolean` | `Schema.Boolean` | |
|
|
253
|
+
| `DateTime` | `Schema.Date` | Or `Schema.DateTimeUtc` |
|
|
254
|
+
| `Json` | `JsonValueSchema` | Recursive schema for JSON |
|
|
255
|
+
| `Bytes` | `Schema.Uint8Array` | |
|
|
256
|
+
| `BigInt` | `Schema.BigInt` | |
|
|
257
|
+
| `Decimal` | `Schema.Decimal` | |
|
|
258
|
+
| `Enum` | `Schema.Literal(...)` | All values as union |
|
|
259
|
+
| Optional (`?`) | `Schema.NullOr(...)` | Wraps the inner type |
|
|
260
|
+
| List (`[]`) | `Schema.Array(...)` | Wraps the inner type |
|
|
261
|
+
|
|
262
|
+
## Advanced Usage
|
|
133
263
|
|
|
134
|
-
|
|
264
|
+
### Enums
|
|
135
265
|
|
|
136
|
-
|
|
266
|
+
Prisma enums are converted to `Schema.Literal` unions:
|
|
137
267
|
|
|
138
|
-
|
|
268
|
+
```prisma
|
|
269
|
+
enum PostStatus {
|
|
270
|
+
DRAFT
|
|
271
|
+
PUBLISHED
|
|
272
|
+
ARCHIVED
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Generates:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
export const PostStatus = Schema.Literal("ARCHIVED", "DRAFT", "PUBLISHED")
|
|
280
|
+
export type PostStatus = typeof PostStatus.Type
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### JSON Fields
|
|
284
|
+
|
|
285
|
+
JSON fields use a recursive schema that matches Prisma's `JsonValue` type:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
type JsonValue = string | number | boolean | null | JsonArray | JsonObject
|
|
289
|
+
type JsonArray = ReadonlyArray<JsonValue>
|
|
290
|
+
type JsonObject = { readonly [key: string]: JsonValue }
|
|
291
|
+
|
|
292
|
+
const JsonValueSchema: Schema.Schema<JsonValue> = Schema.suspend(
|
|
293
|
+
(): Schema.Schema<JsonValue> =>
|
|
294
|
+
Schema.Union(
|
|
295
|
+
Schema.Null,
|
|
296
|
+
Schema.Boolean,
|
|
297
|
+
Schema.Number,
|
|
298
|
+
Schema.String,
|
|
299
|
+
Schema.Array(JsonValueSchema),
|
|
300
|
+
Schema.Record({ key: Schema.String, value: JsonValueSchema })
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Transforming for API Input
|
|
306
|
+
|
|
307
|
+
The generated schemas use `Schema.Date` by default, matching Prisma's output. For API input validation where dates come as ISO strings, you can either:
|
|
308
|
+
|
|
309
|
+
1. **Use `dateTimeHandling = "DateTimeString"`** to generate schemas that expect ISO strings
|
|
310
|
+
|
|
311
|
+
2. **Transform at the boundary**:
|
|
139
312
|
|
|
140
313
|
```typescript
|
|
141
314
|
import { Schema } from "effect";
|
|
142
|
-
import { User } from "./generated/
|
|
315
|
+
import { User } from "./generated/schemas";
|
|
143
316
|
|
|
144
|
-
// For API input validation
|
|
145
|
-
const UserInput =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
createdAt: new Date(input.createdAt),
|
|
150
|
-
}),
|
|
151
|
-
encode: (user) => ({
|
|
152
|
-
...user,
|
|
153
|
-
createdAt: user.createdAt.toISOString(),
|
|
154
|
-
}),
|
|
155
|
-
}),
|
|
156
|
-
);
|
|
317
|
+
// For API input validation (dates as ISO strings)
|
|
318
|
+
const UserInput = Schema.Struct({
|
|
319
|
+
...User.fields,
|
|
320
|
+
createdAt: Schema.DateFromString, // Accepts ISO string, returns Date
|
|
321
|
+
});
|
|
157
322
|
```
|
|
158
323
|
|
|
159
|
-
|
|
324
|
+
### Programmatic API
|
|
160
325
|
|
|
161
|
-
|
|
326
|
+
Use the generator programmatically for custom workflows:
|
|
162
327
|
|
|
163
328
|
```typescript
|
|
164
|
-
import { generate
|
|
165
|
-
import { getDMMF } from "@prisma/
|
|
329
|
+
import { generate } from "prisma-effect-schema";
|
|
330
|
+
import { getDMMF } from "@prisma/internals";
|
|
166
331
|
|
|
167
332
|
const dmmf = await getDMMF({ datamodelPath: "./prisma/schema.prisma" });
|
|
168
|
-
const config = resolveConfig({ useBrandedIds: true });
|
|
169
333
|
|
|
170
|
-
const { content, stats } = generate({
|
|
171
|
-
|
|
334
|
+
const { content, stats } = generate({
|
|
335
|
+
dmmf,
|
|
336
|
+
config: {
|
|
337
|
+
useBrandedIds: true,
|
|
338
|
+
includeRelations: false,
|
|
339
|
+
dateTimeHandling: "Date",
|
|
340
|
+
sortFields: true,
|
|
341
|
+
customHeader: null,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
console.log(`Generated ${stats.modelCount} models, ${stats.enumCount} enums`);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Requirements
|
|
349
|
+
|
|
350
|
+
- Node.js >= 18
|
|
351
|
+
- Prisma >= 6.0
|
|
352
|
+
- Effect >= 3.0
|
|
353
|
+
|
|
354
|
+
## Contributing
|
|
355
|
+
|
|
356
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
# Clone the repository
|
|
360
|
+
git clone https://github.com/frontcore/prisma-effect-schema.git
|
|
361
|
+
|
|
362
|
+
# Install dependencies
|
|
363
|
+
pnpm install
|
|
364
|
+
|
|
365
|
+
# Run tests
|
|
366
|
+
pnpm test
|
|
367
|
+
|
|
368
|
+
# Build
|
|
369
|
+
pnpm build
|
|
172
370
|
```
|
|
173
371
|
|
|
174
372
|
## License
|
|
175
373
|
|
|
176
|
-
MIT
|
|
374
|
+
[MIT](LICENSE) - Frontcore
|