@nocobase/ctl 0.1.5
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 +164 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +14 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +20 -0
- package/dist/commands/env/add.js +51 -0
- package/dist/commands/env/index.js +27 -0
- package/dist/commands/env/list.js +31 -0
- package/dist/commands/env/remove.js +54 -0
- package/dist/commands/env/update.js +54 -0
- package/dist/commands/env/use.js +26 -0
- package/dist/commands/resource/create.js +15 -0
- package/dist/commands/resource/destroy.js +15 -0
- package/dist/commands/resource/get.js +15 -0
- package/dist/commands/resource/index.js +7 -0
- package/dist/commands/resource/list.js +16 -0
- package/dist/commands/resource/query.js +15 -0
- package/dist/commands/resource/update.js +15 -0
- package/dist/generated/command-registry.js +81 -0
- package/dist/lib/api-client.js +196 -0
- package/dist/lib/auth-store.js +92 -0
- package/dist/lib/bootstrap.js +263 -0
- package/dist/lib/build-config.js +10 -0
- package/dist/lib/cli-home.js +30 -0
- package/dist/lib/generated-command.js +113 -0
- package/dist/lib/naming.js +70 -0
- package/dist/lib/openapi.js +254 -0
- package/dist/lib/post-processors.js +23 -0
- package/dist/lib/resource-command.js +331 -0
- package/dist/lib/resource-request.js +103 -0
- package/dist/lib/runtime-generator.js +383 -0
- package/dist/lib/runtime-store.js +56 -0
- package/dist/lib/ui.js +154 -0
- package/dist/post-processors/data-modeling.js +66 -0
- package/dist/post-processors/data-source-manager.js +114 -0
- package/dist/post-processors/index.js +19 -0
- package/nocobase-ctl.config.json +327 -0
- package/package.json +61 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { executeApiRequest } from "./api-client.js";
|
|
3
|
+
import { applyPostProcessor } from "./post-processors.js";
|
|
4
|
+
import { registerPostProcessors } from "../post-processors/index.js";
|
|
5
|
+
function buildParameterFlag(parameter) {
|
|
6
|
+
const hints = [parameter.in];
|
|
7
|
+
if (parameter.type === 'object' || parameter.type === 'array' || parameter.jsonEncoded) {
|
|
8
|
+
hints.push('JSON');
|
|
9
|
+
}
|
|
10
|
+
else if (parameter.isArray) {
|
|
11
|
+
hints.push('repeatable');
|
|
12
|
+
}
|
|
13
|
+
else if (parameter.type) {
|
|
14
|
+
hints.push(parameter.type);
|
|
15
|
+
}
|
|
16
|
+
const description = [
|
|
17
|
+
`${parameter.description ?? ''}${parameter.description ? ' ' : ''}[${hints.join(', ')}]`.trim(),
|
|
18
|
+
parameter.jsonShape ? `Shape: ${parameter.jsonShape}` : undefined,
|
|
19
|
+
]
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join('\n');
|
|
22
|
+
if (parameter.type === 'boolean') {
|
|
23
|
+
return Flags.boolean({
|
|
24
|
+
description,
|
|
25
|
+
...(parameter.required ? { required: true } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (parameter.isArray && !parameter.jsonEncoded) {
|
|
29
|
+
return Flags.string({
|
|
30
|
+
description,
|
|
31
|
+
multiple: true,
|
|
32
|
+
...(parameter.required ? { required: true } : {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return Flags.string({
|
|
36
|
+
description,
|
|
37
|
+
...(parameter.required ? { required: true } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function createGeneratedFlags(operation) {
|
|
41
|
+
const flags = {
|
|
42
|
+
'base-url': Flags.string({
|
|
43
|
+
description: 'NocoBase API base URL, for example http://localhost:13000/api',
|
|
44
|
+
}),
|
|
45
|
+
verbose: Flags.boolean({
|
|
46
|
+
description: 'Show detailed progress output',
|
|
47
|
+
default: false,
|
|
48
|
+
}),
|
|
49
|
+
env: Flags.string({
|
|
50
|
+
char: 'e',
|
|
51
|
+
description: 'Environment name',
|
|
52
|
+
}),
|
|
53
|
+
token: Flags.string({
|
|
54
|
+
char: 't',
|
|
55
|
+
description: 'Bearer token override',
|
|
56
|
+
}),
|
|
57
|
+
'json-output': Flags.boolean({
|
|
58
|
+
char: 'j',
|
|
59
|
+
description: 'Print raw JSON response',
|
|
60
|
+
default: true,
|
|
61
|
+
allowNo: true,
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
for (const parameter of operation.parameters) {
|
|
65
|
+
flags[parameter.flagName] = buildParameterFlag(parameter);
|
|
66
|
+
}
|
|
67
|
+
if (operation.hasBody) {
|
|
68
|
+
flags.body = Flags.string({
|
|
69
|
+
description: 'JSON request body string',
|
|
70
|
+
required: operation.bodyRequired,
|
|
71
|
+
exclusive: ['body-file'],
|
|
72
|
+
});
|
|
73
|
+
flags['body-file'] = Flags.string({
|
|
74
|
+
description: 'Path to JSON request body file',
|
|
75
|
+
required: false,
|
|
76
|
+
exclusive: ['body'],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return flags;
|
|
80
|
+
}
|
|
81
|
+
export class GeneratedApiCommand extends Command {
|
|
82
|
+
static operation;
|
|
83
|
+
async run() {
|
|
84
|
+
registerPostProcessors();
|
|
85
|
+
const ctor = this.constructor;
|
|
86
|
+
const { flags } = await this.parse(ctor);
|
|
87
|
+
const response = await executeApiRequest({
|
|
88
|
+
envName: flags.env,
|
|
89
|
+
baseUrl: flags['base-url'],
|
|
90
|
+
token: flags.token,
|
|
91
|
+
flags,
|
|
92
|
+
operation: {
|
|
93
|
+
method: ctor.operation.method,
|
|
94
|
+
pathTemplate: ctor.operation.pathTemplate,
|
|
95
|
+
parameters: ctor.operation.parameters,
|
|
96
|
+
hasBody: ctor.operation.hasBody,
|
|
97
|
+
bodyRequired: ctor.operation.bodyRequired,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
this.error(`Request failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`);
|
|
102
|
+
}
|
|
103
|
+
const processedData = await applyPostProcessor(response.data, {
|
|
104
|
+
flags,
|
|
105
|
+
operation: ctor.operation,
|
|
106
|
+
});
|
|
107
|
+
if (flags['json-output']) {
|
|
108
|
+
this.log(JSON.stringify(processedData, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.log(`HTTP ${response.status}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function toKebabCase(value) {
|
|
3
|
+
return value
|
|
4
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
5
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
6
|
+
.replace(/-+/g, '-')
|
|
7
|
+
.replace(/^-|-$/g, '')
|
|
8
|
+
.toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
export function splitPathAction(pathTemplate) {
|
|
11
|
+
const normalizedPath = pathTemplate.replace(/^\/+/, '');
|
|
12
|
+
const separatorIndex = normalizedPath.lastIndexOf(':');
|
|
13
|
+
if (separatorIndex === -1) {
|
|
14
|
+
return {
|
|
15
|
+
resourcePath: normalizedPath,
|
|
16
|
+
action: 'call',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
resourcePath: normalizedPath.slice(0, separatorIndex),
|
|
21
|
+
action: normalizedPath.slice(separatorIndex + 1),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function toLogicalResourceName(pathTemplate) {
|
|
25
|
+
const { resourcePath } = splitPathAction(pathTemplate);
|
|
26
|
+
return resourcePath
|
|
27
|
+
.split('/')
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.filter((segment) => !segment.startsWith('{'))
|
|
30
|
+
.map((segment) => toKebabCase(segment))
|
|
31
|
+
.join('.');
|
|
32
|
+
}
|
|
33
|
+
export function toLogicalActionName(pathTemplate) {
|
|
34
|
+
return toKebabCase(splitPathAction(pathTemplate).action);
|
|
35
|
+
}
|
|
36
|
+
export function toResourceSegments(pathTemplate, options) {
|
|
37
|
+
const { resourcePath, action } = splitPathAction(pathTemplate);
|
|
38
|
+
const pathSegments = resourcePath
|
|
39
|
+
.split('/')
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.flatMap((segment) => {
|
|
42
|
+
if (!segment.startsWith('{')) {
|
|
43
|
+
return [toKebabCase(segment)];
|
|
44
|
+
}
|
|
45
|
+
if (!options?.includeParams) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return [`by-${toKebabCase(segment.slice(1, -1))}`];
|
|
49
|
+
});
|
|
50
|
+
return [...pathSegments, toKebabCase(action)].filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
export function toCommandSegments(moduleName, pathTemplate, options) {
|
|
53
|
+
const resourceSegments = toResourceSegments(pathTemplate, options);
|
|
54
|
+
const segments = [options?.omitModule ? '' : toKebabCase(moduleName), ...resourceSegments].filter(Boolean);
|
|
55
|
+
return segments.length ? segments : [toKebabCase(moduleName), 'call'];
|
|
56
|
+
}
|
|
57
|
+
export function toClassName(segments) {
|
|
58
|
+
return segments
|
|
59
|
+
.map((segment) => segment.replace(/(^\w|-\w)/g, (token) => token.replace('-', '').toUpperCase()))
|
|
60
|
+
.join('');
|
|
61
|
+
}
|
|
62
|
+
export function toOutputFile(outputRoot, segments) {
|
|
63
|
+
const folder = path.join(outputRoot, ...segments.slice(0, -1));
|
|
64
|
+
const filePath = path.join(folder, `${segments.at(-1)}.ts`);
|
|
65
|
+
return filePath;
|
|
66
|
+
}
|
|
67
|
+
export function toImportPath(fromFile, targetFile) {
|
|
68
|
+
const relative = path.relative(path.dirname(fromFile), targetFile).replace(/\\/g, '/');
|
|
69
|
+
return relative.startsWith('.') ? relative : `./${relative}`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import SwaggerParser from '@apidevtools/swagger-parser';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
const SWAGGER_FILENAMES = new Set(['index.json', 'index.ts', 'swagger.json']);
|
|
7
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
|
|
8
|
+
function isPluginSwaggerSource(filePath) {
|
|
9
|
+
return (filePath.includes(`${path.sep}packages${path.sep}plugins${path.sep}`) &&
|
|
10
|
+
filePath.includes(`${path.sep}src${path.sep}swagger${path.sep}`));
|
|
11
|
+
}
|
|
12
|
+
function isCoreSwaggerSource(filePath) {
|
|
13
|
+
return filePath.includes(`${path.sep}packages${path.sep}core${path.sep}server${path.sep}src${path.sep}swagger${path.sep}`);
|
|
14
|
+
}
|
|
15
|
+
function toModuleName(filePath) {
|
|
16
|
+
if (isCoreSwaggerSource(filePath)) {
|
|
17
|
+
return 'core';
|
|
18
|
+
}
|
|
19
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
20
|
+
const pluginMatch = normalized.match(/packages\/plugins\/@nocobase\/plugin-([^/]+)\//);
|
|
21
|
+
return pluginMatch?.[1] ?? '';
|
|
22
|
+
}
|
|
23
|
+
function toPackageFile(filePath) {
|
|
24
|
+
if (isCoreSwaggerSource(filePath)) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
28
|
+
const match = normalized.match(/^(.*\/packages\/plugins\/@nocobase\/plugin-[^/]+)\//);
|
|
29
|
+
return match ? `${match[1]}/package.json` : '';
|
|
30
|
+
}
|
|
31
|
+
function toPackageName(filePath) {
|
|
32
|
+
if (isCoreSwaggerSource(filePath)) {
|
|
33
|
+
return '@nocobase/server';
|
|
34
|
+
}
|
|
35
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
36
|
+
const match = normalized.match(/packages\/plugins\/(@nocobase\/plugin-[^/]+)\//);
|
|
37
|
+
return match?.[1] ?? '';
|
|
38
|
+
}
|
|
39
|
+
async function walk(dir, result) {
|
|
40
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const fullPath = path.join(dir, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
await walk(fullPath, result);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!SWAGGER_FILENAMES.has(entry.name)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!isPluginSwaggerSource(fullPath) && !isCoreSwaggerSource(fullPath)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
result.push(fullPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function discoverSwaggerSources(sourceRoot) {
|
|
57
|
+
const files = [];
|
|
58
|
+
await walk(path.join(sourceRoot, 'packages'), files);
|
|
59
|
+
return files
|
|
60
|
+
.sort()
|
|
61
|
+
.map((sourceFile) => ({
|
|
62
|
+
moduleName: toModuleName(sourceFile),
|
|
63
|
+
sourceFile,
|
|
64
|
+
sourceId: path.relative(sourceRoot, sourceFile).replace(/\\/g, '/'),
|
|
65
|
+
packageFile: toPackageFile(sourceFile) || undefined,
|
|
66
|
+
packageName: toPackageName(sourceFile) || undefined,
|
|
67
|
+
format: sourceFile.endsWith('.json') ? 'json' : 'ts',
|
|
68
|
+
}))
|
|
69
|
+
.filter((item) => item.moduleName);
|
|
70
|
+
}
|
|
71
|
+
function getPropertyName(name) {
|
|
72
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
73
|
+
return name.text;
|
|
74
|
+
}
|
|
75
|
+
if (ts.isComputedPropertyName(name) && ts.isStringLiteral(name.expression)) {
|
|
76
|
+
return name.expression.text;
|
|
77
|
+
}
|
|
78
|
+
throw new Error('Unsupported computed property in swagger object.');
|
|
79
|
+
}
|
|
80
|
+
function evaluateExpression(node) {
|
|
81
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
82
|
+
const value = {};
|
|
83
|
+
for (const property of node.properties) {
|
|
84
|
+
if (ts.isPropertyAssignment(property)) {
|
|
85
|
+
value[getPropertyName(property.name)] = evaluateExpression(property.initializer);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
89
|
+
throw new Error(`Unsupported shorthand property "${property.name.text}" in swagger object.`);
|
|
90
|
+
}
|
|
91
|
+
if (ts.isSpreadAssignment(property)) {
|
|
92
|
+
throw new Error('Unsupported spread assignment in swagger object.');
|
|
93
|
+
}
|
|
94
|
+
if (ts.isMethodDeclaration(property) || ts.isAccessor(property)) {
|
|
95
|
+
throw new Error('Unsupported method/accessor in swagger object.');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
101
|
+
return node.elements.map((element) => {
|
|
102
|
+
if (ts.isSpreadElement(element)) {
|
|
103
|
+
throw new Error('Unsupported spread element in swagger array.');
|
|
104
|
+
}
|
|
105
|
+
return evaluateExpression(element);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
109
|
+
return node.text;
|
|
110
|
+
}
|
|
111
|
+
if (ts.isNumericLiteral(node)) {
|
|
112
|
+
return Number(node.text);
|
|
113
|
+
}
|
|
114
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
124
|
+
return evaluateExpression(node.expression);
|
|
125
|
+
}
|
|
126
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
127
|
+
return {
|
|
128
|
+
__target__: evaluateExpression(node.expression),
|
|
129
|
+
__property__: node.name.text,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (ts.isCallExpression(node)) {
|
|
133
|
+
const callee = evaluateExpression(node.expression);
|
|
134
|
+
const args = node.arguments.map((argument) => evaluateExpression(argument));
|
|
135
|
+
if (callee &&
|
|
136
|
+
typeof callee === 'object' &&
|
|
137
|
+
'__target__' in callee &&
|
|
138
|
+
'__property__' in callee &&
|
|
139
|
+
callee.__property__ === 'join' &&
|
|
140
|
+
Array.isArray(callee.__target__)) {
|
|
141
|
+
return callee.__target__.join(args[0] ?? ',');
|
|
142
|
+
}
|
|
143
|
+
throw new Error('Unsupported call expression in swagger object.');
|
|
144
|
+
}
|
|
145
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
146
|
+
const operand = evaluateExpression(node.operand);
|
|
147
|
+
if (node.operator === ts.SyntaxKind.MinusToken) {
|
|
148
|
+
return -operand;
|
|
149
|
+
}
|
|
150
|
+
if (node.operator === ts.SyntaxKind.PlusToken) {
|
|
151
|
+
return +operand;
|
|
152
|
+
}
|
|
153
|
+
if (node.operator === ts.SyntaxKind.ExclamationToken) {
|
|
154
|
+
return !operand;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`Unsupported swagger expression kind: ${ts.SyntaxKind[node.kind]}`);
|
|
158
|
+
}
|
|
159
|
+
function parseTypeScriptSwagger(sourceText, fileName) {
|
|
160
|
+
const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
161
|
+
for (const statement of sourceFile.statements) {
|
|
162
|
+
if (!ts.isExportAssignment(statement)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!ts.isObjectLiteralExpression(statement.expression)) {
|
|
166
|
+
throw new Error('Expected `export default` to be an object literal.');
|
|
167
|
+
}
|
|
168
|
+
return evaluateExpression(statement.expression);
|
|
169
|
+
}
|
|
170
|
+
throw new Error('Missing `export default` in swagger source.');
|
|
171
|
+
}
|
|
172
|
+
function normalizeDocument(document) {
|
|
173
|
+
return {
|
|
174
|
+
...document,
|
|
175
|
+
openapi: document.openapi ?? '3.0.2',
|
|
176
|
+
info: {
|
|
177
|
+
...(document.info ?? {}),
|
|
178
|
+
title: document.info?.title ?? 'NocoBase API',
|
|
179
|
+
version: document.info?.version ?? '1.0.0',
|
|
180
|
+
},
|
|
181
|
+
paths: document.paths ?? {},
|
|
182
|
+
components: document.components ?? {},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export async function loadSwaggerDocument(source) {
|
|
186
|
+
const content = await fs.readFile(source.sourceFile, 'utf8');
|
|
187
|
+
const document = normalizeDocument(source.format === 'json'
|
|
188
|
+
? JSON.parse(content)
|
|
189
|
+
: parseTypeScriptSwagger(content, source.sourceFile));
|
|
190
|
+
try {
|
|
191
|
+
await SwaggerParser.validate(document);
|
|
192
|
+
return (await SwaggerParser.dereference(document));
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
return document;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function resolveLocalRef(document, ref) {
|
|
199
|
+
if (!ref.startsWith('#/')) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
return ref
|
|
203
|
+
.slice(2)
|
|
204
|
+
.split('/')
|
|
205
|
+
.reduce((current, segment) => current?.[segment], document);
|
|
206
|
+
}
|
|
207
|
+
function dereferenceNode(node, document, seen = new Set()) {
|
|
208
|
+
if (Array.isArray(node)) {
|
|
209
|
+
return node.map((item) => dereferenceNode(item, document, seen));
|
|
210
|
+
}
|
|
211
|
+
if (!node || typeof node !== 'object') {
|
|
212
|
+
return node;
|
|
213
|
+
}
|
|
214
|
+
const ref = node.$ref;
|
|
215
|
+
if (typeof ref === 'string') {
|
|
216
|
+
if (seen.has(ref)) {
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
const resolved = resolveLocalRef(document, ref);
|
|
220
|
+
if (!resolved) {
|
|
221
|
+
return node;
|
|
222
|
+
}
|
|
223
|
+
return dereferenceNode(resolved, document, new Set([...seen, ref]));
|
|
224
|
+
}
|
|
225
|
+
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, dereferenceNode(value, document, seen)]));
|
|
226
|
+
}
|
|
227
|
+
export async function sha1File(filePath) {
|
|
228
|
+
const content = await fs.readFile(filePath);
|
|
229
|
+
return createHash('sha1').update(content).digest('hex');
|
|
230
|
+
}
|
|
231
|
+
export function collectOperations(document) {
|
|
232
|
+
const operations = [];
|
|
233
|
+
for (const [pathTemplate, pathItem] of Object.entries(document.paths ?? {})) {
|
|
234
|
+
for (const method of HTTP_METHODS) {
|
|
235
|
+
const operation = pathItem?.[method];
|
|
236
|
+
if (!operation || '$ref' in operation) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const parameters = [...(pathItem.parameters ?? []), ...(operation.parameters ?? [])]
|
|
240
|
+
.map((parameter) => dereferenceNode(parameter, document))
|
|
241
|
+
.filter((parameter) => Boolean(parameter && !('$ref' in parameter)));
|
|
242
|
+
operations.push({
|
|
243
|
+
method,
|
|
244
|
+
pathTemplate,
|
|
245
|
+
operation: {
|
|
246
|
+
...operation,
|
|
247
|
+
parameters,
|
|
248
|
+
requestBody: operation.requestBody ? dereferenceNode(operation.requestBody, document) : undefined,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return operations;
|
|
254
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function buildKey(resource, action) {
|
|
2
|
+
return `${resource}:${action}`;
|
|
3
|
+
}
|
|
4
|
+
class PostProcessorRegistry {
|
|
5
|
+
processors = new Map();
|
|
6
|
+
register(resource, action, processor) {
|
|
7
|
+
this.processors.set(buildKey(resource, action), processor);
|
|
8
|
+
}
|
|
9
|
+
resolve(resource, action) {
|
|
10
|
+
if (!resource || !action) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return this.processors.get(buildKey(resource, action));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export const postProcessorRegistry = new PostProcessorRegistry();
|
|
17
|
+
export async function applyPostProcessor(result, context) {
|
|
18
|
+
const processor = postProcessorRegistry.resolve(context.operation.logicalResourceName, context.operation.actionName);
|
|
19
|
+
if (!processor) {
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
return processor(result, context);
|
|
23
|
+
}
|