@leo-h/create-nodejs-app 1.0.43 → 1.0.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo-h/create-nodejs-app",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
4
4
  "packageManager": "pnpm@9.15.9",
5
5
  "author": "Leonardo Henrique <leonardo0507.business@gmail.com>",
6
6
  "description": "Create a modern Node.js app with TypeScript using one command.",
@@ -62,7 +62,7 @@
62
62
  "prettier": "3.5.3",
63
63
  "rimraf": "6.0.1",
64
64
  "tsx": "4.19.3",
65
- "typescript": "5.8.2",
65
+ "typescript": "5.8.3",
66
66
  "typescript-eslint": "8.28.0",
67
67
  "unbuild": "3.5.0",
68
68
  "vite-tsconfig-paths": "5.1.4",
@@ -33,7 +33,7 @@
33
33
  "prettier": "3.5.3",
34
34
  "rimraf": "6.0.1",
35
35
  "tsx": "4.19.3",
36
- "typescript": "5.8.2",
36
+ "typescript": "5.8.3",
37
37
  "typescript-eslint": "8.28.0",
38
38
  "unbuild": "3.5.0",
39
39
  "vite-tsconfig-paths": "5.1.4",
@@ -26,7 +26,7 @@ importers:
26
26
  version: 22.13.14
27
27
  '@vitest/eslint-plugin':
28
28
  specifier: 1.1.38
29
- version: 1.1.38(@typescript-eslint/utils@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)(vitest@1.6.1(@types/node@22.13.14))
29
+ version: 1.1.38(@typescript-eslint/utils@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)(vitest@1.6.1(@types/node@22.13.14))
30
30
  eslint:
31
31
  specifier: 9.23.0
32
32
  version: 9.23.0(jiti@2.4.2)
@@ -52,17 +52,17 @@ importers:
52
52
  specifier: 4.19.3
53
53
  version: 4.19.3
54
54
  typescript:
55
- specifier: 5.8.2
56
- version: 5.8.2
55
+ specifier: 5.8.3
56
+ version: 5.8.3
57
57
  typescript-eslint:
58
58
  specifier: 8.28.0
59
- version: 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
59
+ version: 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)
60
60
  unbuild:
61
61
  specifier: 3.5.0
62
- version: 3.5.0(typescript@5.8.2)
62
+ version: 3.5.0(typescript@5.8.3)
63
63
  vite-tsconfig-paths:
64
64
  specifier: 5.1.4
65
- version: 5.1.4(typescript@5.8.2)(vite@5.2.13(@types/node@22.13.14))
65
+ version: 5.1.4(typescript@5.8.3)(vite@5.2.13(@types/node@22.13.14))
66
66
  vitest:
67
67
  specifier: 1.6.1
68
68
  version: 1.6.1(@types/node@22.13.14)
@@ -2180,8 +2180,8 @@ packages:
2180
2180
  eslint: ^8.57.0 || ^9.0.0
2181
2181
  typescript: '>=4.8.4 <5.9.0'
2182
2182
 
2183
- typescript@5.8.2:
2184
- resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
2183
+ typescript@5.8.3:
2184
+ resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
2185
2185
  engines: {node: '>=14.17'}
2186
2186
  hasBin: true
2187
2187
 
@@ -2821,32 +2821,32 @@ snapshots:
2821
2821
 
2822
2822
  '@types/resolve@1.20.2': {}
2823
2823
 
2824
- '@typescript-eslint/eslint-plugin@8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)':
2824
+ '@typescript-eslint/eslint-plugin@8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)':
2825
2825
  dependencies:
2826
2826
  '@eslint-community/regexpp': 4.12.1
2827
- '@typescript-eslint/parser': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
2827
+ '@typescript-eslint/parser': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)
2828
2828
  '@typescript-eslint/scope-manager': 8.28.0
2829
- '@typescript-eslint/type-utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
2830
- '@typescript-eslint/utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
2829
+ '@typescript-eslint/type-utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)
2830
+ '@typescript-eslint/utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)
2831
2831
  '@typescript-eslint/visitor-keys': 8.28.0
2832
2832
  eslint: 9.23.0(jiti@2.4.2)
2833
2833
  graphemer: 1.4.0
2834
2834
  ignore: 5.3.1
2835
2835
  natural-compare: 1.4.0
2836
- ts-api-utils: 2.1.0(typescript@5.8.2)
2837
- typescript: 5.8.2
2836
+ ts-api-utils: 2.1.0(typescript@5.8.3)
2837
+ typescript: 5.8.3
2838
2838
  transitivePeerDependencies:
2839
2839
  - supports-color
2840
2840
 
2841
- '@typescript-eslint/parser@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)':
2841
+ '@typescript-eslint/parser@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)':
2842
2842
  dependencies:
2843
2843
  '@typescript-eslint/scope-manager': 8.28.0
2844
2844
  '@typescript-eslint/types': 8.28.0
2845
- '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
2845
+ '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.3)
2846
2846
  '@typescript-eslint/visitor-keys': 8.28.0
2847
2847
  debug: 4.4.0
2848
2848
  eslint: 9.23.0(jiti@2.4.2)
2849
- typescript: 5.8.2
2849
+ typescript: 5.8.3
2850
2850
  transitivePeerDependencies:
2851
2851
  - supports-color
2852
2852
 
@@ -2855,20 +2855,20 @@ snapshots:
2855
2855
  '@typescript-eslint/types': 8.28.0
2856
2856
  '@typescript-eslint/visitor-keys': 8.28.0
2857
2857
 
2858
- '@typescript-eslint/type-utils@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)':
2858
+ '@typescript-eslint/type-utils@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)':
2859
2859
  dependencies:
2860
- '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
2861
- '@typescript-eslint/utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
2860
+ '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.3)
2861
+ '@typescript-eslint/utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)
2862
2862
  debug: 4.4.0
2863
2863
  eslint: 9.23.0(jiti@2.4.2)
2864
- ts-api-utils: 2.1.0(typescript@5.8.2)
2865
- typescript: 5.8.2
2864
+ ts-api-utils: 2.1.0(typescript@5.8.3)
2865
+ typescript: 5.8.3
2866
2866
  transitivePeerDependencies:
2867
2867
  - supports-color
2868
2868
 
2869
2869
  '@typescript-eslint/types@8.28.0': {}
2870
2870
 
2871
- '@typescript-eslint/typescript-estree@8.28.0(typescript@5.8.2)':
2871
+ '@typescript-eslint/typescript-estree@8.28.0(typescript@5.8.3)':
2872
2872
  dependencies:
2873
2873
  '@typescript-eslint/types': 8.28.0
2874
2874
  '@typescript-eslint/visitor-keys': 8.28.0
@@ -2877,19 +2877,19 @@ snapshots:
2877
2877
  is-glob: 4.0.3
2878
2878
  minimatch: 9.0.4
2879
2879
  semver: 7.7.1
2880
- ts-api-utils: 2.1.0(typescript@5.8.2)
2881
- typescript: 5.8.2
2880
+ ts-api-utils: 2.1.0(typescript@5.8.3)
2881
+ typescript: 5.8.3
2882
2882
  transitivePeerDependencies:
2883
2883
  - supports-color
2884
2884
 
2885
- '@typescript-eslint/utils@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)':
2885
+ '@typescript-eslint/utils@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)':
2886
2886
  dependencies:
2887
2887
  '@eslint-community/eslint-utils': 4.4.0(eslint@9.23.0(jiti@2.4.2))
2888
2888
  '@typescript-eslint/scope-manager': 8.28.0
2889
2889
  '@typescript-eslint/types': 8.28.0
2890
- '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
2890
+ '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.3)
2891
2891
  eslint: 9.23.0(jiti@2.4.2)
2892
- typescript: 5.8.2
2892
+ typescript: 5.8.3
2893
2893
  transitivePeerDependencies:
2894
2894
  - supports-color
2895
2895
 
@@ -2898,12 +2898,12 @@ snapshots:
2898
2898
  '@typescript-eslint/types': 8.28.0
2899
2899
  eslint-visitor-keys: 4.2.0
2900
2900
 
2901
- '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)(vitest@1.6.1(@types/node@22.13.14))':
2901
+ '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)(vitest@1.6.1(@types/node@22.13.14))':
2902
2902
  dependencies:
2903
- '@typescript-eslint/utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
2903
+ '@typescript-eslint/utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)
2904
2904
  eslint: 9.23.0(jiti@2.4.2)
2905
2905
  optionalDependencies:
2906
- typescript: 5.8.2
2906
+ typescript: 5.8.3
2907
2907
  vitest: 1.6.1(@types/node@22.13.14)
2908
2908
 
2909
2909
  '@vitest/expect@1.6.1':
@@ -3688,7 +3688,7 @@ snapshots:
3688
3688
 
3689
3689
  minipass@7.1.2: {}
3690
3690
 
3691
- mkdist@2.2.0(typescript@5.8.2):
3691
+ mkdist@2.2.0(typescript@5.8.3):
3692
3692
  dependencies:
3693
3693
  autoprefixer: 10.4.20(postcss@8.5.3)
3694
3694
  citty: 0.1.6
@@ -3704,7 +3704,7 @@ snapshots:
3704
3704
  semver: 7.7.1
3705
3705
  tinyglobby: 0.2.12
3706
3706
  optionalDependencies:
3707
- typescript: 5.8.2
3707
+ typescript: 5.8.3
3708
3708
 
3709
3709
  mlly@1.7.1:
3710
3710
  dependencies:
@@ -4041,11 +4041,11 @@ snapshots:
4041
4041
  glob: 11.0.0
4042
4042
  package-json-from-dist: 1.0.1
4043
4043
 
4044
- rollup-plugin-dts@6.1.1(rollup@4.34.8)(typescript@5.8.2):
4044
+ rollup-plugin-dts@6.1.1(rollup@4.34.8)(typescript@5.8.3):
4045
4045
  dependencies:
4046
4046
  magic-string: 0.30.17
4047
4047
  rollup: 4.34.8
4048
- typescript: 5.8.2
4048
+ typescript: 5.8.3
4049
4049
  optionalDependencies:
4050
4050
  '@babel/code-frame': 7.24.7
4051
4051
 
@@ -4210,13 +4210,13 @@ snapshots:
4210
4210
  dependencies:
4211
4211
  is-number: 7.0.0
4212
4212
 
4213
- ts-api-utils@2.1.0(typescript@5.8.2):
4213
+ ts-api-utils@2.1.0(typescript@5.8.3):
4214
4214
  dependencies:
4215
- typescript: 5.8.2
4215
+ typescript: 5.8.3
4216
4216
 
4217
- tsconfck@3.1.0(typescript@5.8.2):
4217
+ tsconfck@3.1.0(typescript@5.8.3):
4218
4218
  optionalDependencies:
4219
- typescript: 5.8.2
4219
+ typescript: 5.8.3
4220
4220
 
4221
4221
  tsx@4.19.3:
4222
4222
  dependencies:
@@ -4231,23 +4231,23 @@ snapshots:
4231
4231
 
4232
4232
  type-detect@4.0.8: {}
4233
4233
 
4234
- typescript-eslint@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2):
4234
+ typescript-eslint@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3):
4235
4235
  dependencies:
4236
- '@typescript-eslint/eslint-plugin': 8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
4237
- '@typescript-eslint/parser': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
4238
- '@typescript-eslint/utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
4236
+ '@typescript-eslint/eslint-plugin': 8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)
4237
+ '@typescript-eslint/parser': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)
4238
+ '@typescript-eslint/utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.3)
4239
4239
  eslint: 9.23.0(jiti@2.4.2)
4240
- typescript: 5.8.2
4240
+ typescript: 5.8.3
4241
4241
  transitivePeerDependencies:
4242
4242
  - supports-color
4243
4243
 
4244
- typescript@5.8.2: {}
4244
+ typescript@5.8.3: {}
4245
4245
 
4246
4246
  ufo@1.5.3: {}
4247
4247
 
4248
4248
  ufo@1.5.4: {}
4249
4249
 
4250
- unbuild@3.5.0(typescript@5.8.2):
4250
+ unbuild@3.5.0(typescript@5.8.3):
4251
4251
  dependencies:
4252
4252
  '@rollup/plugin-alias': 5.1.1(rollup@4.34.8)
4253
4253
  '@rollup/plugin-commonjs': 28.0.2(rollup@4.34.8)
@@ -4263,18 +4263,18 @@ snapshots:
4263
4263
  hookable: 5.5.3
4264
4264
  jiti: 2.4.2
4265
4265
  magic-string: 0.30.17
4266
- mkdist: 2.2.0(typescript@5.8.2)
4266
+ mkdist: 2.2.0(typescript@5.8.3)
4267
4267
  mlly: 1.7.4
4268
4268
  pathe: 2.0.3
4269
4269
  pkg-types: 2.0.0
4270
4270
  pretty-bytes: 6.1.1
4271
4271
  rollup: 4.34.8
4272
- rollup-plugin-dts: 6.1.1(rollup@4.34.8)(typescript@5.8.2)
4272
+ rollup-plugin-dts: 6.1.1(rollup@4.34.8)(typescript@5.8.3)
4273
4273
  scule: 1.3.0
4274
4274
  tinyglobby: 0.2.12
4275
4275
  untyped: 2.0.0
4276
4276
  optionalDependencies:
4277
- typescript: 5.8.2
4277
+ typescript: 5.8.3
4278
4278
  transitivePeerDependencies:
4279
4279
  - sass
4280
4280
  - vue
@@ -4319,11 +4319,11 @@ snapshots:
4319
4319
  - supports-color
4320
4320
  - terser
4321
4321
 
4322
- vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@5.2.13(@types/node@22.13.14)):
4322
+ vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@5.2.13(@types/node@22.13.14)):
4323
4323
  dependencies:
4324
4324
  debug: 4.4.0
4325
4325
  globrex: 0.1.2
4326
- tsconfck: 3.1.0(typescript@5.8.2)
4326
+ tsconfck: 3.1.0(typescript@5.8.3)
4327
4327
  optionalDependencies:
4328
4328
  vite: 5.2.13(@types/node@22.13.14)
4329
4329
  transitivePeerDependencies:
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "*.{js,ts,json,yaml,yml,md}": "prettier --write --cache",
3
- "*.{js,ts}": ["eslint --max-warnings 0 --fix --cache"]
3
+ "*.{js,ts}": ["eslint --max-warnings 0 --fix --cache --no-ignore"]
4
4
  }
@@ -18,10 +18,12 @@
18
18
  "dependencies": {
19
19
  "@fastify/cookie": "11.0.2",
20
20
  "@fastify/cors": "11.0.1",
21
+ "@fastify/multipart": "9.0.3",
21
22
  "@fastify/swagger": "9.4.2",
22
23
  "@fastify/swagger-ui": "5.2.2",
23
24
  "dotenv": "16.4.7",
24
25
  "fastify": "5.3.2",
26
+ "fastify-plugin": "5.0.1",
25
27
  "fastify-type-provider-zod": "4.0.2",
26
28
  "zod": "3.24.2"
27
29
  },
@@ -14,6 +14,9 @@ importers:
14
14
  '@fastify/cors':
15
15
  specifier: 11.0.1
16
16
  version: 11.0.1
17
+ '@fastify/multipart':
18
+ specifier: 9.0.3
19
+ version: 9.0.3
17
20
  '@fastify/swagger':
18
21
  specifier: 9.4.2
19
22
  version: 9.4.2
@@ -26,6 +29,9 @@ importers:
26
29
  fastify:
27
30
  specifier: 5.3.2
28
31
  version: 5.3.2
32
+ fastify-plugin:
33
+ specifier: 5.0.1
34
+ version: 5.0.1
29
35
  fastify-type-provider-zod:
30
36
  specifier: 4.0.2
31
37
  version: 4.0.2(fastify@5.3.2)(zod@3.24.2)
@@ -292,12 +298,18 @@ packages:
292
298
  '@fastify/ajv-compiler@4.0.2':
293
299
  resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==}
294
300
 
301
+ '@fastify/busboy@3.1.1':
302
+ resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==}
303
+
295
304
  '@fastify/cookie@11.0.2':
296
305
  resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
297
306
 
298
307
  '@fastify/cors@11.0.1':
299
308
  resolution: {integrity: sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ==}
300
309
 
310
+ '@fastify/deepmerge@2.0.2':
311
+ resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==}
312
+
301
313
  '@fastify/error@4.1.0':
302
314
  resolution: {integrity: sha512-KeFcciOr1eo/YvIXHP65S94jfEEqn1RxTRBT1aJaHxY5FK0/GDXYozsQMMWlZoHgi8i0s+YtrLsgj/JkUUjSkQ==}
303
315
 
@@ -310,6 +322,9 @@ packages:
310
322
  '@fastify/merge-json-schemas@0.2.1':
311
323
  resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==}
312
324
 
325
+ '@fastify/multipart@9.0.3':
326
+ resolution: {integrity: sha512-pJogxQCrT12/6I5Fh6jr3narwcymA0pv4B0jbC7c6Bl9wnrxomEUnV0d26w6gUls7gSXmhG8JGRMmHFIPsxt1g==}
327
+
313
328
  '@fastify/proxy-addr@5.0.0':
314
329
  resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==}
315
330
 
@@ -1729,6 +1744,9 @@ packages:
1729
1744
  resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
1730
1745
  engines: {node: '>=10'}
1731
1746
 
1747
+ secure-json-parse@3.0.2:
1748
+ resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==}
1749
+
1732
1750
  secure-json-parse@4.0.0:
1733
1751
  resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==}
1734
1752
 
@@ -2231,6 +2249,8 @@ snapshots:
2231
2249
  ajv-formats: 3.0.1(ajv@8.17.1)
2232
2250
  fast-uri: 3.0.6
2233
2251
 
2252
+ '@fastify/busboy@3.1.1': {}
2253
+
2234
2254
  '@fastify/cookie@11.0.2':
2235
2255
  dependencies:
2236
2256
  cookie: 1.0.2
@@ -2241,6 +2261,8 @@ snapshots:
2241
2261
  fastify-plugin: 5.0.1
2242
2262
  toad-cache: 3.7.0
2243
2263
 
2264
+ '@fastify/deepmerge@2.0.2': {}
2265
+
2244
2266
  '@fastify/error@4.1.0': {}
2245
2267
 
2246
2268
  '@fastify/fast-json-stringify-compiler@5.0.3':
@@ -2253,6 +2275,14 @@ snapshots:
2253
2275
  dependencies:
2254
2276
  dequal: 2.0.3
2255
2277
 
2278
+ '@fastify/multipart@9.0.3':
2279
+ dependencies:
2280
+ '@fastify/busboy': 3.1.1
2281
+ '@fastify/deepmerge': 2.0.2
2282
+ '@fastify/error': 4.1.0
2283
+ fastify-plugin: 5.0.1
2284
+ secure-json-parse: 3.0.2
2285
+
2256
2286
  '@fastify/proxy-addr@5.0.0':
2257
2287
  dependencies:
2258
2288
  '@fastify/forwarded': 3.0.0
@@ -3652,6 +3682,8 @@ snapshots:
3652
3682
 
3653
3683
  safe-stable-stringify@2.5.0: {}
3654
3684
 
3685
+ secure-json-parse@3.0.2: {}
3686
+
3655
3687
  secure-json-parse@4.0.0: {}
3656
3688
 
3657
3689
  seek-bzip@2.0.0:
@@ -29,9 +29,18 @@ export function createControllerResponseSchema<
29
29
  >
30
30
  : ErrorInstance<ErrorClasses, K>["schema"];
31
31
  } {
32
- const errorInstances = errorClasses.map(ErrorClass => {
33
- return new ErrorClass();
34
- });
32
+ const errorInstances = errorClasses
33
+ .map(ErrorClass => {
34
+ return new ErrorClass();
35
+ })
36
+ .filter((errorInstance, index, array) => {
37
+ const prevErrorInstances = array.slice(0, index);
38
+ const errorAlreadyFiltered = prevErrorInstances.some(({ error }) => {
39
+ return error === errorInstance.error;
40
+ });
41
+
42
+ return !errorAlreadyFiltered;
43
+ });
35
44
  const statusCodes = [
36
45
  ...new Set([
37
46
  ...Object.keys(successResponses),
@@ -1,5 +1,6 @@
1
1
  import packageJson from "@/../package.json";
2
2
  import { config } from "dotenv";
3
+ import { existsSync, mkdirSync } from "node:fs";
3
4
  import { resolve } from "node:path";
4
5
  import { z } from "zod";
5
6
 
@@ -21,21 +22,33 @@ if (process.env.npm_lifecycle_event?.includes(":test"))
21
22
  if (!process.env.NODE_ENV)
22
23
  throw new Error("Could not set to the environment variables to use.");
23
24
 
24
- const envFileName =
25
- envFileNames[process.env.NODE_ENV as keyof typeof envFileNames];
25
+ const nodeEnv = process.env.NODE_ENV as keyof typeof envFileNames;
26
+ const envFileName = envFileNames[nodeEnv];
26
27
 
27
28
  config({
28
- path: resolve(__dirname, "..", envFileName),
29
+ path: resolve(
30
+ __dirname,
31
+ nodeEnv === "production" ? "../../" : "..",
32
+ envFileName,
33
+ ),
29
34
  override: true,
30
35
  });
31
36
 
32
37
  const schema = z.object({
33
- NODE_ENV: z
34
- .enum(["production", "development", "test"])
35
- .default(process.env.NODE_ENV as keyof typeof envFileNames),
38
+ NODE_ENV: z.enum(["production", "development", "test"]).default(nodeEnv),
36
39
  API_NAME: z.string().default(packageJson.name),
37
40
  API_PORT: z.coerce.number().default(3333),
38
41
  API_ACCESS_PERMISSION_CLIENT_SIDE: z.string().default("*"),
42
+ TMP_FILES_PATH: z
43
+ .string()
44
+ .default("./tmp")
45
+ .transform(pathFromSrc => {
46
+ const tmpPath = resolve(__dirname, "..", pathFromSrc);
47
+
48
+ if (!existsSync(tmpPath)) mkdirSync(tmpPath);
49
+
50
+ return tmpPath;
51
+ }),
39
52
  });
40
53
 
41
54
  const parsedEnv = schema.safeParse(process.env);
@@ -12,8 +12,9 @@ import {
12
12
  import { env } from "../env";
13
13
  import { errorHandlerPlugin } from "./plugins/error-handler.plugin";
14
14
  import { notFoundErrorHandlerPlugin } from "./plugins/not-found-error-handler.plugin";
15
- import { swaggerUiPlugin } from "./plugins/swagger-ui.plugin";
16
15
  import { routesPlugin } from "./plugins/routes.plugin";
16
+ import { swaggerFileZodSchemaTransformPlugin } from "./plugins/swagger-file-zod-schema-transform.plugin";
17
+ import { swaggerUiPlugin } from "./plugins/swagger-ui.plugin";
17
18
 
18
19
  export const app = fastify().withTypeProvider<ZodTypeProvider>();
19
20
  export const appPrefix = "/v1";
@@ -31,7 +32,13 @@ app.register(fastifySwagger, {
31
32
  },
32
33
  servers: [],
33
34
  },
34
- transform: jsonSchemaTransform,
35
+ transform: data => {
36
+ const jsonSchema = jsonSchemaTransform(data);
37
+
38
+ swaggerFileZodSchemaTransformPlugin(data, jsonSchema);
39
+
40
+ return jsonSchema;
41
+ },
35
42
  });
36
43
  app.register(swaggerUiPlugin);
37
44
  app.register(routesPlugin, { prefix: appPrefix });
@@ -0,0 +1,122 @@
1
+ import { env } from "@/env";
2
+ import { faker } from "@faker-js/faker";
3
+ import {
4
+ createSafeFormData,
5
+ CreateSafeFormDataOutput,
6
+ } from "test/integration/create-safe-form-data";
7
+ import { createTestRequest } from "test/integration/create-test-request";
8
+ import { OverrideProperties } from "type-fest";
9
+ import { beforeEach, describe, expect, test } from "vitest";
10
+ import { appPrefix } from "../app";
11
+ import { ValidationError } from "../errors";
12
+ import {
13
+ HelloMultipartControllerBodyInput,
14
+ helloMultipartControllerMethod,
15
+ helloMultipartControllerUrl,
16
+ } from "./hello-multipart.controller";
17
+
18
+ const sutMethod = helloMultipartControllerMethod;
19
+ const sutUrl = appPrefix + helloMultipartControllerUrl;
20
+
21
+ type FormDataInput = OverrideProperties<
22
+ HelloMultipartControllerBodyInput,
23
+ {
24
+ attachment: Blob;
25
+ }
26
+ >;
27
+
28
+ const createFormData = createSafeFormData<FormDataInput>;
29
+
30
+ const sut = createTestRequest<{
31
+ body: FormData;
32
+ }>({
33
+ method: sutMethod,
34
+ url: sutUrl,
35
+ });
36
+
37
+ describe(`[Controller] ${sutMethod} ${sutUrl}`, () => {
38
+ let defaultFormDataInput: CreateSafeFormDataOutput<FormDataInput>;
39
+
40
+ beforeEach(() => {
41
+ const randomText = faker.lorem.sentences(2);
42
+ const randomTextBlob = new Blob([randomText], { type: "text/plain" });
43
+
44
+ defaultFormDataInput = createFormData({
45
+ show: "true",
46
+ attachment: randomTextBlob,
47
+ });
48
+ });
49
+
50
+ describe("should not be able to get hello world with multipart/form-data content-type", () => {
51
+ describe("if invalid input", () => {
52
+ const error = new ValidationError(null);
53
+
54
+ test("with invalid property show", async () => {
55
+ const formDataInput = createFormData({
56
+ ...defaultFormDataInput.input,
57
+ // @ts-expect-error the value must be a string
58
+ show: 0,
59
+ });
60
+
61
+ const result = await sut({
62
+ body: formDataInput.formData,
63
+ });
64
+
65
+ expect(result.statusCode).toEqual(error.statusCode);
66
+ expect(result.body).toMatchObject({
67
+ ...error.serialize(),
68
+ debug: [
69
+ expect.objectContaining({
70
+ instancePath: "/show",
71
+ }),
72
+ ],
73
+ });
74
+ });
75
+ });
76
+
77
+ test("if property show is set to false", async () => {
78
+ const formDataInput = createFormData({
79
+ ...defaultFormDataInput.input,
80
+ show: "false",
81
+ });
82
+
83
+ const result = await sut({
84
+ body: formDataInput.formData,
85
+ });
86
+
87
+ const error = new ValidationError(
88
+ `Você não quer exibir o "Hello world" :(`,
89
+ );
90
+
91
+ expect(result.statusCode).toEqual(error.statusCode);
92
+ expect(result.body).toStrictEqual(error.serialize());
93
+ });
94
+ });
95
+
96
+ describe("should be able to get hello world with multipart/form-data content-type", async () => {
97
+ test("if property show is set to true", async () => {
98
+ const formDataInput = createFormData({
99
+ ...defaultFormDataInput.input,
100
+ show: "true",
101
+ });
102
+
103
+ const result = await sut({
104
+ body: formDataInput.formData,
105
+ });
106
+
107
+ expect(result.statusCode).toEqual(200);
108
+ expect(result.body).toStrictEqual({
109
+ message: "Hello world!",
110
+ attachment: {
111
+ type: "file",
112
+ encoding: "7bit",
113
+ mimetype: "text/plain",
114
+ fileStream: expect.any(Object),
115
+ fileOriginalName: "blob",
116
+ fileName: expect.any(String),
117
+ filePath: expect.stringContaining(env.TMP_FILES_PATH),
118
+ },
119
+ });
120
+ });
121
+ });
122
+ });
@@ -0,0 +1,67 @@
1
+ import { FastifyZodInstance } from "@/@types/fastify";
2
+ import { createControllerResponseSchema } from "@/core/create-controller-response-schema";
3
+ import { z } from "zod";
4
+ import { InternalServerError, ValidationError } from "../errors";
5
+ import {
6
+ MultipartFormDataDiskFile,
7
+ multipartFormDataPlugin,
8
+ } from "../plugins/multipart-form-data.plugin";
9
+
10
+ export const helloMultipartControllerMethod = "POST" as const;
11
+ export const helloMultipartControllerUrl = "/hello/multipart" as const;
12
+
13
+ export type HelloMultipartControllerBodyInput = z.input<
14
+ typeof helloMultipartControllerBodySchema
15
+ >;
16
+
17
+ const helloMultipartControllerBodySchema = z.object({
18
+ show: z
19
+ .enum(["true", "false"])
20
+ .transform<boolean>(val => JSON.parse(val))
21
+ .default("true"),
22
+ attachment: MultipartFormDataDiskFile.schema,
23
+ });
24
+
25
+ export default function helloMultipartController(app: FastifyZodInstance) {
26
+ app.register(multipartFormDataPlugin.plugin, {
27
+ attachments: {
28
+ storage: "disk",
29
+ allowedMimeTypes: ["text/plain"],
30
+ maxCount: 1,
31
+ maxSize: 1024 * 1024 * 50, // 50MB
32
+ },
33
+ });
34
+
35
+ app.route({
36
+ method: helloMultipartControllerMethod,
37
+ url: helloMultipartControllerUrl,
38
+ schema: {
39
+ operationId: "helloMultipartController",
40
+ tags: ["Hello"],
41
+ summary: "Hello world!",
42
+ consumes: ["multipart/form-data"],
43
+ body: helloMultipartControllerBodySchema,
44
+ response: createControllerResponseSchema(
45
+ {
46
+ 200: z.object({
47
+ message: z.literal("Hello world!"),
48
+ attachment: z.custom(),
49
+ }),
50
+ },
51
+ InternalServerError,
52
+ ValidationError,
53
+ ),
54
+ },
55
+ handler: async (request, response) => {
56
+ const { show, attachment } = request.body;
57
+
58
+ if (!show)
59
+ return new ValidationError(`Você não quer exibir o "Hello world" :(`);
60
+
61
+ response.status(200).send({
62
+ message: "Hello world!",
63
+ attachment: attachment,
64
+ });
65
+ },
66
+ });
67
+ }
@@ -7,10 +7,10 @@ export const helloControllerMethod = "GET" as const;
7
7
  export const helloControllerUrl = "/hello" as const;
8
8
 
9
9
  export type HelloControllerQueryParamsInput = z.input<
10
- typeof helloControllerQueryParamsInputSchema
10
+ typeof helloControllerQueryParamsSchema
11
11
  >;
12
12
 
13
- const helloControllerQueryParamsInputSchema = z.object({
13
+ const helloControllerQueryParamsSchema = z.object({
14
14
  show: z
15
15
  .enum(["true", "false"])
16
16
  .transform<boolean>(val => JSON.parse(val))
@@ -25,7 +25,7 @@ export default function helloController(app: FastifyZodInstance) {
25
25
  operationId: "helloController",
26
26
  tags: ["Hello"],
27
27
  summary: "Hello world!",
28
- querystring: helloControllerQueryParamsInputSchema,
28
+ querystring: helloControllerQueryParamsSchema,
29
29
  response: createControllerResponseSchema(
30
30
  {
31
31
  200: z.object({
@@ -35,7 +35,7 @@ export class InternalServerError extends BaseError {
35
35
  public readonly error = "INTERNAL_SERVER_ERROR";
36
36
  public readonly statusCode = 500;
37
37
  public readonly message =
38
- "Desculpe, um erro inesperado ocorreu. Tente novamente alguns minutos ou nos contate.";
38
+ "Desculpe, um erro inesperado ocorreu. Tente novamente em alguns minutos ou nos contate.";
39
39
 
40
40
  public constructor(public debug: unknown) {
41
41
  super();
@@ -1,6 +1,8 @@
1
1
  import { FastifyZodReply, FastifyZodRequest } from "@/@types/fastify";
2
+ import { FastifyError } from "fastify";
2
3
  import { hasZodFastifySchemaValidationErrors } from "fastify-type-provider-zod";
3
4
  import { BaseError, InternalServerError, ValidationError } from "../errors";
5
+ import { multipartFormDataPluginErrorCodes } from "./multipart-form-data.plugin";
4
6
 
5
7
  export function errorHandlerPlugin(
6
8
  error: unknown,
@@ -9,14 +11,48 @@ export function errorHandlerPlugin(
9
11
  ) {
10
12
  let httpError: BaseError;
11
13
 
12
- if (error instanceof BaseError) {
13
- httpError = error;
14
- } else if (error instanceof SyntaxError) {
15
- httpError = new ValidationError(error.message);
16
- } else if (hasZodFastifySchemaValidationErrors(error)) {
17
- httpError = new ValidationError(error.validation);
18
- } else {
19
- httpError = new InternalServerError(error);
14
+ switch (true) {
15
+ case error instanceof BaseError: {
16
+ httpError = error;
17
+ break;
18
+ }
19
+
20
+ case error instanceof SyntaxError: {
21
+ httpError = new ValidationError(error.message);
22
+ break;
23
+ }
24
+
25
+ case hasZodFastifySchemaValidationErrors(error): {
26
+ httpError = new ValidationError(error.validation);
27
+ break;
28
+ }
29
+
30
+ case error instanceof Error && error.name === "FastifyError": {
31
+ const fastifyError = error as FastifyError;
32
+
33
+ if (fastifyError.code.startsWith("FST_ERR_CTP")) {
34
+ httpError = new ValidationError(error);
35
+ break;
36
+ }
37
+
38
+ const multipartErrorCodes =
39
+ multipartFormDataPluginErrorCodes as unknown as string[];
40
+
41
+ if (multipartErrorCodes.includes(fastifyError.code)) {
42
+ if ("part" in error) delete error.part;
43
+
44
+ httpError = new ValidationError(error);
45
+ break;
46
+ }
47
+
48
+ httpError = new InternalServerError(error);
49
+ break;
50
+ }
51
+
52
+ default: {
53
+ httpError = new InternalServerError(error);
54
+ break;
55
+ }
20
56
  }
21
57
 
22
58
  const isCriticalError = httpError.statusCode.toString().startsWith("5");
@@ -0,0 +1,241 @@
1
+ import { FastifyZodInstance } from "@/@types/fastify";
2
+ import { env } from "@/env";
3
+ import fastifyMultipart, { MultipartFile } from "@fastify/multipart";
4
+ import { errorCodes } from "fastify";
5
+ import fastifyPlugin from "fastify-plugin";
6
+ import { randomUUID } from "node:crypto";
7
+ import { createWriteStream } from "node:fs";
8
+ import { rm } from "node:fs/promises";
9
+ import { basename, extname, resolve } from "node:path";
10
+ import { pipeline } from "node:stream/promises";
11
+ import { z } from "zod";
12
+ import { ValidationError } from "../errors";
13
+
14
+ type InMemoryAttachmentStorage = {
15
+ /**
16
+ * File storage strategy.
17
+ *
18
+ * @default "disk"
19
+ */
20
+ storage?: "in-memory";
21
+ };
22
+
23
+ type DiskAttachmentStorage = {
24
+ storage?: "disk";
25
+ /**
26
+ * File storage directory.
27
+ *
28
+ * @default env.TMP_FILES_PATH
29
+ */
30
+ destinationPath?: string;
31
+ /**
32
+ * Remove files after route handler execution
33
+ *
34
+ * @default true
35
+ */
36
+ removeAfterHandlerExecution?: boolean;
37
+ };
38
+
39
+ type AttachmentsStorage = InMemoryAttachmentStorage | DiskAttachmentStorage;
40
+
41
+ type MultipartFormDataPluginOptions = {
42
+ attachments?: AttachmentsStorage & {
43
+ /**
44
+ * Allowed mime types of files.
45
+ *
46
+ * @example ["image/webp", "image/jpeg"]
47
+ *
48
+ * @default []
49
+ */
50
+ allowedMimeTypes?: string[];
51
+ /**
52
+ * Maximum number of files allowed.
53
+ *
54
+ * @default 1
55
+ */
56
+ maxCount?: number;
57
+ /**
58
+ * Maximum size allowed for each file in bytes.
59
+ *
60
+ * @default 1024 * 1024 * 10 // 10MB
61
+ */
62
+ maxSize?: number;
63
+ };
64
+ };
65
+
66
+ const plugin = fastifyPlugin(
67
+ (app: FastifyZodInstance, _options: MultipartFormDataPluginOptions, done) => {
68
+ const options = {
69
+ ..._options,
70
+ attachments: {
71
+ storage: "disk",
72
+ destinationPath: env.TMP_FILES_PATH,
73
+ removeAfterHandlerExecution: true,
74
+ allowedMimeTypes: [],
75
+ maxCount: 1,
76
+ maxSize: 1024 * 1024 * 10, // 10MB
77
+ ..._options.attachments,
78
+ },
79
+ } satisfies MultipartFormDataPluginOptions;
80
+
81
+ app.addHook("onRequest", async request => {
82
+ const contentType = request.headers["content-type"];
83
+ const { FST_ERR_CTP_INVALID_MEDIA_TYPE } = errorCodes;
84
+
85
+ if (!contentType?.startsWith("multipart/form-data")) {
86
+ throw new ValidationError(FST_ERR_CTP_INVALID_MEDIA_TYPE(contentType));
87
+ }
88
+ });
89
+
90
+ app.register(fastifyMultipart, {
91
+ attachFieldsToBody: "keyValues",
92
+ limits: {
93
+ fieldNameSize: 100,
94
+ fieldSize: 1024 * 1024, // 1MB
95
+ fields: Infinity,
96
+ parts: Infinity,
97
+ headerPairs: 2000,
98
+ files: options.attachments.maxCount,
99
+ fileSize: options.attachments.maxSize,
100
+ },
101
+ async onFile(file) {
102
+ const allowedMimeTypes = options.attachments
103
+ .allowedMimeTypes as string[];
104
+
105
+ if (
106
+ !allowedMimeTypes.includes(file.mimetype) &&
107
+ !allowedMimeTypes.length
108
+ ) {
109
+ throw new ValidationError(
110
+ `Nenhum MIME type de arquivo foi configurado para ser aceito.`,
111
+ );
112
+ }
113
+
114
+ if (
115
+ !allowedMimeTypes.includes(file.mimetype) &&
116
+ allowedMimeTypes.length
117
+ ) {
118
+ throw new ValidationError(
119
+ `Apenas os seguintes MIME types são aceitos: ${allowedMimeTypes.join(", ")}.`,
120
+ );
121
+ }
122
+
123
+ if (options.attachments.storage === "in-memory") {
124
+ const buffer = await file.toBuffer();
125
+
126
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
+ (file as any).value = new MultipartFormDataInMemoryFile(file, buffer);
128
+ return;
129
+ }
130
+
131
+ const uniqueFileName = randomUUID() + extname(file.filename);
132
+ const filePath = resolve(env.TMP_FILES_PATH, uniqueFileName);
133
+
134
+ await pipeline(file.file, createWriteStream(filePath));
135
+
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ (file as any).value = new MultipartFormDataDiskFile(file, filePath);
138
+ },
139
+ });
140
+
141
+ if (
142
+ options.attachments.storage === "disk" &&
143
+ options.attachments.removeAfterHandlerExecution
144
+ ) {
145
+ app.addHook("onResponse", async request => {
146
+ if (!request.body || typeof request.body !== "object") return;
147
+
148
+ const bodyDiskFiles = Object.values(request.body)
149
+ .filter(value => {
150
+ const valueIsDiskFile = value instanceof MultipartFormDataDiskFile;
151
+ const valueIsArrayOfDiskFile =
152
+ Array.isArray(value) &&
153
+ value.some(item => item instanceof MultipartFormDataDiskFile);
154
+
155
+ return valueIsDiskFile || valueIsArrayOfDiskFile;
156
+ })
157
+ .flat() as MultipartFormDataDiskFile[];
158
+
159
+ if (!bodyDiskFiles.length) return;
160
+
161
+ await Promise.all(
162
+ bodyDiskFiles.map(diskFile => rm(diskFile.filePath, { force: true })),
163
+ );
164
+ });
165
+ }
166
+
167
+ done();
168
+ },
169
+ );
170
+
171
+ export const multipartFormDataPlugin = Object.freeze({
172
+ plugin,
173
+ } as const);
174
+
175
+ export const multipartFormDataPluginErrorCodes = [
176
+ "FST_PARTS_LIMIT",
177
+ "FST_FILES_LIMIT",
178
+ "FST_FIELDS_LIMIT",
179
+ "FST_REQ_FILE_TOO_LARGE",
180
+ "FST_PROTO_VIOLATION",
181
+ "FST_INVALID_MULTIPART_CONTENT_TYPE",
182
+ "FST_INVALID_JSON_FIELD_ERROR",
183
+ "FST_FILE_BUFFER_NOT_FOUND",
184
+ "FST_NO_FORM_DATA",
185
+ ] as const;
186
+
187
+ export const MULTIPART_FORM_DATA_FILE_SCHEMA_DESCRIPTION =
188
+ "MULTIPART_FORM_DATA_ATTACHMENT";
189
+
190
+ export class MultipartFormDataFile {
191
+ public type: MultipartFile["type"];
192
+ public encoding: MultipartFile["encoding"];
193
+ public mimetype: MultipartFile["mimetype"];
194
+ public fileStream: MultipartFile["file"];
195
+ public fileOriginalName: MultipartFile["filename"];
196
+
197
+ public constructor(multipartFile: MultipartFile) {
198
+ this.type = multipartFile.type;
199
+ this.encoding = multipartFile.encoding;
200
+ this.mimetype = multipartFile.mimetype;
201
+ this.fileStream = multipartFile.file;
202
+ this.fileOriginalName = multipartFile.filename;
203
+ }
204
+ }
205
+
206
+ export class MultipartFormDataDiskFile extends MultipartFormDataFile {
207
+ public fileName: string;
208
+ public filePath: string;
209
+
210
+ public constructor(multipartFile: MultipartFile, filePath: string) {
211
+ super(multipartFile);
212
+
213
+ this.fileName = basename(filePath);
214
+ this.filePath = filePath;
215
+ }
216
+
217
+ public static get schema() {
218
+ return z
219
+ .instanceof(MultipartFormDataDiskFile)
220
+ .describe(MULTIPART_FORM_DATA_FILE_SCHEMA_DESCRIPTION);
221
+ }
222
+ }
223
+
224
+ export class MultipartFormDataInMemoryFile extends MultipartFormDataFile {
225
+ public buffer: Buffer<ArrayBufferLike>;
226
+
227
+ public constructor(
228
+ multipartFile: MultipartFile,
229
+ buffer: Buffer<ArrayBufferLike>,
230
+ ) {
231
+ super(multipartFile);
232
+
233
+ this.buffer = buffer;
234
+ }
235
+
236
+ public static get schema() {
237
+ return z
238
+ .instanceof(MultipartFormDataInMemoryFile)
239
+ .describe(MULTIPART_FORM_DATA_FILE_SCHEMA_DESCRIPTION);
240
+ }
241
+ }
@@ -0,0 +1,45 @@
1
+ import { FastifyDynamicSwaggerOptions } from "@fastify/swagger";
2
+ import { jsonSchemaTransform } from "fastify-type-provider-zod";
3
+ import { ZodArray, ZodObject } from "zod";
4
+ import { MULTIPART_FORM_DATA_FILE_SCHEMA_DESCRIPTION } from "./multipart-form-data.plugin";
5
+
6
+ type RouteData = Parameters<
7
+ NonNullable<FastifyDynamicSwaggerOptions["transform"]>
8
+ >[0];
9
+
10
+ type JsonSchema = ReturnType<typeof jsonSchemaTransform>;
11
+
12
+ export function swaggerFileZodSchemaTransformPlugin(
13
+ routeData: RouteData,
14
+ jsonSchema: JsonSchema,
15
+ ) {
16
+ if (
17
+ routeData.schema.consumes?.includes("multipart/form-data") &&
18
+ routeData.schema.body instanceof ZodObject
19
+ ) {
20
+ for (const bodyFieldName in routeData.schema.body.shape) {
21
+ const bodyFieldSchema = routeData.schema.body.shape[bodyFieldName];
22
+
23
+ if (
24
+ bodyFieldSchema._def.description ===
25
+ MULTIPART_FORM_DATA_FILE_SCHEMA_DESCRIPTION
26
+ ) {
27
+ jsonSchema.schema.body.properties[bodyFieldName] = {
28
+ type: "string",
29
+ format: "binary",
30
+ };
31
+ }
32
+
33
+ if (
34
+ bodyFieldSchema instanceof ZodArray &&
35
+ bodyFieldSchema._def.type._def.description ===
36
+ MULTIPART_FORM_DATA_FILE_SCHEMA_DESCRIPTION
37
+ ) {
38
+ jsonSchema.schema.body.properties[bodyFieldName] = {
39
+ type: "array",
40
+ items: { type: "string", format: "binary" },
41
+ };
42
+ }
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,18 @@
1
+ type CreateSafeFormDataInput = Record<string, string | Blob>;
2
+
3
+ export type CreateSafeFormDataOutput<Input extends CreateSafeFormDataInput> =
4
+ ReturnType<typeof createSafeFormData<Input>>;
5
+
6
+ export function createSafeFormData<Input extends CreateSafeFormDataInput>(
7
+ input: Input,
8
+ ) {
9
+ const formData = new FormData();
10
+
11
+ for (const inputProperty in input)
12
+ formData.set(inputProperty, input[inputProperty]);
13
+
14
+ return {
15
+ input,
16
+ formData,
17
+ };
18
+ }
@@ -4,6 +4,7 @@ import { defineConfig } from "vitest/config";
4
4
  export default defineConfig({
5
5
  plugins: [viteTsconfigPaths()],
6
6
  test: {
7
+ reporters: ["verbose"],
7
8
  fakeTimers: {
8
9
  toFake: [
9
10
  "Date",
@@ -54,7 +54,7 @@
54
54
  "prettier": "3.5.3",
55
55
  "supertest": "7.1.1",
56
56
  "type-fest": "4.38.0",
57
- "typescript": "5.8.2",
57
+ "typescript": "5.8.3",
58
58
  "typescript-eslint": "8.28.0",
59
59
  "unplugin-swc": "1.5.1",
60
60
  "vitest": "1.6.1"
@@ -59,7 +59,7 @@ importers:
59
59
  version: 10.4.9(@swc/cli@0.6.0(@swc/core@1.11.13)(chokidar@3.6.0))(@swc/core@1.11.13)
60
60
  '@nestjs/schematics':
61
61
  specifier: 10.2.3
62
- version: 10.2.3(chokidar@3.6.0)(typescript@5.8.2)
62
+ version: 10.2.3(chokidar@3.6.0)(typescript@5.8.3)
63
63
  '@nestjs/testing':
64
64
  specifier: 10.4.15
65
65
  version: 10.4.15(@nestjs/common@10.4.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.15)(@nestjs/platform-express@10.3.10)
@@ -83,7 +83,7 @@ importers:
83
83
  version: 1.6.1(vitest@1.6.1(@types/node@22.13.14)(terser@5.31.3))
84
84
  '@vitest/eslint-plugin':
85
85
  specifier: 1.1.38
86
- version: 1.1.38(@typescript-eslint/utils@8.28.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2)(vitest@1.6.1(@types/node@22.13.14)(terser@5.31.3))
86
+ version: 1.1.38(@typescript-eslint/utils@8.28.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0)(typescript@5.8.3)(vitest@1.6.1(@types/node@22.13.14)(terser@5.31.3))
87
87
  eslint:
88
88
  specifier: 9.23.0
89
89
  version: 9.23.0
@@ -109,11 +109,11 @@ importers:
109
109
  specifier: 4.38.0
110
110
  version: 4.38.0
111
111
  typescript:
112
- specifier: 5.8.2
113
- version: 5.8.2
112
+ specifier: 5.8.3
113
+ version: 5.8.3
114
114
  typescript-eslint:
115
115
  specifier: 8.28.0
116
- version: 8.28.0(eslint@9.23.0)(typescript@5.8.2)
116
+ version: 8.28.0(eslint@9.23.0)(typescript@5.8.3)
117
117
  unplugin-swc:
118
118
  specifier: 1.5.1
119
119
  version: 1.5.1(@swc/core@1.11.13)(rollup@4.19.0)
@@ -3049,8 +3049,8 @@ packages:
3049
3049
  engines: {node: '>=14.17'}
3050
3050
  hasBin: true
3051
3051
 
3052
- typescript@5.8.2:
3053
- resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
3052
+ typescript@5.8.3:
3053
+ resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
3054
3054
  engines: {node: '>=14.17'}
3055
3055
  hasBin: true
3056
3056
 
@@ -3664,14 +3664,14 @@ snapshots:
3664
3664
  transitivePeerDependencies:
3665
3665
  - chokidar
3666
3666
 
3667
- '@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.8.2)':
3667
+ '@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.8.3)':
3668
3668
  dependencies:
3669
3669
  '@angular-devkit/core': 17.3.11(chokidar@3.6.0)
3670
3670
  '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0)
3671
3671
  comment-json: 4.2.5
3672
3672
  jsonc-parser: 3.3.1
3673
3673
  pluralize: 8.0.0
3674
- typescript: 5.8.2
3674
+ typescript: 5.8.3
3675
3675
  transitivePeerDependencies:
3676
3676
  - chokidar
3677
3677
 
@@ -3901,32 +3901,32 @@ snapshots:
3901
3901
  '@types/methods': 1.1.4
3902
3902
  '@types/superagent': 8.1.8
3903
3903
 
3904
- '@typescript-eslint/eslint-plugin@8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2)':
3904
+ '@typescript-eslint/eslint-plugin@8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0)(typescript@5.8.3)':
3905
3905
  dependencies:
3906
3906
  '@eslint-community/regexpp': 4.12.1
3907
- '@typescript-eslint/parser': 8.28.0(eslint@9.23.0)(typescript@5.8.2)
3907
+ '@typescript-eslint/parser': 8.28.0(eslint@9.23.0)(typescript@5.8.3)
3908
3908
  '@typescript-eslint/scope-manager': 8.28.0
3909
- '@typescript-eslint/type-utils': 8.28.0(eslint@9.23.0)(typescript@5.8.2)
3910
- '@typescript-eslint/utils': 8.28.0(eslint@9.23.0)(typescript@5.8.2)
3909
+ '@typescript-eslint/type-utils': 8.28.0(eslint@9.23.0)(typescript@5.8.3)
3910
+ '@typescript-eslint/utils': 8.28.0(eslint@9.23.0)(typescript@5.8.3)
3911
3911
  '@typescript-eslint/visitor-keys': 8.28.0
3912
3912
  eslint: 9.23.0
3913
3913
  graphemer: 1.4.0
3914
3914
  ignore: 5.3.1
3915
3915
  natural-compare: 1.4.0
3916
- ts-api-utils: 2.1.0(typescript@5.8.2)
3917
- typescript: 5.8.2
3916
+ ts-api-utils: 2.1.0(typescript@5.8.3)
3917
+ typescript: 5.8.3
3918
3918
  transitivePeerDependencies:
3919
3919
  - supports-color
3920
3920
 
3921
- '@typescript-eslint/parser@8.28.0(eslint@9.23.0)(typescript@5.8.2)':
3921
+ '@typescript-eslint/parser@8.28.0(eslint@9.23.0)(typescript@5.8.3)':
3922
3922
  dependencies:
3923
3923
  '@typescript-eslint/scope-manager': 8.28.0
3924
3924
  '@typescript-eslint/types': 8.28.0
3925
- '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
3925
+ '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.3)
3926
3926
  '@typescript-eslint/visitor-keys': 8.28.0
3927
3927
  debug: 4.4.0
3928
3928
  eslint: 9.23.0
3929
- typescript: 5.8.2
3929
+ typescript: 5.8.3
3930
3930
  transitivePeerDependencies:
3931
3931
  - supports-color
3932
3932
 
@@ -3935,20 +3935,20 @@ snapshots:
3935
3935
  '@typescript-eslint/types': 8.28.0
3936
3936
  '@typescript-eslint/visitor-keys': 8.28.0
3937
3937
 
3938
- '@typescript-eslint/type-utils@8.28.0(eslint@9.23.0)(typescript@5.8.2)':
3938
+ '@typescript-eslint/type-utils@8.28.0(eslint@9.23.0)(typescript@5.8.3)':
3939
3939
  dependencies:
3940
- '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
3941
- '@typescript-eslint/utils': 8.28.0(eslint@9.23.0)(typescript@5.8.2)
3940
+ '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.3)
3941
+ '@typescript-eslint/utils': 8.28.0(eslint@9.23.0)(typescript@5.8.3)
3942
3942
  debug: 4.4.0
3943
3943
  eslint: 9.23.0
3944
- ts-api-utils: 2.1.0(typescript@5.8.2)
3945
- typescript: 5.8.2
3944
+ ts-api-utils: 2.1.0(typescript@5.8.3)
3945
+ typescript: 5.8.3
3946
3946
  transitivePeerDependencies:
3947
3947
  - supports-color
3948
3948
 
3949
3949
  '@typescript-eslint/types@8.28.0': {}
3950
3950
 
3951
- '@typescript-eslint/typescript-estree@8.28.0(typescript@5.8.2)':
3951
+ '@typescript-eslint/typescript-estree@8.28.0(typescript@5.8.3)':
3952
3952
  dependencies:
3953
3953
  '@typescript-eslint/types': 8.28.0
3954
3954
  '@typescript-eslint/visitor-keys': 8.28.0
@@ -3957,19 +3957,19 @@ snapshots:
3957
3957
  is-glob: 4.0.3
3958
3958
  minimatch: 9.0.5
3959
3959
  semver: 7.6.3
3960
- ts-api-utils: 2.1.0(typescript@5.8.2)
3961
- typescript: 5.8.2
3960
+ ts-api-utils: 2.1.0(typescript@5.8.3)
3961
+ typescript: 5.8.3
3962
3962
  transitivePeerDependencies:
3963
3963
  - supports-color
3964
3964
 
3965
- '@typescript-eslint/utils@8.28.0(eslint@9.23.0)(typescript@5.8.2)':
3965
+ '@typescript-eslint/utils@8.28.0(eslint@9.23.0)(typescript@5.8.3)':
3966
3966
  dependencies:
3967
3967
  '@eslint-community/eslint-utils': 4.4.0(eslint@9.23.0)
3968
3968
  '@typescript-eslint/scope-manager': 8.28.0
3969
3969
  '@typescript-eslint/types': 8.28.0
3970
- '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
3970
+ '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.3)
3971
3971
  eslint: 9.23.0
3972
- typescript: 5.8.2
3972
+ typescript: 5.8.3
3973
3973
  transitivePeerDependencies:
3974
3974
  - supports-color
3975
3975
 
@@ -3997,12 +3997,12 @@ snapshots:
3997
3997
  transitivePeerDependencies:
3998
3998
  - supports-color
3999
3999
 
4000
- '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.28.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2)(vitest@1.6.1(@types/node@22.13.14)(terser@5.31.3))':
4000
+ '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.28.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0)(typescript@5.8.3)(vitest@1.6.1(@types/node@22.13.14)(terser@5.31.3))':
4001
4001
  dependencies:
4002
- '@typescript-eslint/utils': 8.28.0(eslint@9.23.0)(typescript@5.8.2)
4002
+ '@typescript-eslint/utils': 8.28.0(eslint@9.23.0)(typescript@5.8.3)
4003
4003
  eslint: 9.23.0
4004
4004
  optionalDependencies:
4005
- typescript: 5.8.2
4005
+ typescript: 5.8.3
4006
4006
  vitest: 1.6.1(@types/node@22.13.14)(terser@5.31.3)
4007
4007
 
4008
4008
  '@vitest/expect@1.6.1':
@@ -6261,9 +6261,9 @@ snapshots:
6261
6261
 
6262
6262
  tree-kill@1.2.2: {}
6263
6263
 
6264
- ts-api-utils@2.1.0(typescript@5.8.2):
6264
+ ts-api-utils@2.1.0(typescript@5.8.3):
6265
6265
  dependencies:
6266
- typescript: 5.8.2
6266
+ typescript: 5.8.3
6267
6267
 
6268
6268
  ts-deepmerge@6.2.1: {}
6269
6269
 
@@ -6302,19 +6302,19 @@ snapshots:
6302
6302
 
6303
6303
  typedarray@0.0.6: {}
6304
6304
 
6305
- typescript-eslint@8.28.0(eslint@9.23.0)(typescript@5.8.2):
6305
+ typescript-eslint@8.28.0(eslint@9.23.0)(typescript@5.8.3):
6306
6306
  dependencies:
6307
- '@typescript-eslint/eslint-plugin': 8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2)
6308
- '@typescript-eslint/parser': 8.28.0(eslint@9.23.0)(typescript@5.8.2)
6309
- '@typescript-eslint/utils': 8.28.0(eslint@9.23.0)(typescript@5.8.2)
6307
+ '@typescript-eslint/eslint-plugin': 8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0)(typescript@5.8.3)
6308
+ '@typescript-eslint/parser': 8.28.0(eslint@9.23.0)(typescript@5.8.3)
6309
+ '@typescript-eslint/utils': 8.28.0(eslint@9.23.0)(typescript@5.8.3)
6310
6310
  eslint: 9.23.0
6311
- typescript: 5.8.2
6311
+ typescript: 5.8.3
6312
6312
  transitivePeerDependencies:
6313
6313
  - supports-color
6314
6314
 
6315
6315
  typescript@5.7.2: {}
6316
6316
 
6317
- typescript@5.8.2: {}
6317
+ typescript@5.8.3: {}
6318
6318
 
6319
6319
  ufo@1.5.4: {}
6320
6320