@travetto/openapi 7.0.0-rc.2 → 7.0.0-rc.4
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/README.md +1 -1
- package/__index__.ts +1 -1
- package/package.json +5 -5
- package/src/config.ts +1 -1
- package/src/{spec-generate.ts → generate.ts} +50 -52
- package/src/service.ts +4 -9
- package/support/bin/help.ts +5 -5
- package/support/cli.openapi_client.ts +4 -4
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ export class ApiSpecConfig {
|
|
|
99
99
|
this.persist = false;
|
|
100
100
|
} else {
|
|
101
101
|
this.output = path.resolve(Runtime.mainSourcePath, this.output);
|
|
102
|
-
this.persist ??= Runtime.
|
|
102
|
+
this.persist ??= !Runtime.production;
|
|
103
103
|
}
|
|
104
104
|
if (this.persist) {
|
|
105
105
|
if (!/[.](json|ya?ml) $/.test(this.output)) { // Assume a folder
|
package/__index__.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/openapi",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.4",
|
|
4
4
|
"description": "OpenAPI integration support for the Travetto framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web",
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"directory": "module/openapi"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^7.0.0-rc.
|
|
30
|
-
"@travetto/schema": "^7.0.0-rc.
|
|
31
|
-
"@travetto/web": "^7.0.0-rc.
|
|
29
|
+
"@travetto/config": "^7.0.0-rc.3",
|
|
30
|
+
"@travetto/schema": "^7.0.0-rc.3",
|
|
31
|
+
"@travetto/web": "^7.0.0-rc.4",
|
|
32
32
|
"openapi3-ts": "^4.5.0",
|
|
33
33
|
"yaml": "^2.8.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
36
|
+
"@travetto/cli": "^7.0.0-rc.3"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@travetto/cli": {
|
package/src/config.ts
CHANGED
|
@@ -73,7 +73,7 @@ export class ApiSpecConfig {
|
|
|
73
73
|
this.persist = false;
|
|
74
74
|
} else {
|
|
75
75
|
this.output = path.resolve(Runtime.mainSourcePath, this.output);
|
|
76
|
-
this.persist ??= Runtime.
|
|
76
|
+
this.persist ??= !Runtime.production;
|
|
77
77
|
}
|
|
78
78
|
if (this.persist) {
|
|
79
79
|
if (!/[.](json|ya?ml)$/.test(this.output)) { // Assume a folder
|
|
@@ -12,8 +12,8 @@ import { ApiSpecConfig } from './config.ts';
|
|
|
12
12
|
|
|
13
13
|
const DEFINITION = '#/components/schemas';
|
|
14
14
|
|
|
15
|
-
const isInputConfig = (
|
|
16
|
-
const isFieldConfig = (
|
|
15
|
+
const isInputConfig = (value: object): value is SchemaInputConfig => !!value && 'class' in value && 'type' in value;
|
|
16
|
+
const isFieldConfig = (value: object): value is SchemaFieldConfig => isInputConfig(value) && 'name' in value;
|
|
17
17
|
|
|
18
18
|
type GeneratedSpec = {
|
|
19
19
|
tags: TagObject[];
|
|
@@ -51,7 +51,7 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
|
|
|
51
51
|
const fields = SchemaRegistryIndex.get(input.type).getFields(input.view);
|
|
52
52
|
const params: ParameterObject[] = [];
|
|
53
53
|
for (const sub of Object.values(fields)) {
|
|
54
|
-
const name = sub.name
|
|
54
|
+
const name = sub.name;
|
|
55
55
|
if (SchemaRegistryIndex.has(sub.type)) {
|
|
56
56
|
const suffix = (sub.array) ? '[]' : '';
|
|
57
57
|
params.push(...this.#schemaToDotParams(location, sub, prefix ? `${prefix}.${name}${suffix}` : `${name}${suffix}.`, rootField));
|
|
@@ -131,48 +131,48 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
|
|
|
131
131
|
* Process schema field
|
|
132
132
|
*/
|
|
133
133
|
#processSchemaField(input: SchemaInputConfig, required: string[]): SchemaObject {
|
|
134
|
-
let
|
|
134
|
+
let config: SchemaObject = this.#getType(input);
|
|
135
135
|
|
|
136
136
|
if (input.examples) {
|
|
137
|
-
|
|
137
|
+
config.example = input.examples;
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
config.description = input.description;
|
|
140
140
|
if (input.match) {
|
|
141
|
-
|
|
141
|
+
config.pattern = input.match.regex!.source;
|
|
142
142
|
}
|
|
143
143
|
if (input.maxlength) {
|
|
144
|
-
|
|
144
|
+
config.maxLength = input.maxlength.limit;
|
|
145
145
|
}
|
|
146
146
|
if (input.minlength) {
|
|
147
|
-
|
|
147
|
+
config.minLength = input.minlength.limit;
|
|
148
148
|
}
|
|
149
149
|
if (input.min) {
|
|
150
|
-
|
|
150
|
+
config.minimum = typeof input.min.limit === 'number' ? input.min.limit : input.min.limit.getTime();
|
|
151
151
|
}
|
|
152
152
|
if (input.max) {
|
|
153
|
-
|
|
153
|
+
config.maximum = typeof input.max.limit === 'number' ? input.max.limit : input.max.limit.getTime();
|
|
154
154
|
}
|
|
155
155
|
if (input.enum) {
|
|
156
|
-
|
|
156
|
+
config.enum = input.enum.values;
|
|
157
157
|
}
|
|
158
158
|
if (isFieldConfig(input)) {
|
|
159
159
|
if (input.required?.active !== false) {
|
|
160
|
-
required.push(input.name
|
|
160
|
+
required.push(input.name);
|
|
161
161
|
}
|
|
162
162
|
if (input.access === 'readonly') {
|
|
163
|
-
|
|
163
|
+
config.readOnly = true;
|
|
164
164
|
} else if (input.access === 'writeonly') {
|
|
165
|
-
|
|
165
|
+
config.writeOnly = true;
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
if (input.array) {
|
|
169
|
-
|
|
169
|
+
config = {
|
|
170
170
|
type: 'array',
|
|
171
|
-
items:
|
|
171
|
+
items: config
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
return
|
|
175
|
+
return config;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/**
|
|
@@ -195,25 +195,25 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
|
|
|
195
195
|
};
|
|
196
196
|
|
|
197
197
|
const properties: Record<string, SchemaObject> = {};
|
|
198
|
-
const
|
|
198
|
+
const base = config;
|
|
199
199
|
const required: string[] = [];
|
|
200
200
|
|
|
201
|
-
for (const fieldName of Object.keys(
|
|
202
|
-
if (SchemaRegistryIndex.has(
|
|
203
|
-
this.onSchema(SchemaRegistryIndex.getConfig(
|
|
201
|
+
for (const fieldName of Object.keys(base.fields)) {
|
|
202
|
+
if (SchemaRegistryIndex.has(base.fields[fieldName].type)) {
|
|
203
|
+
this.onSchema(SchemaRegistryIndex.getConfig(base.fields[fieldName].type));
|
|
204
204
|
}
|
|
205
|
-
properties[fieldName] = this.#processSchemaField(
|
|
205
|
+
properties[fieldName] = this.#processSchemaField(base.fields[fieldName], required);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
const extra: Record<string, unknown> = {};
|
|
209
209
|
if (config.discriminatedBase) {
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
extra.oneOf =
|
|
213
|
-
.filter(
|
|
214
|
-
.map(
|
|
215
|
-
this.onSchema(SchemaRegistryIndex.getConfig(
|
|
216
|
-
return this.#getType(
|
|
210
|
+
const subClasses = SchemaRegistryIndex.getDiscriminatedClasses(cls);
|
|
211
|
+
if (subClasses) {
|
|
212
|
+
extra.oneOf = subClasses
|
|
213
|
+
.filter(subCls => !describeFunction(subCls)?.abstract)
|
|
214
|
+
.map(subCls => {
|
|
215
|
+
this.onSchema(SchemaRegistryIndex.getConfig(subCls));
|
|
216
|
+
return this.#getType(subCls);
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
219
|
}
|
|
@@ -260,7 +260,7 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
|
|
|
260
260
|
/**
|
|
261
261
|
* Process endpoint parameter
|
|
262
262
|
*/
|
|
263
|
-
#processEndpointParam(
|
|
263
|
+
#processEndpointParam(endpoint: EndpointConfig, param: EndpointParameterConfig, input: SchemaParameterConfig): (
|
|
264
264
|
{ requestBody: RequestBodyObject } |
|
|
265
265
|
{ parameters: ParameterObject[] } |
|
|
266
266
|
undefined
|
|
@@ -269,7 +269,7 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
|
|
|
269
269
|
|
|
270
270
|
if (param.location) {
|
|
271
271
|
if (param.location === 'body') {
|
|
272
|
-
const acceptsMime =
|
|
272
|
+
const acceptsMime = endpoint.finalizedResponseHeaders.get('accepts');
|
|
273
273
|
return {
|
|
274
274
|
requestBody: input.specifiers?.includes('file') ? this.#buildUploadBody() : this.#getEndpointBody(input, acceptsMime)
|
|
275
275
|
};
|
|
@@ -291,47 +291,45 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
|
|
|
291
291
|
/**
|
|
292
292
|
* Process controller endpoint
|
|
293
293
|
*/
|
|
294
|
-
onEndpointEnd(
|
|
295
|
-
if (this.#config.skipEndpoints || !
|
|
294
|
+
onEndpointEnd(endpoint: EndpointConfig, controller: ControllerConfig): void {
|
|
295
|
+
if (this.#config.skipEndpoints || !endpoint.httpMethod) {
|
|
296
296
|
return;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
const tagName =
|
|
299
|
+
const tagName = controller.externalName;
|
|
300
300
|
|
|
301
|
-
const schema = SchemaRegistryIndex.get(
|
|
301
|
+
const schema = SchemaRegistryIndex.get(endpoint.class).getMethod(endpoint.methodName);
|
|
302
302
|
|
|
303
|
-
const
|
|
303
|
+
const apiConfig: OperationObject = {
|
|
304
304
|
tags: [tagName],
|
|
305
305
|
responses: {},
|
|
306
306
|
summary: schema.description,
|
|
307
307
|
description: schema.description,
|
|
308
|
-
operationId: `${
|
|
308
|
+
operationId: `${endpoint.class.name}_${endpoint.methodName}`,
|
|
309
309
|
parameters: []
|
|
310
310
|
};
|
|
311
311
|
|
|
312
|
-
const contentTypeMime =
|
|
313
|
-
const
|
|
314
|
-
const code = Object.keys(
|
|
315
|
-
|
|
312
|
+
const contentTypeMime = endpoint.finalizedResponseHeaders.get('content-type');
|
|
313
|
+
const bodyConfig = this.#getEndpointBody(schema.returnType, contentTypeMime);
|
|
314
|
+
const code = Object.keys(bodyConfig.content).length ? 200 : 201;
|
|
315
|
+
apiConfig.responses![code] = bodyConfig;
|
|
316
316
|
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
for (const param of methodSchema.parameters) {
|
|
320
|
-
const result = this.#processEndpointParam(ep, ep.parameters[param.index] ?? {}, param);
|
|
317
|
+
for (const param of schema.parameters) {
|
|
318
|
+
const result = this.#processEndpointParam(endpoint, endpoint.parameters[param.index] ?? {}, param);
|
|
321
319
|
if (result) {
|
|
322
320
|
if ('parameters' in result) {
|
|
323
|
-
(
|
|
321
|
+
(apiConfig.parameters ??= []).push(...result.parameters);
|
|
324
322
|
} else {
|
|
325
|
-
|
|
323
|
+
apiConfig.requestBody ??= result.requestBody;
|
|
326
324
|
}
|
|
327
325
|
}
|
|
328
326
|
}
|
|
329
327
|
|
|
330
|
-
const key =
|
|
328
|
+
const key = endpoint.fullPath.replace(/:([A-Za-z0-9_]+)\b/g, (__, param) => `{${param}}`);
|
|
331
329
|
|
|
332
330
|
this.#paths[key] = {
|
|
333
331
|
...(this.#paths[key] ?? {}),
|
|
334
|
-
[HTTP_METHODS[
|
|
332
|
+
[HTTP_METHODS[endpoint.httpMethod].lower]: apiConfig
|
|
335
333
|
};
|
|
336
334
|
}
|
|
337
335
|
|
|
@@ -356,8 +354,8 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
|
|
|
356
354
|
paths: Object.fromEntries(
|
|
357
355
|
Object.entries(this.#paths)
|
|
358
356
|
.toSorted(([a], [b]) => a.localeCompare(b))
|
|
359
|
-
.map(([
|
|
360
|
-
Object.entries(
|
|
357
|
+
.map(([key, value]) => [key, Object.fromEntries(
|
|
358
|
+
Object.entries(value)
|
|
361
359
|
.toSorted(([a], [b]) => a.localeCompare(b))
|
|
362
360
|
)])
|
|
363
361
|
),
|
package/src/service.ts
CHANGED
|
@@ -3,12 +3,10 @@ import { stringify } from 'yaml';
|
|
|
3
3
|
|
|
4
4
|
import { BinaryUtil } from '@travetto/runtime';
|
|
5
5
|
import { Injectable, Inject } from '@travetto/di';
|
|
6
|
-
import {
|
|
7
|
-
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
8
|
-
import { Registry } from '@travetto/registry';
|
|
6
|
+
import { ControllerVisitUtil, WebConfig } from '@travetto/web';
|
|
9
7
|
|
|
10
8
|
import { ApiHostConfig, ApiInfoConfig, ApiSpecConfig } from './config.ts';
|
|
11
|
-
import { OpenapiVisitor } from './
|
|
9
|
+
import { OpenapiVisitor } from './generate.ts';
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
12
|
* Open API generation service
|
|
@@ -44,9 +42,6 @@ export class OpenApiService {
|
|
|
44
42
|
* Initialize after schemas are readied
|
|
45
43
|
*/
|
|
46
44
|
async postConstruct(): Promise<void> {
|
|
47
|
-
Registry.onClassChange(() => this.resetSpec(), ControllerRegistryIndex);
|
|
48
|
-
Registry.onClassChange(() => this.resetSpec(), SchemaRegistryIndex);
|
|
49
|
-
|
|
50
45
|
if (!this.apiHostConfig.servers && this.webConfig.baseUrl) {
|
|
51
46
|
this.apiHostConfig.servers = [{ url: this.webConfig.baseUrl }];
|
|
52
47
|
}
|
|
@@ -82,8 +77,8 @@ export class OpenApiService {
|
|
|
82
77
|
stringify(spec);
|
|
83
78
|
|
|
84
79
|
await BinaryUtil.bufferedFileWrite(this.apiSpecConfig.output, output, true);
|
|
85
|
-
} catch (
|
|
86
|
-
console.error('Unable to persist openapi spec',
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Unable to persist openapi spec', error);
|
|
87
82
|
}
|
|
88
83
|
}
|
|
89
84
|
}
|
package/support/bin/help.ts
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
import { ExecUtil, Runtime } from '@travetto/runtime';
|
|
5
|
+
import { ExecUtil, JSONUtil, Runtime } from '@travetto/runtime';
|
|
6
6
|
import { cliTpl } from '@travetto/cli';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -18,13 +18,13 @@ export class OpenApiClientHelp {
|
|
|
18
18
|
.split('DOCUMENTATION')[0]
|
|
19
19
|
.trim()
|
|
20
20
|
.split(/\n/g)
|
|
21
|
-
.filter(
|
|
22
|
-
.map(
|
|
21
|
+
.filter(line => /^\s+-/.test(line) && !/\((beta|experimental)\)/.test(line))
|
|
22
|
+
.map(line => line.replace(/^\s+-\s+/, '').trim());
|
|
23
23
|
|
|
24
24
|
await fs.mkdir(path.dirname(formatCache), { recursive: true });
|
|
25
25
|
await fs.writeFile(formatCache, JSON.stringify([...lines.toSorted(),]));
|
|
26
26
|
}
|
|
27
|
-
const list: string[] =
|
|
27
|
+
const list: string[] = await JSONUtil.readFile(formatCache);
|
|
28
28
|
return list;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -36,7 +36,7 @@ export class OpenApiClientHelp {
|
|
|
36
36
|
'',
|
|
37
37
|
cliTpl`${{ subtitle: 'Available Formats' }}`,
|
|
38
38
|
'----------------------------------',
|
|
39
|
-
...formats.map(
|
|
39
|
+
...formats.map(format => cliTpl`* ${{ input: format }}`)
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
return help;
|
|
@@ -16,7 +16,7 @@ export class OpenApiClientCommand implements CliCommandShape {
|
|
|
16
16
|
extendedHelp: boolean = false;
|
|
17
17
|
/** Additional Properties */
|
|
18
18
|
@CliFlag({ short: '-a', full: '--additional-properties' })
|
|
19
|
-
|
|
19
|
+
properties: string[] = [];
|
|
20
20
|
/** Input file */
|
|
21
21
|
input = './openapi.yml';
|
|
22
22
|
/** Output folder */
|
|
@@ -32,7 +32,7 @@ export class OpenApiClientCommand implements CliCommandShape {
|
|
|
32
32
|
this.output = path.resolve(this.output);
|
|
33
33
|
this.input = path.resolve(this.input);
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const subProcess = cp.spawn('docker', [
|
|
36
36
|
'run',
|
|
37
37
|
'--rm',
|
|
38
38
|
'-i',
|
|
@@ -47,12 +47,12 @@ export class OpenApiClientCommand implements CliCommandShape {
|
|
|
47
47
|
'-g', format,
|
|
48
48
|
'-o', '/workspace',
|
|
49
49
|
'-i', `/input/${path.basename(this.input)}`,
|
|
50
|
-
...(this.
|
|
50
|
+
...(this.properties.length ? ['--additional-properties', this.properties.join(',')] : [])
|
|
51
51
|
], {
|
|
52
52
|
stdio: 'inherit'
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
const result = await ExecUtil.getResult(
|
|
55
|
+
const result = await ExecUtil.getResult(subProcess);
|
|
56
56
|
|
|
57
57
|
if (!result.valid) {
|
|
58
58
|
process.exitCode = 1;
|