@vida-global/core 2.3.1 → 2.3.2
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.
|
@@ -56,9 +56,18 @@ Responses with a status `>= 400` additionally document an `errors` object (`{ me
|
|
|
56
56
|
|
|
57
57
|
Parameters and request bodies are **derived automatically** from the action's `parametersFor<ActionName>` validator definitions (see [Validations](./validations.md)) — there is no need to repeat them in YAML. Each declared parameter is placed by location:
|
|
58
58
|
|
|
59
|
-
- **Path parameters** —
|
|
59
|
+
- **Path parameters** — every placeholder in the route (e.g. `:id`). Always required. A matching
|
|
60
|
+
validator definition supplies its schema; otherwise it defaults to a string.
|
|
60
61
|
- **Query parameters** — for `GET`/`DELETE` actions, or any parameter marked `{ queryParameter: true }`.
|
|
61
|
-
- **Request body** — for `POST`/`PUT` actions, declared params become a
|
|
62
|
+
- **Request body** — for `POST`/`PUT`/`PATCH` actions, declared params become a
|
|
63
|
+
`requestBody` object schema;
|
|
64
|
+
non-`optional` params are listed as `required`. The body defaults to `application/json`, but
|
|
65
|
+
switches to `multipart/form-data` automatically when any body param declares
|
|
66
|
+
`{ format: 'binary' }` (rendered as `{ type: 'string', format: 'binary' }` — the standard way to
|
|
67
|
+
document a file upload).
|
|
68
|
+
|
|
69
|
+
An action YAML file may provide a complete `requestBody` for an exceptional `POST`, `PUT`, or
|
|
70
|
+
`PATCH` transport contract.
|
|
62
71
|
|
|
63
72
|
The validator definition itself becomes the parameter's JSON schema, and `example`, `default`, `format`, and `nullable` pass through when present. Set `{ nullable: true }` to document a value that may be `null`.
|
|
64
73
|
|
|
@@ -73,6 +82,26 @@ static parametersForPostImport() {
|
|
|
73
82
|
}
|
|
74
83
|
```
|
|
75
84
|
|
|
85
|
+
When middleware provides a body field that is not available through controller parameters, the
|
|
86
|
+
action YAML may provide a complete OpenAPI `requestBody`. The explicit definition replaces inferred
|
|
87
|
+
body documentation for that action:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
summary: Upload a file
|
|
91
|
+
description: Uploads one file.
|
|
92
|
+
requestBody:
|
|
93
|
+
required: true
|
|
94
|
+
content:
|
|
95
|
+
multipart/form-data:
|
|
96
|
+
schema:
|
|
97
|
+
type: object
|
|
98
|
+
required: [file]
|
|
99
|
+
properties:
|
|
100
|
+
file:
|
|
101
|
+
type: string
|
|
102
|
+
format: binary
|
|
103
|
+
```
|
|
104
|
+
|
|
76
105
|
|
|
77
106
|
## Tags
|
|
78
107
|
|
|
@@ -263,6 +263,8 @@ class ApiDocGenerator {
|
|
|
263
263
|
|
|
264
264
|
|
|
265
265
|
get requestBody() {
|
|
266
|
+
if (this.#details.requestBody) return structuredClone(this.#details.requestBody);
|
|
267
|
+
|
|
266
268
|
const properties = this.bodyProperties;
|
|
267
269
|
if (!Object.keys(properties).length) return undefined;
|
|
268
270
|
|
|
@@ -289,15 +291,18 @@ class ApiDocGenerator {
|
|
|
289
291
|
|
|
290
292
|
|
|
291
293
|
get pathParams() {
|
|
292
|
-
const params =
|
|
293
|
-
|
|
294
|
+
const params = {};
|
|
295
|
+
for (const name of this.#pathParamNames) {
|
|
296
|
+
params[name] = this.#requestInputsByName[name] || { isString: true };
|
|
297
|
+
}
|
|
298
|
+
return this.#formatInputs(params, 'path');
|
|
294
299
|
}
|
|
295
300
|
|
|
296
301
|
|
|
297
302
|
get queryParams() {
|
|
298
303
|
const params = this.#allRequestInputs.filter(([name, details]) => {
|
|
299
304
|
if (this.#pathParamNames.includes(name)) return false;
|
|
300
|
-
if (details?.queryParameter === true)
|
|
305
|
+
if (details?.queryParameter === true) return true;
|
|
301
306
|
return this.method == 'get' || this.method == 'delete';
|
|
302
307
|
});
|
|
303
308
|
return this.#formatInputs(Object.fromEntries(params), 'query');
|
|
@@ -305,9 +310,10 @@ class ApiDocGenerator {
|
|
|
305
310
|
|
|
306
311
|
|
|
307
312
|
get bodyProperties() {
|
|
308
|
-
const inputs = this.#allRequestInputs.filter(([name,
|
|
313
|
+
const inputs = this.#allRequestInputs.filter(([name, details]) => {
|
|
309
314
|
if (this.#pathParamNames.includes(name)) return false;
|
|
310
|
-
|
|
315
|
+
if (details?.queryParameter === true) return false;
|
|
316
|
+
return ['post', 'put', 'patch'].includes(this.method);
|
|
311
317
|
});
|
|
312
318
|
return this.#formatInputs(Object.fromEntries(inputs), 'body');
|
|
313
319
|
}
|
package/package.json
CHANGED
|
@@ -126,6 +126,29 @@ describe('ApiDocGenerator', () => {
|
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
it ('documents every route placeholder when no validator definition exists', () => {
|
|
130
|
+
const action = { action: 'getRecord', method: 'GET', path: '/account/:accountId/job/:jobId' };
|
|
131
|
+
ControllerClass.parametersForAction.mockReturnValue({});
|
|
132
|
+
const generator = new ApiDocGenerator(action, ControllerClass);
|
|
133
|
+
|
|
134
|
+
expect(generator.pathParams).toEqual({
|
|
135
|
+
accountId: {
|
|
136
|
+
name: 'accountId',
|
|
137
|
+
in: 'path',
|
|
138
|
+
required: true,
|
|
139
|
+
description: '',
|
|
140
|
+
schema: { type: 'string' },
|
|
141
|
+
},
|
|
142
|
+
jobId: {
|
|
143
|
+
name: 'jobId',
|
|
144
|
+
in: 'path',
|
|
145
|
+
required: true,
|
|
146
|
+
description: '',
|
|
147
|
+
schema: { type: 'string' },
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
129
152
|
it ('marks path params required even when the definition is optional', () => {
|
|
130
153
|
const action = { action: 'getRecord', method: 'GET', path: '/user/:id' };
|
|
131
154
|
ControllerClass.parametersForAction.mockReturnValue({ id: { isString: true, optional: true } });
|
|
@@ -190,6 +213,7 @@ describe('ApiDocGenerator', () => {
|
|
|
190
213
|
|
|
191
214
|
expect(Object.keys(generator.queryParams)).toEqual(['detail']);
|
|
192
215
|
});
|
|
216
|
+
|
|
193
217
|
});
|
|
194
218
|
|
|
195
219
|
|
|
@@ -217,6 +241,20 @@ describe('ApiDocGenerator', () => {
|
|
|
217
241
|
|
|
218
242
|
expect(generator.bodyProperties).toEqual({});
|
|
219
243
|
});
|
|
244
|
+
|
|
245
|
+
it ('builds body properties for PATCH actions', () => {
|
|
246
|
+
const action = { action: 'patchRecord', method: 'PATCH', path: '/things/:id' };
|
|
247
|
+
ControllerClass.parametersForAction.mockReturnValue({
|
|
248
|
+
id: { isInteger: true },
|
|
249
|
+
name: { isString: true },
|
|
250
|
+
});
|
|
251
|
+
const generator = new ApiDocGenerator(action, ControllerClass);
|
|
252
|
+
|
|
253
|
+
expect(generator.bodyProperties).toEqual({
|
|
254
|
+
name: { type: 'string', description: '' },
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
220
258
|
});
|
|
221
259
|
|
|
222
260
|
|
|
@@ -620,6 +658,29 @@ describe('ApiDocGenerator', () => {
|
|
|
620
658
|
});
|
|
621
659
|
});
|
|
622
660
|
|
|
661
|
+
it ('uses an explicit requestBody from action YAML for middleware-backed input', () => {
|
|
662
|
+
const requestBody = {
|
|
663
|
+
required: true,
|
|
664
|
+
content: {
|
|
665
|
+
'multipart/form-data': {
|
|
666
|
+
schema: {
|
|
667
|
+
type: 'object',
|
|
668
|
+
properties: {
|
|
669
|
+
file: { type: 'string', format: 'binary' },
|
|
670
|
+
},
|
|
671
|
+
required: ['file'],
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
const generator = documented({
|
|
677
|
+
details: { requestBody },
|
|
678
|
+
params: { path: { isString: true } },
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
expect(generator.documentation.requestBody).toEqual(requestBody);
|
|
682
|
+
});
|
|
683
|
+
|
|
623
684
|
it ('includes security when the action is authenticated', () => {
|
|
624
685
|
const generator = documented({ params: {}, security: [{ apiKeyAuth: [] }] });
|
|
625
686
|
|