prisma-generator-express 1.13.0 → 1.14.1

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 (kicks in for GET requests)
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, // make sure you set `isGenerateSelect = true` in prisma-zod-generator
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` | `/` |
@@ -27,7 +27,7 @@ import { ${modelName}GroupBy } from './${modelName}GroupBy';
27
27
  import { createValidatorMiddleware, ValidatorOptions } from '../createValidatorMiddleware'
28
28
  import { createOutputValidatorMiddleware } from '../createOutputValidatorMiddleware'
29
29
  import { RouteConfig, ValidatorConfig } from '../routeConfig'
30
- import { parseQueryParams } from "../ParseQueryParams";
30
+ import { parseQueryParams } from "../parseQueryParams";
31
31
 
32
32
  const defaultBeforeAfter = {
33
33
  before: [] as RequestHandler[],
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.1",
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,211 @@ describe('Cryptocurrency Schema Validation', () => {
553
553
  expect(error).toBeInstanceOf(ZodError)
554
554
  }
555
555
  })
556
+ describe('Prisma example', () => {
557
+ const inputData = {
558
+ select: {
559
+ id: true,
560
+ project_id: true,
561
+ list_id: true,
562
+ user_assignments: {
563
+ select: {
564
+ user: true,
565
+ },
566
+ },
567
+ tags_mappings: {
568
+ select: {
569
+ tag: true,
570
+ },
571
+ },
572
+ attachments: {
573
+ select: {
574
+ attachment: true,
575
+ },
576
+ where: {
577
+ is_image: true,
578
+ },
579
+ take: 100,
580
+ orderBy: {
581
+ created_at: 'desc',
582
+ },
583
+ },
584
+ rendered_description: true,
585
+ description: true,
586
+ created_at: true,
587
+ start_date: true,
588
+ reactions: true,
589
+ intervals: true,
590
+ column_id: true,
591
+ priority: true,
592
+ due_date: true,
593
+ column: true,
594
+ title: true,
595
+ order: true,
596
+ color: true,
597
+ },
598
+ where: {
599
+ id: 'task_id',
600
+ AND: [
601
+ {
602
+ OR: [{ to_delete: false }, { to_delete: null }],
603
+ },
604
+ ],
605
+ },
606
+ }
607
+ it('deep nesting', () => {
608
+ const taskSchema = z.object({
609
+ select: z
610
+ .object({
611
+ id: z.boolean().optional(),
612
+ project_id: z.boolean().optional(),
613
+ list_id: z.boolean().optional(),
614
+ user_assignments: z
615
+ .object({
616
+ select: z.object({
617
+ user: z.boolean().optional(),
618
+ }),
619
+ })
620
+ .optional(),
621
+ tags_mappings: z
622
+ .object({
623
+ select: z.object({
624
+ tag: z.boolean().optional(),
625
+ }),
626
+ })
627
+ .optional(),
628
+ attachments: z
629
+ .object({
630
+ select: z.object({
631
+ attachment: z.boolean().optional(),
632
+ }),
633
+ where: z
634
+ .object({
635
+ is_image: z.boolean().optional(),
636
+ })
637
+ .optional(),
638
+ take: z.number().optional(),
639
+ orderBy: z
640
+ .object({
641
+ created_at: z.enum(['asc', 'desc']).optional(),
642
+ })
643
+ .optional(),
644
+ })
645
+ .optional(),
646
+ rendered_description: z.boolean().optional(),
647
+ description: z.boolean().optional(),
648
+ created_at: z.boolean().optional(),
649
+ start_date: z.boolean().optional(),
650
+ reactions: z.boolean().optional(),
651
+ intervals: z.boolean().optional(),
652
+ column_id: z.boolean().optional(),
653
+ priority: z.boolean().optional(),
654
+ due_date: z.boolean().optional(),
655
+ column: z.boolean().optional(),
656
+ title: z.boolean().optional(),
657
+ order: z.boolean().optional(),
658
+ color: z.boolean().optional(),
659
+ })
660
+ .optional(),
661
+ where: z
662
+ .object({
663
+ id: z.string().optional(),
664
+ AND: z
665
+ .array(
666
+ z.object({
667
+ OR: z
668
+ .array(
669
+ z.object({
670
+ to_delete: z.boolean().nullable().optional(),
671
+ }),
672
+ )
673
+ .optional(),
674
+ }),
675
+ )
676
+ .optional(),
677
+ })
678
+ .optional(),
679
+ })
680
+
681
+ const allowedFields = [
682
+ 'select.id',
683
+ 'select.project_id',
684
+ 'select.list_id',
685
+ 'select.user_assignments.select.user',
686
+ 'select.tags_mappings.select.tag',
687
+ 'select.attachments.select.attachment',
688
+ 'select.attachments.where.is_image',
689
+ 'select.attachments.take',
690
+ 'select.attachments.orderBy.created_at',
691
+ 'select.rendered_description',
692
+ 'select.description',
693
+ 'select.created_at',
694
+ 'select.start_date',
695
+ 'select.reactions',
696
+ 'select.intervals',
697
+ 'select.column_id',
698
+ 'select.priority',
699
+ 'select.due_date',
700
+ 'select.column',
701
+ 'select.order',
702
+ 'select.color',
703
+ 'where.id',
704
+ 'where.AND[].OR[].to_delete',
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
+ })
714
+
715
+ it('deep nesting 2', () => {
716
+ const taskSchema = z.object({
717
+ where: z
718
+ .object({
719
+ id: z.string().optional(),
720
+ AND: z
721
+ .array(
722
+ z.object({
723
+ OR: z
724
+ .array(
725
+ z.object({
726
+ to_delete: z.boolean().nullable().optional(),
727
+ }),
728
+ )
729
+ .optional(),
730
+ }),
731
+ )
732
+ .optional(),
733
+ })
734
+ .optional(),
735
+ })
736
+
737
+ const allowedFields = [
738
+ 'select.id',
739
+ 'select.project_id',
740
+ 'select.list_id',
741
+ 'select.user_assignments.select.user',
742
+ 'select.tags_mappings.select.tag',
743
+ 'select.attachments.select.attachment',
744
+ 'select.attachments.where.is_image',
745
+ 'select.attachments.take',
746
+ 'select.attachments.orderBy.created_at',
747
+ 'select.rendered_description',
748
+ 'select.description',
749
+ 'select.created_at',
750
+ 'select.start_date',
751
+ 'where.id',
752
+ 'where.AND[].OR[].to_delete',
753
+ ]
754
+
755
+ try {
756
+ const result = allow(taskSchema, allowedFields).safeParse(inputData)
757
+ expect(result.success).toBe(false)
758
+ } catch (error) {
759
+ expect(error).toBeInstanceOf(ZodError)
760
+ }
761
+ })
762
+ })
556
763
  })
@@ -9,39 +9,54 @@ 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> {
16
- const rootSchema = schema instanceof z.ZodObject ? schema : undefined
41
+ const rootSchema = schema instanceof z.ZodObject ? schema.strict() : undefined
17
42
 
18
- return schema.transform((data) => {
43
+ return rootSchema?.transform((data) => {
19
44
  const flatData = flattenObject(data, '', rootSchema)
45
+
20
46
  const disallowedPaths: string[] = []
21
47
 
22
48
  for (const key of Object.keys(flatData)) {
23
- if (
24
- allowedPaths.every(
25
- (path) => !(key.startsWith(path) || path.startsWith(key)),
26
- )
27
- ) {
49
+ if (!isKeyAllowed(key, allowedPaths)) {
28
50
  disallowedPaths.push(key)
29
51
  }
30
52
  }
31
53
 
32
54
  if (disallowedPaths.length > 0) {
33
- const errors: ZodIssue[] = []
34
- for (const path of disallowedPaths) {
35
- errors.push({
36
- code: ZodIssueCode.custom,
37
- message: `Field '${path}' is not allowed.`,
38
- path: path.split('.'),
39
- })
40
- }
41
- throw new ZodError(errors)
55
+ throw createZodErrorFromPaths(disallowedPaths, 'Field is not allowed:')
42
56
  }
57
+
43
58
  return data
44
- }) as ZodEffects<T, any, any>
59
+ }) as unknown as ZodEffects<T, any, any>
45
60
  }
46
61
 
47
62
  export function forbid<T extends z.ZodTypeAny>(
@@ -60,30 +75,12 @@ export function forbid<T extends z.ZodTypeAny>(
60
75
  }
61
76
 
62
77
  if (forbiddenMatches.length > 0) {
63
- const errors: ZodIssue[] = []
64
- for (const path of forbiddenMatches) {
65
- errors.push({
66
- code: ZodIssueCode.custom,
67
- message: `Field '${path}' is forbidden.`,
68
- path: [path],
69
- })
70
- }
71
- throw new ZodError(errors)
78
+ throw createZodErrorFromPaths(forbiddenMatches, 'Field is forbidden:')
72
79
  }
73
80
  return data
74
81
  }) as ZodEffects<T, any, any>
75
82
  }
76
83
 
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
84
  export function flattenObject(
88
85
  obj: Record<string, any>,
89
86
  prefix = '',
@@ -91,29 +88,53 @@ export function flattenObject(
91
88
  ): Record<string, any> {
92
89
  const result: Record<string, any> = {}
93
90
 
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
- )
91
+ function flatten(current: any, prop: string, schema?: ZodObject<any>) {
92
+ if (Object(current) !== current) {
93
+ result[prop] = current
94
+ } else if (Array.isArray(current)) {
95
+ current.forEach((item, index) => {
96
+ flatten(item, `${prop}[]`, schema)
97
+ })
113
98
  } else {
114
- result[`${pre}${key}`] = obj[key]
99
+ let isEmpty = true
100
+ for (const key in current) {
101
+ if (current.hasOwnProperty(key)) {
102
+ isEmpty = false
103
+ const currentSchema = schema?.shape[key]
104
+ if (
105
+ currentSchema instanceof z.ZodOptional &&
106
+ current[key] === undefined
107
+ ) {
108
+ continue
109
+ }
110
+ flatten(
111
+ current[key],
112
+ prop ? `${prop}.${key}` : key,
113
+ currentSchema instanceof ZodObject ? currentSchema : undefined,
114
+ )
115
+ }
116
+ }
117
+ if (isEmpty) {
118
+ result[prop] = {}
119
+ }
115
120
  }
116
121
  }
117
122
 
123
+ flatten(obj, prefix, schema)
118
124
  return result
119
125
  }
126
+
127
+ function createZodErrorFromPaths(
128
+ disallowedPaths: string[],
129
+ errorMessage: string,
130
+ ): ZodError {
131
+ const errors: ZodIssue[] = []
132
+ for (const path of disallowedPaths) {
133
+ errors.push({
134
+ code: ZodIssueCode.custom,
135
+ message: `${errorMessage} '${path}'`,
136
+ path: path.split('.'),
137
+ })
138
+ }
139
+ return new ZodError(errors)
140
+ }
@@ -31,7 +31,7 @@ import { ${modelName}GroupBy } from './${modelName}GroupBy';
31
31
  import { createValidatorMiddleware, ValidatorOptions } from '../createValidatorMiddleware'
32
32
  import { createOutputValidatorMiddleware } from '../createOutputValidatorMiddleware'
33
33
  import { RouteConfig, ValidatorConfig } from '../routeConfig'
34
- import { parseQueryParams } from "../ParseQueryParams";
34
+ import { parseQueryParams } from "../parseQueryParams";
35
35
 
36
36
  const defaultBeforeAfter = {
37
37
  before: [] as RequestHandler[],