@furystack/rest-service 10.0.23 → 10.0.24

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 (66) hide show
  1. package/esm/api-manager.d.ts +47 -1
  2. package/esm/api-manager.d.ts.map +1 -1
  3. package/esm/api-manager.js +19 -5
  4. package/esm/api-manager.js.map +1 -1
  5. package/esm/authenticate.d.ts.map +1 -1
  6. package/esm/authenticate.js +3 -1
  7. package/esm/authenticate.js.map +1 -1
  8. package/esm/endpoint-generators/create-get-schema-action.d.ts +16 -0
  9. package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -0
  10. package/esm/endpoint-generators/create-get-schema-action.js +21 -0
  11. package/esm/endpoint-generators/create-get-schema-action.js.map +1 -0
  12. package/esm/endpoint-generators/create-get-swagger-json-action.d.ts +14 -0
  13. package/esm/endpoint-generators/create-get-swagger-json-action.d.ts.map +1 -0
  14. package/esm/endpoint-generators/create-get-swagger-json-action.js +17 -0
  15. package/esm/endpoint-generators/create-get-swagger-json-action.js.map +1 -0
  16. package/esm/endpoint-generators/index.d.ts +1 -0
  17. package/esm/endpoint-generators/index.d.ts.map +1 -1
  18. package/esm/endpoint-generators/index.js +1 -0
  19. package/esm/endpoint-generators/index.js.map +1 -1
  20. package/esm/endpoint-generators/with-schema-and-swagger-action.d.ts +21 -0
  21. package/esm/endpoint-generators/with-schema-and-swagger-action.d.ts.map +1 -0
  22. package/esm/endpoint-generators/with-schema-and-swagger-action.js +2 -0
  23. package/esm/endpoint-generators/with-schema-and-swagger-action.js.map +1 -0
  24. package/esm/get-schema-from-api.d.ts +10 -0
  25. package/esm/get-schema-from-api.d.ts.map +1 -0
  26. package/esm/get-schema-from-api.js +55 -0
  27. package/esm/get-schema-from-api.js.map +1 -0
  28. package/esm/get-schema-from-api.spec.d.ts +2 -0
  29. package/esm/get-schema-from-api.spec.d.ts.map +1 -0
  30. package/esm/get-schema-from-api.spec.js +68 -0
  31. package/esm/get-schema-from-api.spec.js.map +1 -0
  32. package/esm/helpers.d.ts +4 -1
  33. package/esm/helpers.d.ts.map +1 -1
  34. package/esm/index.d.ts +10 -9
  35. package/esm/index.d.ts.map +1 -1
  36. package/esm/index.js +10 -9
  37. package/esm/index.js.map +1 -1
  38. package/esm/swagger/generate-swagger-json.d.ts +14 -0
  39. package/esm/swagger/generate-swagger-json.d.ts.map +1 -0
  40. package/esm/swagger/generate-swagger-json.js +106 -0
  41. package/esm/swagger/generate-swagger-json.js.map +1 -0
  42. package/esm/swagger/generate-swagger-json.spec.d.ts +2 -0
  43. package/esm/swagger/generate-swagger-json.spec.d.ts.map +1 -0
  44. package/esm/swagger/generate-swagger-json.spec.js +204 -0
  45. package/esm/swagger/generate-swagger-json.spec.js.map +1 -0
  46. package/esm/validate.d.ts.map +1 -1
  47. package/esm/validate.integration.spec.js +154 -2
  48. package/esm/validate.integration.spec.js.map +1 -1
  49. package/esm/validate.integration.spec.schema.json +0 -2
  50. package/esm/validate.js +4 -1
  51. package/esm/validate.js.map +1 -1
  52. package/package.json +7 -7
  53. package/src/api-manager.ts +68 -5
  54. package/src/authenticate.ts +5 -1
  55. package/src/endpoint-generators/create-get-schema-action.ts +29 -0
  56. package/src/endpoint-generators/create-get-swagger-json-action.ts +26 -0
  57. package/src/endpoint-generators/index.ts +1 -0
  58. package/src/endpoint-generators/with-schema-and-swagger-action.ts +11 -0
  59. package/src/get-schema-from-api.spec.ts +73 -0
  60. package/src/get-schema-from-api.ts +74 -0
  61. package/src/index.ts +10 -9
  62. package/src/swagger/generate-swagger-json.spec.ts +239 -0
  63. package/src/swagger/generate-swagger-json.ts +123 -0
  64. package/src/validate.integration.spec.schema.json +0 -2
  65. package/src/validate.integration.spec.ts +173 -2
  66. package/src/validate.ts +6 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furystack/rest-service",
3
- "version": "10.0.23",
3
+ "version": "10.0.24",
4
4
  "description": "Repository implementation for FuryStack",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -37,11 +37,11 @@
37
37
  },
38
38
  "homepage": "https://github.com/furystack/furystack",
39
39
  "dependencies": {
40
- "@furystack/core": "^15.0.22",
40
+ "@furystack/core": "^15.0.23",
41
41
  "@furystack/inject": "^12.0.19",
42
- "@furystack/repository": "^10.0.22",
43
- "@furystack/rest": "^8.0.22",
44
- "@furystack/security": "^6.0.22",
42
+ "@furystack/repository": "^10.0.23",
43
+ "@furystack/rest": "^8.0.23",
44
+ "@furystack/security": "^6.0.23",
45
45
  "@furystack/utils": "^8.1.1",
46
46
  "ajv": "^8.17.1",
47
47
  "ajv-formats": "^3.0.1",
@@ -49,8 +49,8 @@
49
49
  "semaphore-async-await": "^1.5.1"
50
50
  },
51
51
  "devDependencies": {
52
- "@furystack/rest-client-fetch": "^8.0.22",
53
- "@types/node": "^24.0.3",
52
+ "@furystack/rest-client-fetch": "^8.0.23",
53
+ "@types/node": "^24.0.4",
54
54
  "typescript": "^5.8.3",
55
55
  "vitest": "^3.2.4"
56
56
  },
@@ -10,11 +10,13 @@ import { match } from 'path-to-regexp'
10
10
  import { ErrorAction } from './actions/error-action.js'
11
11
  import { NotFoundAction } from './actions/not-found-action.js'
12
12
  import { addCorsHeaders } from './add-cors-header.js'
13
+ import { CreateGetSchemaAction } from './endpoint-generators/create-get-schema-action.js'
14
+ import { CreateGetSwaggerJsonAction } from './endpoint-generators/create-get-swagger-json-action.js'
13
15
  import { HttpUserContext } from './http-user-context.js'
14
16
  import type { CorsOptions } from './models/cors-options.js'
15
17
  import { readPostBody } from './read-post-body.js'
16
18
  import type { RequestAction } from './request-action-implementation.js'
17
- import type { OnRequest } from './server-manager.js'
19
+ import type { OnRequest, ServerApi } from './server-manager.js'
18
20
  import { ServerManager } from './server-manager.js'
19
21
  import './server-response-extensions.js'
20
22
 
@@ -25,13 +27,57 @@ export type RestApiImplementation<T extends RestApi> = {
25
27
  }
26
28
 
27
29
  export interface ImplementApiOptions<T extends RestApi> {
30
+ /**
31
+ * The structure of the implemented API.
32
+ */
28
33
  api: RestApiImplementation<T>
34
+ /**
35
+ * The Injector instance to use for dependency injection in the API actions.
36
+ */
29
37
  injector: Injector
38
+ /**
39
+ * The host name for the API Server. If not provided, the default host (ServerManager.DEFAULT_HOST) will be used.
40
+ */
30
41
  hostName?: string
42
+ /**
43
+ * The root path for the API. This will be prepended to all API paths.
44
+ */
31
45
  root: string
46
+ /**
47
+ * The port on which the API server will listen.
48
+ */
32
49
  port: number
50
+ /**
51
+ * CORS options to configure Cross-Origin Resource Sharing for the API.
52
+ */
33
53
  cors?: CorsOptions
54
+ /**
55
+ * An optional function to deserialize query parameters from the URL.
56
+ * This function should take a query string (e.g., "?key=value") and return an object with the parsed parameters.
57
+ * If not provided, the default deserialization will be used.
58
+ */
34
59
  deserializeQueryParams?: (param: string) => any
60
+ /**
61
+ * Adds an additional 'GET /schema' endpoint that returns the schema definitions of the API.
62
+ * Also adds a 'GET /swagger.json' endpoint that returns the API schema in OpenAPI 3.0 (Swagger) format.
63
+ */
64
+ enableGetSchema?: boolean
65
+
66
+ /**
67
+ * Optional name for the API, used in the generated schema.
68
+ * This can be useful for documentation or identification purposes.
69
+ */
70
+ name?: string
71
+ /**
72
+ * Optional description for the API, used in the generated schema.
73
+ * This can provide additional context or information about the API's purpose.
74
+ */
75
+ description?: string
76
+ /**
77
+ * Optional version for the API, used in the generated schema.
78
+ * This can help in versioning the API and tracking changes over time.
79
+ */
80
+ version?: string
35
81
  }
36
82
 
37
83
  export type NewCompiledApiEntry = {
@@ -92,12 +138,27 @@ export class ApiManager implements Disposable {
92
138
  cors,
93
139
  injector,
94
140
  deserializeQueryParams,
141
+ enableGetSchema,
142
+ name,
143
+ description,
144
+ version,
95
145
  }: ImplementApiOptions<T>) {
96
- const supportedMethods = this.getSuportedMethods(api)
146
+ const extendedApi: typeof api = enableGetSchema
147
+ ? {
148
+ ...api,
149
+ GET: {
150
+ ...api.GET,
151
+ '/schema': CreateGetSchemaAction(api, name, description, version),
152
+ '/swagger.json': CreateGetSwaggerJsonAction(api, name, description, version),
153
+ },
154
+ }
155
+ : api
156
+
157
+ const supportedMethods = this.getSuportedMethods(extendedApi)
97
158
  const rootApiPath = PathHelper.normalize(root)
98
159
  const server = await this.serverManager.getOrCreate({ hostName, port })
99
- const compiledApi = this.compileApi(api, root)
100
- server.apis.push({
160
+ const compiledApi = this.compileApi(extendedApi, root)
161
+ const serverApi = {
101
162
  shouldExec: (msg) =>
102
163
  this.shouldExecRequest({
103
164
  ...msg,
@@ -118,7 +179,9 @@ export class ApiManager implements Disposable {
118
179
  hostName,
119
180
  deserializeQueryParams,
120
181
  }),
121
- })
182
+ } satisfies ServerApi
183
+ server.apis.push(serverApi)
184
+ return serverApi
122
185
  }
123
186
 
124
187
  public shouldExecRequest(options: {
@@ -7,7 +7,7 @@ import { JsonResult } from './request-action-implementation.js'
7
7
  export const Authenticate =
8
8
  () =>
9
9
  <T extends { result: unknown }>(action: RequestAction<T>): RequestAction<T> => {
10
- return async (args: RequestActionOptions<T>): Promise<ActionResult<T>> => {
10
+ const wrapped = async (args: RequestActionOptions<T>): Promise<ActionResult<T>> => {
11
11
  const { injector } = args
12
12
  const authenticated = await isAuthenticated(injector)
13
13
  if (!authenticated) {
@@ -20,4 +20,8 @@ export const Authenticate =
20
20
  }
21
21
  return (await action(args)) as ActionResult<T>
22
22
  }
23
+
24
+ wrapped.isAuthenticated = true
25
+
26
+ return wrapped
23
27
  }
@@ -0,0 +1,29 @@
1
+ import { RequestError } from '@furystack/rest'
2
+ import type { RestApiImplementation } from '../api-manager.js'
3
+ import { getSchemaFromApi } from '../get-schema-from-api.js'
4
+ import { JsonResult, type RequestAction } from '../request-action-implementation.js'
5
+
6
+ export type GetSchemaResult = ReturnType<typeof getSchemaFromApi>
7
+
8
+ /**
9
+ * Creates a GET action that returns the schema of the provided API.
10
+ * The schema is returned in JSON format when the request's Accept header includes 'application/schema+json'.
11
+ * If the Accept header does not match, a 406 Not Acceptable error is thrown.
12
+ *
13
+ * @param api - The API implementation from which to extract the schema.
14
+ * @returns A RequestAction that handles the GET request for the schema.
15
+ */
16
+ export const CreateGetSchemaAction = <T extends RestApiImplementation<any>>(
17
+ api: T,
18
+ name = 'FuryStack API',
19
+ description = 'API documentation generated from FuryStack API schema',
20
+ version = '1.0.0',
21
+ ): RequestAction<{ result: GetSchemaResult }> => {
22
+ const schema = getSchemaFromApi({ api, name, description, version })
23
+ return async ({ request }) => {
24
+ if (request.headers.accept?.includes('application/schema+json')) {
25
+ return JsonResult(schema, 200)
26
+ }
27
+ throw new RequestError('The requested content type is not supported. Please use "application/schema+json".', 406)
28
+ }
29
+ }
@@ -0,0 +1,26 @@
1
+ import { type ApiEndpointDefinition, type SwaggerDocument } from '@furystack/rest'
2
+ import type { RestApiImplementation } from '../api-manager.js'
3
+ import { getSchemaFromApi } from '../get-schema-from-api.js'
4
+ import { JsonResult, type RequestAction } from '../request-action-implementation.js'
5
+ import { generateSwaggerJsonFromApiSchema } from '../swagger/generate-swagger-json.js'
6
+
7
+ export type GetSchemaResult = Record<string, ApiEndpointDefinition>
8
+
9
+ /**
10
+ * Generates a RequestAction that retrieves the Swagger JSON schema from a FuryStack API implementation.
11
+ *
12
+ * @param api - The API implementation from which to extract the schema.
13
+ * @returns A RequestAction that handles the GET request for the schema.
14
+ */
15
+ export const CreateGetSwaggerJsonAction = <T extends RestApiImplementation<any>>(
16
+ api: T,
17
+ name = 'FuryStack API',
18
+ description = 'API documentation generated from FuryStack API schema',
19
+ version = '1.0.0',
20
+ ): RequestAction<{ result: GetSchemaResult | SwaggerDocument }> => {
21
+ const { endpoints } = getSchemaFromApi({ api, name, description, version })
22
+ const swaggerJson = generateSwaggerJsonFromApiSchema({ api: endpoints, title: name, description, version })
23
+ return async () => {
24
+ return JsonResult(swaggerJson, 200)
25
+ }
26
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './create-delete-endpoint.js'
2
2
  export * from './create-get-collection-endpoint.js'
3
3
  export * from './create-get-entity-endpoint.js'
4
+ export * from './create-get-schema-action.js'
4
5
  export * from './create-patch-endpoint.js'
5
6
  export * from './create-post-endpoint.js'
@@ -0,0 +1,11 @@
1
+ import type { RestApi, SwaggerDocument } from '@furystack/rest'
2
+
3
+ /**
4
+ * Extends a RestApi with endpoints for both schema.json and swagger.json
5
+ */
6
+ export type WithSchemaAndSwaggerAction<T extends RestApi> = T & {
7
+ GET: {
8
+ '/schema': { result: Record<string, any>; headers: { accept: 'application/schema+json' } }
9
+ '/swagger.json': { result: SwaggerDocument; headers: { accept: 'application/swagger+json' } }
10
+ }
11
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { RestApiImplementation } from './api-manager.js'
3
+ import { defaultSchema, getSchemaFromApi } from './get-schema-from-api.js'
4
+ import { JsonResult, type RequestAction } from './request-action-implementation.js'
5
+ import validationSchema from './validate.integration.spec.schema.json' with { type: 'json' }
6
+ import { Validate } from './validate.js'
7
+
8
+ const ExampleAction: RequestAction<any> = async () => {
9
+ return JsonResult({ success: true })
10
+ }
11
+
12
+ describe('getSchemaFromApi', () => {
13
+ it('Should return the default schema from the API', () => {
14
+ const Api = {
15
+ GET: {
16
+ '/example': ExampleAction,
17
+ },
18
+ } as const satisfies RestApiImplementation<any>
19
+
20
+ const schema = getSchemaFromApi({
21
+ api: Api,
22
+ name: 'Test API',
23
+ description: 'Test API Description',
24
+ version: '1.0.0',
25
+ })
26
+ expect(schema).toEqual({
27
+ name: 'Test API',
28
+ description: 'Test API Description',
29
+ version: '1.0.0',
30
+ endpoints: {
31
+ '/example': {
32
+ isAuthenticated: false,
33
+ path: '/example',
34
+ method: 'GET',
35
+ schema: defaultSchema,
36
+ schemaName: 'default',
37
+ },
38
+ },
39
+ })
40
+ })
41
+
42
+ it('Should return the attached schema from the validation', () => {
43
+ const Api = {
44
+ GET: {
45
+ '/validate-query': Validate({
46
+ schema: validationSchema,
47
+ schemaName: 'ValidateQuery',
48
+ })(async () => JsonResult({ success: true })),
49
+ },
50
+ } as const satisfies RestApiImplementation<any>
51
+
52
+ const schema = getSchemaFromApi({
53
+ api: Api,
54
+ name: 'Test API',
55
+ description: 'Test API Description',
56
+ version: '1.0.0',
57
+ })
58
+ expect(schema).toEqual({
59
+ name: 'Test API',
60
+ description: 'Test API Description',
61
+ version: '1.0.0',
62
+ endpoints: {
63
+ '/validate-query': {
64
+ isAuthenticated: false,
65
+ path: '/validate-query',
66
+ method: 'GET',
67
+ schema: validationSchema,
68
+ schemaName: 'ValidateQuery',
69
+ },
70
+ },
71
+ })
72
+ })
73
+ })
@@ -0,0 +1,74 @@
1
+ import type { ApiEndpointDefinition, ApiEndpointSchema, Method, RestApi, Schema } from '@furystack/rest'
2
+ import type { RestApiImplementation } from './api-manager.js'
3
+ import type { RequestAction } from './request-action-implementation.js'
4
+
5
+ export const defaultSchema: Schema = {
6
+ definitions: {
7
+ default: {
8
+ type: 'object',
9
+ properties: {
10
+ headers: {
11
+ type: 'object',
12
+ additionalProperties: true,
13
+ },
14
+ query: {
15
+ type: 'object',
16
+ additionalProperties: true,
17
+ },
18
+ body: {
19
+ type: 'object',
20
+ additionalProperties: true,
21
+ },
22
+ url: {
23
+ type: 'object',
24
+ additionalProperties: true,
25
+ },
26
+ },
27
+ required: [],
28
+ description: 'Default schema for API endpoints',
29
+ additionalProperties: true,
30
+ },
31
+ },
32
+ }
33
+
34
+ const defaultSchemaName = 'default'
35
+
36
+ const getDefinitionFromAction = (method: Method, path: string, action: RequestAction<any>): ApiEndpointDefinition => {
37
+ return {
38
+ method,
39
+ path,
40
+ schema: 'schema' in action && typeof action.schema === 'object' ? action.schema : defaultSchema,
41
+ schemaName: 'schemaName' in action && typeof action.schemaName === 'string' ? action.schemaName : defaultSchemaName,
42
+ isAuthenticated:
43
+ 'isAuthenticated' in action && typeof action.isAuthenticated === 'boolean' ? action.isAuthenticated : false,
44
+ }
45
+ }
46
+
47
+ export const getSchemaFromApi = <T extends RestApiImplementation<RestApi>>({
48
+ api,
49
+ name = 'FuryStack API',
50
+ description = 'API documentation generated from FuryStack API schema',
51
+ version = '1.0.0',
52
+ }: {
53
+ api: T
54
+ name?: string
55
+ description?: string
56
+ version?: string
57
+ }): ApiEndpointSchema => {
58
+ const endpoints: Record<string, ApiEndpointDefinition> = {}
59
+
60
+ Object.entries(api).forEach(([method, endpointList]) => {
61
+ Object.entries(endpointList as Record<string, RequestAction<any>>).forEach(([url, requestAction]) => {
62
+ if (method && url && requestAction) {
63
+ endpoints[url] = getDefinitionFromAction(method as Method, url, requestAction)
64
+ }
65
+ })
66
+ })
67
+
68
+ return {
69
+ name,
70
+ description,
71
+ version,
72
+ endpoints,
73
+ }
74
+ }
package/src/index.ts CHANGED
@@ -1,18 +1,19 @@
1
- export * from './helpers.js'
1
+ export * from './actions/index.js'
2
2
  export * from './add-cors-header.js'
3
3
  export * from './api-manager.js'
4
- export * from './actions/index.js'
5
4
  export * from './authenticate.js'
6
5
  export * from './authorize.js'
6
+ export * from './endpoint-generators/index.js'
7
+ export * from './get-schema-from-api.js'
8
+ export * from './helpers.js'
7
9
  export * from './http-authentication-settings.js'
8
10
  export * from './http-user-context.js'
9
- export * from './server-manager.js'
10
- export * from './server-response-extensions.js'
11
- export * from './models/index.js'
12
- export * from './endpoint-generators/index.js'
13
- export * from './schema-validator/index.js'
14
- export * from './request-action-implementation.js'
15
- export * from './validate.js'
16
11
  export * from './mime-types.js'
12
+ export * from './models/index.js'
17
13
  export * from './read-post-body.js'
14
+ export * from './request-action-implementation.js'
15
+ export * from './schema-validator/index.js'
16
+ export * from './server-manager.js'
17
+ export * from './server-response-extensions.js'
18
18
  export * from './static-server-manager.js'
19
+ export * from './validate.js'
@@ -0,0 +1,239 @@
1
+ import type { ApiEndpointDefinition, ParameterObject, ReferenceObject, ResponseObject } from '@furystack/rest'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { generateSwaggerJsonFromApiSchema } from './generate-swagger-json.js'
4
+
5
+ describe('generateSwaggerJsonFromApiSchema', () => {
6
+ it('Should generate a basic Swagger document with correct OpenAPI structure', () => {
7
+ const api: Record<string, ApiEndpointDefinition> = {
8
+ '/api/test': {
9
+ method: 'GET',
10
+ path: '/api/test',
11
+ isAuthenticated: false,
12
+ schemaName: 'Test',
13
+ schema: { type: 'object', properties: { id: { type: 'string' } } },
14
+ },
15
+ }
16
+
17
+ const result = generateSwaggerJsonFromApiSchema({ api })
18
+
19
+ // Check basic structure
20
+ expect(result.openapi).toBe('3.1.0')
21
+ expect(result.info.title).toBe('FuryStack API')
22
+ expect(result.info.version).toBe('1.0.0')
23
+
24
+ // Check security scheme is defined correctly for cookie-based auth
25
+ expect(result.components?.securitySchemes?.cookieAuth).toEqual({
26
+ type: 'apiKey',
27
+ in: 'cookie',
28
+ name: 'session',
29
+ })
30
+ })
31
+
32
+ it('Should convert API endpoints to OpenAPI paths correctly', () => {
33
+ const api: Record<string, ApiEndpointDefinition> = {
34
+ '/api/users': {
35
+ method: 'GET',
36
+ path: '/api/users',
37
+ isAuthenticated: true,
38
+ schemaName: 'UserCollection',
39
+ schema: { type: 'array', items: { type: 'object' } },
40
+ },
41
+ '/api/users/:id': {
42
+ method: 'GET',
43
+ path: '/api/users/:id',
44
+ isAuthenticated: true,
45
+ schemaName: 'User',
46
+ schema: { type: 'object', properties: { id: { type: 'string' } } },
47
+ },
48
+ }
49
+
50
+ const result = generateSwaggerJsonFromApiSchema({ api })
51
+
52
+ // Check paths
53
+ expect(result.paths?.['/api/users']).toBeDefined()
54
+ expect(result.paths?.['/api/users/{id}']).toBeDefined()
55
+
56
+ // Check methods
57
+ expect(result.paths?.['/api/users']?.get).toBeDefined()
58
+ expect(result.paths?.['/api/users/{id}']?.get).toBeDefined()
59
+
60
+ // Check security - should use cookieAuth for authenticated endpoints
61
+ expect(result.paths?.['/api/users']?.get?.security).toEqual([{ cookieAuth: [] }])
62
+ expect(result.paths?.['/api/users/{id}']?.get?.security).toEqual([{ cookieAuth: [] }])
63
+ })
64
+
65
+ it('Should extract path parameters correctly', () => {
66
+ const api: Record<string, ApiEndpointDefinition> = {
67
+ '/api/users/:userId/posts/:postId': {
68
+ method: 'GET',
69
+ path: '/api/users/:userId/posts/:postId',
70
+ isAuthenticated: false,
71
+ schemaName: 'Post',
72
+ schema: { type: 'object' },
73
+ },
74
+ }
75
+
76
+ const result = generateSwaggerJsonFromApiSchema({ api })
77
+
78
+ // Check path parameters
79
+ const parameters = result.paths?.['/api/users/{userId}/posts/{postId}']?.get?.parameters as ParameterObject[]
80
+ expect(parameters).toHaveLength(2)
81
+ expect(parameters?.[0].name).toBe('userId')
82
+ expect(parameters?.[0].in).toBe('path')
83
+ expect(parameters?.[0].required).toBe(true)
84
+ expect(parameters?.[1].name).toBe('postId')
85
+ expect(parameters?.[1].in).toBe('path')
86
+ expect(parameters?.[1].required).toBe(true)
87
+ })
88
+
89
+ it('Should handle different HTTP methods correctly', () => {
90
+ const api: Record<string, ApiEndpointDefinition> = {
91
+ '/api/resource-get': {
92
+ method: 'GET',
93
+ path: '/api/resource-get',
94
+ isAuthenticated: false,
95
+ schemaName: 'Resource',
96
+ schema: { type: 'object' },
97
+ },
98
+ '/api/resource-post': {
99
+ method: 'POST',
100
+ path: '/api/resource-post',
101
+ isAuthenticated: true,
102
+ schemaName: 'ResourceInput',
103
+ schema: { type: 'object' },
104
+ },
105
+ '/api/resource-put/:id': {
106
+ method: 'PUT',
107
+ path: '/api/resource-put/:id',
108
+ isAuthenticated: true,
109
+ schemaName: 'ResourceUpdate',
110
+ schema: { type: 'object' },
111
+ },
112
+ '/api/resource-delete/:id': {
113
+ method: 'DELETE',
114
+ path: '/api/resource-delete/:id',
115
+ isAuthenticated: true,
116
+ schemaName: 'ResourceDelete',
117
+ schema: { type: 'object' },
118
+ },
119
+ }
120
+
121
+ const result = generateSwaggerJsonFromApiSchema({ api })
122
+
123
+ // Check multiple methods
124
+ expect(result.paths?.['/api/resource-get'].get).toBeDefined()
125
+ expect(result.paths?.['/api/resource-post'].post).toBeDefined()
126
+ expect(result.paths?.['/api/resource-put/{id}'].put).toBeDefined()
127
+ expect(result.paths?.['/api/resource-delete/{id}'].delete).toBeDefined()
128
+
129
+ // Verify security is applied correctly based on isAuthenticated
130
+ expect(result.paths?.['/api/resource-get'].get?.security).toEqual([])
131
+ expect(result.paths?.['/api/resource-post'].post?.security).toEqual([{ cookieAuth: [] }])
132
+ })
133
+
134
+ it('Should include schemas in components', () => {
135
+ const testSchema = {
136
+ type: 'object',
137
+ properties: {
138
+ id: { type: 'string' },
139
+ name: { type: 'string' },
140
+ age: { type: 'number' },
141
+ },
142
+ }
143
+
144
+ const api: Record<string, ApiEndpointDefinition> = {
145
+ '/api/test': {
146
+ method: 'GET',
147
+ path: '/api/test',
148
+ isAuthenticated: false,
149
+ schemaName: 'TestModel',
150
+ schema: testSchema,
151
+ },
152
+ }
153
+
154
+ const result = generateSwaggerJsonFromApiSchema({ api })
155
+
156
+ // Check schema is included in components
157
+ expect(result.components?.schemas?.TestModel).toEqual(testSchema)
158
+ })
159
+
160
+ it('Should handle responses with correct status codes and content types', () => {
161
+ const api: Record<string, ApiEndpointDefinition> = {
162
+ '/api/test': {
163
+ method: 'GET',
164
+ path: '/api/test',
165
+ isAuthenticated: false,
166
+ schemaName: 'Test',
167
+ schema: { type: 'object' },
168
+ },
169
+ }
170
+
171
+ const result = generateSwaggerJsonFromApiSchema({ api })
172
+
173
+ // Check response structure
174
+ const responses = result.paths?.['/api/test'].get?.responses as Record<string, ResponseObject>
175
+
176
+ const response200 = responses?.['200']
177
+ expect(response200).toBeDefined()
178
+ expect(response200.description).toBe('Successful operation')
179
+ expect((response200.content?.['application/json']?.schema as ReferenceObject).$ref).toBe(
180
+ '#/components/schemas/Test',
181
+ )
182
+ expect(responses?.['401']).toBeDefined()
183
+ expect(responses?.['500']).toBeDefined()
184
+ expect(responses?.['401'].description).toBe('Unauthorized')
185
+ expect(responses?.['500'].description).toBe('Internal server error')
186
+ })
187
+
188
+ it('Should use empty path object if it does not exist - for code coverage', () => {
189
+ const api: Record<string, ApiEndpointDefinition> = {
190
+ '/api/test': {
191
+ method: 'GET',
192
+ path: '/api/test',
193
+ isAuthenticated: false,
194
+ schemaName: 'Test',
195
+ schema: { type: 'object' },
196
+ },
197
+ }
198
+
199
+ const result = generateSwaggerJsonFromApiSchema({ api })
200
+
201
+ // This test is mainly for coverage, checking that the path initialization logic works
202
+ expect(result.paths?.['/api/test']).toBeDefined()
203
+ })
204
+
205
+ it('Should handle endpoints without schemas', () => {
206
+ const api: Record<string, ApiEndpointDefinition> = {
207
+ '/api/no-schema': {
208
+ method: 'GET',
209
+ path: '/api/no-schema',
210
+ isAuthenticated: false,
211
+ schemaName: 'EmptySchema',
212
+ schema: null as any,
213
+ },
214
+ }
215
+
216
+ const result = generateSwaggerJsonFromApiSchema({ api })
217
+
218
+ // Should still create the path without errors
219
+ expect(result.paths?.['/api/no-schema']).toBeDefined()
220
+ expect(result.paths?.['/api/no-schema'].get).toBeDefined()
221
+ })
222
+
223
+ it('Should use operationId based on method and path', () => {
224
+ const api: Record<string, ApiEndpointDefinition> = {
225
+ '/api/users/:id/profile': {
226
+ method: 'GET',
227
+ path: '/api/users/:id/profile',
228
+ isAuthenticated: false,
229
+ schemaName: 'UserProfile',
230
+ schema: { type: 'object' },
231
+ },
232
+ }
233
+
234
+ const result = generateSwaggerJsonFromApiSchema({ api })
235
+
236
+ // Check operationId format
237
+ expect(result.paths?.['/api/users/{id}/profile'].get?.operationId).toBe('get_api_users_id_profile')
238
+ })
239
+ })