prisma-generator-express 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -15
- package/package.json +1 -1
- package/src/copy/transformZod.spec.ts +163 -5
- package/src/copy/transformZod.ts +57 -36
package/README.md
CHANGED
|
@@ -114,9 +114,10 @@ The library will create functions to generate routers per each model in schema.
|
|
|
114
114
|
import express, { json } from 'express'
|
|
115
115
|
import type { Response, Request, NextFunction, RequestHandler } from 'express'
|
|
116
116
|
|
|
117
|
-
import { orderItemRouter } from '../prisma/generated/express/orderItem'
|
|
118
|
-
import RouteConfig from '../prisma/generated/express/routeConfig'
|
|
119
117
|
import { PrismaClient } from '../prisma/generated/client'
|
|
118
|
+
import { UserAccountRouter } from '../prisma/generated/express/UserAccount'
|
|
119
|
+
import { RouteConfig } from '~prisma/generated/express/routeConfig'
|
|
120
|
+
import { UserAccountFindFirstSchema } from '../prisma/generated/prisma-zod-generator/schemas'
|
|
120
121
|
|
|
121
122
|
const app = express()
|
|
122
123
|
|
|
@@ -132,28 +133,27 @@ const addPrisma: RequestHandler = (
|
|
|
132
133
|
next: NextFunction,
|
|
133
134
|
) => {
|
|
134
135
|
req.prisma = prisma
|
|
135
|
-
req.omitOutputValidation = true
|
|
136
|
+
// req.omitOutputValidation = true (not required if you use `select` instead of `include`)
|
|
136
137
|
next()
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
|
-
*
|
|
141
|
-
* Demonstrates how to add custom properties to the request object to be used in later middleware or route handlers.
|
|
141
|
+
* Run context-related operations or modify `req` properties to control the behavior of the route
|
|
142
142
|
*/
|
|
143
|
-
const
|
|
143
|
+
const beforeFindFirst: RequestHandler = (
|
|
144
144
|
req: Request,
|
|
145
145
|
res: Response,
|
|
146
146
|
next: NextFunction,
|
|
147
147
|
) => {
|
|
148
|
-
|
|
148
|
+
req.passToNext = true
|
|
149
149
|
next()
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
153
|
+
* if `req.passToNext` is true, then the result of generated middleware
|
|
154
|
+
* will be available in req.locals?.data for modifications
|
|
155
155
|
*/
|
|
156
|
-
const
|
|
156
|
+
const afterFindFirst: RequestHandler = (
|
|
157
157
|
req: Request,
|
|
158
158
|
res: Response,
|
|
159
159
|
next: NextFunction,
|
|
@@ -162,17 +162,39 @@ const afterFindMany: RequestHandler = (
|
|
|
162
162
|
next()
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* For generated route the middleware order will be as follows:
|
|
167
|
+
* 1. Query parser
|
|
168
|
+
* 2. Custom middlewares: config.{method}.before[]
|
|
169
|
+
* 3. Input validator middleware (Optional): config.{method}.input
|
|
170
|
+
* 4. Generated middleware
|
|
171
|
+
* 5. Output validator middleware: config.{method}.input
|
|
172
|
+
* 6. Custom middlewares: config.{method}.after[] (not available if req.passToNext is falsy)
|
|
173
|
+
*/
|
|
165
174
|
const someRouterConfig: RouteConfig<RequestHandler> = {
|
|
166
|
-
|
|
167
|
-
before: [
|
|
168
|
-
after: [
|
|
175
|
+
FindFirst: {
|
|
176
|
+
before: [beforeFindFirst],
|
|
177
|
+
after: [afterFindFirst],
|
|
178
|
+
input: {
|
|
179
|
+
schema: UserAccountFindFirstSchema,
|
|
180
|
+
allow: [
|
|
181
|
+
'select.id',
|
|
182
|
+
'select.full_name',
|
|
183
|
+
'select.emailAddress',
|
|
184
|
+
'select.orders[].ProductName',
|
|
185
|
+
'select.orders[].quantity',
|
|
186
|
+
'where.id',
|
|
187
|
+
'where.createdAt',
|
|
188
|
+
],
|
|
189
|
+
},
|
|
169
190
|
},
|
|
170
191
|
addModelPrefix: true,
|
|
171
192
|
enableAll: true,
|
|
172
193
|
customUrlPrefix: '/v1',
|
|
173
194
|
}
|
|
174
195
|
|
|
175
|
-
app.use(addPrisma
|
|
196
|
+
app.use(addPrisma)
|
|
197
|
+
app.use(UserAccountRouter(someRouterConfig))
|
|
176
198
|
|
|
177
199
|
app.listen(3000, () => {
|
|
178
200
|
console.log('Server is running on http://localhost:3000')
|
|
@@ -197,7 +219,7 @@ The following properties can be attached to the `req` object to control the beha
|
|
|
197
219
|
| ------------ | -------- | ------------ |
|
|
198
220
|
| `findUnique` | `GET` | `/:id` |
|
|
199
221
|
| `findFirst` | `GET` | `/first` |
|
|
200
|
-
| `
|
|
222
|
+
| `FindFirst` | `GET` | `/` |
|
|
201
223
|
| `create` | `POST` | `/` |
|
|
202
224
|
| `createMany` | `POST` | `/many` |
|
|
203
225
|
| `update` | `PUT` | `/` |
|
package/package.json
CHANGED
|
@@ -30,8 +30,8 @@ describe('Cryptocurrency Schema Validation', () => {
|
|
|
30
30
|
const allowedFields = [
|
|
31
31
|
'wallet.id',
|
|
32
32
|
'wallet.owner.name',
|
|
33
|
-
'transactions.amount',
|
|
34
|
-
'transactions.currency',
|
|
33
|
+
'transactions[].amount',
|
|
34
|
+
'transactions[].currency',
|
|
35
35
|
]
|
|
36
36
|
const forbiddenFields = ['wallet.owner.age', 'transactions.details.fee']
|
|
37
37
|
|
|
@@ -225,9 +225,9 @@ describe('Cryptocurrency Schema Validation', () => {
|
|
|
225
225
|
'wallet.id',
|
|
226
226
|
'wallet.owner.name',
|
|
227
227
|
'wallet.owner.age',
|
|
228
|
-
'transactions.amount',
|
|
229
|
-
'transactions.currency',
|
|
230
|
-
'transactions.details.fee',
|
|
228
|
+
'transactions[].amount',
|
|
229
|
+
'transactions[].currency',
|
|
230
|
+
'transactions[].details.fee',
|
|
231
231
|
]
|
|
232
232
|
|
|
233
233
|
try {
|
|
@@ -553,4 +553,162 @@ describe('Cryptocurrency Schema Validation', () => {
|
|
|
553
553
|
expect(error).toBeInstanceOf(ZodError)
|
|
554
554
|
}
|
|
555
555
|
})
|
|
556
|
+
|
|
557
|
+
it('deep nesting', () => {
|
|
558
|
+
const taskSchema = z.object({
|
|
559
|
+
select: z
|
|
560
|
+
.object({
|
|
561
|
+
id: z.boolean().optional(),
|
|
562
|
+
project_id: z.boolean().optional(),
|
|
563
|
+
list_id: z.boolean().optional(),
|
|
564
|
+
user_assignments: z
|
|
565
|
+
.object({
|
|
566
|
+
select: z.object({
|
|
567
|
+
user: z.boolean().optional(),
|
|
568
|
+
}),
|
|
569
|
+
})
|
|
570
|
+
.optional(),
|
|
571
|
+
tags_mappings: z
|
|
572
|
+
.object({
|
|
573
|
+
select: z.object({
|
|
574
|
+
tag: z.boolean().optional(),
|
|
575
|
+
}),
|
|
576
|
+
})
|
|
577
|
+
.optional(),
|
|
578
|
+
attachments: z
|
|
579
|
+
.object({
|
|
580
|
+
select: z.object({
|
|
581
|
+
attachment: z.boolean().optional(),
|
|
582
|
+
}),
|
|
583
|
+
where: z
|
|
584
|
+
.object({
|
|
585
|
+
is_image: z.boolean().optional(),
|
|
586
|
+
})
|
|
587
|
+
.optional(),
|
|
588
|
+
take: z.number().optional(),
|
|
589
|
+
orderBy: z
|
|
590
|
+
.object({
|
|
591
|
+
created_at: z.enum(['asc', 'desc']).optional(),
|
|
592
|
+
})
|
|
593
|
+
.optional(),
|
|
594
|
+
})
|
|
595
|
+
.optional(),
|
|
596
|
+
rendered_description: z.boolean().optional(),
|
|
597
|
+
description: z.boolean().optional(),
|
|
598
|
+
created_at: z.boolean().optional(),
|
|
599
|
+
start_date: z.boolean().optional(),
|
|
600
|
+
reactions: z.boolean().optional(),
|
|
601
|
+
intervals: z.boolean().optional(),
|
|
602
|
+
column_id: z.boolean().optional(),
|
|
603
|
+
priority: z.boolean().optional(),
|
|
604
|
+
due_date: z.boolean().optional(),
|
|
605
|
+
column: z.boolean().optional(),
|
|
606
|
+
title: z.boolean().optional(),
|
|
607
|
+
order: z.boolean().optional(),
|
|
608
|
+
color: z.boolean().optional(),
|
|
609
|
+
})
|
|
610
|
+
.optional(),
|
|
611
|
+
where: z
|
|
612
|
+
.object({
|
|
613
|
+
id: z.string().optional(),
|
|
614
|
+
AND: z
|
|
615
|
+
.array(
|
|
616
|
+
z.object({
|
|
617
|
+
OR: z
|
|
618
|
+
.array(
|
|
619
|
+
z.object({
|
|
620
|
+
to_delete: z.boolean().nullable().optional(),
|
|
621
|
+
}),
|
|
622
|
+
)
|
|
623
|
+
.optional(),
|
|
624
|
+
}),
|
|
625
|
+
)
|
|
626
|
+
.optional(),
|
|
627
|
+
})
|
|
628
|
+
.optional(),
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
const allowedFields = [
|
|
632
|
+
'select.id',
|
|
633
|
+
'select.project_id',
|
|
634
|
+
'select.list_id',
|
|
635
|
+
'select.user_assignments.select.user',
|
|
636
|
+
'select.tags_mappings.select.tag',
|
|
637
|
+
'select.attachments.select.attachment',
|
|
638
|
+
'select.attachments.where.is_image',
|
|
639
|
+
'select.attachments.take',
|
|
640
|
+
'select.attachments.orderBy.created_at',
|
|
641
|
+
'select.rendered_description',
|
|
642
|
+
'select.description',
|
|
643
|
+
'select.created_at',
|
|
644
|
+
'select.start_date',
|
|
645
|
+
'select.reactions',
|
|
646
|
+
'select.intervals',
|
|
647
|
+
'select.column_id',
|
|
648
|
+
'select.priority',
|
|
649
|
+
'select.due_date',
|
|
650
|
+
'select.column',
|
|
651
|
+
'select.order',
|
|
652
|
+
'select.color',
|
|
653
|
+
'where.id',
|
|
654
|
+
'where.AND[].OR[].to_delete',
|
|
655
|
+
]
|
|
656
|
+
const inputData = {
|
|
657
|
+
select: {
|
|
658
|
+
id: true,
|
|
659
|
+
project_id: true,
|
|
660
|
+
list_id: true,
|
|
661
|
+
user_assignments: {
|
|
662
|
+
select: {
|
|
663
|
+
user: true,
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
tags_mappings: {
|
|
667
|
+
select: {
|
|
668
|
+
tag: true,
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
attachments: {
|
|
672
|
+
select: {
|
|
673
|
+
attachment: true,
|
|
674
|
+
},
|
|
675
|
+
where: {
|
|
676
|
+
is_image: true,
|
|
677
|
+
},
|
|
678
|
+
take: 100,
|
|
679
|
+
orderBy: {
|
|
680
|
+
created_at: 'desc',
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
rendered_description: true,
|
|
684
|
+
description: true,
|
|
685
|
+
created_at: true,
|
|
686
|
+
start_date: true,
|
|
687
|
+
reactions: true,
|
|
688
|
+
intervals: true,
|
|
689
|
+
column_id: true,
|
|
690
|
+
priority: true,
|
|
691
|
+
due_date: true,
|
|
692
|
+
column: true,
|
|
693
|
+
title: true,
|
|
694
|
+
order: true,
|
|
695
|
+
color: true,
|
|
696
|
+
},
|
|
697
|
+
where: {
|
|
698
|
+
id: 'task_id',
|
|
699
|
+
AND: [
|
|
700
|
+
{
|
|
701
|
+
OR: [{ to_delete: false }, { to_delete: null }],
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
},
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
const result = allow(taskSchema, allowedFields).safeParse(inputData)
|
|
709
|
+
expect(result.success).toBe(false)
|
|
710
|
+
} catch (error) {
|
|
711
|
+
expect(error).toBeInstanceOf(ZodError)
|
|
712
|
+
}
|
|
713
|
+
})
|
|
556
714
|
})
|
package/src/copy/transformZod.ts
CHANGED
|
@@ -9,7 +9,32 @@ import {
|
|
|
9
9
|
ZodTypeAny,
|
|
10
10
|
} from 'zod'
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
function startsWith(str: string, prefix: string): boolean {
|
|
13
|
+
return str.slice(0, prefix.length) === prefix
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function every<T>(
|
|
17
|
+
array: T[],
|
|
18
|
+
callback: (value: T, index: number, array: T[]) => boolean,
|
|
19
|
+
): boolean {
|
|
20
|
+
for (let i = 0; i < array.length; i++) {
|
|
21
|
+
if (!callback(array[i], i, array)) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isKeyAllowed(key: string, allowedPaths: string[]): boolean {
|
|
29
|
+
return !every(
|
|
30
|
+
allowedPaths,
|
|
31
|
+
(path) =>
|
|
32
|
+
!startsWith(key.replace(/\[\d+\]/g, ''), path.replace(/\[\d+\]/g, '')) &&
|
|
33
|
+
!startsWith(path.replace(/\[\d+\]/g, ''), key.replace(/\[\d+\]/g, '')),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function allow<T extends ZodTypeAny>(
|
|
13
38
|
schema: T,
|
|
14
39
|
allowedPaths: string[],
|
|
15
40
|
): ZodEffects<T, any, any> {
|
|
@@ -20,11 +45,7 @@ export function allow<T extends z.ZodTypeAny>(
|
|
|
20
45
|
const disallowedPaths: string[] = []
|
|
21
46
|
|
|
22
47
|
for (const key of Object.keys(flatData)) {
|
|
23
|
-
if (
|
|
24
|
-
allowedPaths.every(
|
|
25
|
-
(path) => !(key.startsWith(path) || path.startsWith(key)),
|
|
26
|
-
)
|
|
27
|
-
) {
|
|
48
|
+
if (!isKeyAllowed(key, allowedPaths)) {
|
|
28
49
|
disallowedPaths.push(key)
|
|
29
50
|
}
|
|
30
51
|
}
|
|
@@ -40,6 +61,7 @@ export function allow<T extends z.ZodTypeAny>(
|
|
|
40
61
|
}
|
|
41
62
|
throw new ZodError(errors)
|
|
42
63
|
}
|
|
64
|
+
|
|
43
65
|
return data
|
|
44
66
|
}) as ZodEffects<T, any, any>
|
|
45
67
|
}
|
|
@@ -74,16 +96,6 @@ export function forbid<T extends z.ZodTypeAny>(
|
|
|
74
96
|
}) as ZodEffects<T, any, any>
|
|
75
97
|
}
|
|
76
98
|
|
|
77
|
-
function isJsonLikeUnion(schemaPart: ZodTypeAny): boolean {
|
|
78
|
-
if (schemaPart instanceof z.ZodOptional) {
|
|
79
|
-
schemaPart = schemaPart.unwrap()
|
|
80
|
-
}
|
|
81
|
-
return (
|
|
82
|
-
schemaPart instanceof z.ZodUnion &&
|
|
83
|
-
schemaPart.options.some((option: ZodTypeAny) => option instanceof z.ZodLazy)
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
99
|
export function flattenObject(
|
|
88
100
|
obj: Record<string, any>,
|
|
89
101
|
prefix = '',
|
|
@@ -91,29 +103,38 @@ export function flattenObject(
|
|
|
91
103
|
): Record<string, any> {
|
|
92
104
|
const result: Record<string, any> = {}
|
|
93
105
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (currentSchema && isJsonLikeUnion(currentSchema)) {
|
|
103
|
-
result[`${pre}${key}`] = obj[key]
|
|
104
|
-
} else if (
|
|
105
|
-
typeof obj[key] === 'object' &&
|
|
106
|
-
obj[key] !== null &&
|
|
107
|
-
currentSchema instanceof ZodObject
|
|
108
|
-
) {
|
|
109
|
-
Object.assign(
|
|
110
|
-
result,
|
|
111
|
-
flattenObject(obj[key], `${pre}${key}`, currentSchema),
|
|
112
|
-
)
|
|
106
|
+
function flatten(current: any, prop: string, schema?: ZodObject<any>) {
|
|
107
|
+
if (Object(current) !== current) {
|
|
108
|
+
result[prop] = current
|
|
109
|
+
} else if (Array.isArray(current)) {
|
|
110
|
+
current.forEach((item, index) => {
|
|
111
|
+
flatten(item, `${prop}[]`, schema)
|
|
112
|
+
})
|
|
113
113
|
} else {
|
|
114
|
-
|
|
114
|
+
let isEmpty = true
|
|
115
|
+
for (const key in current) {
|
|
116
|
+
if (current.hasOwnProperty(key)) {
|
|
117
|
+
isEmpty = false
|
|
118
|
+
const currentSchema = schema?.shape[key]
|
|
119
|
+
if (
|
|
120
|
+
currentSchema instanceof z.ZodOptional &&
|
|
121
|
+
current[key] === undefined
|
|
122
|
+
) {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
flatten(
|
|
126
|
+
current[key],
|
|
127
|
+
prop ? `${prop}.${key}` : key,
|
|
128
|
+
currentSchema instanceof ZodObject ? currentSchema : undefined,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (isEmpty) {
|
|
133
|
+
result[prop] = {}
|
|
134
|
+
}
|
|
115
135
|
}
|
|
116
136
|
}
|
|
117
137
|
|
|
138
|
+
flatten(obj, prefix, schema)
|
|
118
139
|
return result
|
|
119
140
|
}
|