@furystack/rest-service 10.1.2 → 11.0.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +36 -76
  2. package/README.md +10 -2
  3. package/esm/authenticate.d.ts +19 -0
  4. package/esm/authenticate.d.ts.map +1 -1
  5. package/esm/authenticate.js +19 -0
  6. package/esm/authenticate.js.map +1 -1
  7. package/esm/authorize.d.ts +19 -0
  8. package/esm/authorize.d.ts.map +1 -1
  9. package/esm/authorize.js +19 -0
  10. package/esm/authorize.js.map +1 -1
  11. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +9 -9
  12. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
  13. package/esm/endpoint-generators/create-get-swagger-json-action.d.ts +2 -2
  14. package/esm/endpoint-generators/create-get-swagger-json-action.d.ts.map +1 -1
  15. package/esm/endpoint-generators/create-patch-endpoint.spec.js +6 -6
  16. package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
  17. package/esm/endpoint-generators/create-post-endpoint.spec.js +6 -6
  18. package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
  19. package/esm/get-schema-from-api.d.ts.map +1 -1
  20. package/esm/get-schema-from-api.js +6 -3
  21. package/esm/get-schema-from-api.js.map +1 -1
  22. package/esm/get-schema-from-api.spec.js +14 -12
  23. package/esm/get-schema-from-api.spec.js.map +1 -1
  24. package/esm/proxy-manager.spec.js +1 -1
  25. package/esm/proxy-manager.spec.js.map +1 -1
  26. package/esm/read-post-body.js.map +1 -1
  27. package/esm/rest-service.integration.spec.js +10 -10
  28. package/esm/rest-service.integration.spec.js.map +1 -1
  29. package/esm/server-response-extensions.js.map +1 -1
  30. package/esm/server-response-extensions.spec.d.ts.map +1 -1
  31. package/esm/server-response-extensions.spec.js +3 -3
  32. package/esm/server-response-extensions.spec.js.map +1 -1
  33. package/esm/swagger/generate-swagger-json.d.ts +2 -2
  34. package/esm/swagger/generate-swagger-json.d.ts.map +1 -1
  35. package/esm/swagger/generate-swagger-json.js +71 -69
  36. package/esm/swagger/generate-swagger-json.js.map +1 -1
  37. package/esm/swagger/generate-swagger-json.spec.js +97 -86
  38. package/esm/swagger/generate-swagger-json.spec.js.map +1 -1
  39. package/esm/validate.d.ts +27 -6
  40. package/esm/validate.d.ts.map +1 -1
  41. package/esm/validate.integration.schema.d.ts +10 -5
  42. package/esm/validate.integration.schema.d.ts.map +1 -1
  43. package/esm/validate.integration.spec.js +76 -31
  44. package/esm/validate.integration.spec.js.map +1 -1
  45. package/esm/validate.integration.spec.schema.json +24 -31
  46. package/esm/validate.js +24 -22
  47. package/esm/validate.js.map +1 -1
  48. package/package.json +11 -11
  49. package/src/authenticate.ts +19 -0
  50. package/src/authorize.ts +19 -0
  51. package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +9 -9
  52. package/src/endpoint-generators/create-get-swagger-json-action.ts +2 -2
  53. package/src/endpoint-generators/create-patch-endpoint.spec.ts +6 -6
  54. package/src/endpoint-generators/create-post-endpoint.spec.ts +6 -6
  55. package/src/get-schema-from-api.spec.ts +14 -12
  56. package/src/get-schema-from-api.ts +13 -8
  57. package/src/proxy-manager.spec.ts +4 -4
  58. package/src/read-post-body.ts +1 -1
  59. package/src/rest-service.integration.spec.ts +10 -10
  60. package/src/server-response-extensions.spec.ts +8 -8
  61. package/src/server-response-extensions.ts +1 -1
  62. package/src/swagger/generate-swagger-json.spec.ts +107 -96
  63. package/src/swagger/generate-swagger-json.ts +80 -71
  64. package/src/validate.integration.schema.ts +11 -5
  65. package/src/validate.integration.spec.schema.json +24 -31
  66. package/src/validate.integration.spec.ts +84 -36
  67. package/src/validate.ts +61 -32
@@ -1,4 +1,11 @@
1
- import type { ApiEndpointDefinition, Operation, ParameterObject, SwaggerDocument } from '@furystack/rest'
1
+ import type {
2
+ ApiEndpointDefinition,
3
+ ApiEndpointSchema,
4
+ Method,
5
+ Operation,
6
+ ParameterObject,
7
+ SwaggerDocument,
8
+ } from '@furystack/rest'
2
9
 
3
10
  /**
4
11
  * Converts a FuryStack API schema to an OpenAPI 3.1 compatible document
@@ -12,7 +19,7 @@ export const generateSwaggerJsonFromApiSchema = ({
12
19
  description = 'API documentation generated from FuryStack API schema',
13
20
  version = '1.0.0',
14
21
  }: {
15
- api: Record<string, ApiEndpointDefinition>
22
+ api: ApiEndpointSchema['endpoints']
16
23
  title?: string
17
24
  description?: string
18
25
  version?: string
@@ -40,82 +47,84 @@ export const generateSwaggerJsonFromApiSchema = ({
40
47
  },
41
48
  }
42
49
 
43
- for (const [path, definition] of Object.entries(api)) {
44
- // Normalize path to OpenAPI format (convert :param to {param})
45
- const normalizedPath = path.replace(/:([^/]+)/g, '{$1}')
46
- if (!swaggerJson.paths![normalizedPath]) {
47
- swaggerJson.paths![normalizedPath] = {}
48
- }
50
+ for (const [methodKey, paths] of Object.entries(api) as Array<[Method, Record<string, ApiEndpointDefinition>]>) {
51
+ for (const [path, definition] of Object.entries(paths)) {
52
+ // Normalize path to OpenAPI format (convert :param to {param})
53
+ const normalizedPath = path.replace(/:([^/]+)/g, '{$1}')
54
+ if (!swaggerJson.paths![normalizedPath]) {
55
+ swaggerJson.paths![normalizedPath] = {}
56
+ }
49
57
 
50
- // Extract path parameters
51
- const pathParams = Array.from(path.matchAll(/:([^/]+)/g), (m) => m[1])
52
- const parameters: ParameterObject[] = pathParams.map((param) => ({
53
- name: param,
54
- in: 'path',
55
- required: true,
56
- description: `Path parameter: ${param}`,
57
- schema: { type: 'string' },
58
- }))
58
+ // Extract path parameters
59
+ const pathParams = Array.from(path.matchAll(/:([^/]+)/g), (m) => m[1])
60
+ const parameters: ParameterObject[] = pathParams.map((param) => ({
61
+ name: param,
62
+ in: 'path',
63
+ required: true,
64
+ description: `Path parameter: ${param}`,
65
+ schema: { type: 'string' },
66
+ }))
59
67
 
60
- // Build operation
61
- const method = definition.method.toLowerCase()
62
- const operation: Operation = {
63
- summary: `${definition.method} ${path}`,
64
- description: `Endpoint for ${path}`,
65
- operationId: `${method}${path.replace(/\//g, '_').replace(/:/g, '').replace(/-/g, '_')}`,
66
- security: definition.isAuthenticated ? [{ cookieAuth: [] }] : [],
67
- parameters,
68
- responses: {
69
- '200': {
70
- description: 'Successful operation',
71
- content: {
72
- 'application/json': {
73
- schema: definition.schemaName
74
- ? { $ref: `#/components/schemas/${definition.schemaName}` }
75
- : { type: 'object' },
68
+ // Build operation
69
+ const method = methodKey.toLowerCase()
70
+ const operation: Operation = {
71
+ summary: `${methodKey} ${path}`,
72
+ description: `Endpoint for ${path}`,
73
+ operationId: `${method}${path.replace(/\//g, '_').replace(/:/g, '').replace(/-/g, '_')}`,
74
+ security: definition.isAuthenticated ? [{ cookieAuth: [] }] : [],
75
+ parameters,
76
+ responses: {
77
+ '200': {
78
+ description: 'Successful operation',
79
+ content: {
80
+ 'application/json': {
81
+ schema: definition.schemaName
82
+ ? { $ref: `#/components/schemas/${definition.schemaName}` }
83
+ : { type: 'object' },
84
+ },
76
85
  },
77
86
  },
87
+ '401': { description: 'Unauthorized' },
88
+ '500': { description: 'Internal server error' },
78
89
  },
79
- '401': { description: 'Unauthorized' },
80
- '500': { description: 'Internal server error' },
81
- },
82
- }
90
+ }
83
91
 
84
- // Add schema to components if not already there
85
- if (definition.schema && definition.schemaName) {
86
- swaggerJson.components!.schemas![definition.schemaName] = definition.schema
87
- }
92
+ // Add schema to components if not already there
93
+ if (definition.schema && definition.schemaName) {
94
+ swaggerJson.components!.schemas![definition.schemaName] = definition.schema
95
+ }
88
96
 
89
- // Assign the operation to the correct HTTP method property of PathItem
90
- const pathItem = swaggerJson.paths![normalizedPath]
91
- switch (method) {
92
- case 'get':
93
- pathItem.get = operation
94
- break
95
- case 'put':
96
- pathItem.put = operation
97
- break
98
- case 'post':
99
- pathItem.post = operation
100
- break
101
- case 'delete':
102
- pathItem.delete = operation
103
- break
104
- case 'options':
105
- pathItem.options = operation
106
- break
107
- case 'head':
108
- pathItem.head = operation
109
- break
110
- case 'patch':
111
- pathItem.patch = operation
112
- break
113
- case 'trace':
114
- pathItem.trace = operation
115
- break
116
- default:
117
- // Ignore unknown methods
118
- break
97
+ // Assign the operation to the correct HTTP method property of PathItem
98
+ const pathItem = swaggerJson.paths![normalizedPath]
99
+ switch (method) {
100
+ case 'get':
101
+ pathItem.get = operation
102
+ break
103
+ case 'put':
104
+ pathItem.put = operation
105
+ break
106
+ case 'post':
107
+ pathItem.post = operation
108
+ break
109
+ case 'delete':
110
+ pathItem.delete = operation
111
+ break
112
+ case 'options':
113
+ pathItem.options = operation
114
+ break
115
+ case 'head':
116
+ pathItem.head = operation
117
+ break
118
+ case 'patch':
119
+ pathItem.patch = operation
120
+ break
121
+ case 'trace':
122
+ pathItem.trace = operation
123
+ break
124
+ default:
125
+ // Ignore unknown methods
126
+ break
127
+ }
119
128
  }
120
129
  }
121
130
 
@@ -29,22 +29,28 @@ export interface ValidateBody {
29
29
  result: { foo: string; bar: number; baz: boolean }
30
30
  }
31
31
 
32
+ export type PostMockEndpoint = PostEndpoint<Mock, 'id', Mock>
33
+ export type PatchMockEndpoint = PatchEndpoint<Mock, 'id', Mock>
34
+ export type DeleteMockEndpoint = DeleteEndpoint<Mock, 'id'>
35
+ export type GetMockCollectionEndpoint = GetCollectionEndpoint<Mock>
36
+ export type GetMockEntityEndpoint = GetEntityEndpoint<Mock, 'id'>
37
+
32
38
  export interface ValidationApi extends RestApi {
33
39
  GET: {
34
40
  '/validate-query': ValidateQuery
35
41
  '/validate-url/:id': ValidateUrl
36
42
  '/validate-headers': ValidateHeaders
37
- '/mock': GetCollectionEndpoint<Mock>
38
- '/mock/:id': GetEntityEndpoint<Mock, 'id'>
43
+ '/mock': GetMockCollectionEndpoint
44
+ '/mock/:id': GetMockEntityEndpoint
39
45
  }
40
46
  POST: {
41
47
  '/validate-body': ValidateBody
42
- '/mock': PostEndpoint<Mock, 'id'>
48
+ '/mock': PostMockEndpoint
43
49
  }
44
50
  PATCH: {
45
- '/mock/:id': PatchEndpoint<Mock, 'id'>
51
+ '/mock/:id': PatchMockEndpoint
46
52
  }
47
53
  DELETE: {
48
- '/mock/:id': DeleteEndpoint<Mock, 'id'>
54
+ '/mock/:id': DeleteMockEndpoint
49
55
  }
50
56
  }
@@ -22,6 +22,9 @@
22
22
  "required": ["url", "result"],
23
23
  "type": "object"
24
24
  },
25
+ "DeleteMockEndpoint": {
26
+ "$ref": "#/definitions/DeleteEndpoint%3CMock%2C%22id%22%3E"
27
+ },
25
28
  "FilterType<Mock>": {
26
29
  "additionalProperties": false,
27
30
  "properties": {
@@ -273,6 +276,12 @@
273
276
  "required": ["query", "url", "result"],
274
277
  "type": "object"
275
278
  },
279
+ "GetMockCollectionEndpoint": {
280
+ "$ref": "#/definitions/GetCollectionEndpoint%3CMock%3E"
281
+ },
282
+ "GetMockEntityEndpoint": {
283
+ "$ref": "#/definitions/GetEntityEndpoint%3CMock%2C%22id%22%3E"
284
+ },
276
285
  "Mock": {
277
286
  "additionalProperties": false,
278
287
  "properties": {
@@ -286,21 +295,12 @@
286
295
  "required": ["id", "value"],
287
296
  "type": "object"
288
297
  },
289
- "PatchEndpoint<Mock,\"id\">": {
298
+ "PatchEndpoint<Mock,\"id\",Mock>": {
290
299
  "additionalProperties": false,
291
300
  "description": "Endpoint model for updating entities",
292
301
  "properties": {
293
302
  "body": {
294
- "additionalProperties": false,
295
- "properties": {
296
- "id": {
297
- "type": "string"
298
- },
299
- "value": {
300
- "type": "string"
301
- }
302
- },
303
- "type": "object"
303
+ "$ref": "#/definitions/Mock"
304
304
  },
305
305
  "result": {
306
306
  "type": "object"
@@ -319,12 +319,15 @@
319
319
  "required": ["body", "url", "result"],
320
320
  "type": "object"
321
321
  },
322
- "PostEndpoint<Mock,\"id\">": {
322
+ "PatchMockEndpoint": {
323
+ "$ref": "#/definitions/PatchEndpoint%3CMock%2C%22id%22%2CMock%3E"
324
+ },
325
+ "PostEndpoint<Mock,\"id\",Mock>": {
323
326
  "additionalProperties": false,
324
327
  "description": "Endpoint model for creating new entities",
325
328
  "properties": {
326
329
  "body": {
327
- "$ref": "#/definitions/WithOptionalId%3CMock%2C%22id%22%3E"
330
+ "$ref": "#/definitions/Mock"
328
331
  },
329
332
  "result": {
330
333
  "$ref": "#/definitions/Mock"
@@ -333,6 +336,9 @@
333
336
  "required": ["body", "result"],
334
337
  "type": "object"
335
338
  },
339
+ "PostMockEndpoint": {
340
+ "$ref": "#/definitions/PostEndpoint%3CMock%2C%22id%22%2CMock%3E"
341
+ },
336
342
  "RestApi": {
337
343
  "additionalProperties": false,
338
344
  "properties": {
@@ -640,7 +646,7 @@
640
646
  "additionalProperties": false,
641
647
  "properties": {
642
648
  "/mock/:id": {
643
- "$ref": "#/definitions/DeleteEndpoint%3CMock%2C%22id%22%3E"
649
+ "$ref": "#/definitions/DeleteMockEndpoint"
644
650
  }
645
651
  },
646
652
  "required": ["/mock/:id"],
@@ -650,10 +656,10 @@
650
656
  "additionalProperties": false,
651
657
  "properties": {
652
658
  "/mock": {
653
- "$ref": "#/definitions/GetCollectionEndpoint%3CMock%3E"
659
+ "$ref": "#/definitions/GetMockCollectionEndpoint"
654
660
  },
655
661
  "/mock/:id": {
656
- "$ref": "#/definitions/GetEntityEndpoint%3CMock%2C%22id%22%3E"
662
+ "$ref": "#/definitions/GetMockEntityEndpoint"
657
663
  },
658
664
  "/validate-headers": {
659
665
  "$ref": "#/definitions/ValidateHeaders"
@@ -702,7 +708,7 @@
702
708
  "additionalProperties": false,
703
709
  "properties": {
704
710
  "/mock/:id": {
705
- "$ref": "#/definitions/PatchEndpoint%3CMock%2C%22id%22%3E"
711
+ "$ref": "#/definitions/PatchMockEndpoint"
706
712
  }
707
713
  },
708
714
  "required": ["/mock/:id"],
@@ -712,7 +718,7 @@
712
718
  "additionalProperties": false,
713
719
  "properties": {
714
720
  "/mock": {
715
- "$ref": "#/definitions/PostEndpoint%3CMock%2C%22id%22%3E"
721
+ "$ref": "#/definitions/PostMockEndpoint"
716
722
  },
717
723
  "/validate-body": {
718
724
  "$ref": "#/definitions/ValidateBody"
@@ -754,19 +760,6 @@
754
760
  },
755
761
  "required": ["GET", "POST", "PATCH", "DELETE"],
756
762
  "type": "object"
757
- },
758
- "WithOptionalId<Mock,\"id\">": {
759
- "additionalProperties": false,
760
- "properties": {
761
- "id": {
762
- "type": "string"
763
- },
764
- "value": {
765
- "type": "string"
766
- }
767
- },
768
- "required": ["value"],
769
- "type": "object"
770
763
  }
771
764
  }
772
765
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
1
2
  import { getStoreManager, InMemoryStore, User } from '@furystack/core'
2
3
  import { getPort } from '@furystack/core/port-generator'
3
4
  import { Injector } from '@furystack/inject'
@@ -6,6 +7,12 @@ import { createClient, ResponseError } from '@furystack/rest-client-fetch'
6
7
  import { usingAsync } from '@furystack/utils'
7
8
  import type Ajv from 'ajv'
8
9
  import { describe, expect, it } from 'vitest'
10
+ import { createDeleteEndpoint } from './endpoint-generators/create-delete-endpoint.js'
11
+ import { createGetCollectionEndpoint } from './endpoint-generators/create-get-collection-endpoint.js'
12
+ import { createGetEntityEndpoint } from './endpoint-generators/create-get-entity-endpoint.js'
13
+ import { createPatchEndpoint } from './endpoint-generators/create-patch-endpoint.js'
14
+ import { createPostEndpoint } from './endpoint-generators/create-post-endpoint.js'
15
+ import { MockClass } from './endpoint-generators/utils.js'
9
16
  import { useRestService } from './helpers.js'
10
17
  import { DefaultSession } from './models/default-session.js'
11
18
  import { JsonResult } from './request-action-implementation.js'
@@ -46,8 +53,14 @@ const createValidateApi = async (options = { enableGetSchema: false }) => {
46
53
  schema,
47
54
  schemaName: 'ValidateHeaders',
48
55
  })(async ({ headers }) => JsonResult({ ...headers })),
49
- '/mock': undefined as any, // ToDo: Generator and test
50
- '/mock/:id': undefined as any, // ToDo: Generator and test
56
+ '/mock': Validate({
57
+ schema,
58
+ schemaName: 'GetMockCollectionEndpoint',
59
+ })(createGetCollectionEndpoint({ model: MockClass, primaryKey: 'id' })),
60
+ '/mock/:id': Validate({
61
+ schema,
62
+ schemaName: 'GetMockEntityEndpoint',
63
+ })(createGetEntityEndpoint({ model: MockClass, primaryKey: 'id' })),
51
64
  },
52
65
  POST: {
53
66
  '/validate-body': Validate({
@@ -57,13 +70,22 @@ const createValidateApi = async (options = { enableGetSchema: false }) => {
57
70
  const body = await getBody()
58
71
  return JsonResult({ ...body })
59
72
  }),
60
- '/mock': undefined as any, // ToDo: Generator and test
73
+ '/mock': Validate({
74
+ schema,
75
+ schemaName: 'PostMockEndpoint',
76
+ })(createPostEndpoint({ model: MockClass, primaryKey: 'id' })),
61
77
  },
62
78
  PATCH: {
63
- '/mock/:id': undefined as any, // ToDo: Generator and test
79
+ '/mock/:id': Validate({
80
+ schema,
81
+ schemaName: 'PatchMockEndpoint',
82
+ })(createPatchEndpoint({ model: MockClass, primaryKey: 'id' })),
64
83
  },
65
84
  DELETE: {
66
- '/mock/:id': undefined as any, // ToDo: Generator and test
85
+ '/mock/:id': Validate({
86
+ schema,
87
+ schemaName: 'DeleteMockEndpoint',
88
+ })(createDeleteEndpoint({ model: MockClass, primaryKey: 'id' })),
67
89
  },
68
90
  },
69
91
  port,
@@ -207,37 +229,63 @@ describe('Validation integration tests', () => {
207
229
  expect(result.result.description).toBe(description)
208
230
  expect(result.result.version).toBe(version)
209
231
 
210
- expect(result.result.endpoints['/validate-query']).toBeDefined()
211
-
212
- expect(result.result.endpoints['/validate-query'].schema).toStrictEqual(schema)
213
- expect(result.result.endpoints['/validate-query'].schemaName).toBe('ValidateQuery')
214
- expect(result.result.endpoints['/validate-query'].method).toBe('GET')
215
- expect(result.result.endpoints['/validate-query'].path).toBe('/validate-query')
216
- expect(result.result.endpoints['/validate-query'].isAuthenticated).toBe(false)
217
-
218
- expect(result.result.endpoints['/validate-url/:id']).toBeDefined()
219
- expect(result.result.endpoints['/validate-url/:id'].schema).toStrictEqual(schema)
220
- expect(result.result.endpoints['/validate-url/:id'].schemaName).toBe('ValidateUrl')
221
- expect(result.result.endpoints['/validate-url/:id'].method).toBe('GET')
222
- expect(result.result.endpoints['/validate-url/:id'].path).toBe('/validate-url/:id')
223
- expect(result.result.endpoints['/validate-url/:id'].isAuthenticated).toBe(false)
224
-
225
- expect(result.result.endpoints['/validate-headers']).toBeDefined()
226
- expect(result.result.endpoints['/validate-headers'].schema).toStrictEqual(schema)
227
- expect(result.result.endpoints['/validate-headers'].schemaName).toBe('ValidateHeaders')
228
- expect(result.result.endpoints['/validate-headers'].method).toBe('GET')
229
- expect(result.result.endpoints['/validate-headers'].path).toBe('/validate-headers')
230
- expect(result.result.endpoints['/validate-headers'].isAuthenticated).toBe(false)
231
-
232
- expect(result.result.endpoints['/validate-body']).toBeDefined()
233
- expect(result.result.endpoints['/validate-body'].schema).toStrictEqual(schema)
234
- expect(result.result.endpoints['/validate-body'].schemaName).toBe('ValidateBody')
235
- expect(result.result.endpoints['/validate-body'].method).toBe('POST')
236
- expect(result.result.endpoints['/validate-body'].path).toBe('/validate-body')
237
- expect(result.result.endpoints['/validate-body'].isAuthenticated).toBe(false)
238
-
239
- expect(result.result.endpoints['/mock']).toBeUndefined()
240
- expect(result.result.endpoints['/mock/:id']).toBeUndefined()
232
+ // GET endpoints
233
+ expect(result.result.endpoints.GET?.['/validate-query']).toBeDefined()
234
+ expect(result.result.endpoints.GET?.['/validate-query']?.schema).toStrictEqual(schema)
235
+ expect(result.result.endpoints.GET?.['/validate-query']?.schemaName).toBe('ValidateQuery')
236
+ expect(result.result.endpoints.GET?.['/validate-query']?.path).toBe('/validate-query')
237
+ expect(result.result.endpoints.GET?.['/validate-query']?.isAuthenticated).toBe(false)
238
+
239
+ expect(result.result.endpoints.GET?.['/validate-url/:id']).toBeDefined()
240
+ expect(result.result.endpoints.GET?.['/validate-url/:id']?.schema).toStrictEqual(schema)
241
+ expect(result.result.endpoints.GET?.['/validate-url/:id']?.schemaName).toBe('ValidateUrl')
242
+ expect(result.result.endpoints.GET?.['/validate-url/:id']?.path).toBe('/validate-url/:id')
243
+ expect(result.result.endpoints.GET?.['/validate-url/:id']?.isAuthenticated).toBe(false)
244
+
245
+ expect(result.result.endpoints.GET?.['/validate-headers']).toBeDefined()
246
+ expect(result.result.endpoints.GET?.['/validate-headers']?.schema).toStrictEqual(schema)
247
+ expect(result.result.endpoints.GET?.['/validate-headers']?.schemaName).toBe('ValidateHeaders')
248
+ expect(result.result.endpoints.GET?.['/validate-headers']?.path).toBe('/validate-headers')
249
+ expect(result.result.endpoints.GET?.['/validate-headers']?.isAuthenticated).toBe(false)
250
+
251
+ expect(result.result.endpoints.GET?.['/mock']).toBeDefined()
252
+ expect(result.result.endpoints.GET?.['/mock']?.schema).toStrictEqual(schema)
253
+ expect(result.result.endpoints.GET?.['/mock']?.schemaName).toBe('GetMockCollectionEndpoint')
254
+ expect(result.result.endpoints.GET?.['/mock']?.path).toBe('/mock')
255
+ expect(result.result.endpoints.GET?.['/mock']?.isAuthenticated).toBe(false)
256
+
257
+ expect(result.result.endpoints.GET?.['/mock/:id']).toBeDefined()
258
+ expect(result.result.endpoints.GET?.['/mock/:id']?.schema).toStrictEqual(schema)
259
+ expect(result.result.endpoints.GET?.['/mock/:id']?.schemaName).toBe('GetMockEntityEndpoint')
260
+ expect(result.result.endpoints.GET?.['/mock/:id']?.path).toBe('/mock/:id')
261
+ expect(result.result.endpoints.GET?.['/mock/:id']?.isAuthenticated).toBe(false)
262
+
263
+ // POST endpoints
264
+ expect(result.result.endpoints.POST?.['/validate-body']).toBeDefined()
265
+ expect(result.result.endpoints.POST?.['/validate-body']?.schema).toStrictEqual(schema)
266
+ expect(result.result.endpoints.POST?.['/validate-body']?.schemaName).toBe('ValidateBody')
267
+ expect(result.result.endpoints.POST?.['/validate-body']?.path).toBe('/validate-body')
268
+ expect(result.result.endpoints.POST?.['/validate-body']?.isAuthenticated).toBe(false)
269
+
270
+ expect(result.result.endpoints.POST?.['/mock']).toBeDefined()
271
+ expect(result.result.endpoints.POST?.['/mock']?.schema).toStrictEqual(schema)
272
+ expect(result.result.endpoints.POST?.['/mock']?.schemaName).toBe('PostMockEndpoint')
273
+ expect(result.result.endpoints.POST?.['/mock']?.path).toBe('/mock')
274
+ expect(result.result.endpoints.POST?.['/mock']?.isAuthenticated).toBe(false)
275
+
276
+ // PATCH endpoints
277
+ expect(result.result.endpoints.PATCH?.['/mock/:id']).toBeDefined()
278
+ expect(result.result.endpoints.PATCH?.['/mock/:id']?.schema).toStrictEqual(schema)
279
+ expect(result.result.endpoints.PATCH?.['/mock/:id']?.schemaName).toBe('PatchMockEndpoint')
280
+ expect(result.result.endpoints.PATCH?.['/mock/:id']?.path).toBe('/mock/:id')
281
+ expect(result.result.endpoints.PATCH?.['/mock/:id']?.isAuthenticated).toBe(false)
282
+
283
+ // DELETE endpoints
284
+ expect(result.result.endpoints.DELETE?.['/mock/:id']).toBeDefined()
285
+ expect(result.result.endpoints.DELETE?.['/mock/:id']?.schema).toStrictEqual(schema)
286
+ expect(result.result.endpoints.DELETE?.['/mock/:id']?.schemaName).toBe('DeleteMockEndpoint')
287
+ expect(result.result.endpoints.DELETE?.['/mock/:id']?.path).toBe('/mock/:id')
288
+ expect(result.result.endpoints.DELETE?.['/mock/:id']?.isAuthenticated).toBe(false)
241
289
  })
242
290
  })
243
291
  })
package/src/validate.ts CHANGED
@@ -1,12 +1,31 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-return */
2
- /* eslint-disable @typescript-eslint/ban-ts-comment */
3
- /* eslint-disable @typescript-eslint/no-unsafe-call */
4
- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
5
1
  import type { ActionResult, RequestAction, RequestActionOptions } from './request-action-implementation.js'
6
2
  import { SchemaValidator } from './schema-validator/schema-validator.js'
7
3
 
4
+ /**
5
+ * Represents a JSON Schema definition structure
6
+ */
7
+ type JsonSchemaDefinition = {
8
+ required?: string[]
9
+ additionalProperties?: boolean
10
+ properties?: {
11
+ headers?: {
12
+ additionalProperties?: boolean
13
+ [key: string]: unknown
14
+ }
15
+ [key: string]: unknown
16
+ }
17
+ [key: string]: unknown
18
+ }
19
+
20
+ /**
21
+ * Represents a JSON Schema with definitions
22
+ */
23
+ type JsonSchemaWithDefinitions = {
24
+ definitions: Record<string, JsonSchemaDefinition>
25
+ }
26
+
8
27
  export const Validate =
9
- <TSchema extends { definitions: { [K: string]: any } }>(validationOptions: {
28
+ <TSchema extends JsonSchemaWithDefinitions>(validationOptions: {
10
29
  /**
11
30
  * The Schema object
12
31
  */
@@ -16,12 +35,17 @@ export const Validate =
16
35
  */
17
36
  schemaName: keyof TSchema['definitions']
18
37
  }) =>
19
- <T extends { result: any }>(action: RequestAction<T>): RequestAction<T> => {
38
+ <T extends { result: unknown }>(
39
+ action: RequestAction<T>,
40
+ ): RequestAction<T> & {
41
+ schema: TSchema
42
+ schemaName: keyof TSchema['definitions']
43
+ } => {
20
44
  const schema = { ...validationOptions.schema }
21
45
 
22
46
  Object.values(schema.definitions).forEach((definition) => {
23
- if (definition.required && definition.required.includes('result')) {
24
- definition.required = definition.required.filter((value: any) => value !== 'result')
47
+ if (definition.required?.includes('result')) {
48
+ definition.required = definition.required.filter((value) => value !== 'result')
25
49
  }
26
50
  definition.additionalProperties = true
27
51
  if (definition.properties?.headers) {
@@ -31,40 +55,45 @@ export const Validate =
31
55
 
32
56
  const validator = new SchemaValidator(schema, { coerceTypes: true, strict: false })
33
57
 
34
- const wrapped = async (args: RequestActionOptions<T>): Promise<ActionResult<T>> => {
35
- const anyArgs = args as any
36
- let body!: any
37
- const { headers } = anyArgs
38
- const query = anyArgs.getQuery?.()
39
- const url = anyArgs.getUrlParams?.()
58
+ const wrapped = async (args: RequestActionOptions<T>): Promise<ActionResult<T['result']>> => {
59
+ const headers = 'headers' in args ? (args.headers as Record<string, string>) : undefined
60
+ const query = 'getQuery' in args ? (args as { getQuery: () => unknown }).getQuery() : undefined
61
+ const url = 'getUrlParams' in args ? (args as { getUrlParams: () => unknown }).getUrlParams() : undefined
62
+
63
+ let body: unknown
40
64
  try {
41
- body = await anyArgs.getBody?.()
42
- } catch (error) {
43
- // ignore
65
+ if ('getBody' in args) {
66
+ body = await (args as { getBody: () => Promise<unknown> }).getBody()
67
+ }
68
+ } catch {
69
+ // Body parsing may fail for requests without body
44
70
  }
71
+
45
72
  validator.isValid(
46
73
  {
47
- ...(query ? { query } : {}),
48
- ...(body ? { body } : {}),
49
- ...(url ? { url } : {}),
50
- ...(headers ? { headers } : {}),
74
+ ...(query !== undefined ? { query } : {}),
75
+ ...(body !== undefined ? { body } : {}),
76
+ ...(url !== undefined ? { url } : {}),
77
+ ...(headers !== undefined ? { headers } : {}),
51
78
  },
52
79
  { schemaName: validationOptions.schemaName },
53
80
  )
54
- // @ts-expect-error
55
- return await action({
81
+
82
+ const validatedArgs = {
56
83
  request: args.request,
57
84
  response: args.response,
58
85
  injector: args.injector,
59
- headers,
60
- getQuery: () => query,
61
- getUrlParams: () => url,
62
- getBody: () => Promise.resolve(body),
63
- })
64
- }
86
+ ...(headers !== undefined && { headers }),
87
+ ...(query !== undefined && { getQuery: () => query }),
88
+ ...(url !== undefined && { getUrlParams: () => url }),
89
+ ...(body !== undefined && { getBody: () => Promise.resolve(body) }),
90
+ } as RequestActionOptions<T>
65
91
 
66
- wrapped.schema = schema
67
- wrapped.schemaName = validationOptions.schemaName
92
+ return await action(validatedArgs)
93
+ }
68
94
 
69
- return wrapped
95
+ return Object.assign(wrapped, {
96
+ schema,
97
+ schemaName: validationOptions.schemaName,
98
+ })
70
99
  }