@travetto/openapi 2.1.3 → 2.2.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/README.md CHANGED
@@ -20,7 +20,7 @@ All of the high level configurations can be found in the following structure:
20
20
  **Code: Config: OpenAPI Configuration**
21
21
  ```typescript
22
22
  import * as path from 'path';
23
- import * as fs from 'fs';
23
+ import * as fs from 'fs/promises';
24
24
  import { ServerObject, ContactObject, LicenseObject } from 'openapi3-ts/src/model/OpenApi';
25
25
 
26
26
  import { Config } from '@travetto/config';
@@ -37,7 +37,7 @@ export class ApiInfoConfig {
37
37
  license: LicenseObject = { name: AppManifest.info.license! };
38
38
  termsOfService?: string;
39
39
  title: string = AppManifest.info.name;
40
- version?: string = AppManifest.info.version;
40
+ version: string = AppManifest.info.version ?? '0.0.0';
41
41
  }
42
42
 
43
43
  /**
@@ -77,7 +77,7 @@ export class ApiSpecConfig {
77
77
  */
78
78
  exposeAllSchemas: boolean = false;
79
79
 
80
- async postConstruct() {
80
+ async postConstruct(): Promise<void> {
81
81
  this.output = PathUtil.toUnix(this.output);
82
82
  if (!this.output || this.output === '-') {
83
83
  this.persist = false;
@@ -86,7 +86,7 @@ export class ApiSpecConfig {
86
86
  if (!/[.](json|ya?ml)$/.test(this.output)) { // Assume a folder
87
87
  this.output = PathUtil.resolveUnix(this.output, 'openapi.yml');
88
88
  }
89
- await fs.promises.mkdir(path.dirname(this.output), { recursive: true });
89
+ await fs.mkdir(path.dirname(this.output), { recursive: true });
90
90
  }
91
91
  }
92
92
  }
@@ -6,7 +6,7 @@ import { BasePlugin } from '@travetto/cli/src/plugin-base';
6
6
  import { AppCache, ExecUtil, PathUtil } from '@travetto/boot';
7
7
  import { color } from '@travetto/cli/src/color';
8
8
 
9
- const presets = JSON.parse(readFileSync(PathUtil.resolveUnix(__dirname, '..', 'resources', 'presets.json'), 'utf8')) as Record<string, [string, object] | [string]>;
9
+ const presets: Record<string, [string, object] | [string]> = JSON.parse(readFileSync(PathUtil.resolveUnix(__dirname, '..', 'resources', 'presets.json'), 'utf8'));
10
10
 
11
11
  /**
12
12
  * CLI for generating the cli client
@@ -14,11 +14,11 @@ const presets = JSON.parse(readFileSync(PathUtil.resolveUnix(__dirname, '..', 'r
14
14
  export class OpenApiClientPlugin extends BasePlugin {
15
15
  name = 'openapi:client';
16
16
 
17
- presetMap(prop?: object) {
17
+ presetMap(prop?: object): string {
18
18
  return !prop || Object.keys(prop).length === 0 ? '' : Object.entries(prop).map(([k, v]) => `${k}=${v}`).join(',');
19
19
  }
20
20
 
21
- getListOfFormats() {
21
+ getListOfFormats(): string[] {
22
22
  const json = AppCache.getOrSet('openapi-formats.json', () => {
23
23
  const stdout = ExecUtil.execSync('docker', ['run', '--rm', this.cmd.dockerImage, 'list']);
24
24
  const lines = stdout
@@ -31,9 +31,11 @@ export class OpenApiClientPlugin extends BasePlugin {
31
31
  ...lines.sort(),
32
32
  ]);
33
33
  });
34
- return JSON.parse(json) as string[];
34
+ const list: string[] = JSON.parse(json);
35
+ return list;
35
36
  }
36
37
 
38
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
37
39
  getOptions() {
38
40
  return {
39
41
  extendedHelp: this.boolOption({ name: 'extended-help', short: 'x', desc: 'Show Extended Help' }),
@@ -45,7 +47,7 @@ export class OpenApiClientPlugin extends BasePlugin {
45
47
  };
46
48
  }
47
49
 
48
- help() {
50
+ help(): string {
49
51
  const presetLen = Math.max(...Object.keys(presets).map(x => x.length));
50
52
  const presetEntries = Object
51
53
  .entries(presets)
@@ -65,11 +67,11 @@ ${this.getListOfFormats().map(x => color`* ${{ input: x }}`).join('\n')} `;
65
67
  return this.cmd.extendedHelp ? `${presetText}\n${formatText}` : presetText;
66
68
  }
67
69
 
68
- getArgs() {
70
+ getArgs(): string {
69
71
  return '[format-or-preset]';
70
72
  }
71
73
 
72
- async action(format: string) {
74
+ async action(format: string): Promise<void> {
73
75
  if (!format) {
74
76
  this.showHelp(new Error('Format is required'));
75
77
  }
@@ -90,7 +92,7 @@ ${this.getListOfFormats().map(x => color`* ${{ input: x }}`).join('\n')} `;
90
92
 
91
93
  const args = [
92
94
  'run',
93
- '--user', `${process.geteuid()}:${process.getgid()}`,
95
+ '--user', `${process.geteuid?.()}:${process.getgid?.()}`,
94
96
  '-v', `${this.cmd.output}:/workspace`,
95
97
  '-v', `${path.dirname(this.cmd.input)}:/input`,
96
98
  '-it',
@@ -8,18 +8,19 @@ import { EnvInit } from '@travetto/base/bin/init';
8
8
  export class OpenApiSpecPlugin extends BasePlugin {
9
9
  name = 'openapi:spec';
10
10
 
11
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
11
12
  getOptions() {
12
13
  return { output: this.option({ desc: 'Output files', def: './openapi.yml' }) };
13
14
  }
14
15
 
15
- envInit() {
16
+ envInit(): void {
16
17
  EnvInit.init({
17
18
  debug: '0',
18
19
  set: { API_SPEC_OUTPUT: this.cmd.output }
19
20
  });
20
21
  }
21
22
 
22
- async action() {
23
+ async action(): Promise<void> {
23
24
  const result = await ExecUtil.workerMain(require.resolve('./generate')).message;
24
25
 
25
26
  if (this.cmd.output === '-' || !this.cmd.output) {
package/bin/generate.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ExecUtil } from '@travetto/boot';
2
2
 
3
- export async function main() {
3
+ export async function main(): Promise<void> {
4
4
  const { PhaseManager } = await import('@travetto/base');
5
5
  await PhaseManager.run('init');
6
6
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@travetto/openapi",
3
3
  "displayName": "OpenAPI Specification",
4
- "version": "2.1.3",
4
+ "version": "2.2.0",
5
5
  "description": "OpenAPI integration support for the travetto framework",
6
6
  "keywords": [
7
7
  "rest",
@@ -28,14 +28,14 @@
28
28
  "directory": "module/openapi"
29
29
  },
30
30
  "dependencies": {
31
- "@travetto/config": "^2.1.3",
32
- "@travetto/rest": "^2.1.3",
33
- "@travetto/schema": "^2.1.3",
34
- "@travetto/yaml": "^2.1.3",
31
+ "@travetto/config": "^2.2.0",
32
+ "@travetto/rest": "^2.2.0",
33
+ "@travetto/schema": "^2.2.0",
34
+ "@travetto/yaml": "^2.2.0",
35
35
  "openapi3-ts": "^2.0.2"
36
36
  },
37
37
  "optionalPeerDependencies": {
38
- "@travetto/cli": "^2.1.2"
38
+ "@travetto/cli": "^2.2.0"
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
package/src/config.ts CHANGED
@@ -16,7 +16,7 @@ export class ApiInfoConfig {
16
16
  license: LicenseObject = { name: AppManifest.info.license! };
17
17
  termsOfService?: string;
18
18
  title: string = AppManifest.info.name;
19
- version?: string = AppManifest.info.version;
19
+ version: string = AppManifest.info.version ?? '0.0.0';
20
20
  }
21
21
 
22
22
  /**
@@ -56,7 +56,7 @@ export class ApiSpecConfig {
56
56
  */
57
57
  exposeAllSchemas: boolean = false;
58
58
 
59
- async postConstruct() {
59
+ async postConstruct(): Promise<void> {
60
60
  this.output = PathUtil.toUnix(this.output);
61
61
  if (!this.output || this.output === '-') {
62
62
  this.persist = false;
package/src/controller.ts CHANGED
@@ -14,13 +14,13 @@ export class OpenApiController {
14
14
  service: OpenApiService;
15
15
 
16
16
  @Get('openapi.json')
17
- async getSpec() {
18
- return this.service.spec as object; // Force output to be simple
17
+ async getSpec(): Promise<object> {
18
+ return this.service.spec; // Force output to be simple
19
19
  }
20
20
 
21
21
  @Get(/openapi[.]ya?ml$/)
22
22
  @SetHeaders({ 'Content-Type': 'text/vnd.yaml' })
23
- async getYmlSpec() {
23
+ async getYmlSpec(): Promise<string> {
24
24
  return YamlUtil.serialize(this.service.spec); // Force output to be simple
25
25
  }
26
26
  }
package/src/service.ts CHANGED
@@ -32,7 +32,7 @@ export class OpenApiService {
32
32
  /**
33
33
  * Reset specification
34
34
  */
35
- async resetSpec() {
35
+ async resetSpec(): Promise<void> {
36
36
  delete this._spec;
37
37
  if (this.apiSpecConfig.persist) {
38
38
  await this.persist();
@@ -42,7 +42,7 @@ export class OpenApiService {
42
42
  /**
43
43
  * Initialize after schemas are readied
44
44
  */
45
- async postConstruct() {
45
+ async postConstruct(): Promise<void> {
46
46
  ControllerRegistry.on(() => this.resetSpec());
47
47
  SchemaRegistry.on(() => this.resetSpec());
48
48
 
@@ -62,7 +62,7 @@ export class OpenApiService {
62
62
  ...this.apiHostConfig,
63
63
  info: { ...this.apiInfoConfig },
64
64
  ...new SpecGenerator().generate(this.apiSpecConfig)
65
- } as OpenAPIObject;
65
+ };
66
66
  }
67
67
  return this._spec;
68
68
  }
@@ -70,7 +70,7 @@ export class OpenApiService {
70
70
  /**
71
71
  * Persist to local file
72
72
  */
73
- async persist() {
73
+ async persist(): Promise<void> {
74
74
  console.debug('Generating OpenAPI Spec', { output: this.apiSpecConfig.output });
75
75
 
76
76
  const output = this.apiSpecConfig.output.endsWith('.json') ?
@@ -1,11 +1,11 @@
1
1
  import { Readable } from 'stream';
2
2
  import {
3
3
  SchemaObject, SchemasObject, ParameterObject, OperationObject,
4
- RequestBodyObject, TagObject, PathItemObject
4
+ RequestBodyObject, TagObject, PathItemObject, OpenAPIObject
5
5
  } from 'openapi3-ts/src/model/OpenApi';
6
6
 
7
7
  import { ControllerRegistry, EndpointConfig, ControllerConfig, ParamConfig, EndpointIOType } from '@travetto/rest';
8
- import { Class, Util } from '@travetto/base';
8
+ import { Class } from '@travetto/base';
9
9
  import { SchemaRegistry, FieldConfig } from '@travetto/schema';
10
10
  import { AllViewⲐ } from '@travetto/schema/src/internal/types';
11
11
 
@@ -14,6 +14,10 @@ import { OpenApiController } from './controller';
14
14
 
15
15
  const DEFINITION = '#/components/schemas';
16
16
 
17
+ function isFieldConfig(val: object): val is FieldConfig {
18
+ return !!val && 'owner' in val && 'type' in val;
19
+ }
20
+
17
21
  /**
18
22
  * Spec generation utilities
19
23
  */
@@ -27,7 +31,7 @@ export class SpecGenerator {
27
31
  * Get type id
28
32
  * @param cls
29
33
  */
30
- #getTypeId(cls: Class) {
34
+ #getTypeId(cls: Class): string {
31
35
  return cls.name?.replace('ᚕsyn', '');
32
36
  }
33
37
 
@@ -35,7 +39,7 @@ export class SpecGenerator {
35
39
  * Get tag name
36
40
  * @param cls
37
41
  */
38
- #getTypeTag(cls: Class) {
42
+ #getTypeTag(cls: Class): string {
39
43
  return cls.name.replace(/(Rest|Controller)$/, '');
40
44
  }
41
45
 
@@ -84,11 +88,13 @@ export class SpecGenerator {
84
88
  /**
85
89
  * Get the type for a given class
86
90
  */
87
- #getType(field: FieldConfig | Class) {
88
- if (!Util.isPlainObject(field)) {
89
- field = { type: field } as FieldConfig;
91
+ #getType(fieldOrClass: FieldConfig | Class): Record<string, unknown> {
92
+ let field: { type: Class<unknown>, precision?: [number, number | undefined] };
93
+ if (!isFieldConfig(fieldOrClass)) {
94
+ field = { type: fieldOrClass };
95
+ } else {
96
+ field = fieldOrClass;
90
97
  }
91
- field = field as FieldConfig;
92
98
  const out: Record<string, unknown> = {};
93
99
  // Handle nested types
94
100
  if (SchemaRegistry.has(field.type)) {
@@ -138,7 +144,7 @@ export class SpecGenerator {
138
144
  /**
139
145
  * Process schema field
140
146
  */
141
- #processSchemaField(field: FieldConfig, required: string[]) {
147
+ #processSchemaField(field: FieldConfig, required: string[]): SchemaObject {
142
148
  let prop: SchemaObject = this.#getType(field);
143
149
 
144
150
  if (field.examples) {
@@ -155,10 +161,10 @@ export class SpecGenerator {
155
161
  prop.minLength = field.minlength.n;
156
162
  }
157
163
  if (field.min) {
158
- prop.minimum = field.min.n as number;
164
+ prop.minimum = typeof field.min.n === 'number' ? field.min.n : field.min.n.getTime();
159
165
  }
160
166
  if (field.max) {
161
- prop.maximum = field.max.n as number;
167
+ prop.maximum = typeof field.max.n === 'number' ? field.max.n : field.max.n.getTime();
162
168
  }
163
169
  if (field.enum) {
164
170
  prop.enum = field.enum.values;
@@ -184,7 +190,7 @@ export class SpecGenerator {
184
190
  /**
185
191
  * Process schema class
186
192
  */
187
- #processSchema(type?: string | Class) {
193
+ #processSchema(type?: string | Class): string | undefined {
188
194
  if (type === undefined || typeof type === 'string') {
189
195
  return undefined;
190
196
  }
@@ -231,13 +237,13 @@ export class SpecGenerator {
231
237
  } else if (body.type === Readable || body.type === Buffer) {
232
238
  return {
233
239
  content: {
234
- [mime ?? 'application/octect-stream']: { type: 'string', format: 'binary' }
240
+ [mime ?? 'application/octet-stream']: { type: 'string', format: 'binary' }
235
241
  },
236
242
  description: ''
237
243
  };
238
244
  } else {
239
245
  const typeId = this.#getTypeId(body.type);
240
- const typeRef = SchemaRegistry.has(body.type) ? this.#getType(body.type) : { type: body.type.name.toLowerCase() as 'string' };
246
+ const typeRef = SchemaRegistry.has(body.type) ? this.#getType(body.type) : { type: body.type.name.toLowerCase() };
241
247
  return {
242
248
  content: {
243
249
  [mime ?? 'application/json']: {
@@ -252,7 +258,11 @@ export class SpecGenerator {
252
258
  /**
253
259
  * Process endpoint parameter
254
260
  */
255
- #processEndpointParam(ep: EndpointConfig, param: ParamConfig, field: FieldConfig) {
261
+ #processEndpointParam(ep: EndpointConfig, param: ParamConfig, field: FieldConfig): (
262
+ { requestBody: RequestBodyObject } |
263
+ { parameters: ParameterObject[] } |
264
+ undefined
265
+ ) {
256
266
  if (param.location) {
257
267
  if (param.location === 'body') {
258
268
  return {
@@ -262,7 +272,7 @@ export class SpecGenerator {
262
272
  return { parameters: this.#schemaToDotParams(param.location, field) };
263
273
  } else if (param.location !== 'context') {
264
274
  const epParam: ParameterObject = {
265
- in: param.location as 'path',
275
+ in: param.location,
266
276
  name: param.name || param.location,
267
277
  description: field.description,
268
278
  required: !!field.required?.active || false,
@@ -278,7 +288,7 @@ export class SpecGenerator {
278
288
  /**
279
289
  * Process controller endpoint
280
290
  */
281
- processEndpoint(ctrl: ControllerConfig, ep: EndpointConfig) {
291
+ processEndpoint(ctrl: ControllerConfig, ep: EndpointConfig): void {
282
292
 
283
293
  const tagName = ctrl.class.name.replace(/(Rest|Controller)$/, '');
284
294
 
@@ -298,14 +308,17 @@ export class SpecGenerator {
298
308
  const schema = SchemaRegistry.getMethodSchema(ep.class, ep.handlerName);
299
309
  for (const field of schema) {
300
310
  const res = this.#processEndpointParam(ep, ep.params[field.index!], field);
301
- if (res?.parameters) {
302
- (op.parameters ??= []).push(...res.parameters);
311
+ if (res) {
312
+ if ('parameters' in res) {
313
+ (op.parameters ??= []).push(...res.parameters);
314
+ } else {
315
+ op.requestBody ??= res.requestBody;
316
+ }
303
317
  }
304
- op.requestBody ??= res?.requestBody;
305
318
  }
306
319
 
307
320
  const epPath = (
308
- !ep.path ? '/' : typeof ep.path === 'string' ? (ep.path as string) : (ep.path as RegExp).source
321
+ !ep.path ? '/' : typeof ep.path === 'string' ? ep.path : ep.path.source
309
322
  ).replace(/:([A-Za-z0-9_]+)\b/g, (__, param) => `{${param}}`);
310
323
 
311
324
  const key = `${ctrl.basePath}${epPath}`.replace(/[\/]+/g, '/');
@@ -324,7 +337,7 @@ export class SpecGenerator {
324
337
  /**
325
338
  * Process each controller
326
339
  */
327
- processController(cls: Class) {
340
+ processController(cls: Class): void {
328
341
  const ctrl = ControllerRegistry.get(cls);
329
342
 
330
343
  this.#tags.push({
@@ -340,7 +353,7 @@ export class SpecGenerator {
340
353
  /**
341
354
  * Generate full specification
342
355
  */
343
- generate(config: Partial<ApiSpecConfig> = {}) {
356
+ generate(config: Partial<ApiSpecConfig> = {}): Pick<OpenAPIObject, 'tags' | 'paths' | 'components'> {
344
357
 
345
358
  for (const cls of ControllerRegistry.getClasses()) {
346
359
  for (const ep of ControllerRegistry.get(cls).endpoints) {