@travetto/openapi 6.0.3 → 7.0.0-rc.1

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 CHANGED
@@ -69,7 +69,7 @@ export class ApiHostConfig {
69
69
  /**
70
70
  * OpenAPI Version
71
71
  */
72
- openapi = '3.0.0';
72
+ openapi = '3.1.0';
73
73
  }
74
74
 
75
75
  /**
@@ -142,7 +142,7 @@ $ trv openapi:client --help
142
142
  Usage: openapi:client [options] <format:string>
143
143
 
144
144
  Options:
145
- -x, --extended-help Show Extended Help
145
+ -x, --extended-help Show Extended Help (default: false)
146
146
  -a, --additional-properties <string> Additional Properties (default: [])
147
147
  -i, --input <string> Input file (default: "./openapi.yml")
148
148
  -o, --output <string> Output folder (default: "./api-client")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/openapi",
3
- "version": "6.0.3",
3
+ "version": "7.0.0-rc.1",
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": "^6.0.1",
30
- "@travetto/schema": "^6.0.1",
31
- "@travetto/web": "^6.0.3",
29
+ "@travetto/config": "^7.0.0-rc.0",
30
+ "@travetto/schema": "^7.0.0-rc.0",
31
+ "@travetto/web": "^7.0.0-rc.1",
32
32
  "openapi3-ts": "^4.5.0",
33
33
  "yaml": "^2.8.1"
34
34
  },
35
35
  "peerDependencies": {
36
- "@travetto/cli": "^6.0.1"
36
+ "@travetto/cli": "^7.0.0-rc.0"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@travetto/cli": {
package/src/config.ts CHANGED
@@ -43,7 +43,7 @@ export class ApiHostConfig {
43
43
  /**
44
44
  * OpenAPI Version
45
45
  */
46
- openapi = '3.0.0';
46
+ openapi = '3.1.0';
47
47
  }
48
48
 
49
49
  /**
package/src/controller.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import { stringify } from 'yaml';
2
2
 
3
- import { ConfigureInterceptor, Controller, CorsInterceptor, Get, SetHeaders, Undocumented } from '@travetto/web';
3
+ import { ConfigureInterceptor, Controller, CorsInterceptor, Get, SetHeaders } from '@travetto/web';
4
4
  import { Inject } from '@travetto/di';
5
+ import { IsPrivate } from '@travetto/schema';
5
6
 
6
7
  import { OpenApiService } from './service.ts';
7
8
 
8
9
  /**
9
10
  * Basic controller for surfacing the api spec
10
11
  */
11
- @Undocumented()
12
+ @IsPrivate()
12
13
  @Controller('/')
13
14
  @ConfigureInterceptor(CorsInterceptor, { origins: ['*'] })
14
15
  export class OpenApiController {
package/src/service.ts CHANGED
@@ -3,8 +3,9 @@ import { stringify } from 'yaml';
3
3
 
4
4
  import { BinaryUtil } from '@travetto/runtime';
5
5
  import { Injectable, Inject } from '@travetto/di';
6
- import { ControllerRegistry, ControllerVisitUtil, WebConfig } from '@travetto/web';
7
- import { SchemaRegistry } from '@travetto/schema';
6
+ import { ControllerRegistryIndex, ControllerVisitUtil, WebConfig } from '@travetto/web';
7
+ import { SchemaRegistryIndex } from '@travetto/schema';
8
+ import { Registry } from '@travetto/registry';
8
9
 
9
10
  import { ApiHostConfig, ApiInfoConfig, ApiSpecConfig } from './config.ts';
10
11
  import { OpenapiVisitor } from './spec-generate.ts';
@@ -43,8 +44,8 @@ export class OpenApiService {
43
44
  * Initialize after schemas are readied
44
45
  */
45
46
  async postConstruct(): Promise<void> {
46
- ControllerRegistry.on(() => this.resetSpec());
47
- SchemaRegistry.on(() => this.resetSpec());
47
+ Registry.onClassChange(() => this.resetSpec(), ControllerRegistryIndex);
48
+ Registry.onClassChange(() => this.resetSpec(), SchemaRegistryIndex);
48
49
 
49
50
  if (!this.apiHostConfig.servers && this.webConfig.baseUrl) {
50
51
  this.apiHostConfig.servers = [{ url: this.webConfig.baseUrl }];
@@ -4,17 +4,16 @@ import type {
4
4
  RequestBodyObject, TagObject, PathsObject, PathItemObject
5
5
  } from 'openapi3-ts/oas31';
6
6
 
7
- import { EndpointConfig, ControllerConfig, EndpointParamConfig, EndpointIOType, ControllerVisitor, HTTP_METHODS } from '@travetto/web';
8
- import { Class, describeFunction } from '@travetto/runtime';
9
- import { SchemaRegistry, FieldConfig, ClassConfig, SchemaNameResolver } from '@travetto/schema';
7
+ import { EndpointConfig, ControllerConfig, EndpointParameterConfig, ControllerVisitor, HTTP_METHODS } from '@travetto/web';
8
+ import { AppError, Class, describeFunction } from '@travetto/runtime';
9
+ import { SchemaFieldConfig, SchemaClassConfig, SchemaNameResolver, SchemaInputConfig, SchemaRegistryIndex, SchemaBasicType, SchemaParameterConfig } from '@travetto/schema';
10
10
 
11
11
  import { ApiSpecConfig } from './config.ts';
12
12
 
13
13
  const DEFINITION = '#/components/schemas';
14
14
 
15
- function isFieldConfig(val: object): val is FieldConfig {
16
- return !!val && 'owner' in val && 'type' in val;
17
- }
15
+ const isInputConfig = (val: object): val is SchemaInputConfig => !!val && 'owner' in val && 'type' in val;
16
+ const isFieldConfig = (val: object): val is SchemaFieldConfig => isInputConfig(val) && 'name' in val;
18
17
 
19
18
  type GeneratedSpec = {
20
19
  tags: TagObject[];
@@ -44,26 +43,27 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
44
43
  /**
45
44
  * Convert schema to a set of dotted parameters
46
45
  */
47
- #schemaToDotParams(location: 'query' | 'header', field: FieldConfig, prefix: string = '', rootField: FieldConfig = field): ParameterObject[] {
48
- const viewConf = SchemaRegistry.has(field.type) && SchemaRegistry.getViewSchema(field.type, field.view);
49
- const schemaConf = viewConf && viewConf.schema;
50
- if (!schemaConf) {
51
- throw new Error(`Unknown class, not registered as a schema: ${field.type.Ⲑid}`);
46
+ #schemaToDotParams(location: 'query' | 'header', input: SchemaInputConfig, prefix: string = '', rootField: SchemaInputConfig = input): ParameterObject[] {
47
+ if (!SchemaRegistryIndex.has(input.type)) {
48
+ throw new AppError(`Unknown class, not registered as a schema: ${input.type.Ⲑid}`);
52
49
  }
50
+
51
+ const fields = SchemaRegistryIndex.getFieldMap(input.type, input.view);
53
52
  const params: ParameterObject[] = [];
54
- for (const sub of Object.values(schemaConf)) {
55
- if (SchemaRegistry.has(sub.type) || SchemaRegistry.hasPending(sub.type)) {
53
+ for (const sub of Object.values(fields)) {
54
+ const name = sub.name.toString();
55
+ if (SchemaRegistryIndex.has(sub.type)) {
56
56
  const suffix = (sub.array) ? '[]' : '';
57
- params.push(...this.#schemaToDotParams(location, sub, prefix ? `${prefix}.${sub.name}${suffix}` : `${sub.name}${suffix}.`, rootField));
57
+ params.push(...this.#schemaToDotParams(location, sub, prefix ? `${prefix}.${name}${suffix}` : `${name}${suffix}.`, rootField));
58
58
  } else {
59
59
  params.push({
60
- name: `${prefix}${sub.name}`,
60
+ name: `${prefix}${name}`,
61
61
  description: sub.description,
62
62
  schema: sub.array ? {
63
63
  type: 'array',
64
64
  ...this.#getType(sub)
65
65
  } : this.#getType(sub),
66
- required: !!(rootField?.required?.active && sub.required?.active),
66
+ required: (rootField?.required?.active !== false && sub.required?.active !== false),
67
67
  in: location
68
68
  });
69
69
  }
@@ -74,17 +74,17 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
74
74
  /**
75
75
  * Get the type for a given class
76
76
  */
77
- #getType(fieldOrClass: FieldConfig | Class): Record<string, unknown> {
77
+ #getType(inputOrClass: SchemaInputConfig | Class): Record<string, unknown> {
78
78
  let field: { type: Class<unknown>, precision?: [number, number | undefined] };
79
- if (!isFieldConfig(fieldOrClass)) {
80
- field = { type: fieldOrClass };
79
+ if (!isInputConfig(inputOrClass)) {
80
+ field = { type: inputOrClass };
81
81
  } else {
82
- field = fieldOrClass;
82
+ field = inputOrClass;
83
83
  }
84
84
  const out: Record<string, unknown> = {};
85
85
  // Handle nested types
86
- if (SchemaRegistry.has(field.type)) {
87
- const id = this.#nameResolver.getName(SchemaRegistry.get(field.type));
86
+ if (SchemaRegistryIndex.has(field.type)) {
87
+ const id = this.#nameResolver.getName(SchemaRegistryIndex.getConfig(field.type));
88
88
  // Exposing
89
89
  this.#schemas[id] = this.#allSchemas[id];
90
90
  out.$ref = `${DEFINITION}/${id}`;
@@ -130,40 +130,42 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
130
130
  /**
131
131
  * Process schema field
132
132
  */
133
- #processSchemaField(field: FieldConfig, required: string[]): SchemaObject {
134
- let prop: SchemaObject = this.#getType(field);
133
+ #processSchemaField(input: SchemaInputConfig, required: string[]): SchemaObject {
134
+ let prop: SchemaObject = this.#getType(input);
135
135
 
136
- if (field.examples) {
137
- prop.example = field.examples;
138
- }
139
- prop.description = field.description;
140
- if (field.match) {
141
- prop.pattern = field.match.re!.source;
136
+ if (input.examples) {
137
+ prop.example = input.examples;
142
138
  }
143
- if (field.maxlength) {
144
- prop.maxLength = field.maxlength.n;
139
+ prop.description = input.description;
140
+ if (input.match) {
141
+ prop.pattern = input.match.re!.source;
145
142
  }
146
- if (field.minlength) {
147
- prop.minLength = field.minlength.n;
143
+ if (input.maxlength) {
144
+ prop.maxLength = input.maxlength.n;
148
145
  }
149
- if (field.min) {
150
- prop.minimum = typeof field.min.n === 'number' ? field.min.n : field.min.n.getTime();
146
+ if (input.minlength) {
147
+ prop.minLength = input.minlength.n;
151
148
  }
152
- if (field.max) {
153
- prop.maximum = typeof field.max.n === 'number' ? field.max.n : field.max.n.getTime();
149
+ if (input.min) {
150
+ prop.minimum = typeof input.min.n === 'number' ? input.min.n : input.min.n.getTime();
154
151
  }
155
- if (field.enum) {
156
- prop.enum = field.enum.values;
152
+ if (input.max) {
153
+ prop.maximum = typeof input.max.n === 'number' ? input.max.n : input.max.n.getTime();
157
154
  }
158
- if (field.required && field.required.active) {
159
- required.push(field.name);
155
+ if (input.enum) {
156
+ prop.enum = input.enum.values;
160
157
  }
161
- if (field.access === 'readonly') {
162
- prop.readOnly = true;
163
- } else if (field.access === 'writeonly') {
164
- prop.writeOnly = true;
158
+ if (isFieldConfig(input)) {
159
+ if (input.required?.active !== false) {
160
+ required.push(input.name.toString());
161
+ }
162
+ if (input.access === 'readonly') {
163
+ prop.readOnly = true;
164
+ } else if (input.access === 'writeonly') {
165
+ prop.writeOnly = true;
166
+ }
165
167
  }
166
- if (field.array) {
168
+ if (input.array) {
167
169
  prop = {
168
170
  type: 'array',
169
171
  items: prop
@@ -176,7 +178,7 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
176
178
  /**
177
179
  * Process schema class
178
180
  */
179
- onSchema(type?: ClassConfig): void {
181
+ onSchema(type?: SchemaClassConfig): void {
180
182
  if (type === undefined) {
181
183
  return;
182
184
  }
@@ -185,33 +187,32 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
185
187
  const typeId = this.#nameResolver.getName(type);
186
188
 
187
189
  if (!this.#allSchemas[typeId]) {
188
- const config = SchemaRegistry.get(cls);
190
+ const config = SchemaRegistryIndex.getConfig(cls);
189
191
  if (config) {
190
192
  this.#allSchemas[typeId] = {
191
- title: config.title || config.description,
192
- description: config.description || config.title,
193
- example: config.examples
193
+ description: config.description,
194
+ examples: config.examples
194
195
  };
195
196
 
196
197
  const properties: Record<string, SchemaObject> = {};
197
- const def = config.totalView;
198
+ const def = config;
198
199
  const required: string[] = [];
199
200
 
200
- for (const fieldName of def.fields) {
201
- if (SchemaRegistry.has(def.schema[fieldName].type)) {
202
- this.onSchema(SchemaRegistry.get(def.schema[fieldName].type));
201
+ for (const fieldName of Object.keys(def.fields)) {
202
+ if (SchemaRegistryIndex.has(def.fields[fieldName].type)) {
203
+ this.onSchema(SchemaRegistryIndex.getConfig(def.fields[fieldName].type));
203
204
  }
204
- properties[fieldName] = this.#processSchemaField(def.schema[fieldName], required);
205
+ properties[fieldName] = this.#processSchemaField(def.fields[fieldName], required);
205
206
  }
206
207
 
207
208
  const extra: Record<string, unknown> = {};
208
- if (describeFunction(cls)?.abstract) {
209
- const map = SchemaRegistry.getSubTypesForClass(cls);
209
+ if (config.discriminatedBase) {
210
+ const map = SchemaRegistryIndex.getDiscriminatedClasses(cls);
210
211
  if (map) {
211
212
  extra.oneOf = map
212
213
  .filter(x => !describeFunction(x)?.abstract)
213
214
  .map(c => {
214
- this.onSchema(SchemaRegistry.get(c));
215
+ this.onSchema(SchemaRegistryIndex.getConfig(c));
215
216
  return this.#getType(c);
216
217
  });
217
218
  }
@@ -223,7 +224,7 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
223
224
  ...extra
224
225
  });
225
226
  } else {
226
- this.#allSchemas[typeId] = { title: typeId };
227
+ this.#allSchemas[typeId] = { description: typeId };
227
228
  }
228
229
  }
229
230
  }
@@ -231,20 +232,20 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
231
232
  /**
232
233
  * Standard payload structure
233
234
  */
234
- #getEndpointBody(body?: EndpointIOType, mime?: string | null): RequestBodyObject {
235
- if (!body) {
235
+ #getEndpointBody(body?: SchemaBasicType, mime?: string | null): RequestBodyObject {
236
+ if (!body || body.type === undefined) {
236
237
  return { content: {}, description: '' };
237
238
  } else if (body.type === Readable || body.type === Buffer) {
238
239
  return {
239
240
  content: {
240
- [mime ?? 'application/octet-stream']: { schema: { type: 'string', format: 'binary' } }
241
+ [mime ?? 'application/octet-stream']: { schema: { type: 'string', format: 'binary' } },
241
242
  },
242
- description: ''
243
+ description: 'Raw binary data'
243
244
  };
244
245
  } else {
245
- const cls = SchemaRegistry.get(body.type);
246
- const typeId = cls ? this.#nameResolver.getName(cls) : body.type.name;
247
- const typeRef = cls ? this.#getType(body.type) : { type: body.type.name.toLowerCase() };
246
+ const schemaConfig = SchemaRegistryIndex.getOptionalConfig(body.type);
247
+ const typeId = schemaConfig ? this.#nameResolver.getName(schemaConfig) : body.type.name;
248
+ const typeRef = schemaConfig ? this.#getType(body.type) : { type: body.type.name.toLowerCase() };
248
249
  return {
249
250
  content: {
250
251
  [mime ?? 'application/json']: {
@@ -259,27 +260,28 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
259
260
  /**
260
261
  * Process endpoint parameter
261
262
  */
262
- #processEndpointParam(ep: EndpointConfig, param: EndpointParamConfig, field: FieldConfig): (
263
+ #processEndpointParam(ep: EndpointConfig, param: EndpointParameterConfig, input: SchemaParameterConfig): (
263
264
  { requestBody: RequestBodyObject } |
264
265
  { parameters: ParameterObject[] } |
265
266
  undefined
266
267
  ) {
267
- const complex = field.type && SchemaRegistry.has(field.type);
268
+ const complex = input.type && SchemaRegistryIndex.has(input.type);
269
+
268
270
  if (param.location) {
269
271
  if (param.location === 'body') {
270
272
  const acceptsMime = ep.finalizedResponseHeaders.get('accepts');
271
273
  return {
272
- requestBody: field.specifiers?.includes('file') ? this.#buildUploadBody() : this.#getEndpointBody(field, acceptsMime)
274
+ requestBody: input.specifiers?.includes('file') ? this.#buildUploadBody() : this.#getEndpointBody(input, acceptsMime)
273
275
  };
274
276
  } else if (complex && (param.location === 'query' || param.location === 'header')) {
275
- return { parameters: this.#schemaToDotParams(param.location, field, param.prefix ? `${param.prefix}.` : '') };
277
+ return { parameters: this.#schemaToDotParams(param.location, input, param.prefix ? `${param.prefix}.` : '') };
276
278
  } else {
277
279
  const epParam: ParameterObject = {
278
280
  in: param.location,
279
- name: param.name || param.location,
280
- description: field.description,
281
- required: !!field.required?.active || false,
282
- schema: field.array ? { type: 'array', items: this.#getType(field) } : this.#getType(field)
281
+ name: input.name ?? param.location,
282
+ description: input.description,
283
+ required: input.required?.active !== false,
284
+ schema: input.array ? { type: 'array', items: this.#getType(input) } : this.#getType(input)
283
285
  };
284
286
  return { parameters: [epParam] };
285
287
  }
@@ -296,23 +298,26 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
296
298
 
297
299
  const tagName = ctrl.externalName;
298
300
 
301
+ const schema = SchemaRegistryIndex.getMethodConfig(ep.class, ep.methodName);
302
+
299
303
  const op: OperationObject = {
300
304
  tags: [tagName],
301
305
  responses: {},
302
- summary: ep.title,
303
- description: ep.description || ep.title,
304
- operationId: `${ep.class.name}_${ep.name}`,
306
+ summary: schema.description,
307
+ description: schema.description,
308
+ operationId: `${ep.class.name}_${ep.methodName.toString()}`,
305
309
  parameters: []
306
310
  };
307
311
 
308
312
  const contentTypeMime = ep.finalizedResponseHeaders.get('content-type');
309
- const pConf = this.#getEndpointBody(ep.responseType, contentTypeMime);
313
+ const pConf = this.#getEndpointBody(schema.returnType, contentTypeMime);
310
314
  const code = Object.keys(pConf.content).length ? 200 : 201;
311
315
  op.responses![code] = pConf;
312
316
 
313
- const schema = SchemaRegistry.getMethodSchema(ep.class, ep.name);
314
- for (const field of schema) {
315
- const result = this.#processEndpointParam(ep, ep.params[field.index!], field);
317
+ const methodSchema = SchemaRegistryIndex.getMethodConfig(ep.class, ep.methodName);
318
+
319
+ for (const param of methodSchema.parameters) {
320
+ const result = this.#processEndpointParam(ep, ep.parameters[param.index] ?? {}, param);
316
321
  if (result) {
317
322
  if ('parameters' in result) {
318
323
  (op.parameters ??= []).push(...result.parameters);
@@ -334,9 +339,10 @@ export class OpenapiVisitor implements ControllerVisitor<GeneratedSpec> {
334
339
  if (this.#config.skipEndpoints) {
335
340
  return;
336
341
  }
342
+ const classSchema = SchemaRegistryIndex.getConfig(controller.class);
337
343
  this.#tags.push({
338
344
  name: controller.externalName,
339
- description: controller.description || controller.title
345
+ description: classSchema.description
340
346
  });
341
347
  }
342
348
 
@@ -11,19 +11,21 @@ import { OpenApiClientHelp } from './bin/help.ts';
11
11
  */
12
12
  @CliCommand()
13
13
  export class OpenApiClientCommand implements CliCommandShape {
14
- @CliFlag({ desc: 'Show Extended Help', short: '-x' })
15
- extendedHelp?: boolean;
16
- @CliFlag({ desc: 'Additional Properties', short: '-a', name: '--additional-properties' })
14
+ /** Show Extended Help */
15
+ @CliFlag({ short: '-x' })
16
+ extendedHelp: boolean = false;
17
+ /** Additional Properties */
18
+ @CliFlag({ short: '-a', full: '--additional-properties' })
17
19
  props: string[] = [];
18
- @CliFlag({ desc: 'Input file' })
20
+ /** Input file */
19
21
  input = './openapi.yml';
20
- @CliFlag({ desc: 'Output folder' })
22
+ /** Output folder */
21
23
  output = './api-client';
22
- @CliFlag({ desc: 'Docker Image to user' })
24
+ /** Docker Image to user */
23
25
  dockerImage = 'openapitools/openapi-generator-cli:latest';
24
26
 
25
27
  async help(): Promise<string[]> {
26
- return OpenApiClientHelp.help(this.dockerImage, this.extendedHelp ?? false);
28
+ return OpenApiClientHelp.help(this.dockerImage, this.extendedHelp);
27
29
  }
28
30
 
29
31
  async main(format: string): Promise<void> {
@@ -3,8 +3,8 @@ import path from 'node:path';
3
3
 
4
4
  import { CliCommandShape, CliCommand } from '@travetto/cli';
5
5
  import { Env } from '@travetto/runtime';
6
- import { RootRegistry } from '@travetto/registry';
7
- import { DependencyRegistry } from '@travetto/di';
6
+ import { Registry } from '@travetto/registry';
7
+ import { DependencyRegistryIndex } from '@travetto/di';
8
8
 
9
9
  /**
10
10
  * CLI for outputting the open api spec to a local file
@@ -22,10 +22,10 @@ export class OpenApiSpecCommand implements CliCommandShape {
22
22
  async main(): Promise<void> {
23
23
  const { OpenApiService } = await import('../src/service.ts');
24
24
 
25
- await RootRegistry.init();
25
+ await Registry.init();
26
26
 
27
- const instance = await DependencyRegistry.getInstance(OpenApiService);
28
- const result = instance.getSpec();
27
+ const instance = await DependencyRegistryIndex.getInstance(OpenApiService);
28
+ const result = await instance.getSpec();
29
29
 
30
30
  if (this.output === '-' || !this.output) {
31
31
  console.log!(JSON.stringify(result, null, 2));