@rsdk/cli.cmd.autodoc 6.0.0-next.7 → 6.0.0-next.9
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/CHANGELOG.md +382 -0
- package/TODO.MD +24 -0
- package/dist/commands/autodoc.cmd.js +8 -10
- package/dist/commands/autodoc.cmd.js.map +1 -1
- package/dist/commands/openapi.variation.js +1 -2
- package/dist/commands/openapi.variation.js.map +1 -1
- package/dist/generators/file-generator.interface.d.ts +1 -1
- package/dist/generators/openapi-json/config.schema.d.ts +16 -139
- package/dist/generators/openapi-json/config.schema.js +19 -128
- package/dist/generators/openapi-json/config.schema.js.map +1 -1
- package/dist/generators/openapi-json/openapi-json.generator.d.ts +4 -5
- package/dist/generators/openapi-json/openapi-json.generator.js +12 -109
- package/dist/generators/openapi-json/openapi-json.generator.js.map +1 -1
- package/dist/generators/openapi-json/schemas/config.schema.d.ts +55 -0
- package/dist/generators/openapi-json/schemas/config.schema.js +36 -0
- package/dist/generators/openapi-json/schemas/config.schema.js.map +1 -0
- package/dist/generators/openapi-json/schemas/index.d.ts +1 -0
- package/dist/generators/openapi-json/schemas/index.js +18 -0
- package/dist/generators/openapi-json/schemas/index.js.map +1 -0
- package/jest.config.e2e.js +1 -0
- package/jest.config.js +1 -0
- package/jest.config.unit.js +1 -0
- package/package.json +15 -32
- package/src/base/app.loader.ts +39 -0
- package/src/base/document.file.ts +33 -0
- package/src/base/index.ts +2 -0
- package/src/commands/autodoc.cmd.ts +93 -0
- package/src/commands/openapi.variation.ts +172 -0
- package/src/generators/autodoc-md/autodoc-md.generator.ts +56 -0
- package/src/generators/autodoc-md/formatters.ts +9 -0
- package/src/generators/autodoc-md/fragments/document-fragment.abstract.ts +6 -0
- package/src/generators/autodoc-md/fragments/extractor/document.extractor.ts +73 -0
- package/src/generators/autodoc-md/fragments/implementations/additional-sources.fragment.ts +41 -0
- package/src/generators/autodoc-md/fragments/implementations/app-metadata.fragment.ts +18 -0
- package/src/generators/autodoc-md/fragments/implementations/autodoc.fragment.ts +30 -0
- package/src/generators/autodoc-md/fragments/implementations/config-section.fragment.ts +148 -0
- package/src/generators/autodoc-md/fragments/implementations/header.fragment.ts +18 -0
- package/src/generators/autodoc-md/fragments/implementations/plugin.fragment.ts +21 -0
- package/src/generators/autodoc-md/fragments/implementations/transports.fragment.ts +36 -0
- package/src/generators/autodoc-md/fragments/index.ts +9 -0
- package/src/generators/autodoc-md/index.ts +1 -0
- package/src/generators/file-generator.interface.ts +22 -0
- package/src/generators/index.ts +4 -0
- package/src/generators/openapi-json/config.schema.spec.ts +265 -0
- package/src/generators/openapi-json/config.schema.ts +37 -0
- package/src/generators/openapi-json/index.ts +2 -0
- package/src/generators/openapi-json/openapi-json.generator.ts +53 -0
- package/src/generators/openapi-json/schemas/config.schema.spec.ts +265 -0
- package/src/generators/openapi-json/schemas/config.schema.ts +37 -0
- package/src/generators/openapi-json/schemas/index.ts +1 -0
- package/src/generators/rsdk-json/index.ts +1 -0
- package/src/generators/rsdk-json/rsdk-json.generator.ts +24 -0
- package/src/index.ts +1 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { DocumentNode } from '@rsdk/autodoc.protocol';
|
|
2
|
+
import { Composite, Table } from '@rsdk/autodoc.protocol';
|
|
3
|
+
import type {
|
|
4
|
+
PlatformAppMetadata,
|
|
5
|
+
SerializablePropertyMetadata,
|
|
6
|
+
SerializableSectionMetadata,
|
|
7
|
+
} from '@rsdk/core';
|
|
8
|
+
import { get, set } from 'lodash';
|
|
9
|
+
|
|
10
|
+
import { MdFormatter } from '../../formatters';
|
|
11
|
+
import { DocumentFragment } from '../document-fragment.abstract';
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* Этот тип представляет собой древовидную структуру, где каждый ключ может
|
|
15
|
+
* быть либо массивом SerializableSectionMetadata,
|
|
16
|
+
* либо другим GroupedSectionTree, что позволяет создавать вложенные секции в конфигурации.
|
|
17
|
+
*/
|
|
18
|
+
type GroupedSectionTree = {
|
|
19
|
+
[key in string]: SerializableSectionMetadata[] | GroupedSectionTree;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const APPLICATION_SECTION_NAME = 'Application';
|
|
23
|
+
|
|
24
|
+
export class ConfigSectionFragment extends DocumentFragment {
|
|
25
|
+
constructor(private metadata: PlatformAppMetadata) {
|
|
26
|
+
super();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private static getSectionNode(
|
|
30
|
+
section: SerializableSectionMetadata,
|
|
31
|
+
): Composite {
|
|
32
|
+
const content: (DocumentNode | string)[] = [];
|
|
33
|
+
|
|
34
|
+
if (section.description) {
|
|
35
|
+
content.push(section.description);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
content.push(
|
|
39
|
+
new Table(
|
|
40
|
+
section.properties.map((property) => ({
|
|
41
|
+
Name: property.expectedInEnv
|
|
42
|
+
? MdFormatter.bold(property.key)
|
|
43
|
+
: property.key,
|
|
44
|
+
Type: property.parser.type,
|
|
45
|
+
'Default value': `${property.defaultValue}`,
|
|
46
|
+
Description: property.description,
|
|
47
|
+
})),
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const nodeTitle = section.name
|
|
52
|
+
? `${section.name} (${MdFormatter.italic(section.constructorName)})`
|
|
53
|
+
: section.constructorName;
|
|
54
|
+
|
|
55
|
+
return new Composite(content, nodeTitle);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private static getSectionNodeTree(
|
|
59
|
+
groupedSectionTree: GroupedSectionTree,
|
|
60
|
+
): Composite[] {
|
|
61
|
+
const sectionNodes: Composite[] = [];
|
|
62
|
+
|
|
63
|
+
for (const [key, value] of Object.entries(groupedSectionTree)) {
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
sectionNodes.push(
|
|
66
|
+
new Composite(
|
|
67
|
+
value.map((section) =>
|
|
68
|
+
ConfigSectionFragment.getSectionNode(section),
|
|
69
|
+
),
|
|
70
|
+
key,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
sectionNodes.push(new Composite(this.getSectionNodeTree(value), key));
|
|
76
|
+
}
|
|
77
|
+
return sectionNodes;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getDocumentNode(): Composite | null {
|
|
81
|
+
const {
|
|
82
|
+
config: { properties },
|
|
83
|
+
} = this.metadata;
|
|
84
|
+
|
|
85
|
+
const title = 'Configuration properties';
|
|
86
|
+
|
|
87
|
+
const printProperty = (meta: SerializablePropertyMetadata): Composite => {
|
|
88
|
+
const { key, defaultValue, description, parser, expectedInEnv } = meta;
|
|
89
|
+
const fragments: string[] = [];
|
|
90
|
+
|
|
91
|
+
fragments.push(
|
|
92
|
+
`- ${key} ${description}`,
|
|
93
|
+
`> Parser: **${parser.type}** ${parser.description}`,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if ('defaultValue' in meta) {
|
|
97
|
+
fragments.push(`> Default value: ${defaultValue}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (expectedInEnv) {
|
|
101
|
+
fragments.push(`> Should be in environment!`);
|
|
102
|
+
}
|
|
103
|
+
fragments.push('');
|
|
104
|
+
return new Composite(fragments.join('\n'));
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const sectionsNode = this.getSectionsNode();
|
|
108
|
+
|
|
109
|
+
const content = [sectionsNode];
|
|
110
|
+
|
|
111
|
+
if (properties.length > 0) {
|
|
112
|
+
const title = `Unbound properties`;
|
|
113
|
+
const propertyNodeContent: DocumentNode[] = [];
|
|
114
|
+
const propertiesNode = new Composite(propertyNodeContent, title);
|
|
115
|
+
|
|
116
|
+
for (const propMeta of properties) {
|
|
117
|
+
const propertyNode = printProperty(propMeta);
|
|
118
|
+
|
|
119
|
+
propertyNodeContent.push(propertyNode);
|
|
120
|
+
}
|
|
121
|
+
content.push(propertiesNode);
|
|
122
|
+
}
|
|
123
|
+
return new Composite(content, title);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private getSectionsNode(): Composite {
|
|
127
|
+
const groupByTags: { [key in string]: SerializableSectionMetadata[] } = {};
|
|
128
|
+
|
|
129
|
+
for (const section of this.metadata.config.sections) {
|
|
130
|
+
if (section.tags?.length) {
|
|
131
|
+
const existed = get(groupByTags, section.tags.join('.'), [section]);
|
|
132
|
+
|
|
133
|
+
set(groupByTags, section.tags.join('.'), existed);
|
|
134
|
+
} else {
|
|
135
|
+
groupByTags[APPLICATION_SECTION_NAME] ??= <
|
|
136
|
+
SerializableSectionMetadata[]
|
|
137
|
+
>[];
|
|
138
|
+
|
|
139
|
+
groupByTags[APPLICATION_SECTION_NAME].push(section);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const content: DocumentNode[] =
|
|
144
|
+
ConfigSectionFragment.getSectionNodeTree(groupByTags);
|
|
145
|
+
|
|
146
|
+
return new Composite(content, 'Sections');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DocumentNode } from '@rsdk/autodoc.protocol';
|
|
2
|
+
import { Composite } from '@rsdk/autodoc.protocol';
|
|
3
|
+
import type { PlatformApp } from '@rsdk/core';
|
|
4
|
+
|
|
5
|
+
import type { DocumentFragment } from '../document-fragment.abstract';
|
|
6
|
+
|
|
7
|
+
export class HeaderFragment implements DocumentFragment {
|
|
8
|
+
constructor(readonly app: PlatformApp) {}
|
|
9
|
+
|
|
10
|
+
async getDocumentNode(): Promise<DocumentNode[] | DocumentNode | null> {
|
|
11
|
+
const metadata = await this.app.getMetadata();
|
|
12
|
+
const appVersion = metadata.version;
|
|
13
|
+
|
|
14
|
+
const headerFragments = [`version: ${appVersion}`];
|
|
15
|
+
|
|
16
|
+
return new Composite(headerFragments);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DocumentNode } from '@rsdk/autodoc.protocol';
|
|
2
|
+
import { Composite, List } from '@rsdk/autodoc.protocol';
|
|
3
|
+
import type { PlatformAppPlugin } from '@rsdk/core';
|
|
4
|
+
|
|
5
|
+
import { DocumentFragment } from '../document-fragment.abstract';
|
|
6
|
+
|
|
7
|
+
export class PluginFragment extends DocumentFragment {
|
|
8
|
+
constructor(private plugins?: PlatformAppPlugin[]) {
|
|
9
|
+
super();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getDocumentNode(): DocumentNode | null {
|
|
13
|
+
if (!this.plugins?.length) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return new Composite(
|
|
17
|
+
new List(this.plugins?.map((plugin) => plugin.constructor.name)),
|
|
18
|
+
'Plugins',
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Composite, List } from '@rsdk/autodoc.protocol';
|
|
2
|
+
import type { ITransport } from '@rsdk/core';
|
|
3
|
+
|
|
4
|
+
import { DocumentFragment } from '../document-fragment.abstract';
|
|
5
|
+
|
|
6
|
+
export class TransportsFragment extends DocumentFragment {
|
|
7
|
+
constructor(private transports?: ITransport[]) {
|
|
8
|
+
super();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
getContent(): string {
|
|
12
|
+
if (!this.transports?.length) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
const fragments: string[] = [];
|
|
16
|
+
|
|
17
|
+
fragments.push('## Transports', '\n');
|
|
18
|
+
for (const transport of this.transports) {
|
|
19
|
+
fragments.push('- ' + transport.getProtocol());
|
|
20
|
+
}
|
|
21
|
+
return fragments.join('\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getDocumentNode(): Composite | null {
|
|
25
|
+
if (!this.transports?.length) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const transportNode = new Composite(
|
|
30
|
+
new List(this.transports.map((tr) => tr.getProtocol())),
|
|
31
|
+
'Transports',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return transportNode;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './implementations/config-section.fragment';
|
|
2
|
+
export * from './implementations/app-metadata.fragment';
|
|
3
|
+
export * from './implementations/config-section.fragment';
|
|
4
|
+
export * from './implementations/transports.fragment';
|
|
5
|
+
export * from './implementations/autodoc.fragment';
|
|
6
|
+
export * from './implementations/plugin.fragment';
|
|
7
|
+
export * from './implementations/additional-sources.fragment';
|
|
8
|
+
export * from './implementations/header.fragment';
|
|
9
|
+
export * from './document-fragment.abstract';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './autodoc-md.generator';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ILogger } from '@rsdk/logging';
|
|
2
|
+
|
|
3
|
+
import type { DocumentFile } from '../base';
|
|
4
|
+
|
|
5
|
+
export abstract class FileGenerator {
|
|
6
|
+
constructor(protected readonly logger: ILogger) {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Принимает имя файла и возвращает абстракцию файла
|
|
10
|
+
* @param filename - Имя файла
|
|
11
|
+
* @returns {DocumentFile} Объект файла
|
|
12
|
+
*/
|
|
13
|
+
async createFile(filename: string): Promise<DocumentFile> {
|
|
14
|
+
this.logger.info(`started generating ${filename}`);
|
|
15
|
+
const content = await this.getContent();
|
|
16
|
+
|
|
17
|
+
this.logger.info(`finished generating ${filename}`);
|
|
18
|
+
return { name: filename, content };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected abstract getContent(): Promise<string>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { TypeBoxValidator } from '@rsdk/common';
|
|
2
|
+
import { parse } from 'yaml';
|
|
3
|
+
|
|
4
|
+
import { OpenApiConfigSchema } from './config.schema';
|
|
5
|
+
|
|
6
|
+
describe('OpenAPI Config Schema', () => {
|
|
7
|
+
const validator = new TypeBoxValidator(OpenApiConfigSchema);
|
|
8
|
+
|
|
9
|
+
it('should validate minimal config', () => {
|
|
10
|
+
const config = parse(`
|
|
11
|
+
out: openapi.json
|
|
12
|
+
title: Test API
|
|
13
|
+
version: 1.0.0
|
|
14
|
+
description: Test API Description
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
const { isValid, errors } = validator.check(config);
|
|
18
|
+
|
|
19
|
+
expect(isValid).toBe(true);
|
|
20
|
+
expect(errors).toHaveLength(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should validate minimal config', () => {
|
|
24
|
+
const config = parse(`
|
|
25
|
+
out: openapi.json
|
|
26
|
+
title: Test API
|
|
27
|
+
version: '1.0.0'
|
|
28
|
+
description: Test API Description
|
|
29
|
+
`);
|
|
30
|
+
|
|
31
|
+
const { isValid, errors } = validator.check(config);
|
|
32
|
+
|
|
33
|
+
expect(isValid).toBe(true);
|
|
34
|
+
expect(errors).toHaveLength(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should validate config with zones', () => {
|
|
38
|
+
const config = parse(`
|
|
39
|
+
out: openapi.json
|
|
40
|
+
title: Test API
|
|
41
|
+
version: '1.0.0'
|
|
42
|
+
description: Test API Description
|
|
43
|
+
includeZones:
|
|
44
|
+
- public
|
|
45
|
+
- api:web
|
|
46
|
+
excludeZones:
|
|
47
|
+
- __internal
|
|
48
|
+
`);
|
|
49
|
+
|
|
50
|
+
const { isValid, errors } = validator.check(config);
|
|
51
|
+
|
|
52
|
+
expect(isValid).toBe(true);
|
|
53
|
+
expect(errors).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should validate config without optional fields', () => {
|
|
57
|
+
const config = parse(`
|
|
58
|
+
out: openapi.json
|
|
59
|
+
version: '1.0.0'
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
const { isValid, errors } = validator.check(config);
|
|
63
|
+
|
|
64
|
+
expect(isValid).toBe(true);
|
|
65
|
+
expect(errors).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('Security Schemes', () => {
|
|
69
|
+
it('should validate API Key auth', () => {
|
|
70
|
+
const config = parse(`
|
|
71
|
+
out: openapi.json
|
|
72
|
+
title: Test API
|
|
73
|
+
version: '1.0.0'
|
|
74
|
+
description: Test API Description
|
|
75
|
+
security:
|
|
76
|
+
ApiKeyAuth:
|
|
77
|
+
type: apiKey
|
|
78
|
+
in: header
|
|
79
|
+
name: x-api-key
|
|
80
|
+
description: API Key Authentication
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
const { isValid, errors } = validator.check(config);
|
|
84
|
+
|
|
85
|
+
expect(isValid).toBe(true);
|
|
86
|
+
expect(errors).toHaveLength(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should validate Bearer auth', () => {
|
|
90
|
+
const config = parse(`
|
|
91
|
+
out: openapi.json
|
|
92
|
+
title: Test API
|
|
93
|
+
version: '1.0.0'
|
|
94
|
+
description: Test API Description
|
|
95
|
+
security:
|
|
96
|
+
BearerAuth:
|
|
97
|
+
type: http
|
|
98
|
+
scheme: bearer
|
|
99
|
+
bearerFormat: JWT
|
|
100
|
+
description: JWT Bearer Authentication
|
|
101
|
+
`);
|
|
102
|
+
|
|
103
|
+
const { isValid, errors } = validator.check(config);
|
|
104
|
+
|
|
105
|
+
expect(isValid).toBe(true);
|
|
106
|
+
expect(errors).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should validate Basic auth', () => {
|
|
110
|
+
const config = parse(`
|
|
111
|
+
out: openapi.json
|
|
112
|
+
title: Test API
|
|
113
|
+
version: '1.0.0'
|
|
114
|
+
description: Test API Description
|
|
115
|
+
security:
|
|
116
|
+
BasicAuth:
|
|
117
|
+
type: http
|
|
118
|
+
scheme: basic
|
|
119
|
+
description: Basic Authentication
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
const { isValid, errors } = validator.check(config);
|
|
123
|
+
|
|
124
|
+
expect(isValid).toBe(true);
|
|
125
|
+
expect(errors).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should validate OAuth2 auth', () => {
|
|
129
|
+
const config = parse(`
|
|
130
|
+
out: openapi.json
|
|
131
|
+
title: Test API
|
|
132
|
+
version: '1.0.0'
|
|
133
|
+
description: Test API Description
|
|
134
|
+
security:
|
|
135
|
+
OAuth2Auth:
|
|
136
|
+
type: oauth2
|
|
137
|
+
flows:
|
|
138
|
+
implicit:
|
|
139
|
+
authorizationUrl: https://auth.example.com/oauth/authorize
|
|
140
|
+
scopes:
|
|
141
|
+
read: Read access
|
|
142
|
+
write: Write access
|
|
143
|
+
`);
|
|
144
|
+
|
|
145
|
+
const { isValid, errors } = validator.check(config);
|
|
146
|
+
|
|
147
|
+
for (const error of errors) {
|
|
148
|
+
console.log(error);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
expect(isValid).toBe(true);
|
|
152
|
+
expect(errors).toHaveLength(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should validate multiple security schemes', () => {
|
|
156
|
+
const config = parse(`
|
|
157
|
+
out: openapi.json
|
|
158
|
+
title: Test API
|
|
159
|
+
version: '1.0.0'
|
|
160
|
+
description: Test API Description
|
|
161
|
+
security:
|
|
162
|
+
BearerAuth:
|
|
163
|
+
type: http
|
|
164
|
+
scheme: bearer
|
|
165
|
+
bearerFormat: JWT
|
|
166
|
+
description: JWT Bearer Authentication
|
|
167
|
+
|
|
168
|
+
ApiKeyAuth:
|
|
169
|
+
type: apiKey
|
|
170
|
+
in: header
|
|
171
|
+
name: x-api-key
|
|
172
|
+
description: API Key Authentication
|
|
173
|
+
|
|
174
|
+
OAuth2Auth:
|
|
175
|
+
type: oauth2
|
|
176
|
+
description: OAuth2 Authentication
|
|
177
|
+
flows:
|
|
178
|
+
implicit:
|
|
179
|
+
authorizationUrl: https://auth.example.com/oauth/authorize
|
|
180
|
+
scopes:
|
|
181
|
+
read: Read access
|
|
182
|
+
write: Write access
|
|
183
|
+
`);
|
|
184
|
+
|
|
185
|
+
const { isValid, errors } = validator.check(config);
|
|
186
|
+
|
|
187
|
+
expect(isValid).toBe(true);
|
|
188
|
+
expect(errors).toHaveLength(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should validate cookie-based auth', () => {
|
|
192
|
+
const config = parse(`
|
|
193
|
+
out: openapi/openapi.web.json
|
|
194
|
+
includeZones:
|
|
195
|
+
- api:web
|
|
196
|
+
title: Web API
|
|
197
|
+
version: '1.3'
|
|
198
|
+
description: Web API methods
|
|
199
|
+
security:
|
|
200
|
+
SessionCookie:
|
|
201
|
+
type: apiKey
|
|
202
|
+
in: cookie
|
|
203
|
+
name: sessionId
|
|
204
|
+
description: Cookie-based authentication
|
|
205
|
+
`);
|
|
206
|
+
|
|
207
|
+
const { isValid, errors } = validator.check(config);
|
|
208
|
+
|
|
209
|
+
expect(isValid).toBe(true);
|
|
210
|
+
expect(errors).toHaveLength(0);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('Validation Errors', () => {
|
|
215
|
+
it('should fail on invalid security type', () => {
|
|
216
|
+
const config = parse(`
|
|
217
|
+
out: openapi.json
|
|
218
|
+
title: Test API
|
|
219
|
+
version: 1.0.0
|
|
220
|
+
description: Test API Description
|
|
221
|
+
security:
|
|
222
|
+
InvalidAuth:
|
|
223
|
+
type: invalid
|
|
224
|
+
`);
|
|
225
|
+
|
|
226
|
+
const { isValid, errors } = validator.check(config);
|
|
227
|
+
|
|
228
|
+
expect(isValid).toBe(false);
|
|
229
|
+
expect(errors).toHaveLength(2);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should fail on missing required fields', () => {
|
|
233
|
+
const config = parse(`
|
|
234
|
+
out: openapi.json
|
|
235
|
+
title: Test API
|
|
236
|
+
description: Test API Description
|
|
237
|
+
`);
|
|
238
|
+
|
|
239
|
+
const { isValid, errors } = validator.check(config);
|
|
240
|
+
|
|
241
|
+
expect(isValid).toBe(true);
|
|
242
|
+
expect(errors).toHaveLength(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should fail on invalid OAuth2 flow configuration', () => {
|
|
246
|
+
const config = parse(`
|
|
247
|
+
out: openapi.json
|
|
248
|
+
title: Test API
|
|
249
|
+
version: 1.0.0
|
|
250
|
+
description: Test API Description
|
|
251
|
+
security:
|
|
252
|
+
- type: oauth2
|
|
253
|
+
schemaName: OAuth2Auth
|
|
254
|
+
flows:
|
|
255
|
+
implicit:
|
|
256
|
+
scopes: invalid
|
|
257
|
+
`);
|
|
258
|
+
|
|
259
|
+
const { isValid, errors } = validator.check(config);
|
|
260
|
+
|
|
261
|
+
expect(isValid).toBe(false);
|
|
262
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { OpenApiOptions } from '@rsdk/http.openapi';
|
|
2
|
+
import type { Static } from '@sinclair/typebox';
|
|
3
|
+
import { Type } from '@sinclair/typebox';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration schema for OpenAPI document generation.
|
|
7
|
+
* @example
|
|
8
|
+
* {
|
|
9
|
+
* "out": "openapi.json",
|
|
10
|
+
* "app": "./dist/app.js",
|
|
11
|
+
* "title": "My API",
|
|
12
|
+
* "version": "1.0.0",
|
|
13
|
+
* "description": "API Description",
|
|
14
|
+
* "includeZones": ["public"],
|
|
15
|
+
* "excludeZones": ["internal"],
|
|
16
|
+
* "security": {
|
|
17
|
+
* "jwt": {
|
|
18
|
+
* "type": "http",
|
|
19
|
+
* "scheme": "bearer",
|
|
20
|
+
* "bearerFormat": "JWT",
|
|
21
|
+
* "name": "JWT Auth"
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
export const OpenApiConfigSchema = Type.Intersect([
|
|
27
|
+
Type.Partial(OpenApiOptions),
|
|
28
|
+
Type.Object({
|
|
29
|
+
/** Output file path for the generated OpenAPI document */
|
|
30
|
+
out: Type.Optional(Type.String()),
|
|
31
|
+
|
|
32
|
+
/** Path to the application file (usually `dist/app.js`) */
|
|
33
|
+
app: Type.Optional(Type.String()),
|
|
34
|
+
}),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export type OpenApiConfigSchema = Static<typeof OpenApiConfigSchema>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PlatformApp, PlatformContext } from '@rsdk/core';
|
|
2
|
+
import { OpenApiGenerator } from '@rsdk/http.openapi';
|
|
3
|
+
import { LoggerFactory } from '@rsdk/logging';
|
|
4
|
+
|
|
5
|
+
import { FileGenerator } from '../file-generator.interface';
|
|
6
|
+
|
|
7
|
+
import type { OpenApiConfigSchema } from './config.schema';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Here we don't need `out` and `app`,
|
|
11
|
+
* but `title`, `version` and `description` are required.
|
|
12
|
+
*/
|
|
13
|
+
export type OpenApiGeneratorOptions = Omit<OpenApiConfigSchema, 'out' | 'app'> &
|
|
14
|
+
Required<Pick<OpenApiConfigSchema, 'title' | 'version' | 'description'>>;
|
|
15
|
+
|
|
16
|
+
export class OpenApiFileGenerator extends FileGenerator {
|
|
17
|
+
private readonly context: PlatformContext;
|
|
18
|
+
private readonly options: Partial<OpenApiGeneratorOptions>;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
app: PlatformApp,
|
|
22
|
+
options: Partial<OpenApiGeneratorOptions> = {},
|
|
23
|
+
) {
|
|
24
|
+
super(LoggerFactory.create(OpenApiFileGenerator));
|
|
25
|
+
this.context = app.context;
|
|
26
|
+
this.options = options;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected async getContent(): Promise<string> {
|
|
30
|
+
this.logger.info('Generate openapi starting');
|
|
31
|
+
|
|
32
|
+
const options = await this.resolveOptions(this.context, this.options);
|
|
33
|
+
const generator = new OpenApiGenerator(this.context, options);
|
|
34
|
+
const document = await generator.generate();
|
|
35
|
+
|
|
36
|
+
this.logger.info('Generate openapi finished');
|
|
37
|
+
return JSON.stringify(document, null, 2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async resolveOptions(
|
|
41
|
+
context: PlatformContext,
|
|
42
|
+
passed: Partial<OpenApiGeneratorOptions>,
|
|
43
|
+
): Promise<OpenApiGeneratorOptions> {
|
|
44
|
+
const appMetadata = await context.getMetadata();
|
|
45
|
+
const defaults: OpenApiGeneratorOptions = {
|
|
46
|
+
title: appMetadata.name,
|
|
47
|
+
version: appMetadata.version,
|
|
48
|
+
description: appMetadata.description,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return { ...defaults, ...passed };
|
|
52
|
+
}
|
|
53
|
+
}
|