@soapjs/cli 1.0.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/.nvmrc +1 -0
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/build/cli.d.ts +3 -0
- package/build/cli.js +50 -0
- package/build/commands/add/add.command.d.ts +2 -0
- package/build/commands/add/add.command.js +709 -0
- package/build/commands/add/command-plan.d.ts +15 -0
- package/build/commands/add/command-plan.js +182 -0
- package/build/commands/add/entity-plan.d.ts +7 -0
- package/build/commands/add/entity-plan.js +106 -0
- package/build/commands/add/event-plan.d.ts +8 -0
- package/build/commands/add/event-plan.js +59 -0
- package/build/commands/add/query-plan.d.ts +10 -0
- package/build/commands/add/query-plan.js +156 -0
- package/build/commands/add/repository-plan.d.ts +11 -0
- package/build/commands/add/repository-plan.js +252 -0
- package/build/commands/add/resource-plan.d.ts +52 -0
- package/build/commands/add/resource-plan.js +2031 -0
- package/build/commands/add/route-plan.d.ts +24 -0
- package/build/commands/add/route-plan.js +256 -0
- package/build/commands/add/socket-plan.d.ts +9 -0
- package/build/commands/add/socket-plan.js +81 -0
- package/build/commands/add/use-case-plan.d.ts +7 -0
- package/build/commands/add/use-case-plan.js +86 -0
- package/build/commands/check/check.command.d.ts +2 -0
- package/build/commands/check/check.command.js +113 -0
- package/build/commands/create/create.command.d.ts +2 -0
- package/build/commands/create/create.command.js +234 -0
- package/build/commands/create/project-plan.d.ts +44 -0
- package/build/commands/create/project-plan.js +1430 -0
- package/build/commands/doctor/doctor.command.d.ts +2 -0
- package/build/commands/doctor/doctor.command.js +38 -0
- package/build/commands/generate/bruno-analysis.d.ts +19 -0
- package/build/commands/generate/bruno-analysis.js +51 -0
- package/build/commands/generate/bruno-plan.d.ts +6 -0
- package/build/commands/generate/bruno-plan.js +326 -0
- package/build/commands/generate/generate.command.d.ts +2 -0
- package/build/commands/generate/generate.command.js +130 -0
- package/build/commands/info/info.command.d.ts +2 -0
- package/build/commands/info/info.command.js +26 -0
- package/build/commands/remove/remove.command.d.ts +2 -0
- package/build/commands/remove/remove.command.js +328 -0
- package/build/commands/shared/common-options.d.ts +10 -0
- package/build/commands/shared/common-options.js +23 -0
- package/build/commands/update/update.command.d.ts +2 -0
- package/build/commands/update/update.command.js +155 -0
- package/build/config/auth-policy.d.ts +4 -0
- package/build/config/auth-policy.js +54 -0
- package/build/config/find-soap-root.d.ts +1 -0
- package/build/config/find-soap-root.js +22 -0
- package/build/config/load-soap-config.d.ts +2 -0
- package/build/config/load-soap-config.js +30 -0
- package/build/config/schemas/types.d.ts +127 -0
- package/build/config/schemas/types.js +2 -0
- package/build/config/schemas/validation.d.ts +5 -0
- package/build/config/schemas/validation.js +130 -0
- package/build/config/soap-config.service.d.ts +4 -0
- package/build/config/soap-config.service.js +24 -0
- package/build/config/write-soap-config.d.ts +8 -0
- package/build/config/write-soap-config.js +25 -0
- package/build/core/command-context.d.ts +20 -0
- package/build/core/command-context.js +30 -0
- package/build/core/errors.d.ts +6 -0
- package/build/core/errors.js +23 -0
- package/build/core/output.d.ts +12 -0
- package/build/core/output.js +30 -0
- package/build/core/result.d.ts +9 -0
- package/build/core/result.js +11 -0
- package/build/dependencies/dependency-resolver.d.ts +6 -0
- package/build/dependencies/dependency-resolver.js +68 -0
- package/build/dependencies/package-manager.d.ts +7 -0
- package/build/dependencies/package-manager.js +54 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +9 -0
- package/build/io/conflict-policy.d.ts +10 -0
- package/build/io/conflict-policy.js +32 -0
- package/build/io/file-writer.d.ts +19 -0
- package/build/io/file-writer.js +65 -0
- package/build/io/format-file.d.ts +1 -0
- package/build/io/format-file.js +13 -0
- package/build/presets/create-presets.d.ts +4 -0
- package/build/presets/create-presets.js +97 -0
- package/build/presets/index.d.ts +2 -0
- package/build/presets/index.js +18 -0
- package/build/presets/preset.types.d.ts +6 -0
- package/build/presets/preset.types.js +2 -0
- package/build/prompts/add-resource.prompt.d.ts +13 -0
- package/build/prompts/add-resource.prompt.js +80 -0
- package/build/prompts/add-route.prompt.d.ts +16 -0
- package/build/prompts/add-route.prompt.js +140 -0
- package/build/prompts/create-project.prompt.d.ts +11 -0
- package/build/prompts/create-project.prompt.js +156 -0
- package/build/prompts/generate-bruno.prompt.d.ts +7 -0
- package/build/prompts/generate-bruno.prompt.js +21 -0
- package/build/prompts/index.d.ts +8 -0
- package/build/prompts/index.js +24 -0
- package/build/prompts/inquirer-prompt-adapter.d.ts +8 -0
- package/build/prompts/inquirer-prompt-adapter.js +52 -0
- package/build/prompts/mock-prompt-adapter.d.ts +13 -0
- package/build/prompts/mock-prompt-adapter.js +60 -0
- package/build/prompts/prompt-adapter.d.ts +7 -0
- package/build/prompts/prompt-adapter.js +2 -0
- package/build/prompts/prompt.types.d.ts +26 -0
- package/build/prompts/prompt.types.js +2 -0
- package/build/registry/registry.service.d.ts +19 -0
- package/build/registry/registry.service.js +68 -0
- package/build/resolvers/add-resource.resolver.d.ts +23 -0
- package/build/resolvers/add-resource.resolver.js +73 -0
- package/build/resolvers/add-route.resolver.d.ts +34 -0
- package/build/resolvers/add-route.resolver.js +83 -0
- package/build/resolvers/create-config.resolver.d.ts +32 -0
- package/build/resolvers/create-config.resolver.js +57 -0
- package/build/resolvers/generate-bruno.resolver.d.ts +17 -0
- package/build/resolvers/generate-bruno.resolver.js +23 -0
- package/build/resolvers/index.d.ts +5 -0
- package/build/resolvers/index.js +21 -0
- package/build/resolvers/resolver.types.d.ts +8 -0
- package/build/resolvers/resolver.types.js +2 -0
- package/build/summary/create-summary.d.ts +2 -0
- package/build/summary/create-summary.js +24 -0
- package/build/summary/index.d.ts +1 -0
- package/build/summary/index.js +17 -0
- package/build/templates/naming.d.ts +11 -0
- package/build/templates/naming.js +30 -0
- package/build/templates/template-context.d.ts +6 -0
- package/build/templates/template-context.js +2 -0
- package/build/templates/template-engine.d.ts +1 -0
- package/build/templates/template-engine.js +10 -0
- package/build/templates/template-resolver.d.ts +2 -0
- package/build/templates/template-resolver.js +17 -0
- package/build/terminal/terminal-capabilities.d.ts +6 -0
- package/build/terminal/terminal-capabilities.js +14 -0
- package/docs/adr/0001-soap-cli-project-aware-generator.md +108 -0
- package/docs/cli/add-resource.md +127 -0
- package/docs/cli/add-route.md +79 -0
- package/docs/cli/bruno.md +58 -0
- package/docs/cli/create.md +73 -0
- package/docs/cli/index.md +92 -0
- package/docs/cli/interactive-mode.md +61 -0
- package/docs/cli/remove.md +45 -0
- package/docs/guides/auth.md +90 -0
- package/docs/guides/cqrs-events-realtime.md +100 -0
- package/docs/guides/index.md +24 -0
- package/docs/guides/quality-and-safety.md +88 -0
- package/docs/guides/regular-api.md +119 -0
- package/docs/guides/storage.md +101 -0
- package/docs/plans/interactive-mode-plan.md +601 -0
- package/package.json +44 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerDoctorCommand = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const command_context_1 = require("../../core/command-context");
|
|
10
|
+
const find_soap_root_1 = require("../../config/find-soap-root");
|
|
11
|
+
const load_soap_config_1 = require("../../config/load-soap-config");
|
|
12
|
+
function registerDoctorCommand(program) {
|
|
13
|
+
program
|
|
14
|
+
.command("doctor")
|
|
15
|
+
.description("Validate local SoapJS CLI/project setup.")
|
|
16
|
+
.action(async (_options, command) => {
|
|
17
|
+
const context = (0, command_context_1.getCommandContext)(command);
|
|
18
|
+
const soapRoot = (0, find_soap_root_1.findSoapRoot)(context.cwd);
|
|
19
|
+
context.output.info(`Node.js: ${process.version}`);
|
|
20
|
+
if (!soapRoot) {
|
|
21
|
+
context.output.warn("No .soap project found from current directory.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
|
|
25
|
+
context.output.success(`Found SoapJS project: ${config.project.name}`);
|
|
26
|
+
const requiredFiles = [".soap/project.json", ".soap/structure.json", ".soap/api.json", ".soap/registry.json"];
|
|
27
|
+
for (const file of requiredFiles) {
|
|
28
|
+
const absolute = path_1.default.join(soapRoot, file);
|
|
29
|
+
if (fs_1.default.existsSync(absolute)) {
|
|
30
|
+
context.output.success(`ok ${file}`);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
context.output.error(`missing ${file}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
exports.registerDoctorCommand = registerDoctorCommand;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PlannedFile } from "../../io/file-writer";
|
|
2
|
+
import { SoapConfig } from "../../config/schemas/types";
|
|
3
|
+
export interface BrunoFileAnalysis {
|
|
4
|
+
file: PlannedFile;
|
|
5
|
+
exists: boolean;
|
|
6
|
+
tracked: boolean;
|
|
7
|
+
modified: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface BrunoGenerationAnalysis {
|
|
10
|
+
routeCount: number;
|
|
11
|
+
files: BrunoFileAnalysis[];
|
|
12
|
+
existingCount: number;
|
|
13
|
+
missingCount: number;
|
|
14
|
+
modifiedCount: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function analyzeBrunoFiles(config: SoapConfig, files: PlannedFile[]): Promise<BrunoGenerationAnalysis>;
|
|
17
|
+
export declare function formatBrunoGenerationAnalysis(analysis: BrunoGenerationAnalysis): string;
|
|
18
|
+
export declare function selectMissingBrunoFiles(analysis: BrunoGenerationAnalysis): PlannedFile[];
|
|
19
|
+
export declare function selectUnmodifiedBrunoFiles(analysis: BrunoGenerationAnalysis): PlannedFile[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.selectUnmodifiedBrunoFiles = exports.selectMissingBrunoFiles = exports.formatBrunoGenerationAnalysis = exports.analyzeBrunoFiles = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const registry_service_1 = require("../../registry/registry.service");
|
|
9
|
+
async function analyzeBrunoFiles(config, files) {
|
|
10
|
+
const analyzedFiles = await Promise.all(files.map((file) => analyzeBrunoFile(config, file)));
|
|
11
|
+
return {
|
|
12
|
+
routeCount: config.registry.routes.length,
|
|
13
|
+
files: analyzedFiles,
|
|
14
|
+
existingCount: analyzedFiles.filter((entry) => entry.exists).length,
|
|
15
|
+
missingCount: analyzedFiles.filter((entry) => !entry.exists).length,
|
|
16
|
+
modifiedCount: analyzedFiles.filter((entry) => entry.modified).length,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
exports.analyzeBrunoFiles = analyzeBrunoFiles;
|
|
20
|
+
function formatBrunoGenerationAnalysis(analysis) {
|
|
21
|
+
return [
|
|
22
|
+
`Detected routes: ${analysis.routeCount}`,
|
|
23
|
+
`Existing Bruno files: ${analysis.existingCount}`,
|
|
24
|
+
`Missing Bruno files: ${analysis.missingCount}`,
|
|
25
|
+
`Modified generated Bruno files: ${analysis.modifiedCount}`,
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
28
|
+
exports.formatBrunoGenerationAnalysis = formatBrunoGenerationAnalysis;
|
|
29
|
+
function selectMissingBrunoFiles(analysis) {
|
|
30
|
+
return analysis.files
|
|
31
|
+
.filter((entry) => !entry.exists)
|
|
32
|
+
.map((entry) => entry.file);
|
|
33
|
+
}
|
|
34
|
+
exports.selectMissingBrunoFiles = selectMissingBrunoFiles;
|
|
35
|
+
function selectUnmodifiedBrunoFiles(analysis) {
|
|
36
|
+
return analysis.files
|
|
37
|
+
.filter((entry) => !entry.modified)
|
|
38
|
+
.map((entry) => entry.file);
|
|
39
|
+
}
|
|
40
|
+
exports.selectUnmodifiedBrunoFiles = selectUnmodifiedBrunoFiles;
|
|
41
|
+
async function analyzeBrunoFile(config, file) {
|
|
42
|
+
const targetPath = path_1.default.join(config.root, file.path);
|
|
43
|
+
const currentHash = await (0, registry_service_1.readFileHash)(targetPath);
|
|
44
|
+
const registryEntry = config.registry.generatedFiles.find((entry) => entry.path === file.path);
|
|
45
|
+
return {
|
|
46
|
+
file,
|
|
47
|
+
exists: currentHash !== undefined,
|
|
48
|
+
tracked: registryEntry !== undefined,
|
|
49
|
+
modified: Boolean(currentHash && registryEntry && currentHash !== registryEntry.hash),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SoapConfig } from "../../config/schemas/types";
|
|
2
|
+
import { PlannedFile } from "../../io/file-writer";
|
|
3
|
+
export interface BrunoGenerationOptions {
|
|
4
|
+
e2e?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function createBrunoFiles(config: SoapConfig, options?: BrunoGenerationOptions): PlannedFile[];
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createBrunoFiles = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const naming_1 = require("../../templates/naming");
|
|
9
|
+
function createBrunoFiles(config, options = {}) {
|
|
10
|
+
const collectionPath = config.api.bruno.collectionPath || "bruno";
|
|
11
|
+
const environment = config.api.bruno.environment || "Local";
|
|
12
|
+
const files = [
|
|
13
|
+
{
|
|
14
|
+
path: path_1.default.posix.join(collectionPath, "bruno.json"),
|
|
15
|
+
type: "bruno",
|
|
16
|
+
content: JSON.stringify({
|
|
17
|
+
version: "1",
|
|
18
|
+
name: config.project.name,
|
|
19
|
+
type: "collection",
|
|
20
|
+
}, null, 2),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
path: path_1.default.posix.join(collectionPath, "environments", `${environment}.bru`),
|
|
24
|
+
type: "bruno",
|
|
25
|
+
content: `vars {
|
|
26
|
+
baseUrl: ${config.api.baseUrl}
|
|
27
|
+
accessToken:
|
|
28
|
+
apiKey:
|
|
29
|
+
id:
|
|
30
|
+
email: admin@example.com
|
|
31
|
+
password: admin123
|
|
32
|
+
}
|
|
33
|
+
`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: path_1.default.posix.join(collectionPath, "Health", "health.bru"),
|
|
37
|
+
type: "bruno",
|
|
38
|
+
content: createRequestBru({
|
|
39
|
+
name: "Health",
|
|
40
|
+
method: config.api.health.method,
|
|
41
|
+
path: config.api.health.path,
|
|
42
|
+
sequence: 1,
|
|
43
|
+
auth: "none",
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
if (usesJwtAuth(config.project.capabilities.auth)) {
|
|
48
|
+
files.push({
|
|
49
|
+
path: path_1.default.posix.join(collectionPath, "Auth", "login.bru"),
|
|
50
|
+
type: "bruno",
|
|
51
|
+
owner: "auth",
|
|
52
|
+
content: createLoginBru(2),
|
|
53
|
+
}, {
|
|
54
|
+
path: path_1.default.posix.join(collectionPath, "Auth", "me.bru"),
|
|
55
|
+
type: "bruno",
|
|
56
|
+
owner: "auth",
|
|
57
|
+
content: createRequestBru({
|
|
58
|
+
name: "Me",
|
|
59
|
+
method: "GET",
|
|
60
|
+
path: "/auth/me",
|
|
61
|
+
sequence: 3,
|
|
62
|
+
auth: "jwt",
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const grouped = groupRoutesByResource(config.registry.routes.filter((route) => route.bruno !== false));
|
|
67
|
+
const fieldsByResource = new Map(config.registry.resources.map((resource) => [resource.name, resource.fields]));
|
|
68
|
+
let sequence = usesJwtAuth(config.project.capabilities.auth) ? 4 : 2;
|
|
69
|
+
for (const [resourceName, routes] of Array.from(grouped.entries()).sort(([left], [right]) => left.localeCompare(right))) {
|
|
70
|
+
const resourceNames = (0, naming_1.createNameVariants)(resourceName);
|
|
71
|
+
for (const route of routes.sort(compareRoutes)) {
|
|
72
|
+
const routeNames = (0, naming_1.createNameVariants)(route.name);
|
|
73
|
+
files.push({
|
|
74
|
+
path: path_1.default.posix.join(collectionPath, resourceNames.pascalName, `${routeNames.pascalName}.bru`),
|
|
75
|
+
type: "bruno",
|
|
76
|
+
owner: resourceName,
|
|
77
|
+
content: createRequestBru({
|
|
78
|
+
name: routeNames.pascalName,
|
|
79
|
+
method: route.method,
|
|
80
|
+
path: route.path,
|
|
81
|
+
sequence,
|
|
82
|
+
auth: route.auth,
|
|
83
|
+
includeJsonBody: ["POST", "PUT", "PATCH"].includes(route.method.toUpperCase()),
|
|
84
|
+
fields: fieldsByResource.get(resourceName),
|
|
85
|
+
expectBody: route.method.toUpperCase() === "GET" && ["list", "get"].includes(route.name),
|
|
86
|
+
captureId: route.method.toUpperCase() === "POST" && route.name === "create",
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
sequence += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (options.e2e) {
|
|
93
|
+
files.push(...createE2eFlowFiles(config, grouped, sequence));
|
|
94
|
+
}
|
|
95
|
+
return files;
|
|
96
|
+
}
|
|
97
|
+
exports.createBrunoFiles = createBrunoFiles;
|
|
98
|
+
function compareRoutes(left, right) {
|
|
99
|
+
const rank = new Map([
|
|
100
|
+
["list", 0],
|
|
101
|
+
["create", 1],
|
|
102
|
+
["get", 2],
|
|
103
|
+
["update", 3],
|
|
104
|
+
["delete", 4],
|
|
105
|
+
]);
|
|
106
|
+
const leftRank = rank.get(left.name) ?? 100;
|
|
107
|
+
const rightRank = rank.get(right.name) ?? 100;
|
|
108
|
+
if (leftRank !== rightRank) {
|
|
109
|
+
return leftRank - rightRank;
|
|
110
|
+
}
|
|
111
|
+
return `${left.method} ${left.path} ${left.name}`.localeCompare(`${right.method} ${right.path} ${right.name}`);
|
|
112
|
+
}
|
|
113
|
+
function createE2eFlowFiles(config, grouped, startSequence) {
|
|
114
|
+
const collectionPath = config.api.bruno.collectionPath || "bruno";
|
|
115
|
+
const files = [];
|
|
116
|
+
let sequence = startSequence;
|
|
117
|
+
for (const [resourceName, routes] of Array.from(grouped.entries()).sort(([left], [right]) => left.localeCompare(right))) {
|
|
118
|
+
const crud = resolveCrudRoutes(routes);
|
|
119
|
+
if (!crud) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const resourceNames = (0, naming_1.createNameVariants)(resourceName);
|
|
123
|
+
const itemNames = (0, naming_1.createNameVariants)(singularizeResourceName(resourceNames.kebabName));
|
|
124
|
+
let step = 1;
|
|
125
|
+
if (usesJwtAuth(config.project.capabilities.auth)) {
|
|
126
|
+
files.push({
|
|
127
|
+
path: path_1.default.posix.join(collectionPath, "E2E Flow", `${formatStep(step)}-login.bru`),
|
|
128
|
+
type: "bruno",
|
|
129
|
+
owner: resourceName,
|
|
130
|
+
content: createLoginBru(sequence),
|
|
131
|
+
});
|
|
132
|
+
step += 1;
|
|
133
|
+
sequence += 1;
|
|
134
|
+
}
|
|
135
|
+
const flowSteps = [
|
|
136
|
+
{ label: `create-${itemNames.kebabName}`, route: crud.create, includeJsonBody: true, captureId: true },
|
|
137
|
+
{ label: `get-${itemNames.kebabName}`, route: crud.get, expectBody: true },
|
|
138
|
+
{ label: `list-${resourceNames.kebabName}`, route: crud.list, expectBody: true },
|
|
139
|
+
{ label: `update-${itemNames.kebabName}`, route: crud.update, includeJsonBody: true, expectBody: true },
|
|
140
|
+
{ label: `delete-${itemNames.kebabName}`, route: crud.delete, expectBody: true },
|
|
141
|
+
];
|
|
142
|
+
for (const flowStep of flowSteps) {
|
|
143
|
+
files.push({
|
|
144
|
+
path: path_1.default.posix.join(collectionPath, "E2E Flow", `${formatStep(step)}-${flowStep.label}.bru`),
|
|
145
|
+
type: "bruno",
|
|
146
|
+
owner: resourceName,
|
|
147
|
+
content: createRequestBru({
|
|
148
|
+
name: flowStep.label,
|
|
149
|
+
method: flowStep.route.method,
|
|
150
|
+
path: flowStep.route.path,
|
|
151
|
+
sequence,
|
|
152
|
+
auth: flowStep.route.auth,
|
|
153
|
+
includeJsonBody: flowStep.includeJsonBody,
|
|
154
|
+
fields: config.registry.resources.find((resource) => resource.name === resourceName)?.fields,
|
|
155
|
+
expectBody: flowStep.expectBody,
|
|
156
|
+
captureId: flowStep.captureId,
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
step += 1;
|
|
160
|
+
sequence += 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return files;
|
|
164
|
+
}
|
|
165
|
+
function resolveCrudRoutes(routes) {
|
|
166
|
+
const byName = new Map(routes.map((route) => [route.name, route]));
|
|
167
|
+
const list = byName.get("list");
|
|
168
|
+
const get = byName.get("get");
|
|
169
|
+
const create = byName.get("create");
|
|
170
|
+
const update = byName.get("update");
|
|
171
|
+
const deleteRoute = byName.get("delete");
|
|
172
|
+
if (!list || !get || !create || !update || !deleteRoute) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
list,
|
|
177
|
+
get,
|
|
178
|
+
create,
|
|
179
|
+
update,
|
|
180
|
+
delete: deleteRoute,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function formatStep(value) {
|
|
184
|
+
return String(value).padStart(2, "0");
|
|
185
|
+
}
|
|
186
|
+
function singularizeResourceName(name) {
|
|
187
|
+
if (name.endsWith("ies")) {
|
|
188
|
+
return `${name.slice(0, -3)}y`;
|
|
189
|
+
}
|
|
190
|
+
if (name.endsWith("ses") || name.endsWith("xes") || name.endsWith("zes") || name.endsWith("ches") || name.endsWith("shes")) {
|
|
191
|
+
return name.slice(0, -2);
|
|
192
|
+
}
|
|
193
|
+
if (name.endsWith("s") && name.length > 1) {
|
|
194
|
+
return name.slice(0, -1);
|
|
195
|
+
}
|
|
196
|
+
return name;
|
|
197
|
+
}
|
|
198
|
+
function usesJwtAuth(values) {
|
|
199
|
+
return values.includes("jwt") || values.includes("local");
|
|
200
|
+
}
|
|
201
|
+
function createLoginBru(sequence) {
|
|
202
|
+
return `meta {
|
|
203
|
+
name: Login
|
|
204
|
+
type: http
|
|
205
|
+
seq: ${sequence}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
post {
|
|
209
|
+
url: {{baseUrl}}/auth/login
|
|
210
|
+
body: json
|
|
211
|
+
auth: none
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
body:json {
|
|
215
|
+
{
|
|
216
|
+
"email": "{{email}}",
|
|
217
|
+
"password": "{{password}}"
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
script:post-response {
|
|
222
|
+
const data = res.getBody();
|
|
223
|
+
if (data?.accessToken) {
|
|
224
|
+
bru.setEnvVar("accessToken", data.accessToken);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
tests {
|
|
229
|
+
function onResponse(request, response) {
|
|
230
|
+
expect(response.status).to.be.within(200, 299);
|
|
231
|
+
const body = response.getBody();
|
|
232
|
+
expect(body.accessToken).to.be.a('string').and.not.empty;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
function groupRoutesByResource(routes) {
|
|
238
|
+
const grouped = new Map();
|
|
239
|
+
for (const route of routes) {
|
|
240
|
+
const routesForResource = grouped.get(route.resource) ?? [];
|
|
241
|
+
routesForResource.push(route);
|
|
242
|
+
grouped.set(route.resource, routesForResource);
|
|
243
|
+
}
|
|
244
|
+
return grouped;
|
|
245
|
+
}
|
|
246
|
+
function createRequestBru(options) {
|
|
247
|
+
const method = options.method.toLowerCase();
|
|
248
|
+
const requestPath = toBrunoPath(options.path);
|
|
249
|
+
const bodyType = options.includeJsonBody ? "json" : "none";
|
|
250
|
+
const authBlock = options.auth === "jwt"
|
|
251
|
+
? `
|
|
252
|
+
headers {
|
|
253
|
+
Authorization: Bearer {{accessToken}}
|
|
254
|
+
}
|
|
255
|
+
`
|
|
256
|
+
: options.auth === "api-key"
|
|
257
|
+
? `
|
|
258
|
+
headers {
|
|
259
|
+
x-api-key: {{apiKey}}
|
|
260
|
+
}
|
|
261
|
+
`
|
|
262
|
+
: "";
|
|
263
|
+
const bodyBlock = options.includeJsonBody
|
|
264
|
+
? `
|
|
265
|
+
body:json {
|
|
266
|
+
${createExampleJsonBody(options.fields)}
|
|
267
|
+
}
|
|
268
|
+
`
|
|
269
|
+
: "";
|
|
270
|
+
const createScriptBlock = options.captureId
|
|
271
|
+
? `
|
|
272
|
+
script:post-response {
|
|
273
|
+
const data = res.getBody();
|
|
274
|
+
const id = data?.content?.id || data?.id;
|
|
275
|
+
if (id) {
|
|
276
|
+
bru.setEnvVar("id", id);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
`
|
|
280
|
+
: "";
|
|
281
|
+
const bodyExpectation = options.expectBody
|
|
282
|
+
? `
|
|
283
|
+
expect(response.getBody()).to.exist;`
|
|
284
|
+
: "";
|
|
285
|
+
const testsBlock = `
|
|
286
|
+
tests {
|
|
287
|
+
function onResponse(request, response) {
|
|
288
|
+
expect(response.status).to.be.within(200, 299);${bodyExpectation}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
`;
|
|
292
|
+
return `meta {
|
|
293
|
+
name: ${options.name}
|
|
294
|
+
type: http
|
|
295
|
+
seq: ${options.sequence}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
${method} {
|
|
299
|
+
url: {{baseUrl}}${requestPath}
|
|
300
|
+
body: ${bodyType}
|
|
301
|
+
auth: none
|
|
302
|
+
}
|
|
303
|
+
${authBlock}${bodyBlock}${createScriptBlock}${testsBlock}`;
|
|
304
|
+
}
|
|
305
|
+
function createExampleJsonBody(fields) {
|
|
306
|
+
const bodyFields = fields && fields.length > 0
|
|
307
|
+
? fields
|
|
308
|
+
: [{ name: "name", type: "string", required: true }];
|
|
309
|
+
const body = Object.fromEntries(bodyFields.map((field) => [field.name, exampleValueForField(field)]));
|
|
310
|
+
return JSON.stringify(body, null, 2)
|
|
311
|
+
.split("\n")
|
|
312
|
+
.map((line) => ` ${line}`)
|
|
313
|
+
.join("\n");
|
|
314
|
+
}
|
|
315
|
+
function exampleValueForField(field) {
|
|
316
|
+
if (field.type === "number")
|
|
317
|
+
return 42;
|
|
318
|
+
if (field.type === "boolean")
|
|
319
|
+
return true;
|
|
320
|
+
if (field.type === "date")
|
|
321
|
+
return "2026-01-01T00:00:00.000Z";
|
|
322
|
+
return "Example";
|
|
323
|
+
}
|
|
324
|
+
function toBrunoPath(routePath) {
|
|
325
|
+
return routePath.replace(/:([A-Za-z0-9_]+)/g, "{{$1}}");
|
|
326
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerGenerateCommand = void 0;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const command_context_1 = require("../../core/command-context");
|
|
10
|
+
const errors_1 = require("../../core/errors");
|
|
11
|
+
const load_soap_config_1 = require("../../config/load-soap-config");
|
|
12
|
+
const write_soap_config_1 = require("../../config/write-soap-config");
|
|
13
|
+
const file_writer_1 = require("../../io/file-writer");
|
|
14
|
+
const common_options_1 = require("../shared/common-options");
|
|
15
|
+
const bruno_plan_1 = require("./bruno-plan");
|
|
16
|
+
const generate_bruno_resolver_1 = require("../../resolvers/generate-bruno.resolver");
|
|
17
|
+
const prompts_1 = require("../../prompts");
|
|
18
|
+
const bruno_analysis_1 = require("./bruno-analysis");
|
|
19
|
+
function registerGenerateCommand(program) {
|
|
20
|
+
const generate = program.command("generate").description("Generate secondary artifacts.");
|
|
21
|
+
(0, common_options_1.addConflictOption)((0, common_options_1.addInteractiveOption)(generate
|
|
22
|
+
.command("bruno")
|
|
23
|
+
.description("Generate Bruno collection files from .soap registry metadata.")
|
|
24
|
+
.option("--e2e", "generate runnable CRUD E2E flow requests", false)
|
|
25
|
+
.option("--force", "overwrite generated files even when modified", false)
|
|
26
|
+
.option("--write-new", "write modified generated files as .new", false)))
|
|
27
|
+
.action(async (options, command) => {
|
|
28
|
+
(0, common_options_1.assertInteractiveTerminal)(options);
|
|
29
|
+
const context = (0, command_context_1.getCommandContext)(command);
|
|
30
|
+
const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
|
|
31
|
+
const resolved = generate_bruno_resolver_1.generateBrunoResolver.resolve({ flags: options, projectConfig: config });
|
|
32
|
+
let files = (0, bruno_plan_1.createBrunoFiles)(config, { e2e: resolved.e2e });
|
|
33
|
+
if (options.interactive) {
|
|
34
|
+
const prompt = new prompts_1.InquirerPromptAdapter();
|
|
35
|
+
const baseFiles = (0, bruno_plan_1.createBrunoFiles)(config);
|
|
36
|
+
const baseAnalysis = await (0, bruno_analysis_1.analyzeBrunoFiles)(config, baseFiles);
|
|
37
|
+
context.output.info((0, bruno_analysis_1.formatBrunoGenerationAnalysis)(baseAnalysis));
|
|
38
|
+
const mode = isCliOption(command, "e2e")
|
|
39
|
+
? "e2e"
|
|
40
|
+
: (await (0, prompts_1.promptGenerateBruno)(prompt, baseAnalysis)).mode;
|
|
41
|
+
if (mode === "abort") {
|
|
42
|
+
context.output.warn("Bruno generation aborted.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (mode === "missing") {
|
|
46
|
+
files = (0, bruno_analysis_1.selectMissingBrunoFiles)(baseAnalysis);
|
|
47
|
+
}
|
|
48
|
+
if (mode === "all") {
|
|
49
|
+
files = options.force ? baseFiles : (0, bruno_analysis_1.selectUnmodifiedBrunoFiles)(baseAnalysis);
|
|
50
|
+
}
|
|
51
|
+
if (mode === "e2e") {
|
|
52
|
+
const e2eFiles = (0, bruno_plan_1.createBrunoFiles)(config, { e2e: true });
|
|
53
|
+
const e2eAnalysis = await (0, bruno_analysis_1.analyzeBrunoFiles)(config, e2eFiles);
|
|
54
|
+
files = options.force ? e2eFiles : (0, bruno_analysis_1.selectUnmodifiedBrunoFiles)(e2eAnalysis);
|
|
55
|
+
}
|
|
56
|
+
if (files.length === 0) {
|
|
57
|
+
context.output.warn("No Bruno files selected for generation.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
await (0, file_writer_1.writePlannedFiles)({
|
|
62
|
+
root: config.root,
|
|
63
|
+
files,
|
|
64
|
+
registry: config.registry,
|
|
65
|
+
force: options.force,
|
|
66
|
+
writeNew: options.writeNew,
|
|
67
|
+
onConflict: options.onConflict,
|
|
68
|
+
skipModified: !options.force && !options.writeNew,
|
|
69
|
+
}, context);
|
|
70
|
+
await (0, write_soap_config_1.writeSoapConfig)(config.root, config, context);
|
|
71
|
+
context.output.success(`Generated Bruno collection with ${files.length} files.`);
|
|
72
|
+
});
|
|
73
|
+
generate
|
|
74
|
+
.command("openapi")
|
|
75
|
+
.description("Fetch OpenAPI JSON from the running local API.")
|
|
76
|
+
.option("--output <path>", "write OpenAPI JSON to a file instead of stdout")
|
|
77
|
+
.action(async (options, command) => {
|
|
78
|
+
const context = (0, command_context_1.getCommandContext)(command);
|
|
79
|
+
const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
|
|
80
|
+
if (!config.project.capabilities.docs.includes("openapi")) {
|
|
81
|
+
throw new errors_1.CliError("OpenAPI is not enabled for this project. Create the project with `--docs openapi`.");
|
|
82
|
+
}
|
|
83
|
+
const openApiUrl = createOpenApiUrl(config.api.baseUrl);
|
|
84
|
+
if (options.output) {
|
|
85
|
+
const outputPath = path_1.default.resolve(context.cwd, options.output);
|
|
86
|
+
if (context.dryRun) {
|
|
87
|
+
context.output.info(`[dry-run] write ${outputPath}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const content = await fetchOpenApiJson(openApiUrl);
|
|
91
|
+
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
|
|
92
|
+
await promises_1.default.writeFile(outputPath, content, "utf8");
|
|
93
|
+
context.output.success(`Wrote OpenAPI JSON to ${options.output}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const content = await fetchOpenApiJson(openApiUrl);
|
|
97
|
+
context.output.info(content);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
exports.registerGenerateCommand = registerGenerateCommand;
|
|
101
|
+
function isCliOption(command, name) {
|
|
102
|
+
return command.getOptionValueSource(name) === "cli";
|
|
103
|
+
}
|
|
104
|
+
function createOpenApiUrl(baseUrl) {
|
|
105
|
+
const normalized = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
106
|
+
return `${normalized}/openapi.json`;
|
|
107
|
+
}
|
|
108
|
+
async function fetchOpenApiJson(url) {
|
|
109
|
+
let response;
|
|
110
|
+
try {
|
|
111
|
+
response = await fetch(url, {
|
|
112
|
+
headers: {
|
|
113
|
+
accept: "application/json",
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw new errors_1.CliError(`Could not fetch OpenAPI JSON from ${url}. Start the API and run again.`);
|
|
119
|
+
}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new errors_1.CliError(`Could not fetch OpenAPI JSON from ${url}. Server returned ${response.status} ${response.statusText}.`);
|
|
122
|
+
}
|
|
123
|
+
const text = await response.text();
|
|
124
|
+
try {
|
|
125
|
+
return `${JSON.stringify(JSON.parse(text), null, 2)}\n`;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw new errors_1.CliError(`OpenAPI endpoint ${url} did not return valid JSON.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerInfoCommand = void 0;
|
|
4
|
+
const command_context_1 = require("../../core/command-context");
|
|
5
|
+
const load_soap_config_1 = require("../../config/load-soap-config");
|
|
6
|
+
function registerInfoCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("info")
|
|
9
|
+
.description("Print SoapJS project metadata.")
|
|
10
|
+
.action(async (_options, command) => {
|
|
11
|
+
const context = (0, command_context_1.getCommandContext)(command);
|
|
12
|
+
const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
|
|
13
|
+
const project = config.project;
|
|
14
|
+
context.output.info(`Name: ${project.name}`);
|
|
15
|
+
context.output.info(`Root: ${config.root}`);
|
|
16
|
+
context.output.info(`Framework: ${project.framework}`);
|
|
17
|
+
context.output.info(`Architecture: ${project.architecture}`);
|
|
18
|
+
context.output.info(`Package manager: ${project.packageManager}`);
|
|
19
|
+
context.output.info(`Databases: ${project.capabilities.databases.join(", ") || "none"}`);
|
|
20
|
+
context.output.info(`Auth: ${project.capabilities.auth.join(", ") || "none"}`);
|
|
21
|
+
context.output.info(`Messaging: ${project.capabilities.messaging.join(", ") || "none"}`);
|
|
22
|
+
context.output.info(`Zones: ${project.zones.join(", ")}`);
|
|
23
|
+
context.output.info(`Generated files: ${config.registry.generatedFiles.length}`);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
exports.registerInfoCommand = registerInfoCommand;
|