@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,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addRouteResolver = exports.AddRouteResolver = void 0;
|
|
4
|
+
const errors_1 = require("../core/errors");
|
|
5
|
+
const route_plan_1 = require("../commands/add/route-plan");
|
|
6
|
+
class AddRouteResolver {
|
|
7
|
+
resolve(input) {
|
|
8
|
+
const config = requireRouteConfig(input.projectConfig);
|
|
9
|
+
const flags = input.flags;
|
|
10
|
+
const promptAnswers = input.promptAnswers ?? {};
|
|
11
|
+
const preset = input.preset ?? {};
|
|
12
|
+
const method = normalizeRouteMethod(pick(flags.method, promptAnswers.method, preset.method, "get"));
|
|
13
|
+
const auth = normalizeRouteAuth(pick(flags.auth, promptAnswers.auth, preset.auth, config.resource.auth));
|
|
14
|
+
const zone = pick(flags.zone, promptAnswers.zone, preset.zone, config.resource.zone);
|
|
15
|
+
const useCase = pick(flags.useCase, promptAnswers.useCase, preset.useCase);
|
|
16
|
+
const command = pick(flags.command, promptAnswers.command, preset.command);
|
|
17
|
+
const query = pick(flags.query, promptAnswers.query, preset.query);
|
|
18
|
+
assertSingleRouteTarget({ useCase, command, query });
|
|
19
|
+
if ((command || query) && config.project.project.architecture !== "cqrs") {
|
|
20
|
+
throw new errors_1.CliError("CQRS route targets require a project created with `--architecture cqrs`.");
|
|
21
|
+
}
|
|
22
|
+
assertCapability("auth", auth, enabledRouteAuthValues(config.project.project.capabilities.auth));
|
|
23
|
+
assertCapability("zone", zone, config.project.project.zones);
|
|
24
|
+
return {
|
|
25
|
+
method,
|
|
26
|
+
path: pick(flags.path, promptAnswers.path, preset.path),
|
|
27
|
+
useCase,
|
|
28
|
+
command,
|
|
29
|
+
query,
|
|
30
|
+
auth,
|
|
31
|
+
zone,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.AddRouteResolver = AddRouteResolver;
|
|
36
|
+
function requireRouteConfig(config) {
|
|
37
|
+
if (!config) {
|
|
38
|
+
throw new errors_1.CliError("Project and resource config are required to resolve route input.");
|
|
39
|
+
}
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
function pick(...values) {
|
|
43
|
+
return values.find((item) => item !== undefined);
|
|
44
|
+
}
|
|
45
|
+
function normalizeRouteMethod(value) {
|
|
46
|
+
const method = (value ?? "get").toLowerCase();
|
|
47
|
+
if (!route_plan_1.routeMethods.includes(method)) {
|
|
48
|
+
throw new errors_1.CliError(`Unsupported HTTP method "${value}". Allowed values: ${route_plan_1.routeMethods.join(", ")}.`);
|
|
49
|
+
}
|
|
50
|
+
return method;
|
|
51
|
+
}
|
|
52
|
+
function normalizeRouteAuth(value) {
|
|
53
|
+
if (value === "local") {
|
|
54
|
+
return "jwt";
|
|
55
|
+
}
|
|
56
|
+
if (value === "jwt" || value === "api-key" || value === "none") {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
throw new errors_1.CliError(`Auth "${value}" cannot protect routes. Use jwt, api-key, or none.`);
|
|
60
|
+
}
|
|
61
|
+
function enabledRouteAuthValues(values) {
|
|
62
|
+
const allowed = ["none"];
|
|
63
|
+
if (values.includes("jwt") || values.includes("local")) {
|
|
64
|
+
allowed.push("jwt");
|
|
65
|
+
}
|
|
66
|
+
if (values.includes("api-key")) {
|
|
67
|
+
allowed.push("api-key");
|
|
68
|
+
}
|
|
69
|
+
return allowed;
|
|
70
|
+
}
|
|
71
|
+
function assertCapability(label, value, allowed) {
|
|
72
|
+
if (!allowed.includes(value)) {
|
|
73
|
+
const allowedValues = allowed.length > 0 ? allowed.join(", ") : "none";
|
|
74
|
+
throw new errors_1.CliError(`${label} "${value}" is not enabled for this project. Allowed values: ${allowedValues}.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function assertSingleRouteTarget(options) {
|
|
78
|
+
const targets = [options.useCase, options.command, options.query].filter(Boolean);
|
|
79
|
+
if (targets.length > 1) {
|
|
80
|
+
throw new errors_1.CliError("Route target options are mutually exclusive. Use only one of --use-case, --command, or --query.");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.addRouteResolver = new AddRouteResolver();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PackageManager } from "../core/command-context";
|
|
2
|
+
import { ApiZone, Architecture, ProjectCapabilities } from "../config/schemas/types";
|
|
3
|
+
import { CommandInputResolver } from "./resolver.types";
|
|
4
|
+
export interface CreateConfigInput {
|
|
5
|
+
framework?: string;
|
|
6
|
+
architecture?: Architecture;
|
|
7
|
+
db?: string | string[];
|
|
8
|
+
auth?: string | string[];
|
|
9
|
+
messaging?: string | string[];
|
|
10
|
+
telemetry?: string | string[];
|
|
11
|
+
docs?: string | string[];
|
|
12
|
+
contracts?: string | string[];
|
|
13
|
+
apiClient?: string | string[];
|
|
14
|
+
realtime?: string | string[];
|
|
15
|
+
zones?: string | string[];
|
|
16
|
+
packageManager?: PackageManager;
|
|
17
|
+
}
|
|
18
|
+
export interface CreateConfigResult {
|
|
19
|
+
framework: "express";
|
|
20
|
+
architecture: Architecture;
|
|
21
|
+
capabilities: ProjectCapabilities;
|
|
22
|
+
zones: ApiZone[];
|
|
23
|
+
packageManager?: PackageManager;
|
|
24
|
+
}
|
|
25
|
+
export declare class CreateConfigResolver implements CommandInputResolver<CreateConfigInput, CreateConfigInput, undefined, CreateConfigResult> {
|
|
26
|
+
resolve(input: {
|
|
27
|
+
flags: CreateConfigInput;
|
|
28
|
+
promptAnswers?: CreateConfigInput;
|
|
29
|
+
preset?: Partial<CreateConfigResult>;
|
|
30
|
+
}): CreateConfigResult;
|
|
31
|
+
}
|
|
32
|
+
export declare const createConfigResolver: CreateConfigResolver;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createConfigResolver = exports.CreateConfigResolver = void 0;
|
|
4
|
+
const errors_1 = require("../core/errors");
|
|
5
|
+
const project_plan_1 = require("../commands/create/project-plan");
|
|
6
|
+
class CreateConfigResolver {
|
|
7
|
+
resolve(input) {
|
|
8
|
+
const flags = input.flags;
|
|
9
|
+
const promptAnswers = input.promptAnswers ?? {};
|
|
10
|
+
const preset = input.preset ?? {};
|
|
11
|
+
const capabilities = (0, project_plan_1.createDefaultCapabilities)();
|
|
12
|
+
const framework = pick(flags.framework, promptAnswers.framework, preset.framework, "express");
|
|
13
|
+
if (framework !== "express") {
|
|
14
|
+
throw new errors_1.CliError("Only framework \"express\" is supported.");
|
|
15
|
+
}
|
|
16
|
+
const architecture = pick(flags.architecture, promptAnswers.architecture, preset.architecture, "regular");
|
|
17
|
+
if (architecture !== "regular" && architecture !== "cqrs") {
|
|
18
|
+
throw new errors_1.CliError("Architecture must be regular or cqrs.");
|
|
19
|
+
}
|
|
20
|
+
capabilities.databases = parseOptionList(pickList(flags.db, promptAnswers.db, preset.capabilities?.databases), project_plan_1.databaseOptions, "database");
|
|
21
|
+
capabilities.auth = parseOptionList(pickList(flags.auth, promptAnswers.auth, preset.capabilities?.auth), project_plan_1.authOptions, "auth");
|
|
22
|
+
capabilities.messaging = parseOptionList(pickList(flags.messaging, promptAnswers.messaging, preset.capabilities?.messaging), project_plan_1.messagingOptions, "messaging");
|
|
23
|
+
capabilities.telemetry = parseOptionList(pickList(flags.telemetry, promptAnswers.telemetry, preset.capabilities?.telemetry), project_plan_1.telemetryOptions, "telemetry");
|
|
24
|
+
capabilities.docs = parseOptionList(pickList(flags.docs, promptAnswers.docs, preset.capabilities?.docs), project_plan_1.docsOptions, "docs");
|
|
25
|
+
capabilities.contracts = parseOptionList(pickList(flags.contracts, promptAnswers.contracts, preset.capabilities?.contracts), project_plan_1.contractsOptions, "contracts");
|
|
26
|
+
capabilities.apiClient = parseOptionList(pickList(flags.apiClient, promptAnswers.apiClient, preset.capabilities?.apiClient), ["bruno"], "api-client");
|
|
27
|
+
capabilities.realtime = parseOptionList(pickList(flags.realtime, promptAnswers.realtime, preset.capabilities?.realtime), project_plan_1.realtimeOptions, "realtime");
|
|
28
|
+
if (capabilities.messaging.length === 0)
|
|
29
|
+
capabilities.messaging = ["in-memory"];
|
|
30
|
+
if (capabilities.telemetry.length === 0)
|
|
31
|
+
capabilities.telemetry = ["logs"];
|
|
32
|
+
const zones = parseOptionList(pick(flags.zones, promptAnswers.zones, preset.zones, "public,private,admin"), project_plan_1.zonesOptions, "zone");
|
|
33
|
+
return {
|
|
34
|
+
framework,
|
|
35
|
+
architecture,
|
|
36
|
+
capabilities,
|
|
37
|
+
zones: zones.length > 0 ? zones : ["public", "private", "admin"],
|
|
38
|
+
packageManager: pick(flags.packageManager, promptAnswers.packageManager, preset.packageManager),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.CreateConfigResolver = CreateConfigResolver;
|
|
43
|
+
function pick(...values) {
|
|
44
|
+
return values.find((value) => value !== undefined);
|
|
45
|
+
}
|
|
46
|
+
function pickList(...values) {
|
|
47
|
+
return values.find((value) => value !== undefined && (!Array.isArray(value) || value.length > 0));
|
|
48
|
+
}
|
|
49
|
+
function parseOptionList(value, allowed, label) {
|
|
50
|
+
try {
|
|
51
|
+
return (0, project_plan_1.parseCsvOption)(value, allowed);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new errors_1.CliError(`Invalid ${label} option: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.createConfigResolver = new CreateConfigResolver();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SoapConfig } from "../config/schemas/types";
|
|
2
|
+
import { CommandInputResolver } from "./resolver.types";
|
|
3
|
+
export interface GenerateBrunoInput {
|
|
4
|
+
e2e?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface GenerateBrunoResult {
|
|
7
|
+
e2e: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class GenerateBrunoResolver implements CommandInputResolver<GenerateBrunoInput, GenerateBrunoInput, SoapConfig, GenerateBrunoResult> {
|
|
10
|
+
resolve(input: {
|
|
11
|
+
flags: GenerateBrunoInput;
|
|
12
|
+
promptAnswers?: GenerateBrunoInput;
|
|
13
|
+
projectConfig?: SoapConfig;
|
|
14
|
+
preset?: Partial<GenerateBrunoResult>;
|
|
15
|
+
}): GenerateBrunoResult;
|
|
16
|
+
}
|
|
17
|
+
export declare const generateBrunoResolver: GenerateBrunoResolver;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateBrunoResolver = exports.GenerateBrunoResolver = void 0;
|
|
4
|
+
const errors_1 = require("../core/errors");
|
|
5
|
+
class GenerateBrunoResolver {
|
|
6
|
+
resolve(input) {
|
|
7
|
+
const config = input.projectConfig;
|
|
8
|
+
if (!config) {
|
|
9
|
+
throw new errors_1.CliError("Project config is required to resolve Bruno generation input.");
|
|
10
|
+
}
|
|
11
|
+
if (!config.project.capabilities.apiClient.includes("bruno") && !config.api.bruno.enabled) {
|
|
12
|
+
throw new errors_1.CliError("Bruno is not enabled for this project. Create the project with `--api-client bruno`.");
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
e2e: Boolean(pick(input.flags.e2e, input.promptAnswers?.e2e, input.preset?.e2e, false)),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.GenerateBrunoResolver = GenerateBrunoResolver;
|
|
20
|
+
function pick(...values) {
|
|
21
|
+
return values.find((value) => value !== undefined);
|
|
22
|
+
}
|
|
23
|
+
exports.generateBrunoResolver = new GenerateBrunoResolver();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./resolver.types"), exports);
|
|
18
|
+
__exportStar(require("./create-config.resolver"), exports);
|
|
19
|
+
__exportStar(require("./add-resource.resolver"), exports);
|
|
20
|
+
__exportStar(require("./add-route.resolver"), exports);
|
|
21
|
+
__exportStar(require("./generate-bruno.resolver"), exports);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCreateSummary = void 0;
|
|
4
|
+
function formatCreateSummary(plan) {
|
|
5
|
+
return [
|
|
6
|
+
`Project: ${plan.name}`,
|
|
7
|
+
`Framework: ${plan.framework}`,
|
|
8
|
+
`Architecture: ${plan.architecture}`,
|
|
9
|
+
`Databases: ${formatList(plan.capabilities.databases)}`,
|
|
10
|
+
`Auth: ${formatList(plan.capabilities.auth)}`,
|
|
11
|
+
`Messaging: ${formatList(plan.capabilities.messaging)}`,
|
|
12
|
+
`Realtime: ${formatList(plan.capabilities.realtime)}`,
|
|
13
|
+
`Telemetry: ${formatList(plan.capabilities.telemetry)}`,
|
|
14
|
+
`Docs: ${formatList(plan.capabilities.docs)}`,
|
|
15
|
+
`Contracts: ${formatList(plan.capabilities.contracts)}`,
|
|
16
|
+
`API client: ${formatList(plan.capabilities.apiClient)}`,
|
|
17
|
+
`Zones: ${formatList(plan.zones)}`,
|
|
18
|
+
`Package manager: ${plan.packageManager}`,
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
exports.formatCreateSummary = formatCreateSummary;
|
|
22
|
+
function formatList(values) {
|
|
23
|
+
return values.length > 0 ? values.join(", ") : "none";
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./create-summary";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./create-summary"), exports);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface NameVariants {
|
|
2
|
+
rawName: string;
|
|
3
|
+
pascalName: string;
|
|
4
|
+
camelName: string;
|
|
5
|
+
kebabName: string;
|
|
6
|
+
snakeName: string;
|
|
7
|
+
constantName: string;
|
|
8
|
+
pluralName: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function pluralize(value: string): string;
|
|
11
|
+
export declare function createNameVariants(name: string): NameVariants;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNameVariants = exports.pluralize = void 0;
|
|
4
|
+
const change_case_1 = require("change-case");
|
|
5
|
+
function pluralize(value) {
|
|
6
|
+
if (value.endsWith("s")) {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
if (value.endsWith("y") && !/[aeiou]y$/i.test(value)) {
|
|
10
|
+
return `${value.slice(0, -1)}ies`;
|
|
11
|
+
}
|
|
12
|
+
if (/(s|x|z|ch|sh)$/i.test(value)) {
|
|
13
|
+
return `${value}es`;
|
|
14
|
+
}
|
|
15
|
+
return `${value}s`;
|
|
16
|
+
}
|
|
17
|
+
exports.pluralize = pluralize;
|
|
18
|
+
function createNameVariants(name) {
|
|
19
|
+
const kebabName = (0, change_case_1.paramCase)(name);
|
|
20
|
+
return {
|
|
21
|
+
rawName: name,
|
|
22
|
+
pascalName: (0, change_case_1.pascalCase)(name),
|
|
23
|
+
camelName: (0, change_case_1.camelCase)(name),
|
|
24
|
+
kebabName,
|
|
25
|
+
snakeName: (0, change_case_1.snakeCase)(name),
|
|
26
|
+
constantName: (0, change_case_1.constantCase)(name),
|
|
27
|
+
pluralName: pluralize(kebabName),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
exports.createNameVariants = createNameVariants;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderTemplate(template: string, values: Record<string, string | number | boolean | undefined>): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderTemplate = void 0;
|
|
4
|
+
function renderTemplate(template, values) {
|
|
5
|
+
return template.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_, key) => {
|
|
6
|
+
const value = values[key];
|
|
7
|
+
return value === undefined ? "" : String(value);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
exports.renderTemplate = renderTemplate;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveTemplate = exports.registerTemplate = void 0;
|
|
4
|
+
const errors_1 = require("../core/errors");
|
|
5
|
+
const templates = new Map();
|
|
6
|
+
function registerTemplate(name, content) {
|
|
7
|
+
templates.set(name, content);
|
|
8
|
+
}
|
|
9
|
+
exports.registerTemplate = registerTemplate;
|
|
10
|
+
function resolveTemplate(name) {
|
|
11
|
+
const content = templates.get(name);
|
|
12
|
+
if (!content) {
|
|
13
|
+
throw new errors_1.CliError(`Missing template: ${name}`);
|
|
14
|
+
}
|
|
15
|
+
return content;
|
|
16
|
+
}
|
|
17
|
+
exports.resolveTemplate = resolveTemplate;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canRunInteractiveMode = exports.getTerminalCapabilities = void 0;
|
|
4
|
+
function getTerminalCapabilities() {
|
|
5
|
+
return {
|
|
6
|
+
stdinIsTty: Boolean(process.stdin.isTTY),
|
|
7
|
+
stdoutIsTty: Boolean(process.stdout.isTTY),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
exports.getTerminalCapabilities = getTerminalCapabilities;
|
|
11
|
+
function canRunInteractiveMode(capabilities = getTerminalCapabilities()) {
|
|
12
|
+
return capabilities.stdinIsTty && capabilities.stdoutIsTty;
|
|
13
|
+
}
|
|
14
|
+
exports.canRunInteractiveMode = canRunInteractiveMode;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# ADR 0001: Soap CLI Project-Aware Generator
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
SoapJS CLI generates service projects and later modifies them by adding resources, routes, API clients, documentation, auth, and infrastructure capabilities. The generator has to be repeatable, safe around user edits, and explicit about which files it owns.
|
|
10
|
+
|
|
11
|
+
The MVP intentionally targets predictable project shapes instead of arbitrary existing codebases.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
SoapJS CLI is a template-based, project-aware generator.
|
|
16
|
+
|
|
17
|
+
Generated projects include a `.soap` directory:
|
|
18
|
+
|
|
19
|
+
- `.soap/project.json` stores selected capabilities and project metadata.
|
|
20
|
+
- `.soap/structure.json` stores source layout conventions.
|
|
21
|
+
- `.soap/api.json` stores API runtime settings used by generators.
|
|
22
|
+
- `.soap/registry.json` stores resources, routes, generated files, owners, and hashes.
|
|
23
|
+
|
|
24
|
+
The registry is the authority for follow-up commands such as `soap add`, `soap generate`, `soap check`, `soap remove`, and `soap update config`.
|
|
25
|
+
|
|
26
|
+
## Why `.soap` Exists
|
|
27
|
+
|
|
28
|
+
The CLI needs project-local state that is independent of package manager files and source code formatting. `.soap` gives commands a stable contract for:
|
|
29
|
+
|
|
30
|
+
- enabled capabilities
|
|
31
|
+
- expected project structure
|
|
32
|
+
- API conventions
|
|
33
|
+
- generated resource and route metadata
|
|
34
|
+
- generated file ownership and hashes
|
|
35
|
+
|
|
36
|
+
This avoids guessing project state from source files.
|
|
37
|
+
|
|
38
|
+
## Why The Registry Exists
|
|
39
|
+
|
|
40
|
+
The registry makes generated changes deterministic and safe.
|
|
41
|
+
|
|
42
|
+
It records:
|
|
43
|
+
|
|
44
|
+
- generated resources and route paths
|
|
45
|
+
- generated file paths
|
|
46
|
+
- generated file type and owner
|
|
47
|
+
- content hashes from the last CLI write
|
|
48
|
+
|
|
49
|
+
This allows the CLI to detect manual edits and skip, overwrite, or write `.new` files depending on command options.
|
|
50
|
+
|
|
51
|
+
Without a registry, commands like `soap remove resource users` would have to infer ownership from filenames and could delete user-owned files.
|
|
52
|
+
|
|
53
|
+
## Why The MVP Avoids Broad AST Mutation
|
|
54
|
+
|
|
55
|
+
AST mutation is useful when editing arbitrary source code, but it increases complexity and risk:
|
|
56
|
+
|
|
57
|
+
- user formatting and local patterns vary
|
|
58
|
+
- imports and decorators can be organized many ways
|
|
59
|
+
- partial edits can leave code in a hard-to-debug state
|
|
60
|
+
- reliable idempotence requires many language-aware edge cases
|
|
61
|
+
|
|
62
|
+
The MVP instead regenerates known composition files and generated artifacts from templates. This keeps behavior predictable and makes safety checks based on registry hashes straightforward.
|
|
63
|
+
|
|
64
|
+
Future versions can add narrow AST transforms where there is a clear contract and test coverage.
|
|
65
|
+
|
|
66
|
+
## Why Bruno Is Generated
|
|
67
|
+
|
|
68
|
+
Bruno collections are generated from the route registry so API test artifacts stay aligned with generated routes.
|
|
69
|
+
|
|
70
|
+
This gives every generated project a runnable local API smoke path:
|
|
71
|
+
|
|
72
|
+
- health check
|
|
73
|
+
- auth requests when auth is enabled
|
|
74
|
+
- resource CRUD requests
|
|
75
|
+
- optional E2E CRUD flow
|
|
76
|
+
|
|
77
|
+
Because Bruno files are tracked in the registry, user edits are protected by the same hash-based safety behavior as TypeScript files.
|
|
78
|
+
|
|
79
|
+
## Why Optional Adapters Are Wired In The Composition Root
|
|
80
|
+
|
|
81
|
+
Optional adapters such as Mongo, Postgres, Kafka, OpenAPI, auth, and WebSocket support are wired in generated composition files:
|
|
82
|
+
|
|
83
|
+
- `src/index.ts`
|
|
84
|
+
- `src/config/config.ts`
|
|
85
|
+
- `src/config/dependencies.ts`
|
|
86
|
+
- `src/config/controllers.ts`
|
|
87
|
+
- `src/config/resources.ts`
|
|
88
|
+
|
|
89
|
+
This keeps feature code focused on domain/application concerns and keeps infrastructure selection in one predictable layer.
|
|
90
|
+
|
|
91
|
+
It also makes `soap update config` practical: adding a capability rewrites known infrastructure files and adds missing adapter files without invasive edits across resource code.
|
|
92
|
+
|
|
93
|
+
## Consequences
|
|
94
|
+
|
|
95
|
+
Positive:
|
|
96
|
+
|
|
97
|
+
- deterministic generated output
|
|
98
|
+
- safer overwrite/remove behavior
|
|
99
|
+
- simple validation through `soap check routes`
|
|
100
|
+
- straightforward generated project structure
|
|
101
|
+
- capability updates are practical in the MVP
|
|
102
|
+
|
|
103
|
+
Tradeoffs:
|
|
104
|
+
|
|
105
|
+
- arbitrary app migrations are out of scope
|
|
106
|
+
- manually rewritten composition files may be skipped unless `--force` is used
|
|
107
|
+
- the CLI favors project conventions over flexible source discovery
|
|
108
|
+
- some future changes may require explicit migration commands
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# `soap add resource`
|
|
2
|
+
|
|
3
|
+
Add a resource to an existing SoapJS project.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
soap add resource invoice --crud --db postgres --auth jwt --zone public
|
|
7
|
+
soap add resource invoice --crud --db mysql --auth jwt --zone private
|
|
8
|
+
soap add resource note --crud --db sqlite
|
|
9
|
+
soap add resource product --crud --field title:string --field price:number --field active:boolean:optional
|
|
10
|
+
soap add resource report --crud --auth jwt --policy roles:admin,editor
|
|
11
|
+
soap add resource invoice -i
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Interactive Flow
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
soap add resource invoice -i
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The command reads `.soap` before prompting and only offers enabled project capabilities:
|
|
21
|
+
|
|
22
|
+
- CRUD generation
|
|
23
|
+
- storage from enabled databases plus `none`
|
|
24
|
+
- route auth from enabled auth strategies plus `none`
|
|
25
|
+
- API zone from project zones
|
|
26
|
+
- Bruno generation when Bruno is enabled
|
|
27
|
+
- enabling Bruno when it is not enabled
|
|
28
|
+
- optional dry-run-first preview
|
|
29
|
+
|
|
30
|
+
Interactive mode prints the expanded resource plan before writing and asks for confirmation.
|
|
31
|
+
|
|
32
|
+
Use `--yes` to skip final confirmation:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
soap add resource invoice -i --yes
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Dry Run
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
soap add resource invoice --crud --db postgres --dry-run
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Dry run prints the generated file groups without writing files.
|
|
45
|
+
|
|
46
|
+
## Fields
|
|
47
|
+
|
|
48
|
+
Store resource field metadata in `.soap/registry.json`:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
soap add resource product --crud \
|
|
52
|
+
--field title:string \
|
|
53
|
+
--field price:number \
|
|
54
|
+
--field active:boolean:optional
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Supported field types:
|
|
58
|
+
|
|
59
|
+
- `string`
|
|
60
|
+
- `number`
|
|
61
|
+
- `boolean`
|
|
62
|
+
- `date`
|
|
63
|
+
|
|
64
|
+
Fields are used by generated entity props, Zod contracts when enabled, and Bruno request bodies. If no fields are provided, the generator uses `name:string`.
|
|
65
|
+
|
|
66
|
+
## Storage
|
|
67
|
+
|
|
68
|
+
Resource repositories are generated for:
|
|
69
|
+
|
|
70
|
+
- `none` -> in-memory repository
|
|
71
|
+
- `mongo` -> Mongo repository
|
|
72
|
+
- `postgres` -> SQL repository using PostgreSQL config
|
|
73
|
+
- `mysql` -> SQL repository using MySQL config
|
|
74
|
+
- `sqlite` -> SQL repository using SQLite config
|
|
75
|
+
|
|
76
|
+
`redis` is currently infrastructure-only and does not generate a resource repository adapter.
|
|
77
|
+
|
|
78
|
+
## Auth Policies
|
|
79
|
+
|
|
80
|
+
Attach a policy to protected generated routes:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
soap add resource report --crud --auth jwt --policy roles:admin,editor
|
|
84
|
+
soap add resource audit-log --crud --auth api-key --policy admin
|
|
85
|
+
soap add resource invoice --crud --auth jwt --policy custom:billing-owner
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Supported policies:
|
|
89
|
+
|
|
90
|
+
- `admin` -> `@AdminOnly('<strategy>')`
|
|
91
|
+
- `roles:a,b` -> `@Auth('<strategy>', { roles: ['a', 'b'] })`
|
|
92
|
+
- `custom:name` -> `@Auth('<strategy>', { policy: 'name' })`
|
|
93
|
+
|
|
94
|
+
Policies require route auth. Use `--auth jwt` or `--auth api-key`.
|
|
95
|
+
|
|
96
|
+
## CRUD Route Matrix
|
|
97
|
+
|
|
98
|
+
Override per-operation route metadata:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
soap add resource report --crud \
|
|
102
|
+
--crud-route list:get:/search:jwt:private:admin:no-bruno \
|
|
103
|
+
--crud-route create:post:/submit:jwt:private:roles=admin,editor:bruno \
|
|
104
|
+
--crud-route update:patch:/:id:jwt:private:custom=report-owner:bruno
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Format:
|
|
108
|
+
|
|
109
|
+
```txt
|
|
110
|
+
operation:method:path[:auth][:zone][:policy][:bruno|no-bruno]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Supported operations are `list`, `get`, `create`, `update`, and `delete`. Paths are relative to the resource base unless they already start with the resource path. Matrix policies use `admin`, `roles=a,b`, or `custom=name`. `no-bruno` excludes the operation from generated Bruno requests.
|
|
114
|
+
|
|
115
|
+
## Contracts
|
|
116
|
+
|
|
117
|
+
When the project has `contracts: ["zod"]`, generated resource route contracts parse input through Zod schemas.
|
|
118
|
+
|
|
119
|
+
## Bruno
|
|
120
|
+
|
|
121
|
+
If Bruno is enabled, resource generation refreshes the Bruno collection.
|
|
122
|
+
|
|
123
|
+
If Bruno is disabled, interactive mode can enable it. Non-interactive mode can do the same explicitly:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
soap add resource invoice --enable-bruno --bruno
|
|
127
|
+
```
|