swallowkit 1.0.0-beta.2 → 1.0.0-beta.20

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.
Files changed (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +312 -215
  3. package/README.md +369 -216
  4. package/dist/__tests__/fixtures.d.ts +22 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +146 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/add-auth.d.ts +10 -0
  9. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  10. package/dist/cli/commands/add-auth.js +444 -0
  11. package/dist/cli/commands/add-auth.js.map +1 -0
  12. package/dist/cli/commands/add-connector.d.ts +20 -0
  13. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  14. package/dist/cli/commands/add-connector.js +163 -0
  15. package/dist/cli/commands/add-connector.js.map +1 -0
  16. package/dist/cli/commands/create-model.d.ts +1 -4
  17. package/dist/cli/commands/create-model.d.ts.map +1 -1
  18. package/dist/cli/commands/create-model.js +21 -82
  19. package/dist/cli/commands/create-model.js.map +1 -1
  20. package/dist/cli/commands/dev-seeds.d.ts +35 -0
  21. package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
  22. package/dist/cli/commands/dev-seeds.js +292 -0
  23. package/dist/cli/commands/dev-seeds.js.map +1 -0
  24. package/dist/cli/commands/dev.d.ts +19 -0
  25. package/dist/cli/commands/dev.d.ts.map +1 -1
  26. package/dist/cli/commands/dev.js +476 -117
  27. package/dist/cli/commands/dev.js.map +1 -1
  28. package/dist/cli/commands/index.d.ts +1 -0
  29. package/dist/cli/commands/index.d.ts.map +1 -1
  30. package/dist/cli/commands/index.js +3 -1
  31. package/dist/cli/commands/index.js.map +1 -1
  32. package/dist/cli/commands/init.d.ts +13 -0
  33. package/dist/cli/commands/init.d.ts.map +1 -1
  34. package/dist/cli/commands/init.js +2627 -1708
  35. package/dist/cli/commands/init.js.map +1 -1
  36. package/dist/cli/commands/scaffold.d.ts +3 -0
  37. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  38. package/dist/cli/commands/scaffold.js +617 -129
  39. package/dist/cli/commands/scaffold.js.map +1 -1
  40. package/dist/cli/index.d.ts +4 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +162 -42
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/core/config.d.ts +8 -2
  45. package/dist/core/config.d.ts.map +1 -1
  46. package/dist/core/config.js +90 -4
  47. package/dist/core/config.js.map +1 -1
  48. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  49. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  50. package/dist/core/mock/connector-mock-server.js +480 -0
  51. package/dist/core/mock/connector-mock-server.js.map +1 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  53. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  54. package/dist/core/mock/zod-mock-generator.js +163 -0
  55. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  56. package/dist/core/operations/create-model.d.ts +15 -0
  57. package/dist/core/operations/create-model.d.ts.map +1 -0
  58. package/dist/core/operations/create-model.js +171 -0
  59. package/dist/core/operations/create-model.js.map +1 -0
  60. package/dist/core/operations/runtime.d.ts +32 -0
  61. package/dist/core/operations/runtime.d.ts.map +1 -0
  62. package/dist/core/operations/runtime.js +225 -0
  63. package/dist/core/operations/runtime.js.map +1 -0
  64. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  65. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  66. package/dist/core/operations/scaffold-machine.js +63 -0
  67. package/dist/core/operations/scaffold-machine.js.map +1 -0
  68. package/dist/core/project/manifest.d.ts +92 -0
  69. package/dist/core/project/manifest.d.ts.map +1 -0
  70. package/dist/core/project/manifest.js +321 -0
  71. package/dist/core/project/manifest.js.map +1 -0
  72. package/dist/core/project/validation.d.ts +20 -0
  73. package/dist/core/project/validation.d.ts.map +1 -0
  74. package/dist/core/project/validation.js +204 -0
  75. package/dist/core/project/validation.js.map +1 -0
  76. package/dist/core/scaffold/auth-generator.d.ts +38 -0
  77. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  78. package/dist/core/scaffold/auth-generator.js +1244 -0
  79. package/dist/core/scaffold/auth-generator.js.map +1 -0
  80. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  81. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  82. package/dist/core/scaffold/connector-functions-generator.js +1027 -0
  83. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  84. package/dist/core/scaffold/functions-generator.d.ts +7 -1
  85. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  86. package/dist/core/scaffold/functions-generator.js +920 -213
  87. package/dist/core/scaffold/functions-generator.js.map +1 -1
  88. package/dist/core/scaffold/model-parser.d.ts +20 -1
  89. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  90. package/dist/core/scaffold/model-parser.js +329 -135
  91. package/dist/core/scaffold/model-parser.js.map +1 -1
  92. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  93. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  94. package/dist/core/scaffold/nextjs-generator.js +314 -182
  95. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  96. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  97. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  98. package/dist/core/scaffold/openapi-generator.js +190 -0
  99. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  100. package/dist/core/scaffold/ui-generator.d.ts +10 -4
  101. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  102. package/dist/core/scaffold/ui-generator.js +768 -663
  103. package/dist/core/scaffold/ui-generator.js.map +1 -1
  104. package/dist/database/base-model.d.ts +3 -3
  105. package/dist/database/base-model.js +3 -3
  106. package/dist/index.d.ts +2 -2
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +2 -1
  109. package/dist/index.js.map +1 -1
  110. package/dist/machine/contracts.d.ts +16 -0
  111. package/dist/machine/contracts.d.ts.map +1 -0
  112. package/dist/machine/contracts.js +3 -0
  113. package/dist/machine/contracts.js.map +1 -0
  114. package/dist/machine/errors.d.ts +11 -0
  115. package/dist/machine/errors.d.ts.map +1 -0
  116. package/dist/machine/errors.js +34 -0
  117. package/dist/machine/errors.js.map +1 -0
  118. package/dist/machine/index.d.ts +3 -0
  119. package/dist/machine/index.d.ts.map +1 -0
  120. package/dist/machine/index.js +156 -0
  121. package/dist/machine/index.js.map +1 -0
  122. package/dist/mcp/index.d.ts +25 -0
  123. package/dist/mcp/index.d.ts.map +1 -0
  124. package/dist/mcp/index.js +184 -0
  125. package/dist/mcp/index.js.map +1 -0
  126. package/dist/types/index.d.ts +65 -0
  127. package/dist/types/index.d.ts.map +1 -1
  128. package/dist/utils/package-manager.d.ts +109 -0
  129. package/dist/utils/package-manager.d.ts.map +1 -0
  130. package/dist/utils/package-manager.js +215 -0
  131. package/dist/utils/package-manager.js.map +1 -0
  132. package/package.json +85 -73
  133. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
  134. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  135. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
  136. package/src/__tests__/auth.test.ts +654 -0
  137. package/src/__tests__/config.test.ts +263 -0
  138. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  139. package/src/__tests__/connector-mock-server.test.ts +439 -0
  140. package/src/__tests__/connector-model-bff.test.ts +162 -0
  141. package/src/__tests__/dev-seeds.test.ts +112 -0
  142. package/src/__tests__/dev.test.ts +148 -0
  143. package/src/__tests__/fixtures.ts +144 -0
  144. package/src/__tests__/functions-generator.test.ts +237 -0
  145. package/src/__tests__/init.test.ts +80 -0
  146. package/src/__tests__/machine.test.ts +212 -0
  147. package/src/__tests__/mcp.test.ts +56 -0
  148. package/src/__tests__/model-parser.test.ts +72 -0
  149. package/src/__tests__/nextjs-generator.test.ts +97 -0
  150. package/src/__tests__/openapi-generator.test.ts +43 -0
  151. package/src/__tests__/package-manager.test.ts +189 -0
  152. package/src/__tests__/scaffold.test.ts +39 -0
  153. package/src/__tests__/string-utils.test.ts +75 -0
  154. package/src/__tests__/ui-generator.test.ts +144 -0
  155. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  156. package/src/cli/commands/add-auth.ts +500 -0
  157. package/src/cli/commands/add-connector.ts +158 -0
  158. package/src/cli/commands/create-model.ts +62 -0
  159. package/src/cli/commands/dev-seeds.ts +358 -0
  160. package/src/cli/commands/dev.ts +962 -0
  161. package/src/cli/commands/index.ts +9 -0
  162. package/src/cli/commands/init.ts +3371 -0
  163. package/src/cli/commands/provision.ts +193 -0
  164. package/src/cli/commands/scaffold.ts +1211 -0
  165. package/src/cli/index.ts +191 -0
  166. package/src/core/config.ts +308 -0
  167. package/src/core/mock/connector-mock-server.ts +555 -0
  168. package/src/core/mock/zod-mock-generator.ts +205 -0
  169. package/src/core/operations/create-model.ts +174 -0
  170. package/src/core/operations/runtime.ts +235 -0
  171. package/src/core/operations/scaffold-machine.ts +91 -0
  172. package/src/core/project/manifest.ts +402 -0
  173. package/src/core/project/validation.ts +221 -0
  174. package/src/core/scaffold/auth-generator.ts +1284 -0
  175. package/src/core/scaffold/connector-functions-generator.ts +1128 -0
  176. package/src/core/scaffold/functions-generator.ts +970 -0
  177. package/src/core/scaffold/model-parser.ts +841 -0
  178. package/src/core/scaffold/nextjs-generator.ts +370 -0
  179. package/src/core/scaffold/openapi-generator.ts +212 -0
  180. package/src/core/scaffold/ui-generator.ts +1061 -0
  181. package/src/database/base-model.ts +184 -0
  182. package/src/database/client.ts +140 -0
  183. package/src/database/repository.ts +104 -0
  184. package/src/database/runtime-check.ts +25 -0
  185. package/src/index.ts +27 -0
  186. package/src/machine/contracts.ts +17 -0
  187. package/src/machine/errors.ts +34 -0
  188. package/src/machine/index.ts +173 -0
  189. package/src/mcp/index.ts +185 -0
  190. package/src/types/index.ts +134 -0
  191. package/src/utils/package-manager.ts +229 -0
@@ -0,0 +1,1211 @@
1
+ /**
2
+ * SwallowKit Scaffold ć‚³ćƒžćƒ³ćƒ‰
3
+ * Zod ćƒ¢ćƒ‡ćƒ«ć‹ć‚‰ Azure Functions と Next.js BFF 恮 CRUD ć‚³ćƒ¼ćƒ‰ć‚’ē”Ÿęˆ
4
+ */
5
+
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import { spawn, spawnSync } from "child_process";
9
+ import { getBackendLanguage, getConnectorDefinition, getAuthConfig, ensureSwallowKitProject } from "../../core/config";
10
+ import { ModelInfo, parseModelFile, toKebabCase, toPascalCase, toCamelCase } from "../../core/scaffold/model-parser";
11
+ import {
12
+ generateCSharpAzureFunctionsCRUD,
13
+ generateCompactAzureFunctionsCRUD,
14
+ generatePythonAzureFunctionsCRUD,
15
+ } from "../../core/scaffold/functions-generator";
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";
25
+ import { generateOpenApiDocument } from "../../core/scaffold/openapi-generator";
26
+ import {
27
+ generateListPage,
28
+ generateDetailPage,
29
+ generateFormComponent,
30
+ generateNewPage,
31
+ generateEditPage,
32
+ UIAuthOptions,
33
+ } from "../../core/scaffold/ui-generator";
34
+ import { detectFromProject, getCommands } from "../../utils/package-manager";
35
+ import {
36
+ BackendLanguage,
37
+ RdbConnectorConfig,
38
+ ApiConnectorConfig,
39
+ RdbModelConnectorConfig,
40
+ ApiModelConnectorConfig,
41
+ ModelAuthPolicy,
42
+ AuthConfig,
43
+ } from "../../types";
44
+ import { syncProjectManifest } from "../../core/project/manifest";
45
+
46
+ interface ScaffoldOptions {
47
+ model: string; // ćƒ¢ćƒ‡ćƒ«ćƒ•ć‚”ć‚¤ćƒ«ć®ćƒ‘ć‚¹ļ¼ˆä¾‹: "lib/models/todo.ts" or "todo")
48
+ functionsDir?: string; // Azure Functions ć®ćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆ: "functions")
49
+ apiDir?: string; // Next.js API routes ć®ćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆ: "app/api")
50
+ apiOnly?: boolean; // true ć®å “åˆć€UI ć‚’ē”Ÿęˆć—ćŖć„ļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆ: false)
51
+ }
52
+
53
+ function getMachineAwareStdio(): "inherit" | "pipe" {
54
+ return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
55
+ }
56
+
57
+ function runSpawnSyncCommand(command: string, args: string[], cwd: string): void {
58
+ const result = spawnSync(command, args, {
59
+ cwd,
60
+ stdio: getMachineAwareStdio(),
61
+ shell: true,
62
+ });
63
+
64
+ if (typeof result.status === "number" && result.status !== 0) {
65
+ throw new Error(`${command} ${args.join(" ")} exited with code ${result.status}`);
66
+ }
67
+
68
+ if (result.error) {
69
+ throw result.error;
70
+ }
71
+ }
72
+
73
+ export async function scaffoldCommand(options: ScaffoldOptions) {
74
+ // SwallowKit ćƒ—ćƒ­ć‚øć‚§ć‚Æćƒˆćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖć‹ć©ć†ć‹ć‚’ę¤œčØ¼
75
+ ensureSwallowKitProject("scaffold");
76
+
77
+ console.log("šŸ—ļø SwallowKit Scaffold: Generating CRUD operations...\n");
78
+
79
+ try {
80
+ // 1. Resolve model file path
81
+ const modelPath = resolveModelPath(options.model);
82
+ console.log(`šŸ“„ Model file: ${modelPath}`);
83
+
84
+ // 2. Parse model file
85
+ console.log("šŸ” Parsing model file...");
86
+ const modelInfo = await parseModelFile(modelPath);
87
+ console.log(`āœ… Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
88
+
89
+ const backendLanguage = getBackendLanguage();
90
+ console.log(`🧠 Backend language: ${backendLanguage}`);
91
+
92
+ // ć‚³ćƒć‚Æć‚æćƒ¢ćƒ‡ćƒ«ć‹ć©ć†ć‹ć‚’åˆ¤å®š
93
+ const isConnectorModel = !!modelInfo.connectorConfig;
94
+ if (isConnectorModel) {
95
+ console.log(`šŸ”Œ Connector model detected: ${modelInfo.connectorConfig!.connector}`);
96
+ console.log(` Operations: ${modelInfo.connectorConfig!.operations.join(", ")}`);
97
+ }
98
+
99
+ // ćƒć‚¹ćƒˆć‚¹ć‚­ćƒ¼ćƒžå‚ē…§ćŒć‚ć‚Œć°č”Øē¤ŗ
100
+ if (modelInfo.nestedSchemaRefs.length > 0) {
101
+ console.log(`šŸ”— Nested schema references detected:`);
102
+ for (const ref of modelInfo.nestedSchemaRefs) {
103
+ const relType = ref.isArray ? 'array' : 'single';
104
+ const optional = ref.isOptional ? ' (optional)' : '';
105
+ console.log(` - ${ref.fieldName}: ${ref.modelName} [${relType}]${optional}`);
106
+ }
107
+ }
108
+
109
+ // 3. Check for ID field
110
+ if (!modelInfo.hasId) {
111
+ console.warn(
112
+ "āš ļø Warning: Model does not have an 'id' field. CRUD operations may not work correctly."
113
+ );
114
+ }
115
+
116
+ // 4. Read shared package name
117
+ const functionsDir = options.functionsDir || "functions";
118
+ const sharedPackageName = readSharedPackageName();
119
+ const relatedModels = backendLanguage === "typescript"
120
+ ? [modelInfo]
121
+ : await collectModelGraph(modelPath);
122
+
123
+ // 5. Generate BFF callFunction helper
124
+ await generateCallFunctionHelper();
125
+
126
+ // 6. Generate Azure Functions code
127
+ if (isConnectorModel) {
128
+ await generateConnectorFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
129
+ // 6b. Install connector driver dependencies
130
+ await installConnectorDriverDependencies(modelInfo, functionsDir, backendLanguage);
131
+ } else {
132
+ await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
133
+
134
+ if (backendLanguage !== "typescript") {
135
+ await generateLanguageSchemaArtifacts(relatedModels, modelInfo, functionsDir, backendLanguage);
136
+ }
137
+ }
138
+
139
+ // 7. Generate Next.js BFF API Routes
140
+ const apiDir = options.apiDir || "app/api";
141
+ if (isConnectorModel) {
142
+ await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName);
143
+ } else {
144
+ await generateBFFRoutes(modelInfo, apiDir, sharedPackageName);
145
+ }
146
+
147
+ // 8. Generate Cosmos DB container Bicep file (skip for connector models)
148
+ if (!isConnectorModel) {
149
+ await generateCosmosContainer(modelInfo);
150
+ }
151
+
152
+ // 9. Generate UI components (unless --api-only)
153
+ if (!options.apiOnly) {
154
+ const uiAuthConfig = getAuthConfig();
155
+ const uiAuthPolicy = resolveAuthPolicy(modelInfo, uiAuthConfig);
156
+ const uiAuthOptions: UIAuthOptions | undefined =
157
+ uiAuthPolicy && uiAuthConfig && uiAuthConfig.provider !== 'none'
158
+ ? { authPolicy: uiAuthPolicy }
159
+ : undefined;
160
+ await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
161
+ await updateNavigationMenu(modelInfo);
162
+ }
163
+
164
+ await syncProjectManifest();
165
+
166
+ console.log("\nāœ… Scaffold completed successfully!");
167
+ console.log("\nšŸ“ Next steps:");
168
+ console.log(` 1. Review generated files in ${describeFunctionsOutputPath(functionsDir, backendLanguage)} and ${apiDir}/`);
169
+ if (!options.apiOnly) {
170
+ console.log(` 2. Check the generated UI pages in app/${toKebabCase(modelInfo.name)}/`);
171
+ console.log(" 3. Navigate to the model from the homepage menu");
172
+ }
173
+ if (backendLanguage !== "typescript") {
174
+ console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
175
+ }
176
+ console.log(
177
+ ` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in your .env.local file`
178
+ );
179
+ console.log(
180
+ ` ${options.apiOnly ? (backendLanguage === "typescript" ? "3" : "4") : (backendLanguage === "typescript" ? "5" : "6")}. Configure CosmosDBConnection in functions/local.settings.json`
181
+ );
182
+ console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).dlx} swallowkit dev' to test the generated code`);
183
+ } catch (error: any) {
184
+ console.error("\nāŒ Scaffold failed:", error.message);
185
+ process.exit(1);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * ćƒ¢ćƒ‡ćƒ«ć® authPolicy と auth config ć‹ć‚‰å®ŸåŠ¹ēš„ćŖčŖåÆćƒćƒŖć‚·ćƒ¼ć‚’č§£ę±ŗ
191
+ * - ćƒ¢ćƒ‡ćƒ«ć« authPolicy ćŒć‚ć‚Œć°ćć‚Œć‚’ä½æē”Ø
192
+ * - ćŖć‘ć‚Œć° auth.authorization.defaultPolicy に従う
193
+ * - auth čØ­å®šćŒćŖć‘ć‚Œć° undefinedļ¼ˆć‚¬ćƒ¼ćƒ‰ćŖć—ļ¼‰
194
+ */
195
+ function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
196
+ // ćƒ¢ćƒ‡ćƒ«ć«ę˜Žē¤ŗēš„ćŖ authPolicy ćŒć‚ć‚‹å “åˆćÆćć‚Œć‚’ä½æē”Ø
197
+ if (modelInfo.authPolicy) {
198
+ return modelInfo.authPolicy;
199
+ }
200
+
201
+ // auth čØ­å®šćŒćŖć„å “åˆćÆć‚¬ćƒ¼ćƒ‰ćŖć—
202
+ if (!authConfig || authConfig.provider === 'none') {
203
+ return undefined;
204
+ }
205
+
206
+ // defaultPolicy が 'authenticated' ćŖć‚‰čŖčØ¼ć®ćæļ¼ˆćƒ­ćƒ¼ćƒ«ęŒ‡å®šćŖć—ļ¼‰ć®ćƒćƒŖć‚·ćƒ¼ć‚’čæ”ć™
207
+ const defaultPolicy = authConfig.authorization?.defaultPolicy ?? 'authenticated';
208
+ if (defaultPolicy === 'authenticated') {
209
+ return {}; // ē©ŗć®ćƒćƒŖć‚·ćƒ¼ = čŖčØ¼ć®ćæć€ćƒ­ćƒ¼ćƒ«åˆ¶é™ćŖć—
210
+ }
211
+
212
+ // defaultPolicy が 'anonymous' ćŖć‚‰čŖčØ¼äøč¦
213
+ return undefined;
214
+ }
215
+
216
+ /**
217
+ * ćƒ¢ćƒ‡ćƒ«ćƒ•ć‚”ć‚¤ćƒ«ć®ćƒ‘ć‚¹ć‚’č§£ę±ŗ
218
+ */
219
+ function resolveModelPath(modelInput: string): string {
220
+ const cwd = process.cwd();
221
+
222
+ // ēµ¶åÆ¾ćƒ‘ć‚¹ć®å “åˆ
223
+ if (path.isAbsolute(modelInput)) {
224
+ return modelInput;
225
+ }
226
+
227
+ // ę‹”å¼µå­ćŒć‚ć‚‹å “åˆļ¼ˆē›øåÆ¾ćƒ‘ć‚¹ļ¼‰
228
+ if (modelInput.endsWith(".ts")) {
229
+ const fullPath = path.join(cwd, modelInput);
230
+ if (fs.existsSync(fullPath)) {
231
+ return fullPath;
232
+ }
233
+ }
234
+
235
+ // ę‹”å¼µå­ćŒćŖć„å “åˆć€shared/models/ ć§ęŽ¢ć™
236
+ const defaultPath = path.join(cwd, "shared", "models", `${modelInput}.ts`);
237
+ if (fs.existsSync(defaultPath)) {
238
+ return defaultPath;
239
+ }
240
+
241
+ // lib/models/ ć§ć‚‚ęŽ¢ć™ļ¼ˆå¾Œę–¹äŗ’ę›ę€§ļ¼‰
242
+ const libPath = path.join(cwd, "lib", "models", `${modelInput}.ts`);
243
+ if (fs.existsSync(libPath)) {
244
+ return libPath;
245
+ }
246
+
247
+ // src/models/ ć§ć‚‚ęŽ¢ć™
248
+ const srcPath = path.join(cwd, "src", "models", `${modelInput}.ts`);
249
+ if (fs.existsSync(srcPath)) {
250
+ return srcPath;
251
+ }
252
+
253
+ // č¦‹ć¤ć‹ć‚‰ćŖć„å “åˆćÆć‚Øćƒ©ćƒ¼
254
+ throw new Error(
255
+ `Model file not found: ${modelInput}\n` +
256
+ ` Tried:\n` +
257
+ ` - ${modelInput}\n` +
258
+ ` - ${defaultPath}\n` +
259
+ ` - ${libPath}\n` +
260
+ ` - ${srcPath}\n` +
261
+ ` Please specify a valid model file path.`
262
+ );
263
+ }
264
+
265
+ /**
266
+ * Read shared package name from shared/package.json
267
+ */
268
+ function readSharedPackageName(): string {
269
+ const cwd = process.cwd();
270
+ const sharedPkgPath = path.join(cwd, "shared", "package.json");
271
+
272
+ if (!fs.existsSync(sharedPkgPath)) {
273
+ throw new Error(
274
+ "shared/package.json not found.\n" +
275
+ "The shared package is required for model imports.\n" +
276
+ `Run "${getCommands(detectFromProject()).dlx} swallowkit init" to set up your project.`
277
+ );
278
+ }
279
+
280
+ const pkg = JSON.parse(fs.readFileSync(sharedPkgPath, "utf-8"));
281
+ return pkg.name;
282
+ }
283
+
284
+ /**
285
+ * Generate BFF callFunction helper (lib/api/call-function.ts)
286
+ */
287
+ async function generateCallFunctionHelper(): Promise<void> {
288
+ console.log("\nšŸ“¦ Generating BFF callFunction helper...");
289
+
290
+ const cwd = process.cwd();
291
+
292
+ const helperDir = path.join(cwd, "lib", "api");
293
+ const helperPath = path.join(helperDir, "call-function.ts");
294
+
295
+ if (!fs.existsSync(helperDir)) {
296
+ fs.mkdirSync(helperDir, { recursive: true });
297
+ }
298
+
299
+ // auth čØ­å®šćŒć‚ć‚‹å “åˆćÆ Authorization ćƒ˜ćƒƒćƒ€ćƒ¼č»¢é€ē‰ˆć‚’ē”Ÿęˆ
300
+ const authConfig = getAuthConfig();
301
+ const hasAuth = authConfig && authConfig.provider !== 'none';
302
+ let helperCode: string;
303
+ if (hasAuth) {
304
+ const { generateBFFCallFunctionWithAuth } = await import("../../core/scaffold/auth-generator");
305
+ helperCode = generateBFFCallFunctionWithAuth();
306
+ } else {
307
+ helperCode = generateBFFCallFunction();
308
+ }
309
+
310
+ fs.writeFileSync(helperPath, helperCode, "utf-8");
311
+ console.log(`āœ… Created: ${helperPath}`);
312
+ }
313
+
314
+ /**
315
+ * Generate Azure Functions CRUD code
316
+ */
317
+ async function generateFunctionsCode(
318
+ modelInfo: ModelInfo,
319
+ functionsDir: string,
320
+ sharedPackageName: string,
321
+ backendLanguage: BackendLanguage
322
+ ): Promise<void> {
323
+ console.log("\nšŸ”Ø Generating Azure Functions CRUD code...");
324
+
325
+ // Resolve auth policy: model-level authPolicy or global defaultPolicy
326
+ const authConfig = getAuthConfig();
327
+ const authPolicy = resolveAuthPolicy(modelInfo, authConfig);
328
+ if (authPolicy) {
329
+ console.log(`šŸ” Auth policy detected: ${JSON.stringify(authPolicy)}`);
330
+ }
331
+
332
+ const modelKebab = toKebabCase(modelInfo.name);
333
+ if (backendLanguage === "typescript") {
334
+ const functionFilePath = path.join(
335
+ process.cwd(),
336
+ functionsDir,
337
+ "src",
338
+ `${modelKebab}.ts`
339
+ );
340
+
341
+ const functionDir = path.dirname(functionFilePath);
342
+ if (!fs.existsSync(functionDir)) {
343
+ fs.mkdirSync(functionDir, { recursive: true });
344
+ }
345
+
346
+ const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy);
347
+ fs.writeFileSync(functionFilePath, code, "utf-8");
348
+ console.log(`āœ… Created: ${functionFilePath}`);
349
+ return;
350
+ }
351
+
352
+ if (backendLanguage === "csharp") {
353
+ const crudDir = path.join(process.cwd(), functionsDir, "Crud");
354
+ const functionFilePath = path.join(crudDir, `${modelInfo.name}Functions.cs`);
355
+ fs.mkdirSync(crudDir, { recursive: true });
356
+
357
+ // Remove init-generated template (singular) to avoid route conflicts
358
+ const templatePath = path.join(crudDir, `${modelInfo.name}Function.cs`);
359
+ if (fs.existsSync(templatePath)) {
360
+ fs.unlinkSync(templatePath);
361
+ console.log(`šŸ—‘ļø Removed template: ${templatePath}`);
362
+ }
363
+
364
+ fs.writeFileSync(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo, authPolicy), "utf-8");
365
+ console.log(`āœ… Created: ${functionFilePath}`);
366
+ return;
367
+ }
368
+
369
+ const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
370
+ const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
371
+ fs.mkdirSync(blueprintsDir, { recursive: true });
372
+
373
+ const { blueprint, registration } = generatePythonAzureFunctionsCRUD(modelInfo, authPolicy);
374
+ fs.writeFileSync(blueprintPath, blueprint, "utf-8");
375
+ updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration);
376
+ console.log(`āœ… Created: ${blueprintPath}`);
377
+ }
378
+
379
+ /**
380
+ * ć‚³ćƒć‚Æć‚æćƒ¢ćƒ‡ćƒ«ē”Ø Azure Functions ć‚³ćƒ¼ćƒ‰ē”Ÿęˆ
381
+ */
382
+ async function generateConnectorFunctionsCode(
383
+ modelInfo: ModelInfo,
384
+ functionsDir: string,
385
+ sharedPackageName: string,
386
+ backendLanguage: BackendLanguage
387
+ ): Promise<void> {
388
+ console.log("\nšŸ”Œ Generating Connector Azure Functions code...");
389
+
390
+ const connectorConfig = modelInfo.connectorConfig!;
391
+ const connectorDef = getConnectorDefinition(connectorConfig.connector);
392
+
393
+ if (!connectorDef) {
394
+ throw new Error(
395
+ `Connector '${connectorConfig.connector}' not found in swallowkit.config.js.\n` +
396
+ ` Please add it to the 'connectors' section of your configuration.`
397
+ );
398
+ }
399
+
400
+ // Resolve auth policy (same logic as Cosmos model scaffolding)
401
+ const authConfig = getAuthConfig();
402
+ const authPolicy = resolveAuthPolicy(modelInfo, authConfig);
403
+ if (authPolicy) {
404
+ console.log(`šŸ” Auth policy detected: ${JSON.stringify(authPolicy)}`);
405
+ }
406
+
407
+ const modelKebab = toKebabCase(modelInfo.name);
408
+
409
+ if (backendLanguage === "typescript") {
410
+ const functionFilePath = path.join(
411
+ process.cwd(),
412
+ functionsDir,
413
+ "src",
414
+ `${modelKebab}.ts`
415
+ );
416
+ fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
417
+
418
+ let code: string;
419
+ if (connectorDef.type === "rdb") {
420
+ code = generateRdbConnectorFunctionTS(
421
+ modelInfo, sharedPackageName,
422
+ connectorDef as RdbConnectorConfig,
423
+ connectorConfig as RdbModelConnectorConfig,
424
+ authPolicy
425
+ );
426
+ } else {
427
+ code = generateApiConnectorFunctionTS(
428
+ modelInfo, sharedPackageName,
429
+ connectorDef as ApiConnectorConfig,
430
+ connectorConfig as ApiModelConnectorConfig,
431
+ authPolicy
432
+ );
433
+ }
434
+
435
+ fs.writeFileSync(functionFilePath, code, "utf-8");
436
+ console.log(`āœ… Created: ${functionFilePath}`);
437
+ return;
438
+ }
439
+
440
+ if (backendLanguage === "csharp") {
441
+ const functionFilePath = path.join(
442
+ process.cwd(),
443
+ functionsDir,
444
+ "Connectors",
445
+ `${modelInfo.name}ConnectorFunctions.cs`
446
+ );
447
+ fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
448
+
449
+ let code: string;
450
+ if (connectorDef.type === "rdb") {
451
+ code = generateRdbConnectorFunctionCSharp(
452
+ modelInfo,
453
+ connectorDef as RdbConnectorConfig,
454
+ connectorConfig as RdbModelConnectorConfig
455
+ );
456
+ } else {
457
+ code = generateApiConnectorFunctionCSharp(
458
+ modelInfo,
459
+ connectorDef as ApiConnectorConfig,
460
+ connectorConfig as ApiModelConnectorConfig
461
+ );
462
+ }
463
+
464
+ fs.writeFileSync(functionFilePath, code, "utf-8");
465
+ console.log(`āœ… Created: ${functionFilePath}`);
466
+ return;
467
+ }
468
+
469
+ // Python
470
+ const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
471
+ const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
472
+ fs.mkdirSync(blueprintsDir, { recursive: true });
473
+
474
+ let result: { blueprint: string; registration: string };
475
+ if (connectorDef.type === "rdb") {
476
+ result = generateRdbConnectorFunctionPython(
477
+ modelInfo,
478
+ connectorDef as RdbConnectorConfig,
479
+ connectorConfig as RdbModelConnectorConfig
480
+ );
481
+ } else {
482
+ result = generateApiConnectorFunctionPython(
483
+ modelInfo,
484
+ connectorDef as ApiConnectorConfig,
485
+ connectorConfig as ApiModelConnectorConfig
486
+ );
487
+ }
488
+
489
+ fs.writeFileSync(blueprintPath, result.blueprint, "utf-8");
490
+ updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), result.registration);
491
+ console.log(`āœ… Created: ${blueprintPath}`);
492
+ }
493
+
494
+ /**
495
+ * ć‚³ćƒć‚Æć‚æćƒ¢ćƒ‡ćƒ«ć® scaffold å¾Œć«ć€ē”Ÿęˆć‚³ćƒ¼ćƒ‰ćŒåæ…č¦ćØć™ć‚‹ RDB/API ćƒ‰ćƒ©ć‚¤ćƒć‚’
496
+ * functions ćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖć«ć‚¤ćƒ³ć‚¹ćƒˆćƒ¼ćƒ«ć™ć‚‹ć€‚
497
+ */
498
+ async function installConnectorDriverDependencies(
499
+ modelInfo: ModelInfo,
500
+ functionsDir: string,
501
+ backendLanguage: BackendLanguage
502
+ ): Promise<void> {
503
+ const connectorConfig = modelInfo.connectorConfig!;
504
+ const connectorDef = getConnectorDefinition(connectorConfig.connector);
505
+ if (!connectorDef || connectorDef.type !== "rdb") return;
506
+
507
+ const rdbDef = connectorDef as RdbConnectorConfig;
508
+ const functionsPath = path.join(process.cwd(), functionsDir);
509
+
510
+ if (backendLanguage === "typescript") {
511
+ const driverMap: Record<string, { deps: string[]; devDeps: string[] }> = {
512
+ mysql: { deps: ["mysql2"], devDeps: [] },
513
+ postgres: { deps: ["pg"], devDeps: ["@types/pg"] },
514
+ sqlserver: { deps: ["mssql"], devDeps: [] },
515
+ };
516
+ const entry = driverMap[rdbDef.provider];
517
+ if (!entry) return;
518
+
519
+ // package.json ć‚’čŖ­ć‚“ć§ć€ę—¢ć«ć‚¤ćƒ³ć‚¹ćƒˆćƒ¼ćƒ«ęøˆćæćŖć‚‰ć‚¹ć‚­ćƒƒćƒ—
520
+ const pkgJsonPath = path.join(functionsPath, "package.json");
521
+ if (fs.existsSync(pkgJsonPath)) {
522
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
523
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
524
+ const missingDeps = entry.deps.filter(d => !allDeps[d]);
525
+ const missingDevDeps = entry.devDeps.filter(d => !allDeps[d]);
526
+ if (missingDeps.length === 0 && missingDevDeps.length === 0) return;
527
+ }
528
+
529
+ console.log(`\nšŸ“¦ Installing ${rdbDef.provider} driver dependencies...`);
530
+ const pm = detectFromProject(functionsPath);
531
+ const cmds = getCommands(pm);
532
+
533
+ if (entry.deps.length > 0) {
534
+ runSpawnSyncCommand(cmds.name, [pm === "pnpm" ? "add" : "install", ...entry.deps], functionsPath);
535
+ }
536
+ if (entry.devDeps.length > 0) {
537
+ runSpawnSyncCommand(cmds.name, [pm === "pnpm" ? "add" : "install", "-D", ...entry.devDeps], functionsPath);
538
+ }
539
+ console.log(`āœ… ${rdbDef.provider} driver installed`);
540
+ return;
541
+ }
542
+
543
+ if (backendLanguage === "csharp") {
544
+ const nugetMap: Record<string, string> = {
545
+ mysql: "MySqlConnector",
546
+ postgres: "Npgsql",
547
+ sqlserver: "Microsoft.Data.SqlClient",
548
+ };
549
+ const pkg = nugetMap[rdbDef.provider];
550
+ if (!pkg) return;
551
+ console.log(`\nšŸ“¦ Installing ${rdbDef.provider} NuGet package...`);
552
+ runSpawnSyncCommand("dotnet", ["add", path.join(functionsPath, "functions.csproj"), "package", pkg], functionsPath);
553
+ console.log(`āœ… ${pkg} installed`);
554
+ return;
555
+ }
556
+
557
+ // Python — requirements.txt に追記
558
+ const pipMap: Record<string, string> = {
559
+ mysql: "mysql-connector-python",
560
+ postgres: "psycopg2-binary",
561
+ sqlserver: "pymssql",
562
+ };
563
+ const pipPkg = pipMap[rdbDef.provider];
564
+ if (!pipPkg) return;
565
+ const reqPath = path.join(functionsPath, "requirements.txt");
566
+ if (fs.existsSync(reqPath)) {
567
+ const existing = fs.readFileSync(reqPath, "utf-8");
568
+ if (!existing.includes(pipPkg)) {
569
+ console.log(`\nšŸ“¦ Adding ${pipPkg} to requirements.txt...`);
570
+ fs.appendFileSync(reqPath, `\n${pipPkg}\n`);
571
+ console.log(`āœ… ${pipPkg} added`);
572
+ }
573
+ }
574
+ }
575
+
576
+ /**
577
+ * ć‚³ćƒć‚Æć‚æćƒ¢ćƒ‡ćƒ«ē”Ø BFF ćƒ«ćƒ¼ćƒˆē”Ÿęˆļ¼ˆę“ä½œåˆ¶é™ć«åÆ¾åæœļ¼‰
578
+ */
579
+ async function generateConnectorBFFRoutesFiles(
580
+ modelInfo: ModelInfo,
581
+ apiDir: string,
582
+ sharedPackageName: string
583
+ ): Promise<void> {
584
+ console.log("\nšŸ”Œ Generating Connector BFF API routes...");
585
+
586
+ const modelCamel = toCamelCase(modelInfo.name);
587
+ const operations = modelInfo.connectorConfig!.operations;
588
+
589
+ const listRoutePath = path.join(process.cwd(), apiDir, modelCamel, "route.ts");
590
+ const detailRoutePath = path.join(process.cwd(), apiDir, modelCamel, "[id]", "route.ts");
591
+
592
+ fs.mkdirSync(path.dirname(listRoutePath), { recursive: true });
593
+ fs.mkdirSync(path.dirname(detailRoutePath), { recursive: true });
594
+
595
+ const routes = generateConnectorBFFRoutes(modelInfo, sharedPackageName, operations);
596
+
597
+ fs.writeFileSync(listRoutePath, routes.listRoute, "utf-8");
598
+ fs.writeFileSync(detailRoutePath, routes.detailRoute, "utf-8");
599
+
600
+ console.log(`āœ… Created: ${listRoutePath}`);
601
+ console.log(`āœ… Created: ${detailRoutePath}`);
602
+ }
603
+
604
+ async function collectModelGraph(modelPath: string, seen = new Map<string, ModelInfo>()): Promise<ModelInfo[]> {
605
+ const resolvedPath = path.resolve(modelPath);
606
+ if (seen.has(resolvedPath)) {
607
+ return Array.from(seen.values());
608
+ }
609
+
610
+ const modelInfo = await parseModelFile(resolvedPath);
611
+ seen.set(resolvedPath, modelInfo);
612
+
613
+ for (const ref of modelInfo.nestedSchemaRefs) {
614
+ const nestedPath = resolveNestedModelPath(resolvedPath, ref.importPath);
615
+ await collectModelGraph(nestedPath, seen);
616
+ }
617
+
618
+ return Array.from(seen.values());
619
+ }
620
+
621
+ function resolveNestedModelPath(modelPath: string, importPath: string): string {
622
+ let resolvedPath = path.resolve(path.dirname(modelPath), importPath);
623
+ if (!resolvedPath.endsWith(".ts")) {
624
+ resolvedPath += ".ts";
625
+ }
626
+ return resolvedPath;
627
+ }
628
+
629
+ function describeFunctionsOutputPath(functionsDir: string, backendLanguage: BackendLanguage): string {
630
+ if (backendLanguage === "typescript") {
631
+ return `${functionsDir}/src/`;
632
+ }
633
+ if (backendLanguage === "csharp") {
634
+ return `${functionsDir}/Crud/`;
635
+ }
636
+ return `${functionsDir}/blueprints/`;
637
+ }
638
+
639
+ async function generateLanguageSchemaArtifacts(
640
+ models: ModelInfo[],
641
+ rootModel: ModelInfo,
642
+ functionsDir: string,
643
+ backendLanguage: Exclude<BackendLanguage, "typescript">
644
+ ): Promise<void> {
645
+ console.log("\n🧬 Generating OpenAPI-backed schema artifacts...");
646
+
647
+ const openApiDir = path.join(process.cwd(), functionsDir, "openapi");
648
+ fs.mkdirSync(openApiDir, { recursive: true });
649
+
650
+ const specPath = path.join(openApiDir, `${toKebabCase(rootModel.name)}.openapi.json`);
651
+ fs.writeFileSync(specPath, generateOpenApiDocument(models, rootModel), "utf-8");
652
+ console.log(`āœ… Created: ${specPath}`);
653
+
654
+ const outputDir = path.join(
655
+ process.cwd(),
656
+ functionsDir,
657
+ "generated",
658
+ backendLanguage === "csharp" ? "csharp-models" : "python-models"
659
+ );
660
+
661
+ fs.mkdirSync(outputDir, { recursive: true });
662
+ await runOpenApiGenerator(specPath, outputDir, backendLanguage);
663
+ if (backendLanguage === "csharp") {
664
+ pruneGeneratedCSharpArtifacts(outputDir);
665
+ }
666
+ console.log(`āœ… Generated ${backendLanguage} schema assets: ${outputDir}`);
667
+ }
668
+
669
+ /**
670
+ * OpenAPI Generator ć®å®Ÿč”Œå‰ć« Java ćƒćƒ¼ć‚øćƒ§ćƒ³ć‚’ē¢ŗčŖć™ć‚‹ć€‚
671
+ * Java 11 ęœŖęŗ€ć®å “åˆćÆę˜Žē¢ŗćŖć‚Øćƒ©ćƒ¼ćƒ”ćƒƒć‚»ćƒ¼ć‚øć‚’č”Øē¤ŗć—ć¦å‡¦ē†ć‚’äø­ę–­ć™ć‚‹ć€‚
672
+ */
673
+ function checkJavaVersion(): void {
674
+ try {
675
+ const result = spawnSync("java", ["-version"], { encoding: "utf8" });
676
+ // java -version は stderr ć«å‡ŗåŠ›ć™ć‚‹
677
+ const versionOutput = (result.stderr || "") + (result.stdout || "");
678
+ const versionMatch = versionOutput.match(/version "(\d+)(?:\.(\d+))?/);
679
+ if (!versionMatch) return;
680
+
681
+ const major = parseInt(versionMatch[1]);
682
+ // Java 8 ä»„å‰ćÆ "1.8" å½¢å¼ć€Java 9 ä»„é™ćÆ "17" 形式
683
+ const effectiveMajor = major === 1 ? parseInt(versionMatch[2] ?? "0") : major;
684
+
685
+ if (effectiveMajor < 11) {
686
+ const versionStr = versionOutput.match(/version "([^"]+)"/)?.[1] ?? "unknown";
687
+ console.error(`\nāŒ OpenAPI Generator requires Java 11 or later.`);
688
+ console.error(` Detected: Java ${versionStr}`);
689
+ if (process.env.JAVA_HOME) {
690
+ console.error(` Current JAVA_HOME: ${process.env.JAVA_HOME}`);
691
+ }
692
+ console.error(` Please set JAVA_HOME to a Java 11+ installation and retry.`);
693
+ console.error(` Example (Windows): $env:JAVA_HOME = "C:\\path\\to\\jdk-17"`);
694
+ process.exit(1);
695
+ }
696
+ } catch {
697
+ // java ć‚³ćƒžćƒ³ćƒ‰ćŒč¦‹ć¤ć‹ć‚‰ćŖć„å “åˆćÆ OpenAPI Generator č‡Ŗčŗ«ćŒć‚Øćƒ©ćƒ¼ć‚’å‡ŗć™
698
+ }
699
+ }
700
+
701
+ async function runOpenApiGenerator(
702
+ specPath: string,
703
+ outputDir: string,
704
+ backendLanguage: Exclude<BackendLanguage, "typescript">
705
+ ): Promise<void> {
706
+ checkJavaVersion();
707
+ const pm = detectFromProject();
708
+ const command = pm === "pnpm" ? "pnpm" : "npx";
709
+ const args = pm === "pnpm"
710
+ ? ["exec", "openapi-generator-cli", ...getOpenApiGeneratorArgs(specPath, outputDir, backendLanguage)]
711
+ : ["openapi-generator-cli", ...getOpenApiGeneratorArgs(specPath, outputDir, backendLanguage)];
712
+
713
+ await new Promise<void>((resolve, reject) => {
714
+ const child = spawn(command, args, {
715
+ cwd: process.cwd(),
716
+ shell: true,
717
+ stdio: getMachineAwareStdio(),
718
+ });
719
+
720
+ child.on("close", (code) => {
721
+ if (code === 0) {
722
+ resolve();
723
+ return;
724
+ }
725
+
726
+ reject(new Error(`OpenAPI generator exited with code ${code}`));
727
+ });
728
+
729
+ child.on("error", reject);
730
+ });
731
+ }
732
+
733
+ export function getOpenApiGeneratorArgs(
734
+ specPath: string,
735
+ outputDir: string,
736
+ backendLanguage: Exclude<BackendLanguage, "typescript">
737
+ ): string[] {
738
+ const globalProperty = backendLanguage === "csharp"
739
+ ? "models,apis=false,modelDocs=false,modelTests=false"
740
+ : "models,apis=false,supportingFiles=false,modelDocs=false,modelTests=false";
741
+
742
+ const baseArgs = [
743
+ "generate",
744
+ "-i",
745
+ specPath,
746
+ "-o",
747
+ outputDir,
748
+ "--global-property",
749
+ globalProperty,
750
+ ];
751
+
752
+ if (backendLanguage === "csharp") {
753
+ return [
754
+ ...baseArgs,
755
+ "-g",
756
+ "csharp",
757
+ "--additional-properties",
758
+ "packageName=SwallowKitBackendModels,targetFramework=net8.0,nullableReferenceTypes=true",
759
+ ];
760
+ }
761
+
762
+ return [
763
+ ...baseArgs,
764
+ "-g",
765
+ "python",
766
+ "--additional-properties",
767
+ "packageName=backend_models,projectName=backend-models",
768
+ ];
769
+ }
770
+
771
+ export function getCSharpSchemaArtifactPruneTargets(outputDir: string): string[] {
772
+ return [
773
+ path.join(outputDir, "src", "SwallowKitBackendModels.Test"),
774
+ path.join(outputDir, "src", "SwallowKitBackendModels", "Api"),
775
+ path.join(outputDir, "src", "SwallowKitBackendModels", "Extensions"),
776
+ ];
777
+ }
778
+
779
+ function pruneGeneratedCSharpArtifacts(outputDir: string): void {
780
+ for (const target of getCSharpSchemaArtifactPruneTargets(outputDir)) {
781
+ if (fs.existsSync(target)) {
782
+ fs.rmSync(target, { recursive: true, force: true });
783
+ }
784
+ }
785
+
786
+ const clientDir = path.join(outputDir, "src", "SwallowKitBackendModels", "Client");
787
+ if (!fs.existsSync(clientDir)) {
788
+ // --global-property models ではClient/ćŒē”Ÿęˆć•ć‚ŒćŖć„ćŸć‚ć€
789
+ // ćƒ¢ćƒ‡ćƒ«ćŒä¾å­˜ć™ć‚‹ęœ€å°é™ć® Option<T> ć‚’č‡Ŗå‰ć§ä½œęˆć™ć‚‹
790
+ fs.mkdirSync(clientDir, { recursive: true });
791
+ fs.writeFileSync(
792
+ path.join(clientDir, "Option.cs"),
793
+ generateMinimalOptionCs(),
794
+ "utf-8"
795
+ );
796
+ return;
797
+ }
798
+
799
+ for (const entry of fs.readdirSync(clientDir, { withFileTypes: true })) {
800
+ if (!entry.isFile() || entry.name === "Option.cs") {
801
+ continue;
802
+ }
803
+
804
+ fs.rmSync(path.join(clientDir, entry.name), { force: true });
805
+ }
806
+ }
807
+
808
+ /**
809
+ * OpenAPI Generator 恮 csharp ćƒ†ćƒ³ćƒ—ćƒ¬ćƒ¼ćƒˆćŒē”Ÿęˆć™ć‚‹ćƒ¢ćƒ‡ćƒ«ćÆ Option<T> ć«ä¾å­˜ć™ć‚‹ćŒć€
810
+ * supportingFiles ć‚’é™¤å¤–ć—ć¦ć„ć‚‹ćŸć‚ Client/Option.cs ćŒē”Ÿęˆć•ć‚ŒćŖć„ć€‚
811
+ * Polly ē­‰ć®äøč¦ćŖä¾å­˜ć‚’éæć‘ć¤ć¤ćƒ¢ćƒ‡ćƒ«ć‚’ć‚³ćƒ³ćƒ‘ć‚¤ćƒ«åÆčƒ½ć«ć™ć‚‹ćŸć‚ć€ęœ€å°é™ć® Option<T> ć‚’ęä¾›ć™ć‚‹ć€‚
812
+ */
813
+ function generateMinimalOptionCs(): string {
814
+ return `// <auto-generated>
815
+ // Minimal Option<T> for OpenAPI Generator model compatibility.
816
+ // Full client supporting files are excluded to avoid Polly version conflicts.
817
+ // </auto-generated>
818
+
819
+ #nullable enable
820
+
821
+ namespace SwallowKitBackendModels.Client
822
+ {
823
+ /// <summary>
824
+ /// A wrapper for nullable/optional properties generated by OpenAPI Generator.
825
+ /// Tracks whether a value has been explicitly set (distinguishing null from absent).
826
+ /// </summary>
827
+ public readonly struct Option<TValue>
828
+ {
829
+ /// <summary>Whether this option has been explicitly set.</summary>
830
+ public bool IsSet { get; }
831
+
832
+ /// <summary>The contained value (may be default if not set).</summary>
833
+ public TValue Value { get; }
834
+
835
+ /// <summary>Create an Option with an explicit value.</summary>
836
+ public Option(TValue value)
837
+ {
838
+ IsSet = true;
839
+ Value = value;
840
+ }
841
+
842
+ /// <summary>Implicit conversion from Option to its inner value.</summary>
843
+ public static implicit operator TValue(Option<TValue> option) => option.Value;
844
+ }
845
+ }
846
+ `;
847
+ }
848
+
849
+ function updatePythonFunctionRegistrations(functionAppPath: string, registration: string): void {
850
+ if (!fs.existsSync(functionAppPath)) {
851
+ throw new Error(`Python Functions entrypoint not found: ${functionAppPath}`);
852
+ }
853
+
854
+ const content = fs.readFileSync(functionAppPath, "utf-8");
855
+
856
+ // Check if import line already exists (handles init-generated layout)
857
+ const importLine = registration.split("\n").find((l) => l.startsWith("from ") || l.startsWith("import "));
858
+ if (importLine && content.includes(importLine)) {
859
+ return;
860
+ }
861
+ if (content.includes(registration)) {
862
+ return;
863
+ }
864
+
865
+ const marker = "# SwallowKit scaffold registrations";
866
+ if (!content.includes(marker)) {
867
+ throw new Error(`Could not find scaffold registration marker in ${functionAppPath}`);
868
+ }
869
+
870
+ const updated = content.replace(marker, `${registration}\n${marker}`);
871
+ fs.writeFileSync(functionAppPath, updated, "utf-8");
872
+ }
873
+
874
+ /**
875
+ * Next.js BFF API Routes ć‚’ē”Ÿęˆ
876
+ */
877
+ async function generateBFFRoutes(
878
+ modelInfo: any,
879
+ apiDir: string,
880
+ sharedPackageName: string
881
+ ): Promise<void> {
882
+ console.log("\nšŸ”Ø Generating Next.js BFF API routes...");
883
+
884
+ const modelCamel = modelInfo.name.charAt(0).toLowerCase() + modelInfo.name.slice(1);
885
+
886
+ // List route: app/api/[model]/route.ts
887
+ const listRoutePath = path.join(
888
+ process.cwd(),
889
+ apiDir,
890
+ modelCamel,
891
+ "route.ts"
892
+ );
893
+
894
+ // Detail route: app/api/[model]/[id]/route.ts
895
+ const detailRoutePath = path.join(
896
+ process.cwd(),
897
+ apiDir,
898
+ modelCamel,
899
+ "[id]",
900
+ "route.ts"
901
+ );
902
+
903
+ // ćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖć‚’ä½œęˆ
904
+ const listRouteDir = path.dirname(listRoutePath);
905
+ const detailRouteDir = path.dirname(detailRoutePath);
906
+
907
+ if (!fs.existsSync(listRouteDir)) {
908
+ fs.mkdirSync(listRouteDir, { recursive: true });
909
+ }
910
+
911
+ if (!fs.existsSync(detailRouteDir)) {
912
+ fs.mkdirSync(detailRouteDir, { recursive: true });
913
+ }
914
+
915
+ // ć‚³ćƒ¼ćƒ‰ć‚’ē”Ÿęˆ
916
+ const routes = generateCompactBFFRoutes(modelInfo, sharedPackageName);
917
+
918
+ // ćƒ•ć‚”ć‚¤ćƒ«ć«ę›øćč¾¼ćæ
919
+ fs.writeFileSync(listRoutePath, routes.listRoute, "utf-8");
920
+ fs.writeFileSync(detailRoutePath, routes.detailRoute, "utf-8");
921
+
922
+ console.log(`āœ… Created: ${listRoutePath}`);
923
+ console.log(`āœ… Created: ${detailRoutePath}`);
924
+ }
925
+
926
+ /**
927
+ * Generate Cosmos DB container Bicep file
928
+ */
929
+ async function generateCosmosContainer(modelInfo: any): Promise<void> {
930
+ console.log("\nšŸ—„ļø Generating Cosmos DB container Bicep file...");
931
+
932
+ const modelKebab = toKebabCase(modelInfo.name);
933
+ const modelPascal = toPascalCase(modelInfo.name);
934
+ const cwd = process.cwd();
935
+
936
+ // Check if infra directory exists
937
+ const infraDir = path.join(cwd, "infra");
938
+ if (!fs.existsSync(infraDir)) {
939
+ console.log("ā„¹ļø infra directory not found. Skipping Cosmos DB container generation.");
940
+ return;
941
+ }
942
+
943
+ // Check if containers directory exists, if not create it
944
+ const containersDir = path.join(infraDir, "containers");
945
+ if (!fs.existsSync(containersDir)) {
946
+ fs.mkdirSync(containersDir, { recursive: true });
947
+ }
948
+
949
+ // Generate container Bicep file
950
+ const containerFileName = `${modelKebab}-container.bicep`;
951
+ const containerFilePath = path.join(containersDir, containerFileName);
952
+
953
+ const bicepContent = `@description('Cosmos DB account name')
954
+ param cosmosAccountName string
955
+
956
+ @description('Database name')
957
+ param databaseName string
958
+
959
+ @description('Container name')
960
+ param containerName string = '${modelPascal}s'
961
+
962
+ @description('Partition key path')
963
+ param partitionKeyPath string = '${modelInfo.partitionKey}'
964
+
965
+ @description('Throughput (RU/s) - only used for Free Tier')
966
+ param throughput int = 400
967
+
968
+ @description('Cosmos DB mode: freetier or serverless')
969
+ param cosmosDbMode string
970
+
971
+ // Reference existing Cosmos DB account
972
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
973
+ name: cosmosAccountName
974
+ }
975
+
976
+ // Reference existing database
977
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {
978
+ parent: cosmosAccount
979
+ name: databaseName
980
+ }
981
+
982
+ // Container for ${modelPascal}
983
+ resource container 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-11-15' = {
984
+ parent: database
985
+ name: containerName
986
+ properties: {
987
+ resource: {
988
+ id: containerName
989
+ partitionKey: {
990
+ paths: [
991
+ partitionKeyPath
992
+ ]
993
+ kind: 'Hash'
994
+ }
995
+ indexingPolicy: {
996
+ automatic: true
997
+ indexingMode: 'consistent'
998
+ includedPaths: [
999
+ {
1000
+ path: '/*'
1001
+ }
1002
+ ]
1003
+ excludedPaths: [
1004
+ {
1005
+ path: '/_etag/?'
1006
+ }
1007
+ ]
1008
+ }
1009
+ }
1010
+ options: cosmosDbMode == 'freetier' ? {
1011
+ throughput: throughput
1012
+ } : {}
1013
+ }
1014
+ }
1015
+
1016
+ output containerName string = container.name
1017
+ `;
1018
+
1019
+ // Write Bicep file (overwrite if exists)
1020
+ fs.writeFileSync(containerFilePath, bicepContent, "utf-8");
1021
+ console.log(`āœ… Created: ${containerFilePath}`);
1022
+
1023
+ // Update main.bicep to include this container module
1024
+ await updateMainBicepWithContainer(modelKebab, modelPascal);
1025
+ }
1026
+
1027
+ /**
1028
+ * Update main.bicep to include new container module
1029
+ */
1030
+ async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string): Promise<void> {
1031
+ const cwd = process.cwd();
1032
+ const mainBicepPath = path.join(cwd, "infra", "main.bicep");
1033
+
1034
+ if (!fs.existsSync(mainBicepPath)) {
1035
+ console.log("ā„¹ļø main.bicep not found. Please manually add the container module.");
1036
+ return;
1037
+ }
1038
+
1039
+ let mainBicepContent = fs.readFileSync(mainBicepPath, "utf-8");
1040
+
1041
+ // Check if container module already exists
1042
+ const containerModuleName = `${modelKebab.replace(/-/g, '')}Container`;
1043
+ if (mainBicepContent.includes(`module ${containerModuleName}`)) {
1044
+ console.log(`ā„¹ļø Container module '${containerModuleName}' already exists in main.bicep`);
1045
+ return;
1046
+ }
1047
+
1048
+ // Find the position to insert the container module (after cosmosDb modules)
1049
+ // Look for the end of both cosmosDb modules (FreeTier and Serverless)
1050
+ const cosmosModulePattern = /module cosmosDbServerless ['"]modules\/cosmosdb-serverless\.bicep['"] = if \(cosmosDbMode == 'serverless'\) \{[\s\S]*?\n\}/;
1051
+ const cosmosModuleMatch = mainBicepContent.match(cosmosModulePattern);
1052
+
1053
+ if (!cosmosModuleMatch) {
1054
+ console.log("āš ļø Could not find Cosmos DB Serverless module in main.bicep. Please manually add the container module:");
1055
+ console.log(`\nmodule ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
1056
+ name: '${modelKebab}-container'
1057
+ params: {
1058
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1059
+ databaseName: databaseName
1060
+ cosmosDbMode: cosmosDbMode
1061
+ }
1062
+ dependsOn: [
1063
+ cosmosDbFreeTier
1064
+ cosmosDbServerless
1065
+ ]
1066
+ }\n`);
1067
+ return;
1068
+ }
1069
+
1070
+ // Find the end of the cosmosDbServerless module
1071
+ const insertPosition = cosmosModuleMatch.index! + cosmosModuleMatch[0].length;
1072
+
1073
+ // Create the container module declaration
1074
+ const containerModule = `
1075
+
1076
+ // ${modelPascal} Container
1077
+ module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
1078
+ name: '${modelKebab}-container'
1079
+ params: {
1080
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1081
+ databaseName: '$` + `{projectName}Database'
1082
+ cosmosDbMode: cosmosDbMode
1083
+ }
1084
+ dependsOn: [
1085
+ cosmosDbFreeTier
1086
+ cosmosDbServerless
1087
+ ]
1088
+ }`;
1089
+
1090
+ // Insert the module
1091
+ mainBicepContent =
1092
+ mainBicepContent.slice(0, insertPosition) +
1093
+ containerModule +
1094
+ mainBicepContent.slice(insertPosition);
1095
+
1096
+ fs.writeFileSync(mainBicepPath, mainBicepContent, "utf-8");
1097
+ console.log(`āœ… Added container module to main.bicep`);
1098
+ }
1099
+
1100
+ /**
1101
+ * Generate UI components (list, detail, form, create, edit pages)
1102
+ */
1103
+ async function generateUIComponents(modelInfo: any, sharedPackageName: string, authOptions?: UIAuthOptions): Promise<void> {
1104
+ console.log("\nšŸŽØ Generating UI components...");
1105
+
1106
+ const modelKebab = toKebabCase(modelInfo.name);
1107
+ const modelName = modelInfo.name;
1108
+ const cwd = process.cwd();
1109
+
1110
+ // Create directory structure: app/[model]/
1111
+ const modelDir = path.join(cwd, "app", modelKebab);
1112
+ const componentsDir = path.join(modelDir, "_components");
1113
+ const newDir = path.join(modelDir, "new");
1114
+ const idDir = path.join(modelDir, "[id]");
1115
+ const editDir = path.join(idDir, "edit");
1116
+
1117
+ // Create directories
1118
+ [componentsDir, newDir, editDir].forEach(dir => {
1119
+ if (!fs.existsSync(dir)) {
1120
+ fs.mkdirSync(dir, { recursive: true });
1121
+ }
1122
+ });
1123
+
1124
+ // Generate and write files
1125
+ const listPage = generateListPage(modelInfo, sharedPackageName, authOptions);
1126
+ const detailPage = generateDetailPage(modelInfo, sharedPackageName, authOptions);
1127
+ const formComponent = generateFormComponent(modelInfo, sharedPackageName);
1128
+ const newPage = generateNewPage(modelInfo, authOptions);
1129
+ const editPage = generateEditPage(modelInfo, sharedPackageName, authOptions);
1130
+
1131
+ fs.writeFileSync(path.join(modelDir, "page.tsx"), listPage, "utf-8");
1132
+ fs.writeFileSync(path.join(idDir, "page.tsx"), detailPage, "utf-8");
1133
+ fs.writeFileSync(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, "utf-8");
1134
+ fs.writeFileSync(path.join(newDir, "page.tsx"), newPage, "utf-8");
1135
+ fs.writeFileSync(path.join(editDir, "page.tsx"), editPage, "utf-8");
1136
+
1137
+ console.log(`āœ… Created: ${path.join(modelDir, "page.tsx")}`);
1138
+ console.log(`āœ… Created: ${path.join(idDir, "page.tsx")}`);
1139
+ console.log(`āœ… Created: ${path.join(componentsDir, `${modelName}Form.tsx`)}`);
1140
+ console.log(`āœ… Created: ${path.join(newDir, "page.tsx")}`);
1141
+ console.log(`āœ… Created: ${path.join(editDir, "page.tsx")}`);
1142
+ }
1143
+
1144
+ /**
1145
+ * Update navigation menu on the homepage
1146
+ */
1147
+ async function updateNavigationMenu(modelInfo: any): Promise<void> {
1148
+ console.log("\nšŸ“‹ Updating navigation menu...");
1149
+
1150
+ const modelKebab = toKebabCase(modelInfo.name);
1151
+ const cwd = process.cwd();
1152
+
1153
+ // Update scaffold config
1154
+ const configPath = path.join(cwd, "lib", "scaffold-config.ts");
1155
+
1156
+ if (!fs.existsSync(configPath)) {
1157
+ console.log("āš ļø scaffold-config.ts not found. Skipping navigation menu update.");
1158
+ return;
1159
+ }
1160
+
1161
+ // Read existing config
1162
+ const configContent = fs.readFileSync(configPath, "utf-8");
1163
+
1164
+ // Parse models array - extract each model entry
1165
+ const models: Array<{ name: string; path: string; label: string }> = [];
1166
+ const modelsMatch = configContent.match(/models:\s*\[([\s\S]*?)\]\s*as\s*ScaffoldModel\[\]/);
1167
+
1168
+ if (modelsMatch) {
1169
+ const modelsArrayContent = modelsMatch[1];
1170
+ // Match each object in the array: { name: '...', path: '...', label: '...' }
1171
+ const modelPattern = /\{\s*name:\s*['"]([^'"]+)['"]\s*,\s*path:\s*['"]([^'"]+)['"]\s*,\s*label:\s*['"]([^'"]+)['"]\s*\}/g;
1172
+ let match;
1173
+ while ((match = modelPattern.exec(modelsArrayContent)) !== null) {
1174
+ models.push({
1175
+ name: match[1],
1176
+ path: match[2],
1177
+ label: match[3]
1178
+ });
1179
+ }
1180
+ }
1181
+
1182
+ // Check if model already exists
1183
+ if (models.find(m => m.name === modelInfo.name)) {
1184
+ console.log(`ā„¹ļø ${modelInfo.name} already in navigation menu`);
1185
+ return;
1186
+ }
1187
+
1188
+ // Add new model
1189
+ models.push({
1190
+ name: modelInfo.name,
1191
+ path: `/${modelKebab}`,
1192
+ label: modelInfo.displayName
1193
+ });
1194
+
1195
+ // Generate new scaffold-config.ts content
1196
+ const newConfigContent = `export interface ScaffoldModel {
1197
+ name: string;
1198
+ path: string;
1199
+ label: string;
1200
+ }
1201
+
1202
+ export const scaffoldConfig = {
1203
+ models: [
1204
+ ${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label}' },`).join('\n')}
1205
+ ] as ScaffoldModel[]
1206
+ };
1207
+ `;
1208
+
1209
+ fs.writeFileSync(configPath, newConfigContent, "utf-8");
1210
+ console.log(`āœ… Added ${modelInfo.name} to navigation menu`);
1211
+ }