galbe 0.10.1 → 0.12.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.
- package/.prettierrc +1 -1
- package/README.md +3 -0
- package/bin/commands/generate/client.ts +58 -30
- package/bin/commands/generate/code/openapi.parser.ts +72 -45
- package/bin/res/cli.template.js +20 -18
- package/bin/res/client.template.ts +52 -26
- package/bin/util.ts +27 -7
- package/docs/cli.md +25 -22
- package/docs/configuration.md +80 -0
- package/docs/context.md +38 -52
- package/docs/error-handler.md +20 -31
- package/docs/getting-started.md +40 -157
- package/docs/handler.md +35 -21
- package/docs/hooks.md +16 -16
- package/docs/plugins.md +50 -57
- package/docs/router.md +2 -2
- package/docs/routes.md +56 -39
- package/docs/schemas.md +115 -69
- package/package.json +1 -1
- package/src/extras/spec/openapi.serializer.ts +83 -56
- package/src/index.ts +16 -11
- package/src/parser.ts +199 -147
- package/src/router.ts +2 -2
- package/src/schema.ts +156 -66
- package/src/server.ts +25 -17
- package/src/types.ts +54 -42
- package/src/util.ts +37 -9
- package/src/validator.ts +23 -19
- package/test/parser.test.ts +375 -272
- package/test/requests.test.ts +242 -176
- package/test/resources/static/chameleon.png +0 -0
- package/test/resources/static/index.html +13 -0
- package/test/resources/static/sub/index.html +13 -0
- package/test/resources/static/sub/other.html +13 -0
- package/test/responses.test.ts +49 -49
- package/test/router.test.ts +14 -0
- package/test/test.utils.ts +4 -2
- package/test/types.test.ts +909 -0
package/docs/schemas.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Schemas
|
|
2
2
|
|
|
3
|
-
Galbe provides a custom Schema Type processor that offers type safety, data parsing, and validation. The
|
|
3
|
+
Galbe provides a custom Schema Type processor that offers type safety, data parsing, and validation. The use of Schemas highly simplifies request input validation and automatic error handling. Additionally, it enhances the developer experience by inferring static TypeScript types from schema definitions.
|
|
4
4
|
|
|
5
5
|
## Schema Types
|
|
6
6
|
|
|
7
|
-
To
|
|
7
|
+
To use Schema definitions, import `$T` from the `galbe` library:
|
|
8
8
|
|
|
9
9
|
```js
|
|
10
10
|
import { $T } from 'galbe'
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Here the list of available Schema types in Galbe:
|
|
13
|
+
Here is the list of available Schema types in Galbe:
|
|
14
14
|
|
|
15
|
-
####
|
|
15
|
+
#### boolean
|
|
16
16
|
|
|
17
17
|
Schema Type matching `boolean` values.
|
|
18
18
|
|
|
@@ -20,15 +20,15 @@ Schema Type matching `boolean` values.
|
|
|
20
20
|
const boolSchema = $T.boolean()
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
####
|
|
23
|
+
#### string
|
|
24
24
|
|
|
25
|
-
Schema Type matching `string`
|
|
25
|
+
Schema Type matching `string` values.
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
28
|
const strSchema = $T.string(options)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
####
|
|
31
|
+
#### number
|
|
32
32
|
|
|
33
33
|
Schema Type matching `number` values.
|
|
34
34
|
|
|
@@ -36,7 +36,7 @@ Schema Type matching `number` values.
|
|
|
36
36
|
const numSchema = $T.number(options)
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
####
|
|
39
|
+
#### integer
|
|
40
40
|
|
|
41
41
|
Schema Type matching integer `number` values.
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@ Schema Type matching integer `number` values.
|
|
|
44
44
|
const intSchema = $T.integer(options)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
####
|
|
47
|
+
#### null
|
|
48
48
|
|
|
49
49
|
Schema Type matching `null` values.
|
|
50
50
|
|
|
@@ -52,7 +52,7 @@ Schema Type matching `null` values.
|
|
|
52
52
|
const nullSchema = $T.null(options)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
####
|
|
55
|
+
#### any
|
|
56
56
|
|
|
57
57
|
Schema Type matching `any` of the previous Schema Types.
|
|
58
58
|
|
|
@@ -60,7 +60,7 @@ Schema Type matching `any` of the previous Schema Types.
|
|
|
60
60
|
const anySchema = $T.any()
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
####
|
|
63
|
+
#### array
|
|
64
64
|
|
|
65
65
|
Schema Type matching `array` values.
|
|
66
66
|
|
|
@@ -68,41 +68,41 @@ Schema Type matching `array` values.
|
|
|
68
68
|
const arraySchema = $T.array($T.any(), options)
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
####
|
|
71
|
+
#### optional
|
|
72
72
|
|
|
73
|
-
Makes any type optional
|
|
73
|
+
Makes any type optional, allowing `undefined` values.
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
76
|
const optionalSchema = $T.optional($T.string())
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
####
|
|
79
|
+
#### nullable
|
|
80
80
|
|
|
81
|
-
Makes any type nullable
|
|
81
|
+
Makes any type nullable, allowing `null` values.
|
|
82
82
|
|
|
83
83
|
```ts
|
|
84
84
|
const nullableSchema = $T.nullable($T.string())
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
####
|
|
87
|
+
#### nullish
|
|
88
88
|
|
|
89
|
-
Makes any type nullish
|
|
89
|
+
Makes any type nullish, allowing both `undefined` and `null` values.
|
|
90
90
|
|
|
91
91
|
```ts
|
|
92
92
|
const nullishSchema = $T.nullish($T.string())
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
####
|
|
95
|
+
#### union
|
|
96
96
|
|
|
97
|
-
Creates
|
|
97
|
+
Creates a union of Schema Types.
|
|
98
98
|
|
|
99
99
|
```ts
|
|
100
100
|
const unionSchema = $T.union([$T.string(), $T.number()])
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
## Request Schema
|
|
103
|
+
## Request Schema Definition
|
|
104
104
|
|
|
105
|
-
The Request Schema definition allows you to define a schema for your request
|
|
105
|
+
The Request Schema definition allows you to define a schema for your request in your [Route Definition](routes.md#route-definition). It must be defined right after the route path.
|
|
106
106
|
|
|
107
107
|
```js
|
|
108
108
|
const schema = {}
|
|
@@ -117,9 +117,9 @@ The Request Schema has four optional properties:
|
|
|
117
117
|
headers: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
Defines request headers with their respective Schema types.
|
|
121
121
|
|
|
122
|
-
**Example
|
|
122
|
+
**Example:**
|
|
123
123
|
|
|
124
124
|
```ts
|
|
125
125
|
const schema = {
|
|
@@ -135,9 +135,9 @@ const schema = {
|
|
|
135
135
|
params: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Defines route parameters with their respective Schema types.
|
|
139
139
|
|
|
140
|
-
**Example
|
|
140
|
+
**Example:**
|
|
141
141
|
|
|
142
142
|
```ts
|
|
143
143
|
const schema = {
|
|
@@ -149,9 +149,7 @@ const schema = {
|
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
> [!WARNING]
|
|
152
|
-
> Every key should match an existing [route path](routes.md#route-
|
|
153
|
-
>
|
|
154
|
-
> By default, if no schema is defined for a given parameter. Galbe will assume it is of type `string`.
|
|
152
|
+
> Every key should match an existing [route path](routes.md#route-definition) parameter. Otherwise, TypeScript will show an error. If no schema is defined for a given parameter, Galbe assumes it is of type `string`.
|
|
155
153
|
|
|
156
154
|
### query
|
|
157
155
|
|
|
@@ -159,9 +157,9 @@ const schema = {
|
|
|
159
157
|
query: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
|
|
160
158
|
```
|
|
161
159
|
|
|
162
|
-
|
|
160
|
+
Defines query parameters with their respective Schema types.
|
|
163
161
|
|
|
164
|
-
**Example
|
|
162
|
+
**Example:**
|
|
165
163
|
|
|
166
164
|
```ts
|
|
167
165
|
const schema = {
|
|
@@ -176,56 +174,96 @@ const schema = {
|
|
|
176
174
|
|
|
177
175
|
<!-- prettier-ignore -->
|
|
178
176
|
```ts
|
|
179
|
-
body:
|
|
177
|
+
body: {
|
|
178
|
+
byteArray?: STByteArray | STStream
|
|
179
|
+
text?: STString | STLiteral | STBoolean | STNumber | STInteger | STUnion | STStream
|
|
180
|
+
json?: STJson | STObject | STBoolean | STInteger | STNumber | STString | STArray | STUnion
|
|
181
|
+
urlForm?: STObject | STStream | STUnion
|
|
182
|
+
multipart?: STMultipartForm | STStream | STUnion
|
|
183
|
+
default?: STString | STByteArray | STStream | STAny
|
|
184
|
+
}
|
|
180
185
|
```
|
|
181
186
|
|
|
182
|
-
|
|
187
|
+
Defines the request body Schema type based on content type.
|
|
188
|
+
|
|
189
|
+
#### Byte Array
|
|
183
190
|
|
|
184
|
-
|
|
191
|
+
Defines an `application/octet-stream` request body.
|
|
185
192
|
|
|
186
193
|
```ts
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
})
|
|
194
|
+
const body = {
|
|
195
|
+
byteArray: $T.byteArray()
|
|
196
|
+
}
|
|
191
197
|
```
|
|
192
198
|
|
|
193
|
-
####
|
|
199
|
+
#### Text
|
|
194
200
|
|
|
195
|
-
|
|
201
|
+
Defines an `text/*` request body.
|
|
196
202
|
|
|
197
203
|
```ts
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
})
|
|
204
|
+
const body = {
|
|
205
|
+
text: $T.string()
|
|
206
|
+
}
|
|
202
207
|
```
|
|
203
208
|
|
|
204
|
-
####
|
|
209
|
+
#### JSON
|
|
205
210
|
|
|
206
|
-
|
|
211
|
+
Defines an `application/json` request body.
|
|
207
212
|
|
|
208
213
|
```ts
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
})
|
|
214
|
+
const body = {
|
|
215
|
+
json: $T.object({
|
|
216
|
+
name: $T.string(),
|
|
217
|
+
age: $T.integer({ min: 0 })
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### URL Form
|
|
223
|
+
|
|
224
|
+
Defines an `application/x-www-form-urlencoded` request body.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const body = {
|
|
228
|
+
urlForm: {$T.object({
|
|
229
|
+
name: $T.string(),
|
|
230
|
+
age: $T.integer({ min: 0 })
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Multipart Form
|
|
236
|
+
|
|
237
|
+
Defines a `multipart/form-data` request body.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const body = {
|
|
241
|
+
multipart: $T.multipartForm({
|
|
242
|
+
name: $T.string(),
|
|
243
|
+
age: $T.integer({ min: 0 })
|
|
244
|
+
})
|
|
245
|
+
}
|
|
213
246
|
```
|
|
214
247
|
|
|
215
|
-
####
|
|
248
|
+
#### stream
|
|
216
249
|
|
|
217
|
-
|
|
250
|
+
Certain request body types can be streamed using `STStream` wrapper, improving performance by validating data incrementally.
|
|
251
|
+
This can be usefull to imporve performances in case you have heavy body payloads by leveraging early validation and fail fast behaviors.
|
|
218
252
|
|
|
219
|
-
|
|
253
|
+
**Example**
|
|
254
|
+
|
|
255
|
+
Let's consider a `multipart/form-data` body request that has two properties: `username` and `heavyImageFile`:
|
|
220
256
|
|
|
221
257
|
```ts
|
|
222
258
|
galbe.post(
|
|
223
259
|
'user/create',
|
|
224
260
|
{
|
|
225
|
-
body:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
261
|
+
body: {
|
|
262
|
+
multipart: $T.multipartForm({
|
|
263
|
+
username: $T.string(),
|
|
264
|
+
heavyImageFile: $T.byteArray()
|
|
265
|
+
})
|
|
266
|
+
}
|
|
229
267
|
},
|
|
230
268
|
ctx => {
|
|
231
269
|
// At that point, the full body request has been processed
|
|
@@ -236,20 +274,22 @@ galbe.post(
|
|
|
236
274
|
})
|
|
237
275
|
```
|
|
238
276
|
|
|
239
|
-
|
|
277
|
+
In that case, even if the `username` doesn't pass the validation, the full request body, including the `heavyImageFile` is processed before sending the response. This induces unnecessary time and resource consumption because the `heavyImageFile` is processed despite never been used.
|
|
240
278
|
|
|
241
|
-
|
|
279
|
+
A better approach would consist in leveraging `STStream` wrapper to implement early validation and fail fast behavior. By defining the request body as a stream. Instead of receiving a plain js object as `ctx.body`, you will receive an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator).
|
|
242
280
|
|
|
243
281
|
```ts
|
|
244
282
|
galbe.post(
|
|
245
283
|
'user/create',
|
|
246
284
|
{
|
|
247
|
-
body:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
285
|
+
body: {
|
|
286
|
+
multipart: $T.stream($T.multipartForm({
|
|
287
|
+
username: $T.string(),
|
|
288
|
+
heavyImageFile: $T.byteArray()
|
|
289
|
+
}))
|
|
290
|
+
}
|
|
251
291
|
},
|
|
252
|
-
ctx => {
|
|
292
|
+
async ctx => {
|
|
253
293
|
// At that point, the body has not been processed yet.
|
|
254
294
|
for await (const [key, value] of ctx.body) {
|
|
255
295
|
if (key === "username" && !isValid(value)) {
|
|
@@ -266,16 +306,22 @@ galbe.post(
|
|
|
266
306
|
|
|
267
307
|
<!-- prettier-ignore -->
|
|
268
308
|
```ts
|
|
269
|
-
response: Record<number, STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STStream>
|
|
309
|
+
response: Record<number | 'default', STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STStream>
|
|
270
310
|
```
|
|
271
311
|
|
|
272
|
-
|
|
312
|
+
Defines response validation by associating schema types with specific HTTP status codes. The special key `default` can also be used to define the Response Schema for the remaining status codes.
|
|
273
313
|
|
|
274
|
-
|
|
314
|
+
**Example:**
|
|
275
315
|
|
|
276
316
|
```ts
|
|
277
317
|
const response = {
|
|
278
|
-
200: $T.object({ data: $T.array($T.number()) })
|
|
279
|
-
404: $T.literal("Not found")
|
|
318
|
+
200: $T.object({ data: $T.array($T.number()) }),
|
|
319
|
+
404: $T.literal("Not found"),
|
|
320
|
+
default: $T.string(),
|
|
280
321
|
}
|
|
281
322
|
```
|
|
323
|
+
|
|
324
|
+
This ensures all responses adhere to the defined schema.
|
|
325
|
+
|
|
326
|
+
> [!NOTE]
|
|
327
|
+
> The response validation is enabled by default, meaning that every enpoint response that has a schema defined will be validated at runtime. To disable runtime validation, you can set the `responseValidator?.enabled` option to `false` in the [configuration](getting-started.md#configuration).
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
STArray,
|
|
3
|
+
STIntersection,
|
|
4
|
+
STJson,
|
|
5
|
+
STLiteral,
|
|
6
|
+
STObject,
|
|
7
|
+
STProps,
|
|
8
|
+
STSchema,
|
|
9
|
+
STUnion,
|
|
10
|
+
} from '../../../src/schema'
|
|
2
11
|
|
|
3
12
|
import { Galbe } from '../../../src'
|
|
4
|
-
import { walkRoutes, HttpStatus } from '../../../src/util'
|
|
13
|
+
import { walkRoutes, HttpStatus, inferContentType } from '../../../src/util'
|
|
5
14
|
import { Kind, Optional } from '../../../src/schema'
|
|
6
15
|
|
|
7
16
|
import { OpenAPIV3 } from 'openapi-types'
|
|
@@ -12,10 +21,10 @@ const schemaToMedia = ({ type, format, isJson }: SchemaType) =>
|
|
|
12
21
|
isJson || (type && ['object', 'number', 'boolean', 'array'].includes(type))
|
|
13
22
|
? 'application/json'
|
|
14
23
|
: format === 'byte'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
? 'application/octet-stream'
|
|
25
|
+
: type === 'string'
|
|
26
|
+
? 'text/plain'
|
|
27
|
+
: '*/*'
|
|
19
28
|
|
|
20
29
|
export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<OpenAPIV3.Document> => {
|
|
21
30
|
let paths: any = {}
|
|
@@ -24,7 +33,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
24
33
|
schemas: {},
|
|
25
34
|
parameters: {},
|
|
26
35
|
requestBodies: {},
|
|
27
|
-
responses: {}
|
|
36
|
+
responses: {},
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
const schemaToOpenapi = (
|
|
@@ -54,7 +63,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
54
63
|
|
|
55
64
|
if (kind === 'null') {
|
|
56
65
|
s = {
|
|
57
|
-
anyOf: ['null']
|
|
66
|
+
anyOf: ['null'],
|
|
58
67
|
}
|
|
59
68
|
} else if (kind === 'boolean') s = { type: 'boolean' }
|
|
60
69
|
else if (kind === 'byteArray') s = { type: 'string', format: 'byte' }
|
|
@@ -64,7 +73,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
64
73
|
...(exclusiveMinimum ? { exclusiveMinimum } : {}),
|
|
65
74
|
...(exclusiveMaximum ? { exclusiveMaximum } : {}),
|
|
66
75
|
...(minimum ? { minimum } : {}),
|
|
67
|
-
...(maximum ? { maximum } : {})
|
|
76
|
+
...(maximum ? { maximum } : {}),
|
|
68
77
|
}
|
|
69
78
|
else if (kind === 'integer')
|
|
70
79
|
s = {
|
|
@@ -72,14 +81,14 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
72
81
|
...(exclusiveMinimum ? { exclusiveMinimum } : {}),
|
|
73
82
|
...(exclusiveMaximum ? { exclusiveMaximum } : {}),
|
|
74
83
|
...(minimum ? { minimum } : {}),
|
|
75
|
-
...(maximum ? { maximum } : {})
|
|
84
|
+
...(maximum ? { maximum } : {}),
|
|
76
85
|
}
|
|
77
86
|
else if (kind === 'string')
|
|
78
87
|
s = {
|
|
79
88
|
type: 'string',
|
|
80
89
|
...(pattern ? { pattern } : {}),
|
|
81
90
|
...(minLength ? { minLength } : {}),
|
|
82
|
-
...(maxLength ? { maxLength } : {})
|
|
91
|
+
...(maxLength ? { maxLength } : {}),
|
|
83
92
|
}
|
|
84
93
|
else if (kind === 'any') s = { type: 'string' }
|
|
85
94
|
else if (kind === 'literal') {
|
|
@@ -91,7 +100,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
91
100
|
items: schemaToOpenapi((schema as STArray).items).schema,
|
|
92
101
|
...(minItems ? { minItems } : {}),
|
|
93
102
|
...(maxItems ? { maxItems } : {}),
|
|
94
|
-
...(uniqueItems ? { uniqueItems } : {})
|
|
103
|
+
...(uniqueItems ? { uniqueItems } : {}),
|
|
95
104
|
}
|
|
96
105
|
} else if (kind === 'object') {
|
|
97
106
|
let props = (schema as STObject).props || {}
|
|
@@ -101,7 +110,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
101
110
|
s = {
|
|
102
111
|
type: 'object',
|
|
103
112
|
properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
|
|
104
|
-
...(required.length ? { required } : {})
|
|
113
|
+
...(required.length ? { required } : {}),
|
|
105
114
|
}
|
|
106
115
|
} else if (kind === 'json') {
|
|
107
116
|
let props = ((schema as STJson).props || {}) as STProps
|
|
@@ -115,13 +124,13 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
115
124
|
type: type,
|
|
116
125
|
...(type === 'object'
|
|
117
126
|
? {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
: {})
|
|
127
|
+
properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
|
|
128
|
+
...(required.length ? { required } : {}),
|
|
129
|
+
}
|
|
130
|
+
: {}),
|
|
122
131
|
}
|
|
123
132
|
} else if (kind === 'union') {
|
|
124
|
-
let anyOf = (schema as STUnion).anyOf
|
|
133
|
+
let anyOf: STSchema[] = (schema as STUnion).anyOf
|
|
125
134
|
let nullable = anyOf.some(s => s[Kind] === 'null')
|
|
126
135
|
anyOf = anyOf.filter(s => s[Kind] !== 'null')
|
|
127
136
|
|
|
@@ -131,12 +140,23 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
131
140
|
s = schemaToOpenapi(anyOf[0]).schema
|
|
132
141
|
} else if (anyOf.length > 1) {
|
|
133
142
|
s = {
|
|
134
|
-
anyOf: anyOf.map(
|
|
143
|
+
anyOf: anyOf.map(e => schemaToOpenapi(e).schema),
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
//@ts-ignore
|
|
139
148
|
if (nullable) s.nullable = nullable
|
|
149
|
+
} else if (kind === 'intersection') {
|
|
150
|
+
let allOf: STSchema[] = (schema as STIntersection).allOf
|
|
151
|
+
if (allOf.length === 0) {
|
|
152
|
+
s = {}
|
|
153
|
+
} else if (allOf.length === 1) {
|
|
154
|
+
s = schemaToOpenapi(allOf[0]).schema
|
|
155
|
+
} else if (allOf.length > 1) {
|
|
156
|
+
s = {
|
|
157
|
+
allOf: allOf.map(s => schemaToOpenapi(s).schema),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
140
160
|
}
|
|
141
161
|
|
|
142
162
|
s = { title: schema.title, description: schema.description, ...s }
|
|
@@ -168,7 +188,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
168
188
|
description: param?.description,
|
|
169
189
|
required: kind === 'path' ? true : !param[Optional] || undefined,
|
|
170
190
|
deprecated: param.deprecated,
|
|
171
|
-
schema
|
|
191
|
+
schema,
|
|
172
192
|
}
|
|
173
193
|
if (components.parameters && param.id) components.parameters[param.id] = p
|
|
174
194
|
return p
|
|
@@ -178,18 +198,17 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
178
198
|
(routes, c) => ({ ...routes, ...c.routes }),
|
|
179
199
|
{} as Record<string, Record<string, Record<string, any>>>
|
|
180
200
|
)
|
|
181
|
-
let metaStatic = Object.fromEntries(Object.entries(metaRoutes || {}).filter((
|
|
201
|
+
let metaStatic = Object.fromEntries(Object.entries(metaRoutes || {}).filter(([_, d]) => d?.static))
|
|
182
202
|
|
|
183
203
|
walkRoutes(g.router.routes, r => {
|
|
184
204
|
let meta = metaRoutes?.[r.path]?.[r.method]
|
|
185
|
-
if (r.static?.root)
|
|
186
|
-
meta = metaStatic[r.static?.root]?.static
|
|
205
|
+
if (r.static?.root) meta = metaStatic[r.static?.root]?.static
|
|
187
206
|
if (meta?.hide) return
|
|
188
207
|
let path = r.path.replaceAll(/:([^\/]+)/g, '{$1}')
|
|
189
208
|
if (!(path in paths)) paths[path] = {}
|
|
190
209
|
let tags = [
|
|
191
210
|
...(meta?.tags?.split(' ')?.map((t: string) => t.trim()) || []),
|
|
192
|
-
...(typeof meta?.tag === 'string' ? [meta?.tag] : meta?.tag || [])
|
|
211
|
+
...(typeof meta?.tag === 'string' ? [meta?.tag] : meta?.tag || []),
|
|
193
212
|
]
|
|
194
213
|
let security: Record<string, any> = []
|
|
195
214
|
|
|
@@ -201,52 +220,62 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
201
220
|
: []
|
|
202
221
|
let headerParam = r.schema?.headers
|
|
203
222
|
? Object.entries(r.schema?.headers as Record<string, STSchema>)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
.map(([k, v]) => {
|
|
224
|
+
let p = parseParam(k, v, 'header')
|
|
225
|
+
if (k.match(/authorization/i)) {
|
|
226
|
+
// TODO: handle other auth methods
|
|
227
|
+
if (v.pattern && v?.pattern?.toString() === '/^Bearer /') {
|
|
228
|
+
security.push({ bearerAuth: [] })
|
|
229
|
+
components.securitySchemes = { bearerAuth: { type: 'http', scheme: 'bearer' } }
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
212
232
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
.filter(p => p)
|
|
233
|
+
return p
|
|
234
|
+
})
|
|
235
|
+
.filter(p => p)
|
|
217
236
|
: []
|
|
218
237
|
// TODO cookieParam
|
|
219
238
|
let parameters = [...pathParam, ...queryParam, ...headerParam]
|
|
220
239
|
|
|
221
240
|
let requestBody
|
|
222
241
|
if (r.schema.body) {
|
|
223
|
-
let
|
|
224
|
-
let
|
|
225
|
-
let
|
|
242
|
+
let description: string | undefined
|
|
243
|
+
let conflictDescription = false
|
|
244
|
+
let required = false
|
|
245
|
+
let content = Object.fromEntries(
|
|
246
|
+
Object.entries(r.schema.body).map(([bodyType, schema]) => {
|
|
247
|
+
const s = schema.description
|
|
248
|
+
const isDefined = typeof s === 'string' && s !== ''
|
|
249
|
+
if (s?.[Optional] === false) required = true
|
|
250
|
+
if (isDefined) {
|
|
251
|
+
if (description === undefined) {
|
|
252
|
+
description = s
|
|
253
|
+
} else if (description !== s) {
|
|
254
|
+
conflictDescription = true
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
description = conflictDescription ? undefined : description ?? undefined
|
|
258
|
+
return [inferContentType(bodyType), { schema: schemaToOpenapi(schema).schema }]
|
|
259
|
+
})
|
|
260
|
+
)
|
|
226
261
|
requestBody = {
|
|
227
|
-
description
|
|
228
|
-
required
|
|
229
|
-
content
|
|
230
|
-
[media]: { schema }
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if (r.schema.body.id && components.requestBodies) {
|
|
234
|
-
components.requestBodies[r.schema.body.id] = requestBody
|
|
235
|
-
requestBody = { $ref: `#/components/requestBodies/${r.schema.body.id}` }
|
|
262
|
+
description,
|
|
263
|
+
required,
|
|
264
|
+
content,
|
|
236
265
|
}
|
|
237
266
|
}
|
|
238
267
|
let responses
|
|
239
268
|
if (r.schema.response && Object.keys(r.schema.response).length) {
|
|
240
269
|
responses = Object.fromEntries(
|
|
241
270
|
Object.entries(r.schema.response).map(([status, v]) => {
|
|
242
|
-
if(!v) return []
|
|
271
|
+
if (!v) return []
|
|
243
272
|
let s = status as keyof typeof HttpStatus | 'default'
|
|
244
273
|
let { schema, isJson } = schemaToOpenapi(v)
|
|
245
274
|
let { type, format } = resolveRef(schema)
|
|
246
275
|
let media = schemaToMedia({ type, format, isJson } as SchemaType)
|
|
247
276
|
let response: OpenAPIV3.ResponseObject = {
|
|
248
277
|
description: v.description || HttpStatus[s as keyof typeof HttpStatus] || 'Response',
|
|
249
|
-
content: { [media]: { schema: schema } }
|
|
278
|
+
content: { [media]: { schema: schema } },
|
|
250
279
|
}
|
|
251
280
|
if (components.responses && r.schema.response?.[s]?.id) {
|
|
252
281
|
components.responses[r.schema.response?.[s]?.id as string] = response
|
|
@@ -258,12 +287,10 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
258
287
|
)
|
|
259
288
|
} else {
|
|
260
289
|
responses = {
|
|
261
|
-
default: { description: HttpStatus[200] }
|
|
290
|
+
default: { description: HttpStatus[200] },
|
|
262
291
|
}
|
|
263
292
|
}
|
|
264
293
|
let summary = meta?.head.match(/^([^\n]+)/)?.[1]
|
|
265
|
-
console.log('#', r.method, r.path)
|
|
266
|
-
console.log(r.schema.body)
|
|
267
294
|
paths[path][r.method] = {
|
|
268
295
|
tags: tags.length ? tags : undefined,
|
|
269
296
|
summary: summary,
|
|
@@ -272,7 +299,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
272
299
|
requestBody,
|
|
273
300
|
responses,
|
|
274
301
|
...(security.length ? { security } : {}),
|
|
275
|
-
deprecated: meta?.deprecated ? true : undefined
|
|
302
|
+
deprecated: meta?.deprecated ? true : undefined,
|
|
276
303
|
}
|
|
277
304
|
})
|
|
278
305
|
|
|
@@ -285,9 +312,9 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
285
312
|
openapi: version,
|
|
286
313
|
info: {
|
|
287
314
|
title: 'Galbe app',
|
|
288
|
-
version: '0.1.0'
|
|
315
|
+
version: '0.1.0',
|
|
289
316
|
},
|
|
290
317
|
paths,
|
|
291
|
-
components: Object.keys(components)?.length ? components : undefined
|
|
318
|
+
components: Object.keys(components)?.length ? components : undefined,
|
|
292
319
|
}
|
|
293
320
|
}
|