@furystack/rest-service 10.0.23 → 10.0.25
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/esm/api-manager.d.ts +47 -1
- package/esm/api-manager.d.ts.map +1 -1
- package/esm/api-manager.js +19 -5
- package/esm/api-manager.js.map +1 -1
- package/esm/authenticate.d.ts.map +1 -1
- package/esm/authenticate.js +3 -1
- package/esm/authenticate.js.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts +16 -0
- package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -0
- package/esm/endpoint-generators/create-get-schema-action.js +21 -0
- package/esm/endpoint-generators/create-get-schema-action.js.map +1 -0
- package/esm/endpoint-generators/create-get-swagger-json-action.d.ts +14 -0
- package/esm/endpoint-generators/create-get-swagger-json-action.d.ts.map +1 -0
- package/esm/endpoint-generators/create-get-swagger-json-action.js +17 -0
- package/esm/endpoint-generators/create-get-swagger-json-action.js.map +1 -0
- package/esm/endpoint-generators/index.d.ts +1 -0
- package/esm/endpoint-generators/index.d.ts.map +1 -1
- package/esm/endpoint-generators/index.js +1 -0
- package/esm/endpoint-generators/index.js.map +1 -1
- package/esm/endpoint-generators/with-schema-and-swagger-action.d.ts +21 -0
- package/esm/endpoint-generators/with-schema-and-swagger-action.d.ts.map +1 -0
- package/esm/endpoint-generators/with-schema-and-swagger-action.js +2 -0
- package/esm/endpoint-generators/with-schema-and-swagger-action.js.map +1 -0
- package/esm/get-schema-from-api.d.ts +10 -0
- package/esm/get-schema-from-api.d.ts.map +1 -0
- package/esm/get-schema-from-api.js +55 -0
- package/esm/get-schema-from-api.js.map +1 -0
- package/esm/get-schema-from-api.spec.d.ts +2 -0
- package/esm/get-schema-from-api.spec.d.ts.map +1 -0
- package/esm/get-schema-from-api.spec.js +68 -0
- package/esm/get-schema-from-api.spec.js.map +1 -0
- package/esm/helpers.d.ts +4 -1
- package/esm/helpers.d.ts.map +1 -1
- package/esm/index.d.ts +10 -9
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +10 -9
- package/esm/index.js.map +1 -1
- package/esm/swagger/generate-swagger-json.d.ts +14 -0
- package/esm/swagger/generate-swagger-json.d.ts.map +1 -0
- package/esm/swagger/generate-swagger-json.js +106 -0
- package/esm/swagger/generate-swagger-json.js.map +1 -0
- package/esm/swagger/generate-swagger-json.spec.d.ts +2 -0
- package/esm/swagger/generate-swagger-json.spec.d.ts.map +1 -0
- package/esm/swagger/generate-swagger-json.spec.js +204 -0
- package/esm/swagger/generate-swagger-json.spec.js.map +1 -0
- package/esm/validate.d.ts.map +1 -1
- package/esm/validate.integration.spec.js +154 -2
- package/esm/validate.integration.spec.js.map +1 -1
- package/esm/validate.integration.spec.schema.json +0 -2
- package/esm/validate.js +4 -1
- package/esm/validate.js.map +1 -1
- package/package.json +10 -10
- package/src/api-manager.ts +68 -5
- package/src/authenticate.ts +5 -1
- package/src/endpoint-generators/create-get-schema-action.ts +29 -0
- package/src/endpoint-generators/create-get-swagger-json-action.ts +26 -0
- package/src/endpoint-generators/index.ts +1 -0
- package/src/endpoint-generators/with-schema-and-swagger-action.ts +11 -0
- package/src/get-schema-from-api.spec.ts +73 -0
- package/src/get-schema-from-api.ts +74 -0
- package/src/index.ts +10 -9
- package/src/swagger/generate-swagger-json.spec.ts +239 -0
- package/src/swagger/generate-swagger-json.ts +123 -0
- package/src/validate.integration.spec.schema.json +0 -2
- package/src/validate.integration.spec.ts +173 -2
- 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.
|
|
3
|
+
"version": "10.0.25",
|
|
4
4
|
"description": "Repository implementation for FuryStack",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -37,21 +37,21 @@
|
|
|
37
37
|
},
|
|
38
38
|
"homepage": "https://github.com/furystack/furystack",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@furystack/core": "^15.0.
|
|
41
|
-
"@furystack/inject": "^12.0.
|
|
42
|
-
"@furystack/repository": "^10.0.
|
|
43
|
-
"@furystack/rest": "^8.0.
|
|
44
|
-
"@furystack/security": "^6.0.
|
|
45
|
-
"@furystack/utils": "^8.1.
|
|
40
|
+
"@furystack/core": "^15.0.24",
|
|
41
|
+
"@furystack/inject": "^12.0.20",
|
|
42
|
+
"@furystack/repository": "^10.0.24",
|
|
43
|
+
"@furystack/rest": "^8.0.24",
|
|
44
|
+
"@furystack/security": "^6.0.24",
|
|
45
|
+
"@furystack/utils": "^8.1.2",
|
|
46
46
|
"ajv": "^8.17.1",
|
|
47
47
|
"ajv-formats": "^3.0.1",
|
|
48
48
|
"path-to-regexp": "^8.2.0",
|
|
49
49
|
"semaphore-async-await": "^1.5.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@furystack/rest-client-fetch": "^8.0.
|
|
53
|
-
"@types/node": "^24.0
|
|
54
|
-
"typescript": "^5.
|
|
52
|
+
"@furystack/rest-client-fetch": "^8.0.24",
|
|
53
|
+
"@types/node": "^24.3.0",
|
|
54
|
+
"typescript": "^5.9.2",
|
|
55
55
|
"vitest": "^3.2.4"
|
|
56
56
|
},
|
|
57
57
|
"gitHead": "1045d854bfd8c475b7035471d130d401417a2321"
|
package/src/api-manager.ts
CHANGED
|
@@ -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
|
|
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(
|
|
100
|
-
|
|
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: {
|
package/src/authenticate.ts
CHANGED
|
@@ -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
|
-
|
|
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 './
|
|
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
|
+
})
|