prisma-generator-express 1.14.0 → 1.14.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/README.md CHANGED
@@ -10,7 +10,7 @@ This tool helps you quickly create API endpoints in your Express app using your
10
10
 
11
11
  When you run `npx prisma generate`, it automatically creates two things:
12
12
 
13
- - Service functions that you can import into your Express routes. By default these functions handle CRUD operations and output validation. This behavior can be controlled.
13
+ - Service functions that you can import into your Express routes. By default, these functions handle CRUD operations and output validation. This behavior can be controlled.
14
14
  - Router generator function that lets you select which routes to add to the application and which middlewares to apply.
15
15
 
16
16
  ## Table of Contents
@@ -19,7 +19,7 @@ When you run `npx prisma generate`, it automatically creates two things:
19
19
  - [Basic Usage](#basic-usage)
20
20
  - [Router Generator Usage](#router-generator-usage)
21
21
  - [Request Object Properties](#request-object-properties)
22
- - [Roadmap](#roadmap)
22
+ - [Router Schema](#router-schema)
23
23
 
24
24
  # Installation
25
25
 
@@ -72,7 +72,7 @@ app.use((req, res, next) => {
72
72
  - Here’s how you can use a generated function in your Express app:
73
73
 
74
74
  ```ts
75
- import { UserFindUnique } from './generated/UserFindUnique' // Adjust the path as necessary
75
+ import { UserFindUnique } from './generated/api/UserFindUnique' // Adjust the path as necessary
76
76
  import { FindUniqueUserSchema } from './prisma-zod-generator/schemas/FindUniqueUser.schema' // Adjust the path as necessary
77
77
  import { FindUniqueUserSchemaOutput } from './prisma-zod-generator/schemas/FindUniqueUserOutput.schema' // Adjust the path as necessary
78
78
 
@@ -133,7 +133,7 @@ const addPrisma: RequestHandler = (
133
133
  next: NextFunction,
134
134
  ) => {
135
135
  req.prisma = prisma
136
- // req.omitOutputValidation = true (not required if you use `select` instead of `include`)
136
+ // req.omitOutputValidation = true (output validation is not required if you use `select` instead of `include`)
137
137
  next()
138
138
  }
139
139
 
@@ -164,19 +164,19 @@ const afterFindFirst: RequestHandler = (
164
164
 
165
165
  /**
166
166
  * For generated route the middleware order will be as follows:
167
- * 1. Query parser
167
+ * 1. Query parser (kicks in for GET requests)
168
168
  * 2. Custom middlewares: config.{method}.before[]
169
- * 3. Input validator middleware (Optional): config.{method}.input
169
+ * 3. Input validator middleware (Optional): config.{method}.input. For GET request validates `req.query`, for others - `req.body`
170
170
  * 4. Generated middleware
171
171
  * 5. Output validator middleware: config.{method}.input
172
172
  * 6. Custom middlewares: config.{method}.after[] (not available if req.passToNext is falsy)
173
173
  */
174
- const someRouterConfig: RouteConfig<RequestHandler> = {
174
+ const userAccounRouterConfig: RouteConfig<RequestHandler> = {
175
175
  FindFirst: {
176
176
  before: [beforeFindFirst],
177
177
  after: [afterFindFirst],
178
178
  input: {
179
- schema: UserAccountFindFirstSchema,
179
+ schema: UserAccountFindFirstSchema, // make sure you set `isGenerateSelect = true` in prisma-zod-generator
180
180
  allow: [
181
181
  'select.id',
182
182
  'select.full_name',
@@ -194,7 +194,7 @@ const someRouterConfig: RouteConfig<RequestHandler> = {
194
194
  }
195
195
 
196
196
  app.use(addPrisma)
197
- app.use(UserAccountRouter(someRouterConfig))
197
+ app.use(UserAccountRouter(userAccounRouterConfig))
198
198
 
199
199
  app.listen(3000, () => {
200
200
  console.log('Server is running on http://localhost:3000')
@@ -208,9 +208,8 @@ The following properties can be attached to the `req` object to control the beha
208
208
  | Property | Type | Description |
209
209
  | ---------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
210
210
  | `prisma` | PrismaClient | An instance of PrismaClient that allows the middleware to interact with your database. |
211
- | `passToNext` | boolean | Optional, if `true` - the result of a Prisma request will be passed to a next middleware as `if (req.locals) req.locals.data` |
212
- | `query` | Object | A structured object that conforms to Prisma's API for the selected method. |
213
- | `outputValidation` | ZodTypeAny | (Optional) A Zod schema used to validate the data returned from the Prisma query before sending it to the client. This helps ensure the response adheres to expected data formats. |
211
+ | `passToNext` | boolean | Optional, if `true` - the result of a Prisma request will be passed to the next middleware as `if (req.locals) req.locals.data` |
212
+ | `outputValidation` | ZodTypeAny | (Optional) A Zod schema used to validate the data returned from the Prisma query before sending it to the client. |
214
213
  | `omitOutputValidation` | Boolean | (Optional) A flag that, if set to `true`, disables output validation even if a Zod schema is provided. |
215
214
 
216
215
  ## Router Schema
@@ -219,7 +218,10 @@ The following properties can be attached to the `req` object to control the beha
219
218
  | ------------ | -------- | ------------ |
220
219
  | `findUnique` | `GET` | `/:id` |
221
220
  | `findFirst` | `GET` | `/first` |
222
- | `FindFirst` | `GET` | `/` |
221
+ | `findMany` | `GET` | `/` |
222
+ | `aggregate` | `GET` | `/aggregate` |
223
+ | `count` | `GET` | `/count` |
224
+ | `groupBy` | `GET` | `/groupby` |
223
225
  | `create` | `POST` | `/` |
224
226
  | `createMany` | `POST` | `/many` |
225
227
  | `update` | `PUT` | `/` |
@@ -227,9 +229,7 @@ The following properties can be attached to the `req` object to control the beha
227
229
  | `upsert` | `PATCH` | `/` |
228
230
  | `delete` | `DELETE` | `/` |
229
231
  | `deleteMany` | `DELETE` | `/many` |
230
- | `aggregate` | `GET` | `/aggregate` |
231
- | `count` | `GET` | `/count` |
232
- | `groupBy` | `GET` | `/groupby` |
232
+
233
233
 
234
234
  ## Helper functions
235
235
 
@@ -248,7 +248,7 @@ interface ValidatorOptions {
248
248
 
249
249
  ### encodeQueryParams(params: Params)
250
250
 
251
- Can be used on frontend to encode Prisma compatible queries. Alternatively `qs` can be used, but it probably won't work with `OR: [{ blah: false }, { blah: null }]` or some other edge cases.
251
+ It can be used on the frontend to encode Prisma-compatible queries. Alternatively `qs` can be used, but it probably won't work with `OR: [{ blah: false }, { blah: null }]` or some other edge cases.
252
252
 
253
253
  ```ts
254
254
  type RecursiveUrlParams = {
@@ -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.14.0",
4
+ "version": "1.14.2",
5
5
  "main": "dist/generator.js",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -35,12 +35,12 @@
35
35
  "@types/express": "^4.17.21",
36
36
  "@types/jest": "29.5.12",
37
37
  "@types/lodash": "^4.17.4",
38
- "@types/node": "20.12.12",
38
+ "@types/node": "20.12.13",
39
39
  "@types/prettier": "3.0.0",
40
40
  "jest": "29.7.0",
41
41
  "prisma": "5.14.0",
42
42
  "semantic-release": "^23.1.1",
43
- "ts-jest": "29.1.3",
43
+ "ts-jest": "29.1.4",
44
44
  "typescript": "5.4.5"
45
45
  },
46
46
  "homepage": "https://github.com/multipliedtwice/prisma-generator-express/blob/master/README.md",
@@ -553,106 +553,7 @@ 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
- ]
556
+ describe('Prisma example', () => {
656
557
  const inputData = {
657
558
  select: {
658
559
  id: true,
@@ -703,12 +604,160 @@ describe('Cryptocurrency Schema Validation', () => {
703
604
  ],
704
605
  },
705
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
+ })
706
680
 
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
- }
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
+ })
713
762
  })
714
763
  })
@@ -38,10 +38,11 @@ export function allow<T extends ZodTypeAny>(
38
38
  schema: T,
39
39
  allowedPaths: string[],
40
40
  ): ZodEffects<T, any, any> {
41
- const rootSchema = schema instanceof z.ZodObject ? schema : undefined
41
+ const rootSchema = schema instanceof z.ZodObject ? schema.strict() : undefined
42
42
 
43
- return schema.transform((data) => {
43
+ return rootSchema?.transform((data) => {
44
44
  const flatData = flattenObject(data, '', rootSchema)
45
+
45
46
  const disallowedPaths: string[] = []
46
47
 
47
48
  for (const key of Object.keys(flatData)) {
@@ -51,19 +52,11 @@ export function allow<T extends ZodTypeAny>(
51
52
  }
52
53
 
53
54
  if (disallowedPaths.length > 0) {
54
- const errors: ZodIssue[] = []
55
- for (const path of disallowedPaths) {
56
- errors.push({
57
- code: ZodIssueCode.custom,
58
- message: `Field '${path}' is not allowed.`,
59
- path: path.split('.'),
60
- })
61
- }
62
- throw new ZodError(errors)
55
+ throw createZodErrorFromPaths(disallowedPaths, 'Field is not allowed:')
63
56
  }
64
57
 
65
58
  return data
66
- }) as ZodEffects<T, any, any>
59
+ }) as unknown as ZodEffects<T, any, any>
67
60
  }
68
61
 
69
62
  export function forbid<T extends z.ZodTypeAny>(
@@ -82,15 +75,7 @@ export function forbid<T extends z.ZodTypeAny>(
82
75
  }
83
76
 
84
77
  if (forbiddenMatches.length > 0) {
85
- const errors: ZodIssue[] = []
86
- for (const path of forbiddenMatches) {
87
- errors.push({
88
- code: ZodIssueCode.custom,
89
- message: `Field '${path}' is forbidden.`,
90
- path: [path],
91
- })
92
- }
93
- throw new ZodError(errors)
78
+ throw createZodErrorFromPaths(forbiddenMatches, 'Field is forbidden:')
94
79
  }
95
80
  return data
96
81
  }) as ZodEffects<T, any, any>
@@ -138,3 +123,18 @@ export function flattenObject(
138
123
  flatten(obj, prefix, schema)
139
124
  return result
140
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[],