swallowkit 1.0.0-beta.9 → 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/README.ja.md +87 -181
- package/README.md +87 -238
- package/dist/__tests__/fixtures.d.ts +12 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -1
- package/dist/__tests__/fixtures.js +101 -0
- package/dist/__tests__/fixtures.js.map +1 -1
- package/dist/cli/commands/add-auth.d.ts +15 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +790 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +164 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +20 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +27 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
- package/dist/cli/commands/dev-seeds.js +188 -6
- package/dist/cli/commands/dev-seeds.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +30 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +495 -129
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +69 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +788 -766
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +24 -3
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +408 -189
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +335 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +13 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +151 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/auth-operations.d.ts +61 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +272 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +185 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/file-session.d.ts +56 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +203 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +37 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +248 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +51 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +187 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/agent-instructions.d.ts +29 -0
- package/dist/core/project/agent-instructions.d.ts.map +1 -0
- package/dist/core/project/agent-instructions.js +160 -0
- package/dist/core/project/agent-instructions.js.map +1 -0
- package/dist/core/project/artifacts.d.ts +58 -0
- package/dist/core/project/artifacts.d.ts.map +1 -0
- package/dist/core/project/artifacts.js +179 -0
- package/dist/core/project/artifacts.js.map +1 -0
- package/dist/core/project/boundaries.d.ts +50 -0
- package/dist/core/project/boundaries.d.ts.map +1 -0
- package/dist/core/project/boundaries.js +117 -0
- package/dist/core/project/boundaries.js.map +1 -0
- package/dist/core/project/capabilities.d.ts +58 -0
- package/dist/core/project/capabilities.d.ts.map +1 -0
- package/dist/core/project/capabilities.js +135 -0
- package/dist/core/project/capabilities.js.map +1 -0
- package/dist/core/project/drift.d.ts +29 -0
- package/dist/core/project/drift.d.ts.map +1 -0
- package/dist/core/project/drift.js +188 -0
- package/dist/core/project/drift.js.map +1 -0
- package/dist/core/project/infra.d.ts +49 -0
- package/dist/core/project/infra.d.ts.map +1 -0
- package/dist/core/project/infra.js +167 -0
- package/dist/core/project/infra.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/state.d.ts +15 -0
- package/dist/core/project/state.d.ts.map +1 -0
- package/dist/core/project/state.js +106 -0
- package/dist/core/project/state.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +274 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/project/workflows.d.ts +43 -0
- package/dist/core/project/workflows.d.ts.map +1 -0
- package/dist/core/project/workflows.js +299 -0
- package/dist/core/project/workflows.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +63 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1774 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1029 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +4 -3
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +405 -134
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +229 -78
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +753 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +152 -8
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +17 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +174 -34
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +69 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +412 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/machine/contracts.d.ts +24 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +14 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +42 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +566 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +425 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +105 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +7 -11
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +42 -17
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +21 -0
- package/dist/utils/python-uv.d.ts.map +1 -0
- package/dist/utils/python-uv.js +111 -0
- package/dist/utils/python-uv.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +49 -0
- package/dist/version.js.map +1 -0
- package/package.json +15 -17
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
- package/src/__tests__/agent-instructions.test.ts +237 -0
- package/src/__tests__/agent-loop-phase34.test.ts +513 -0
- package/src/__tests__/auth.test.ts +770 -0
- package/src/__tests__/bff-swa-auth.test.ts +275 -0
- package/src/__tests__/config.test.ts +176 -3
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +459 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
- package/src/__tests__/dev-seeds.test.ts +124 -0
- package/src/__tests__/dev.test.ts +254 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/fixtures.ts +101 -0
- package/src/__tests__/functions-generator.test.ts +137 -1
- package/src/__tests__/init.test.ts +366 -2
- package/src/__tests__/jest-globals.d.ts +8 -0
- package/src/__tests__/machine.test.ts +371 -0
- package/src/__tests__/mcp.test.ts +253 -0
- package/src/__tests__/model-parser.test.ts +142 -0
- package/src/__tests__/multi-auth.test.ts +160 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/__tests__/package-manager.test.ts +59 -4
- package/src/__tests__/plan-apply.test.ts +499 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +287 -24
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +104 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +853 -0
- package/src/cli/commands/add-connector.ts +159 -0
- package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
- package/src/cli/commands/create-model.ts +21 -100
- package/src/cli/commands/dev-seeds.ts +278 -7
- package/src/cli/commands/dev.ts +607 -160
- package/src/cli/commands/init.ts +851 -786
- package/src/cli/commands/scaffold.ts +531 -236
- package/src/cli/index.ts +338 -60
- package/src/core/config.ts +145 -9
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/auth-operations.ts +360 -0
- package/src/core/operations/create-model.ts +191 -0
- package/src/core/operations/file-session.ts +222 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +258 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +77 -0
- package/src/core/operations/scaffold-plan.ts +215 -0
- package/src/core/project/agent-instructions.ts +167 -0
- package/src/core/project/agent-templates/AGENTS.md +504 -0
- package/src/core/project/agent-templates/CLAUDE.md +31 -0
- package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
- package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
- package/src/core/project/agent-templates/copilot-instructions.md +27 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
- package/src/core/project/artifacts.ts +205 -0
- package/src/core/project/boundaries.ts +170 -0
- package/src/core/project/capabilities.ts +193 -0
- package/src/core/project/drift.ts +202 -0
- package/src/core/project/infra.ts +196 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/state.ts +77 -0
- package/src/core/project/validation.ts +295 -0
- package/src/core/project/workflows.ts +295 -0
- package/src/core/scaffold/auth-generator.ts +1824 -0
- package/src/core/scaffold/connector-functions-generator.ts +1133 -0
- package/src/core/scaffold/functions-generator.ts +432 -134
- package/src/core/scaffold/model-parser.ts +244 -90
- package/src/core/scaffold/native-schema-generator.ts +904 -0
- package/src/core/scaffold/nextjs-generator.ts +174 -9
- package/src/core/scaffold/ui-generator.ts +191 -36
- package/src/core/verify/index.ts +468 -0
- package/src/machine/contracts.ts +33 -0
- package/src/machine/errors.ts +45 -0
- package/src/machine/index.ts +690 -0
- package/src/mcp/index.ts +423 -0
- package/src/types/index.ts +137 -0
- package/src/utils/package-manager.ts +41 -19
- package/src/utils/python-uv.ts +96 -0
- package/src/version.ts +13 -0
|
@@ -5,25 +5,53 @@
|
|
|
5
5
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
|
-
import {
|
|
9
|
-
import { getBackendLanguage, ensureSwallowKitProject } from "../../core/config";
|
|
10
|
-
import { ModelInfo, parseModelFile, toKebabCase, toPascalCase } from "../../core/scaffold/model-parser";
|
|
8
|
+
import { spawnSync } from "child_process";
|
|
9
|
+
import { getBackendLanguage, getConnectorDefinition, getAuthConfig, getFullConfig, getNormalizedAuthConfig, ensureSwallowKitProject, normalizeAuthConfig, validateConfig } from "../../core/config";
|
|
10
|
+
import { ModelInfo, parseModelFile, toKebabCase, toPascalCase, toCamelCase } from "../../core/scaffold/model-parser";
|
|
11
11
|
import {
|
|
12
12
|
generateCSharpAzureFunctionsCRUD,
|
|
13
13
|
generateCompactAzureFunctionsCRUD,
|
|
14
14
|
generatePythonAzureFunctionsCRUD,
|
|
15
15
|
} from "../../core/scaffold/functions-generator";
|
|
16
|
-
import {
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
generateRdbConnectorFunctionTS,
|
|
18
|
+
generateApiConnectorFunctionTS,
|
|
19
|
+
generateRdbConnectorFunctionCSharp,
|
|
20
|
+
generateApiConnectorFunctionCSharp,
|
|
21
|
+
generateRdbConnectorFunctionPython,
|
|
22
|
+
generateApiConnectorFunctionPython,
|
|
23
|
+
} from "../../core/scaffold/connector-functions-generator";
|
|
24
|
+
import { generateCompactBFFRoutes, generateBFFCallFunction, generateConnectorBFFRoutes } from "../../core/scaffold/nextjs-generator";
|
|
18
25
|
import {
|
|
19
26
|
generateListPage,
|
|
20
27
|
generateDetailPage,
|
|
21
28
|
generateFormComponent,
|
|
22
29
|
generateNewPage,
|
|
23
30
|
generateEditPage,
|
|
31
|
+
generateAuthLayout,
|
|
32
|
+
UIAuthOptions,
|
|
24
33
|
} from "../../core/scaffold/ui-generator";
|
|
34
|
+
import { generateLanguageSchemaArtifacts, validateNativeSchemaToolchain } from "../../core/scaffold/native-schema-generator";
|
|
25
35
|
import { detectFromProject, getCommands } from "../../utils/package-manager";
|
|
26
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
BackendLanguage,
|
|
38
|
+
RdbConnectorConfig,
|
|
39
|
+
ApiConnectorConfig,
|
|
40
|
+
RdbModelConnectorConfig,
|
|
41
|
+
ApiModelConnectorConfig,
|
|
42
|
+
ModelAuthPolicy,
|
|
43
|
+
AuthConfig,
|
|
44
|
+
} from "../../types";
|
|
45
|
+
import { syncProjectManifest } from "../../core/project/manifest";
|
|
46
|
+
import { generateBFFCallFunctionWithAuth, generateBFFCallFunctionWithMultipleAuth, generateBFFCallFunctionWithSwaAuth, generateNamedAuthRouterCSharp, generateNamedAuthRouterPython, generateNamedAuthRouterTS } from "../../core/scaffold/auth-generator";
|
|
47
|
+
import {
|
|
48
|
+
FileOperationSession,
|
|
49
|
+
getActiveFileSession,
|
|
50
|
+
hashFileIfExists,
|
|
51
|
+
} from "../../core/operations/file-session";
|
|
52
|
+
import { recordSessionOperations } from "../../core/project/artifacts";
|
|
53
|
+
|
|
54
|
+
const SCAFFOLD_GENERATOR = "scaffold";
|
|
27
55
|
|
|
28
56
|
interface ScaffoldOptions {
|
|
29
57
|
model: string; // モデルファイルのパス(例: "lib/models/todo.ts" or "todo")
|
|
@@ -32,10 +60,36 @@ interface ScaffoldOptions {
|
|
|
32
60
|
apiOnly?: boolean; // true の場合、UI を生成しない(デフォルト: false)
|
|
33
61
|
}
|
|
34
62
|
|
|
63
|
+
function getMachineAwareStdio(): "inherit" | "pipe" {
|
|
64
|
+
return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runSpawnSyncCommand(command: string, args: string[], cwd: string): void {
|
|
68
|
+
const result = spawnSync(command, args, {
|
|
69
|
+
cwd,
|
|
70
|
+
stdio: getMachineAwareStdio(),
|
|
71
|
+
shell: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
75
|
+
throw new Error(`${command} ${args.join(" ")} exited with code ${result.status}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (result.error) {
|
|
79
|
+
throw result.error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
35
83
|
export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
36
84
|
// SwallowKit プロジェクトディレクトリかどうかを検証
|
|
37
85
|
ensureSwallowKitProject("scaffold");
|
|
38
86
|
|
|
87
|
+
const configValidation = validateConfig(getFullConfig());
|
|
88
|
+
if (!configValidation.valid) throw new Error(`Invalid SwallowKit configuration:\n${configValidation.errors.map(error => `- ${error}`).join("\n")}`);
|
|
89
|
+
|
|
90
|
+
// Plan(dry-run) や Apply からセッションが供給される。単体実行時は commit セッションを生成。
|
|
91
|
+
const session = getActiveFileSession() ?? new FileOperationSession("commit");
|
|
92
|
+
|
|
39
93
|
console.log("🏗️ SwallowKit Scaffold: Generating CRUD operations...\n");
|
|
40
94
|
|
|
41
95
|
try {
|
|
@@ -51,6 +105,13 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
51
105
|
const backendLanguage = getBackendLanguage();
|
|
52
106
|
console.log(`🧠 Backend language: ${backendLanguage}`);
|
|
53
107
|
|
|
108
|
+
// コネクタモデルかどうかを判定
|
|
109
|
+
const isConnectorModel = !!modelInfo.connectorConfig;
|
|
110
|
+
if (isConnectorModel) {
|
|
111
|
+
console.log(`🔌 Connector model detected: ${modelInfo.connectorConfig!.connector}`);
|
|
112
|
+
console.log(` Operations: ${modelInfo.connectorConfig!.operations.join(", ")}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
54
115
|
// ネストスキーマ参照があれば表示
|
|
55
116
|
if (modelInfo.nestedSchemaRefs.length > 0) {
|
|
56
117
|
console.log(`🔗 Nested schema references detected:`);
|
|
@@ -74,28 +135,74 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
74
135
|
const relatedModels = backendLanguage === "typescript"
|
|
75
136
|
? [modelInfo]
|
|
76
137
|
: await collectModelGraph(modelPath);
|
|
138
|
+
for (const relatedModel of relatedModels) {
|
|
139
|
+
session.registerInput(relatedModel.filePath);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 4b. 書き込み前に外部 codegen toolchain を検証し、失敗時はプロジェクト無変更で終了する
|
|
143
|
+
if (session.mode === "commit" && backendLanguage !== "typescript" && !isConnectorModel) {
|
|
144
|
+
await validateNativeSchemaToolchain(
|
|
145
|
+
relatedModels,
|
|
146
|
+
modelInfo,
|
|
147
|
+
backendLanguage,
|
|
148
|
+
path.join(process.cwd(), functionsDir)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (options.apiOnly) {
|
|
153
|
+
console.log("ℹ️ --api-only skips UI generation only; Functions, BFF routes, OpenAPI, and native schema assets are still updated.");
|
|
154
|
+
}
|
|
77
155
|
|
|
78
156
|
// 5. Generate BFF callFunction helper
|
|
79
|
-
await generateCallFunctionHelper();
|
|
157
|
+
await generateCallFunctionHelper(session);
|
|
80
158
|
|
|
81
159
|
// 6. Generate Azure Functions code
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
await
|
|
160
|
+
if (isConnectorModel) {
|
|
161
|
+
await generateConnectorFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage, session);
|
|
162
|
+
// 6b. Install connector driver dependencies
|
|
163
|
+
await installConnectorDriverDependencies(modelInfo, functionsDir, backendLanguage, session);
|
|
164
|
+
} else {
|
|
165
|
+
await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage, session);
|
|
166
|
+
|
|
167
|
+
if (backendLanguage !== "typescript") {
|
|
168
|
+
await generateLanguageSchemaArtifacts(relatedModels, modelInfo, functionsDir, backendLanguage, session);
|
|
169
|
+
}
|
|
86
170
|
}
|
|
87
171
|
|
|
88
172
|
// 7. Generate Next.js BFF API Routes
|
|
89
173
|
const apiDir = options.apiDir || "app/api";
|
|
90
|
-
|
|
174
|
+
if (isConnectorModel) {
|
|
175
|
+
await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName, session);
|
|
176
|
+
} else {
|
|
177
|
+
await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
|
|
178
|
+
}
|
|
91
179
|
|
|
92
|
-
// 8. Generate Cosmos DB container Bicep file
|
|
93
|
-
|
|
180
|
+
// 8. Generate Cosmos DB container Bicep file (skip for connector models)
|
|
181
|
+
if (!isConnectorModel) {
|
|
182
|
+
await generateCosmosContainer(modelInfo, session);
|
|
183
|
+
}
|
|
94
184
|
|
|
95
185
|
// 9. Generate UI components (unless --api-only)
|
|
96
186
|
if (!options.apiOnly) {
|
|
97
|
-
|
|
98
|
-
|
|
187
|
+
const uiAuthConfig = getAuthConfig();
|
|
188
|
+
const uiAuthPolicy = resolveAuthPolicy(modelInfo, uiAuthConfig);
|
|
189
|
+
const uiAuthOptions: UIAuthOptions | undefined =
|
|
190
|
+
uiAuthPolicy && uiAuthConfig && uiAuthConfig.provider !== 'none'
|
|
191
|
+
? createUIAuthOptions(uiAuthPolicy, uiAuthConfig)
|
|
192
|
+
: undefined;
|
|
193
|
+
await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions, session);
|
|
194
|
+
if (uiAuthOptions?.authenticatedFetchImport) {
|
|
195
|
+
applyExternalTokenFetchToGeneratedUi(modelInfo, session, uiAuthOptions.authenticatedFetchImport);
|
|
196
|
+
}
|
|
197
|
+
await updateNavigationMenu(modelInfo, session);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (session.mode === "commit") {
|
|
201
|
+
recordSessionOperations(session.operations, {
|
|
202
|
+
schemaHash: hashFileIfExists(modelPath) ?? undefined,
|
|
203
|
+
sourceModel: modelInfo.name,
|
|
204
|
+
});
|
|
205
|
+
await syncProjectManifest();
|
|
99
206
|
}
|
|
100
207
|
|
|
101
208
|
console.log("\n✅ Scaffold completed successfully!");
|
|
@@ -106,7 +213,7 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
106
213
|
console.log(" 3. Navigate to the model from the homepage menu");
|
|
107
214
|
}
|
|
108
215
|
if (backendLanguage !== "typescript") {
|
|
109
|
-
console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
|
|
216
|
+
console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI export and native schema assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
|
|
110
217
|
}
|
|
111
218
|
console.log(
|
|
112
219
|
` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in your .env.local file`
|
|
@@ -114,17 +221,73 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
114
221
|
console.log(
|
|
115
222
|
` ${options.apiOnly ? (backendLanguage === "typescript" ? "3" : "4") : (backendLanguage === "typescript" ? "5" : "6")}. Configure CosmosDBConnection in functions/local.settings.json`
|
|
116
223
|
);
|
|
117
|
-
console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).
|
|
224
|
+
console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).exec} swallowkit dev' to test the generated code`);
|
|
118
225
|
} catch (error: any) {
|
|
119
226
|
console.error("\n❌ Scaffold failed:", error.message);
|
|
120
227
|
process.exit(1);
|
|
121
228
|
}
|
|
122
229
|
}
|
|
123
230
|
|
|
231
|
+
function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: FileOperationSession, importPath = '@/lib/auth/authenticated-fetch'): void {
|
|
232
|
+
const uiPrefix = `app/${toKebabCase(modelInfo.name)}/`;
|
|
233
|
+
for (const operation of session.operations) {
|
|
234
|
+
if (!operation.path.startsWith(uiPrefix) || !operation.path.endsWith('.tsx') || operation.action === 'delete') continue;
|
|
235
|
+
let content = session.readFile(operation.absolutePath);
|
|
236
|
+
if (!content.includes('fetch(')) continue;
|
|
237
|
+
content = content.replace(/\bfetch\(/g, 'authenticatedFetch(');
|
|
238
|
+
content = content.replace(
|
|
239
|
+
/(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
|
|
240
|
+
`if (res.status === 401) throw new Error('Authentication required');\n $1`
|
|
241
|
+
);
|
|
242
|
+
if (!content.includes(importPath)) {
|
|
243
|
+
const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
|
|
244
|
+
content = `${directive}import { authenticatedFetch } from '${importPath}';\n${content.slice(directive.length)}`;
|
|
245
|
+
}
|
|
246
|
+
session.writeFile(operation.absolutePath, content, {
|
|
247
|
+
ownership: 'managed',
|
|
248
|
+
generator: SCAFFOLD_GENERATOR,
|
|
249
|
+
sourceModel: modelInfo.name,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* モデルの authPolicy と auth config から実効的な認可ポリシーを解決
|
|
256
|
+
* - モデルに authPolicy があればそれを使用
|
|
257
|
+
* - なければ auth.authorization.defaultPolicy に従う
|
|
258
|
+
* - auth 設定がなければ undefined(ガードなし)
|
|
259
|
+
*/
|
|
260
|
+
export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
|
|
261
|
+
// モデルに明示的な authPolicy がある場合はそれを使用
|
|
262
|
+
if (modelInfo.authPolicy) {
|
|
263
|
+
const normalized = normalizeAuthConfig(authConfig);
|
|
264
|
+
for (const policyName of [modelInfo.authPolicy.policy, typeof modelInfo.authPolicy.read === 'string' ? modelInfo.authPolicy.read : undefined, typeof modelInfo.authPolicy.write === 'string' ? modelInfo.authPolicy.write : undefined].filter((value): value is string => Boolean(value))) {
|
|
265
|
+
if (!normalized?.authorization.policies[policyName]) throw new Error(`Model '${modelInfo.name}' references undefined auth policy '${policyName}'. Define auth.authorization.policies.${policyName}.`);
|
|
266
|
+
}
|
|
267
|
+
return modelInfo.authPolicy;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// auth 設定がない場合はガードなし
|
|
271
|
+
if (!authConfig || authConfig.provider === 'none') {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// defaultPolicy が 'authenticated' なら認証のみ(ロール指定なし)のポリシーを返す
|
|
276
|
+
const defaultPolicy = authConfig.authorization?.defaultPolicy === 'public' ? 'anonymous' : authConfig.authorization?.defaultPolicy ?? 'authenticated';
|
|
277
|
+
if (defaultPolicy === 'authenticated') {
|
|
278
|
+
return {}; // 空のポリシー = 認証のみ、ロール制限なし
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (defaultPolicy !== 'anonymous') return { policy: defaultPolicy };
|
|
282
|
+
|
|
283
|
+
// defaultPolicy が 'anonymous' なら認証不要
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
124
287
|
/**
|
|
125
288
|
* モデルファイルのパスを解決
|
|
126
289
|
*/
|
|
127
|
-
function resolveModelPath(modelInput: string): string {
|
|
290
|
+
export function resolveModelPath(modelInput: string): string {
|
|
128
291
|
const cwd = process.cwd();
|
|
129
292
|
|
|
130
293
|
// 絶対パスの場合
|
|
@@ -192,7 +355,7 @@ function readSharedPackageName(): string {
|
|
|
192
355
|
/**
|
|
193
356
|
* Generate BFF callFunction helper (lib/api/call-function.ts)
|
|
194
357
|
*/
|
|
195
|
-
async function generateCallFunctionHelper(): Promise<void> {
|
|
358
|
+
async function generateCallFunctionHelper(session: FileOperationSession): Promise<void> {
|
|
196
359
|
console.log("\n📦 Generating BFF callFunction helper...");
|
|
197
360
|
|
|
198
361
|
const cwd = process.cwd();
|
|
@@ -200,12 +363,21 @@ async function generateCallFunctionHelper(): Promise<void> {
|
|
|
200
363
|
const helperDir = path.join(cwd, "lib", "api");
|
|
201
364
|
const helperPath = path.join(helperDir, "call-function.ts");
|
|
202
365
|
|
|
203
|
-
|
|
204
|
-
|
|
366
|
+
// auth 設定がある場合は Authorization ヘッダー転送版を生成
|
|
367
|
+
const authConfig = getAuthConfig();
|
|
368
|
+
const normalizedAuth = normalizeAuthConfig(authConfig);
|
|
369
|
+
const hasAuth = Boolean(normalizedAuth && Object.keys(normalizedAuth.schemes).length > 0);
|
|
370
|
+
let helperCode: string;
|
|
371
|
+
if (hasAuth) {
|
|
372
|
+
const providers = new Set(Object.values(normalizedAuth!.schemes).map(scheme => scheme.provider));
|
|
373
|
+
helperCode = normalizedAuth!.schemes.default && Object.keys(normalizedAuth!.schemes).length === 1
|
|
374
|
+
? authConfig?.provider === "swa" ? generateBFFCallFunctionWithSwaAuth() : generateBFFCallFunctionWithAuth()
|
|
375
|
+
: providers.has("swa") ? generateBFFCallFunctionWithMultipleAuth() : generateBFFCallFunctionWithAuth();
|
|
376
|
+
} else {
|
|
377
|
+
helperCode = generateBFFCallFunction();
|
|
205
378
|
}
|
|
206
379
|
|
|
207
|
-
|
|
208
|
-
fs.writeFileSync(helperPath, helperCode, "utf-8");
|
|
380
|
+
session.writeFile(helperPath, helperCode, { ownership: "managed", generator: SCAFFOLD_GENERATOR });
|
|
209
381
|
console.log(`✅ Created: ${helperPath}`);
|
|
210
382
|
}
|
|
211
383
|
|
|
@@ -216,11 +388,121 @@ async function generateFunctionsCode(
|
|
|
216
388
|
modelInfo: ModelInfo,
|
|
217
389
|
functionsDir: string,
|
|
218
390
|
sharedPackageName: string,
|
|
219
|
-
backendLanguage: BackendLanguage
|
|
391
|
+
backendLanguage: BackendLanguage,
|
|
392
|
+
session: FileOperationSession
|
|
220
393
|
): Promise<void> {
|
|
221
394
|
console.log("\n🔨 Generating Azure Functions CRUD code...");
|
|
222
395
|
|
|
396
|
+
// Resolve auth policy: model-level authPolicy or global defaultPolicy
|
|
397
|
+
const authConfig = getAuthConfig();
|
|
398
|
+
const authPolicy = resolveAuthPolicy(modelInfo, authConfig);
|
|
399
|
+
if (authPolicy) {
|
|
400
|
+
console.log(`🔐 Auth policy detected: ${JSON.stringify(authPolicy)}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const modelKebab = toKebabCase(modelInfo.name);
|
|
404
|
+
const managedMeta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
|
|
405
|
+
if (backendLanguage === "typescript") {
|
|
406
|
+
const normalizedAuth = getNormalizedAuthConfig();
|
|
407
|
+
if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
|
|
408
|
+
const routerPath = path.join(process.cwd(), functionsDir, "src", "auth", "auth-router.ts");
|
|
409
|
+
session.writeFile(routerPath, generateNamedAuthRouterTS(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
|
|
410
|
+
}
|
|
411
|
+
const functionFilePath = path.join(
|
|
412
|
+
process.cwd(),
|
|
413
|
+
functionsDir,
|
|
414
|
+
"src",
|
|
415
|
+
`${modelKebab}.ts`
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy, authConfig?.provider);
|
|
419
|
+
session.writeFile(functionFilePath, code, managedMeta);
|
|
420
|
+
console.log(`✅ Created: ${functionFilePath}`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (backendLanguage === "csharp") {
|
|
425
|
+
const normalizedAuth = getNormalizedAuthConfig();
|
|
426
|
+
if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
|
|
427
|
+
const routerPath = path.join(process.cwd(), functionsDir, "Auth", "AuthRouter.cs");
|
|
428
|
+
session.writeFile(routerPath, generateNamedAuthRouterCSharp(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
|
|
429
|
+
}
|
|
430
|
+
const crudDir = path.join(process.cwd(), functionsDir, "Crud");
|
|
431
|
+
const functionFilePath = path.join(crudDir, `${modelInfo.name}CrudFunctions.cs`);
|
|
432
|
+
|
|
433
|
+
// Remove init-generated template (singular) to avoid route conflicts
|
|
434
|
+
const templatePath = path.join(crudDir, `${modelInfo.name}Function.cs`);
|
|
435
|
+
if (session.fileExists(templatePath)) {
|
|
436
|
+
session.deleteFile(templatePath, managedMeta);
|
|
437
|
+
console.log(`🗑️ Removed template: ${templatePath}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
session.writeFile(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo, authPolicy), managedMeta);
|
|
441
|
+
console.log(`✅ Created: ${functionFilePath}`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const normalizedAuth = getNormalizedAuthConfig();
|
|
446
|
+
if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
|
|
447
|
+
const routerPath = path.join(process.cwd(), functionsDir, "auth", "auth_router.py");
|
|
448
|
+
session.writeFile(routerPath, generateNamedAuthRouterPython(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
|
|
452
|
+
const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
|
|
453
|
+
|
|
454
|
+
const { blueprint, registration } = generatePythonAzureFunctionsCRUD(modelInfo, authPolicy);
|
|
455
|
+
session.writeFile(blueprintPath, blueprint, managedMeta);
|
|
456
|
+
updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration, session);
|
|
457
|
+
console.log(`✅ Created: ${blueprintPath}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function createUIAuthOptions(policy: ModelAuthPolicy, authConfig: AuthConfig): UIAuthOptions {
|
|
461
|
+
const normalized = normalizeAuthConfig(authConfig);
|
|
462
|
+
const readPolicyName = typeof policy.read === 'string' ? policy.read : policy.policy;
|
|
463
|
+
const writePolicyName = typeof policy.write === 'string' ? policy.write : policy.policy;
|
|
464
|
+
const readNamed = readPolicyName ? normalized?.authorization.policies[readPolicyName] : undefined;
|
|
465
|
+
const named = writePolicyName ? normalized?.authorization.policies[writePolicyName] : undefined;
|
|
466
|
+
const writeRoles = named?.roles ?? (Array.isArray(policy.write) ? policy.write : policy.roles) ?? [];
|
|
467
|
+
const externalScheme = [...(readNamed?.schemes ?? []), ...(named?.schemes ?? [])].find(name => normalized?.schemes[name]?.provider === 'external-token');
|
|
468
|
+
const authScheme = readNamed?.schemes[0] ?? named?.schemes[0];
|
|
469
|
+
const authenticatedFetchImport = externalScheme ? `@/lib/auth/schemes/${toKebabCase(externalScheme)}/authenticated-fetch` : authConfig.provider === 'external-token' ? '@/lib/auth/authenticated-fetch' : undefined;
|
|
470
|
+
const authContextImport = authScheme ? `@/lib/auth/schemes/${toKebabCase(authScheme)}/auth-context` : '@/lib/auth/auth-context';
|
|
471
|
+
return { authPolicy: policy, writeRoles, authenticatedFetchImport, authContextImport };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* コネクタモデル用 Azure Functions コード生成
|
|
476
|
+
*/
|
|
477
|
+
async function generateConnectorFunctionsCode(
|
|
478
|
+
modelInfo: ModelInfo,
|
|
479
|
+
functionsDir: string,
|
|
480
|
+
sharedPackageName: string,
|
|
481
|
+
backendLanguage: BackendLanguage,
|
|
482
|
+
session: FileOperationSession
|
|
483
|
+
): Promise<void> {
|
|
484
|
+
console.log("\n🔌 Generating Connector Azure Functions code...");
|
|
485
|
+
|
|
486
|
+
const connectorConfig = modelInfo.connectorConfig!;
|
|
487
|
+
const connectorDef = getConnectorDefinition(connectorConfig.connector);
|
|
488
|
+
|
|
489
|
+
if (!connectorDef) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Connector '${connectorConfig.connector}' not found in swallowkit.config.js.\n` +
|
|
492
|
+
` Please add it to the 'connectors' section of your configuration.`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Resolve auth policy (same logic as Cosmos model scaffolding)
|
|
497
|
+
const authConfig = getAuthConfig();
|
|
498
|
+
const authPolicy = resolveAuthPolicy(modelInfo, authConfig);
|
|
499
|
+
if (authPolicy) {
|
|
500
|
+
console.log(`🔐 Auth policy detected: ${JSON.stringify(authPolicy)}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
223
503
|
const modelKebab = toKebabCase(modelInfo.name);
|
|
504
|
+
const connectorManagedMeta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
|
|
505
|
+
|
|
224
506
|
if (backendLanguage === "typescript") {
|
|
225
507
|
const functionFilePath = path.join(
|
|
226
508
|
process.cwd(),
|
|
@@ -229,13 +511,26 @@ async function generateFunctionsCode(
|
|
|
229
511
|
`${modelKebab}.ts`
|
|
230
512
|
);
|
|
231
513
|
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
514
|
+
let code: string;
|
|
515
|
+
if (connectorDef.type === "rdb") {
|
|
516
|
+
code = generateRdbConnectorFunctionTS(
|
|
517
|
+
modelInfo, sharedPackageName,
|
|
518
|
+
connectorDef as RdbConnectorConfig,
|
|
519
|
+
connectorConfig as RdbModelConnectorConfig,
|
|
520
|
+
authPolicy,
|
|
521
|
+
authConfig?.provider
|
|
522
|
+
);
|
|
523
|
+
} else {
|
|
524
|
+
code = generateApiConnectorFunctionTS(
|
|
525
|
+
modelInfo, sharedPackageName,
|
|
526
|
+
connectorDef as ApiConnectorConfig,
|
|
527
|
+
connectorConfig as ApiModelConnectorConfig,
|
|
528
|
+
authPolicy,
|
|
529
|
+
authConfig?.provider
|
|
530
|
+
);
|
|
235
531
|
}
|
|
236
532
|
|
|
237
|
-
|
|
238
|
-
fs.writeFileSync(functionFilePath, code, "utf-8");
|
|
533
|
+
session.writeFile(functionFilePath, code, connectorManagedMeta);
|
|
239
534
|
console.log(`✅ Created: ${functionFilePath}`);
|
|
240
535
|
return;
|
|
241
536
|
}
|
|
@@ -244,25 +539,175 @@ async function generateFunctionsCode(
|
|
|
244
539
|
const functionFilePath = path.join(
|
|
245
540
|
process.cwd(),
|
|
246
541
|
functionsDir,
|
|
247
|
-
"
|
|
248
|
-
`${modelInfo.name}
|
|
542
|
+
"Connectors",
|
|
543
|
+
`${modelInfo.name}ConnectorFunctions.cs`
|
|
249
544
|
);
|
|
250
|
-
|
|
251
|
-
|
|
545
|
+
|
|
546
|
+
let code: string;
|
|
547
|
+
if (connectorDef.type === "rdb") {
|
|
548
|
+
code = generateRdbConnectorFunctionCSharp(
|
|
549
|
+
modelInfo,
|
|
550
|
+
connectorDef as RdbConnectorConfig,
|
|
551
|
+
connectorConfig as RdbModelConnectorConfig
|
|
552
|
+
);
|
|
553
|
+
} else {
|
|
554
|
+
code = generateApiConnectorFunctionCSharp(
|
|
555
|
+
modelInfo,
|
|
556
|
+
connectorDef as ApiConnectorConfig,
|
|
557
|
+
connectorConfig as ApiModelConnectorConfig
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
session.writeFile(functionFilePath, code, connectorManagedMeta);
|
|
252
562
|
console.log(`✅ Created: ${functionFilePath}`);
|
|
253
563
|
return;
|
|
254
564
|
}
|
|
255
565
|
|
|
566
|
+
// Python
|
|
256
567
|
const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
|
|
257
568
|
const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
|
|
258
|
-
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
259
569
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
570
|
+
let result: { blueprint: string; registration: string };
|
|
571
|
+
if (connectorDef.type === "rdb") {
|
|
572
|
+
result = generateRdbConnectorFunctionPython(
|
|
573
|
+
modelInfo,
|
|
574
|
+
connectorDef as RdbConnectorConfig,
|
|
575
|
+
connectorConfig as RdbModelConnectorConfig
|
|
576
|
+
);
|
|
577
|
+
} else {
|
|
578
|
+
result = generateApiConnectorFunctionPython(
|
|
579
|
+
modelInfo,
|
|
580
|
+
connectorDef as ApiConnectorConfig,
|
|
581
|
+
connectorConfig as ApiModelConnectorConfig
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
session.writeFile(blueprintPath, result.blueprint, connectorManagedMeta);
|
|
586
|
+
updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), result.registration, session);
|
|
263
587
|
console.log(`✅ Created: ${blueprintPath}`);
|
|
264
588
|
}
|
|
265
589
|
|
|
590
|
+
/**
|
|
591
|
+
* コネクタモデルの scaffold 後に、生成コードが必要とする RDB/API ドライバを
|
|
592
|
+
* functions ディレクトリにインストールする。
|
|
593
|
+
*/
|
|
594
|
+
async function installConnectorDriverDependencies(
|
|
595
|
+
modelInfo: ModelInfo,
|
|
596
|
+
functionsDir: string,
|
|
597
|
+
backendLanguage: BackendLanguage,
|
|
598
|
+
session: FileOperationSession
|
|
599
|
+
): Promise<void> {
|
|
600
|
+
const connectorConfig = modelInfo.connectorConfig!;
|
|
601
|
+
const connectorDef = getConnectorDefinition(connectorConfig.connector);
|
|
602
|
+
if (!connectorDef || connectorDef.type !== "rdb") return;
|
|
603
|
+
|
|
604
|
+
const rdbDef = connectorDef as RdbConnectorConfig;
|
|
605
|
+
const functionsPath = path.join(process.cwd(), functionsDir);
|
|
606
|
+
|
|
607
|
+
if (backendLanguage === "typescript") {
|
|
608
|
+
const driverMap: Record<string, { deps: string[]; devDeps: string[] }> = {
|
|
609
|
+
mysql: { deps: ["mysql2"], devDeps: [] },
|
|
610
|
+
postgres: { deps: ["pg"], devDeps: ["@types/pg"] },
|
|
611
|
+
sqlserver: { deps: ["mssql"], devDeps: [] },
|
|
612
|
+
};
|
|
613
|
+
const entry = driverMap[rdbDef.provider];
|
|
614
|
+
if (!entry) return;
|
|
615
|
+
|
|
616
|
+
// package.json を読んで、既にインストール済みならスキップ
|
|
617
|
+
const pkgJsonPath = path.join(functionsPath, "package.json");
|
|
618
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
619
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
620
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
621
|
+
const missingDeps = entry.deps.filter(d => !allDeps[d]);
|
|
622
|
+
const missingDevDeps = entry.devDeps.filter(d => !allDeps[d]);
|
|
623
|
+
if (missingDeps.length === 0 && missingDevDeps.length === 0) return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (session.mode === "collect") {
|
|
627
|
+
session.addWarning(
|
|
628
|
+
`external-install: ${rdbDef.provider} driver dependencies (${[...entry.deps, ...entry.devDeps].join(", ")}) will be installed into ${functionsDir}/ on apply.`
|
|
629
|
+
);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
console.log(`\n📦 Installing ${rdbDef.provider} driver dependencies...`);
|
|
634
|
+
const pm = detectFromProject(functionsPath);
|
|
635
|
+
const cmds = getCommands(pm);
|
|
636
|
+
|
|
637
|
+
if (entry.deps.length > 0) {
|
|
638
|
+
runSpawnSyncCommand(cmds.name, [pm === "pnpm" ? "add" : "install", ...entry.deps], functionsPath);
|
|
639
|
+
}
|
|
640
|
+
if (entry.devDeps.length > 0) {
|
|
641
|
+
runSpawnSyncCommand(cmds.name, [pm === "pnpm" ? "add" : "install", "-D", ...entry.devDeps], functionsPath);
|
|
642
|
+
}
|
|
643
|
+
console.log(`✅ ${rdbDef.provider} driver installed`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (backendLanguage === "csharp") {
|
|
648
|
+
const nugetMap: Record<string, string> = {
|
|
649
|
+
mysql: "MySqlConnector",
|
|
650
|
+
postgres: "Npgsql",
|
|
651
|
+
sqlserver: "Microsoft.Data.SqlClient",
|
|
652
|
+
};
|
|
653
|
+
const pkg = nugetMap[rdbDef.provider];
|
|
654
|
+
if (!pkg) return;
|
|
655
|
+
if (session.mode === "collect") {
|
|
656
|
+
session.addWarning(`external-install: NuGet package ${pkg} will be added to ${functionsDir}/functions.csproj on apply.`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
console.log(`\n📦 Installing ${rdbDef.provider} NuGet package...`);
|
|
660
|
+
runSpawnSyncCommand("dotnet", ["add", path.join(functionsPath, "functions.csproj"), "package", pkg], functionsPath);
|
|
661
|
+
console.log(`✅ ${pkg} installed`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Python — requirements.txt に追記
|
|
666
|
+
const pipMap: Record<string, string> = {
|
|
667
|
+
mysql: "mysql-connector-python",
|
|
668
|
+
postgres: "psycopg2-binary",
|
|
669
|
+
sqlserver: "pymssql",
|
|
670
|
+
};
|
|
671
|
+
const pipPkg = pipMap[rdbDef.provider];
|
|
672
|
+
if (!pipPkg) return;
|
|
673
|
+
const reqPath = path.join(functionsPath, "requirements.txt");
|
|
674
|
+
if (session.fileExists(reqPath)) {
|
|
675
|
+
const existing = session.readFile(reqPath);
|
|
676
|
+
if (!existing.includes(pipPkg)) {
|
|
677
|
+
console.log(`\n📦 Adding ${pipPkg} to requirements.txt...`);
|
|
678
|
+
session.writeFile(reqPath, `${existing}\n${pipPkg}\n`, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
|
|
679
|
+
console.log(`✅ ${pipPkg} added`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* コネクタモデル用 BFF ルート生成(操作制限に対応)
|
|
686
|
+
*/
|
|
687
|
+
async function generateConnectorBFFRoutesFiles(
|
|
688
|
+
modelInfo: ModelInfo,
|
|
689
|
+
apiDir: string,
|
|
690
|
+
sharedPackageName: string,
|
|
691
|
+
session: FileOperationSession
|
|
692
|
+
): Promise<void> {
|
|
693
|
+
console.log("\n🔌 Generating Connector BFF API routes...");
|
|
694
|
+
|
|
695
|
+
const modelCamel = toCamelCase(modelInfo.name);
|
|
696
|
+
const operations = modelInfo.connectorConfig!.operations;
|
|
697
|
+
|
|
698
|
+
const listRoutePath = path.join(process.cwd(), apiDir, modelCamel, "route.ts");
|
|
699
|
+
const detailRoutePath = path.join(process.cwd(), apiDir, modelCamel, "[id]", "route.ts");
|
|
700
|
+
|
|
701
|
+
const routes = generateConnectorBFFRoutes(modelInfo, sharedPackageName, operations);
|
|
702
|
+
|
|
703
|
+
const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
|
|
704
|
+
session.writeFile(listRoutePath, routes.listRoute, meta);
|
|
705
|
+
session.writeFile(detailRoutePath, routes.detailRoute, meta);
|
|
706
|
+
|
|
707
|
+
console.log(`✅ Created: ${listRoutePath}`);
|
|
708
|
+
console.log(`✅ Created: ${detailRoutePath}`);
|
|
709
|
+
}
|
|
710
|
+
|
|
266
711
|
async function collectModelGraph(modelPath: string, seen = new Map<string, ModelInfo>()): Promise<ModelInfo[]> {
|
|
267
712
|
const resolvedPath = path.resolve(modelPath);
|
|
268
713
|
if (seen.has(resolvedPath)) {
|
|
@@ -298,140 +743,18 @@ function describeFunctionsOutputPath(functionsDir: string, backendLanguage: Back
|
|
|
298
743
|
return `${functionsDir}/blueprints/`;
|
|
299
744
|
}
|
|
300
745
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
functionsDir: string,
|
|
305
|
-
backendLanguage: Exclude<BackendLanguage, "typescript">
|
|
306
|
-
): Promise<void> {
|
|
307
|
-
console.log("\n🧬 Generating OpenAPI-backed schema artifacts...");
|
|
308
|
-
|
|
309
|
-
const openApiDir = path.join(process.cwd(), functionsDir, "openapi");
|
|
310
|
-
fs.mkdirSync(openApiDir, { recursive: true });
|
|
311
|
-
|
|
312
|
-
const specPath = path.join(openApiDir, `${toKebabCase(rootModel.name)}.openapi.json`);
|
|
313
|
-
fs.writeFileSync(specPath, generateOpenApiDocument(models, rootModel), "utf-8");
|
|
314
|
-
console.log(`✅ Created: ${specPath}`);
|
|
315
|
-
|
|
316
|
-
const outputDir = path.join(
|
|
317
|
-
process.cwd(),
|
|
318
|
-
functionsDir,
|
|
319
|
-
"generated",
|
|
320
|
-
backendLanguage === "csharp" ? "csharp-models" : "python-models"
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
324
|
-
await runOpenApiGenerator(specPath, outputDir, backendLanguage);
|
|
325
|
-
if (backendLanguage === "csharp") {
|
|
326
|
-
pruneGeneratedCSharpArtifacts(outputDir);
|
|
746
|
+
function updatePythonFunctionRegistrations(functionAppPath: string, registration: string, session: FileOperationSession): void {
|
|
747
|
+
if (!session.fileExists(functionAppPath)) {
|
|
748
|
+
throw new Error(`Python Functions entrypoint not found: ${functionAppPath}`);
|
|
327
749
|
}
|
|
328
|
-
console.log(`✅ Generated ${backendLanguage} schema assets: ${outputDir}`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async function runOpenApiGenerator(
|
|
332
|
-
specPath: string,
|
|
333
|
-
outputDir: string,
|
|
334
|
-
backendLanguage: Exclude<BackendLanguage, "typescript">
|
|
335
|
-
): Promise<void> {
|
|
336
|
-
const pm = detectFromProject();
|
|
337
|
-
const command = pm === "pnpm" ? "pnpm" : "npx";
|
|
338
|
-
const args = pm === "pnpm"
|
|
339
|
-
? ["exec", "openapi-generator-cli", ...getOpenApiGeneratorArgs(specPath, outputDir, backendLanguage)]
|
|
340
|
-
: ["openapi-generator-cli", ...getOpenApiGeneratorArgs(specPath, outputDir, backendLanguage)];
|
|
341
|
-
|
|
342
|
-
await new Promise<void>((resolve, reject) => {
|
|
343
|
-
const child = spawn(command, args, {
|
|
344
|
-
cwd: process.cwd(),
|
|
345
|
-
shell: true,
|
|
346
|
-
stdio: "inherit",
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
child.on("close", (code) => {
|
|
350
|
-
if (code === 0) {
|
|
351
|
-
resolve();
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
750
|
|
|
355
|
-
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
child.on("error", reject);
|
|
359
|
-
});
|
|
360
|
-
}
|
|
751
|
+
const content = session.readFile(functionAppPath);
|
|
361
752
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
backendLanguage: Exclude<BackendLanguage, "typescript">
|
|
366
|
-
): string[] {
|
|
367
|
-
const globalProperty = backendLanguage === "csharp"
|
|
368
|
-
? "models,supportingFiles,apis=false,modelDocs=false,modelTests=false"
|
|
369
|
-
: "models,apis=false,supportingFiles=false,modelDocs=false,modelTests=false";
|
|
370
|
-
|
|
371
|
-
const baseArgs = [
|
|
372
|
-
"generate",
|
|
373
|
-
"-i",
|
|
374
|
-
specPath,
|
|
375
|
-
"-o",
|
|
376
|
-
outputDir,
|
|
377
|
-
"--global-property",
|
|
378
|
-
globalProperty,
|
|
379
|
-
];
|
|
380
|
-
|
|
381
|
-
if (backendLanguage === "csharp") {
|
|
382
|
-
return [
|
|
383
|
-
...baseArgs,
|
|
384
|
-
"-g",
|
|
385
|
-
"csharp",
|
|
386
|
-
"--additional-properties",
|
|
387
|
-
"packageName=SwallowKitBackendModels,targetFramework=net8.0,nullableReferenceTypes=true",
|
|
388
|
-
];
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
return [
|
|
392
|
-
...baseArgs,
|
|
393
|
-
"-g",
|
|
394
|
-
"python",
|
|
395
|
-
"--additional-properties",
|
|
396
|
-
"packageName=backend_models,projectName=backend-models",
|
|
397
|
-
];
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export function getCSharpSchemaArtifactPruneTargets(outputDir: string): string[] {
|
|
401
|
-
return [
|
|
402
|
-
path.join(outputDir, "src", "SwallowKitBackendModels.Test"),
|
|
403
|
-
path.join(outputDir, "src", "SwallowKitBackendModels", "Api"),
|
|
404
|
-
path.join(outputDir, "src", "SwallowKitBackendModels", "Extensions"),
|
|
405
|
-
];
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function pruneGeneratedCSharpArtifacts(outputDir: string): void {
|
|
409
|
-
for (const target of getCSharpSchemaArtifactPruneTargets(outputDir)) {
|
|
410
|
-
if (fs.existsSync(target)) {
|
|
411
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const clientDir = path.join(outputDir, "src", "SwallowKitBackendModels", "Client");
|
|
416
|
-
if (!fs.existsSync(clientDir)) {
|
|
753
|
+
// Check if import line already exists (handles init-generated layout)
|
|
754
|
+
const importLine = registration.split("\n").find((l) => l.startsWith("from ") || l.startsWith("import "));
|
|
755
|
+
if (importLine && content.includes(importLine)) {
|
|
417
756
|
return;
|
|
418
757
|
}
|
|
419
|
-
|
|
420
|
-
for (const entry of fs.readdirSync(clientDir, { withFileTypes: true })) {
|
|
421
|
-
if (!entry.isFile() || entry.name === "Option.cs") {
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
fs.rmSync(path.join(clientDir, entry.name), { force: true });
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function updatePythonFunctionRegistrations(functionAppPath: string, registration: string): void {
|
|
430
|
-
if (!fs.existsSync(functionAppPath)) {
|
|
431
|
-
throw new Error(`Python Functions entrypoint not found: ${functionAppPath}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const content = fs.readFileSync(functionAppPath, "utf-8");
|
|
435
758
|
if (content.includes(registration)) {
|
|
436
759
|
return;
|
|
437
760
|
}
|
|
@@ -442,7 +765,7 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
|
|
|
442
765
|
}
|
|
443
766
|
|
|
444
767
|
const updated = content.replace(marker, `${registration}\n${marker}`);
|
|
445
|
-
|
|
768
|
+
session.writeFile(functionAppPath, updated, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
|
|
446
769
|
}
|
|
447
770
|
|
|
448
771
|
/**
|
|
@@ -451,11 +774,11 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
|
|
|
451
774
|
async function generateBFFRoutes(
|
|
452
775
|
modelInfo: any,
|
|
453
776
|
apiDir: string,
|
|
454
|
-
sharedPackageName: string
|
|
777
|
+
sharedPackageName: string,
|
|
778
|
+
session: FileOperationSession
|
|
455
779
|
): Promise<void> {
|
|
456
780
|
console.log("\n🔨 Generating Next.js BFF API routes...");
|
|
457
781
|
|
|
458
|
-
const modelKebab = toKebabCase(modelInfo.name);
|
|
459
782
|
const modelCamel = modelInfo.name.charAt(0).toLowerCase() + modelInfo.name.slice(1);
|
|
460
783
|
|
|
461
784
|
// List route: app/api/[model]/route.ts
|
|
@@ -475,24 +798,13 @@ async function generateBFFRoutes(
|
|
|
475
798
|
"route.ts"
|
|
476
799
|
);
|
|
477
800
|
|
|
478
|
-
// ディレクトリを作成
|
|
479
|
-
const listRouteDir = path.dirname(listRoutePath);
|
|
480
|
-
const detailRouteDir = path.dirname(detailRoutePath);
|
|
481
|
-
|
|
482
|
-
if (!fs.existsSync(listRouteDir)) {
|
|
483
|
-
fs.mkdirSync(listRouteDir, { recursive: true });
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (!fs.existsSync(detailRouteDir)) {
|
|
487
|
-
fs.mkdirSync(detailRouteDir, { recursive: true });
|
|
488
|
-
}
|
|
489
|
-
|
|
490
801
|
// コードを生成
|
|
491
802
|
const routes = generateCompactBFFRoutes(modelInfo, sharedPackageName);
|
|
492
803
|
|
|
493
804
|
// ファイルに書き込み
|
|
494
|
-
|
|
495
|
-
|
|
805
|
+
const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
|
|
806
|
+
session.writeFile(listRoutePath, routes.listRoute, meta);
|
|
807
|
+
session.writeFile(detailRoutePath, routes.detailRoute, meta);
|
|
496
808
|
|
|
497
809
|
console.log(`✅ Created: ${listRoutePath}`);
|
|
498
810
|
console.log(`✅ Created: ${detailRoutePath}`);
|
|
@@ -501,7 +813,7 @@ async function generateBFFRoutes(
|
|
|
501
813
|
/**
|
|
502
814
|
* Generate Cosmos DB container Bicep file
|
|
503
815
|
*/
|
|
504
|
-
async function generateCosmosContainer(modelInfo: any): Promise<void> {
|
|
816
|
+
export async function generateCosmosContainer(modelInfo: any, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
|
|
505
817
|
console.log("\n🗄️ Generating Cosmos DB container Bicep file...");
|
|
506
818
|
|
|
507
819
|
const modelKebab = toKebabCase(modelInfo.name);
|
|
@@ -515,13 +827,8 @@ async function generateCosmosContainer(modelInfo: any): Promise<void> {
|
|
|
515
827
|
return;
|
|
516
828
|
}
|
|
517
829
|
|
|
518
|
-
// Check if containers directory exists, if not create it
|
|
519
|
-
const containersDir = path.join(infraDir, "containers");
|
|
520
|
-
if (!fs.existsSync(containersDir)) {
|
|
521
|
-
fs.mkdirSync(containersDir, { recursive: true });
|
|
522
|
-
}
|
|
523
|
-
|
|
524
830
|
// Generate container Bicep file
|
|
831
|
+
const containersDir = path.join(infraDir, "containers");
|
|
525
832
|
const containerFileName = `${modelKebab}-container.bicep`;
|
|
526
833
|
const containerFilePath = path.join(containersDir, containerFileName);
|
|
527
834
|
|
|
@@ -535,13 +842,7 @@ param databaseName string
|
|
|
535
842
|
param containerName string = '${modelPascal}s'
|
|
536
843
|
|
|
537
844
|
@description('Partition key path')
|
|
538
|
-
param partitionKeyPath string = '
|
|
539
|
-
|
|
540
|
-
@description('Throughput (RU/s) - only used for Free Tier')
|
|
541
|
-
param throughput int = 400
|
|
542
|
-
|
|
543
|
-
@description('Cosmos DB mode: freetier or serverless')
|
|
544
|
-
param cosmosDbMode string
|
|
845
|
+
param partitionKeyPath string = '${modelInfo.partitionKey}'
|
|
545
846
|
|
|
546
847
|
// Reference existing Cosmos DB account
|
|
547
848
|
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
@@ -582,9 +883,6 @@ resource container 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/container
|
|
|
582
883
|
]
|
|
583
884
|
}
|
|
584
885
|
}
|
|
585
|
-
options: cosmosDbMode == 'freetier' ? {
|
|
586
|
-
throughput: throughput
|
|
587
|
-
} : {}
|
|
588
886
|
}
|
|
589
887
|
}
|
|
590
888
|
|
|
@@ -592,29 +890,29 @@ output containerName string = container.name
|
|
|
592
890
|
`;
|
|
593
891
|
|
|
594
892
|
// Write Bicep file (overwrite if exists)
|
|
595
|
-
|
|
893
|
+
session.writeFile(containerFilePath, bicepContent, { ownership: "managed", generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name });
|
|
596
894
|
console.log(`✅ Created: ${containerFilePath}`);
|
|
597
895
|
|
|
598
896
|
// Update main.bicep to include this container module
|
|
599
|
-
await updateMainBicepWithContainer(modelKebab, modelPascal);
|
|
897
|
+
await updateMainBicepWithContainer(modelKebab, modelPascal, session);
|
|
600
898
|
}
|
|
601
899
|
|
|
602
900
|
/**
|
|
603
901
|
* Update main.bicep to include new container module
|
|
604
902
|
*/
|
|
605
|
-
async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string): Promise<void> {
|
|
903
|
+
async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string, session: FileOperationSession): Promise<void> {
|
|
606
904
|
const cwd = process.cwd();
|
|
607
905
|
const mainBicepPath = path.join(cwd, "infra", "main.bicep");
|
|
608
906
|
|
|
609
|
-
if (!
|
|
907
|
+
if (!session.fileExists(mainBicepPath)) {
|
|
610
908
|
console.log("ℹ️ main.bicep not found. Please manually add the container module.");
|
|
611
909
|
return;
|
|
612
910
|
}
|
|
613
911
|
|
|
614
|
-
let mainBicepContent =
|
|
912
|
+
let mainBicepContent = session.readFile(mainBicepPath);
|
|
615
913
|
|
|
616
914
|
// Check if container module already exists
|
|
617
|
-
const containerModuleName = `${
|
|
915
|
+
const containerModuleName = `${modelPascal.charAt(0).toLowerCase()}${modelPascal.slice(1)}Container`;
|
|
618
916
|
if (mainBicepContent.includes(`module ${containerModuleName}`)) {
|
|
619
917
|
console.log(`ℹ️ Container module '${containerModuleName}' already exists in main.bicep`);
|
|
620
918
|
return;
|
|
@@ -653,13 +951,8 @@ module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
|
|
|
653
951
|
name: '${modelKebab}-container'
|
|
654
952
|
params: {
|
|
655
953
|
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
656
|
-
databaseName: '
|
|
657
|
-
cosmosDbMode: cosmosDbMode
|
|
954
|
+
databaseName: '\${projectName}Database'
|
|
658
955
|
}
|
|
659
|
-
dependsOn: [
|
|
660
|
-
cosmosDbFreeTier
|
|
661
|
-
cosmosDbServerless
|
|
662
|
-
]
|
|
663
956
|
}`;
|
|
664
957
|
|
|
665
958
|
// Insert the module
|
|
@@ -668,14 +961,14 @@ module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
|
|
|
668
961
|
containerModule +
|
|
669
962
|
mainBicepContent.slice(insertPosition);
|
|
670
963
|
|
|
671
|
-
|
|
964
|
+
session.writeFile(mainBicepPath, mainBicepContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
|
|
672
965
|
console.log(`✅ Added container module to main.bicep`);
|
|
673
966
|
}
|
|
674
967
|
|
|
675
968
|
/**
|
|
676
969
|
* Generate UI components (list, detail, form, create, edit pages)
|
|
677
970
|
*/
|
|
678
|
-
async function generateUIComponents(modelInfo:
|
|
971
|
+
export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
|
|
679
972
|
console.log("\n🎨 Generating UI components...");
|
|
680
973
|
|
|
681
974
|
const modelKebab = toKebabCase(modelInfo.name);
|
|
@@ -689,37 +982,38 @@ async function generateUIComponents(modelInfo: any, sharedPackageName: string):
|
|
|
689
982
|
const idDir = path.join(modelDir, "[id]");
|
|
690
983
|
const editDir = path.join(idDir, "edit");
|
|
691
984
|
|
|
692
|
-
// Create directories
|
|
693
|
-
[componentsDir, newDir, editDir].forEach(dir => {
|
|
694
|
-
if (!fs.existsSync(dir)) {
|
|
695
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
|
|
699
985
|
// Generate and write files
|
|
700
|
-
const listPage = generateListPage(modelInfo, sharedPackageName);
|
|
701
|
-
const detailPage = generateDetailPage(modelInfo, sharedPackageName);
|
|
986
|
+
const listPage = generateListPage(modelInfo, sharedPackageName, authOptions);
|
|
987
|
+
const detailPage = generateDetailPage(modelInfo, sharedPackageName, authOptions);
|
|
702
988
|
const formComponent = generateFormComponent(modelInfo, sharedPackageName);
|
|
703
|
-
const newPage = generateNewPage(modelInfo);
|
|
704
|
-
const editPage = generateEditPage(modelInfo, sharedPackageName);
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
989
|
+
const newPage = generateNewPage(modelInfo, authOptions);
|
|
990
|
+
const editPage = generateEditPage(modelInfo, sharedPackageName, authOptions);
|
|
991
|
+
const authLayout = generateAuthLayout(modelInfo, authOptions);
|
|
992
|
+
|
|
993
|
+
const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
|
|
994
|
+
session.writeFile(path.join(modelDir, "page.tsx"), listPage, meta);
|
|
995
|
+
session.writeFile(path.join(idDir, "page.tsx"), detailPage, meta);
|
|
996
|
+
session.writeFile(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, meta);
|
|
997
|
+
session.writeFile(path.join(newDir, "page.tsx"), newPage, meta);
|
|
998
|
+
session.writeFile(path.join(editDir, "page.tsx"), editPage, meta);
|
|
999
|
+
if (authLayout) {
|
|
1000
|
+
session.writeFile(path.join(modelDir, "layout.tsx"), authLayout, meta);
|
|
1001
|
+
}
|
|
711
1002
|
|
|
712
1003
|
console.log(`✅ Created: ${path.join(modelDir, "page.tsx")}`);
|
|
713
1004
|
console.log(`✅ Created: ${path.join(idDir, "page.tsx")}`);
|
|
714
1005
|
console.log(`✅ Created: ${path.join(componentsDir, `${modelName}Form.tsx`)}`);
|
|
715
1006
|
console.log(`✅ Created: ${path.join(newDir, "page.tsx")}`);
|
|
716
1007
|
console.log(`✅ Created: ${path.join(editDir, "page.tsx")}`);
|
|
1008
|
+
if (authLayout) {
|
|
1009
|
+
console.log(`✅ Created: ${path.join(modelDir, "layout.tsx")}`);
|
|
1010
|
+
}
|
|
717
1011
|
}
|
|
718
1012
|
|
|
719
1013
|
/**
|
|
720
1014
|
* Update navigation menu on the homepage
|
|
721
1015
|
*/
|
|
722
|
-
async function updateNavigationMenu(modelInfo: any): Promise<void> {
|
|
1016
|
+
async function updateNavigationMenu(modelInfo: any, session: FileOperationSession): Promise<void> {
|
|
723
1017
|
console.log("\n📋 Updating navigation menu...");
|
|
724
1018
|
|
|
725
1019
|
const modelKebab = toKebabCase(modelInfo.name);
|
|
@@ -728,13 +1022,13 @@ async function updateNavigationMenu(modelInfo: any): Promise<void> {
|
|
|
728
1022
|
// Update scaffold config
|
|
729
1023
|
const configPath = path.join(cwd, "lib", "scaffold-config.ts");
|
|
730
1024
|
|
|
731
|
-
if (!
|
|
1025
|
+
if (!session.fileExists(configPath)) {
|
|
732
1026
|
console.log("⚠️ scaffold-config.ts not found. Skipping navigation menu update.");
|
|
733
1027
|
return;
|
|
734
1028
|
}
|
|
735
1029
|
|
|
736
1030
|
// Read existing config
|
|
737
|
-
const configContent =
|
|
1031
|
+
const configContent = session.readFile(configPath);
|
|
738
1032
|
|
|
739
1033
|
// Parse models array - extract each model entry
|
|
740
1034
|
const models: Array<{ name: string; path: string; label: string }> = [];
|
|
@@ -781,6 +1075,7 @@ ${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label
|
|
|
781
1075
|
};
|
|
782
1076
|
`;
|
|
783
1077
|
|
|
784
|
-
|
|
1078
|
+
session.writeFile(configPath, newConfigContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
|
|
785
1079
|
console.log(`✅ Added ${modelInfo.name} to navigation menu`);
|
|
786
1080
|
}
|
|
1081
|
+
|