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 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
- * Before middleware to set a custom property on the request object.
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 beforeFindMany: RequestHandler = (
143
+ const beforeFindFirst: RequestHandler = (
144
144
  req: Request,
145
145
  res: Response,
146
146
  next: NextFunction,
147
147
  ) => {
148
- ;(req as any).passToNext = true
148
+ req.passToNext = true
149
149
  next()
150
150
  }
151
151
 
152
152
  /**
153
- * After middleware placeholder for any post-processing after the main route handler.
154
- * This example just calls next() but can be extended to perform actions like logging or response modification.
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 afterFindMany: RequestHandler = (
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
- findMany: {
167
- before: [beforeFindMany],
168
- after: [afterFindMany],
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, orderItemRouter(someRouterConfig))
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
- | `findMany` | `GET` | `/` |
222
+ | `FindFirst` | `GET` | `/` |
201
223
  | `create` | `POST` | `/` |
202
224
  | `createMany` | `POST` | `/many` |
203
225
  | `update` | `PUT` | `/` |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "prisma-generator-express",
3
3
  "description": "Prisma generator of Express CRUD API",
4
- "version": "1.13.0",
4
+ "version": "1.14.0",
5
5
  "main": "dist/generator.js",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -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
  })
@@ -9,7 +9,32 @@ import {
9
9
  ZodTypeAny,
10
10
  } from 'zod'
11
11
 
12
- export function allow<T extends z.ZodTypeAny>(
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
- for (const key of Object.keys(obj)) {
95
- const pre = prefix.length ? `${prefix}.` : ''
96
- const currentSchema = schema?.shape[key]
97
-
98
- if (currentSchema instanceof z.ZodOptional && obj[key] === undefined) {
99
- continue
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
- result[`${pre}${key}`] = obj[key]
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
  }