swallowkit 1.0.0-beta.6 → 1.0.0-beta.60

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 (296) hide show
  1. package/README.ja.md +86 -177
  2. package/README.md +86 -178
  3. package/dist/__tests__/fixtures.d.ts +12 -0
  4. package/dist/__tests__/fixtures.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures.js +101 -0
  6. package/dist/__tests__/fixtures.js.map +1 -1
  7. package/dist/cli/commands/add-auth.d.ts +15 -0
  8. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  9. package/dist/cli/commands/add-auth.js +790 -0
  10. package/dist/cli/commands/add-auth.js.map +1 -0
  11. package/dist/cli/commands/add-connector.d.ts +20 -0
  12. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  13. package/dist/cli/commands/add-connector.js +163 -0
  14. package/dist/cli/commands/add-connector.js.map +1 -0
  15. package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
  16. package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
  17. package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
  18. package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
  19. package/dist/cli/commands/create-model.d.ts +1 -4
  20. package/dist/cli/commands/create-model.d.ts.map +1 -1
  21. package/dist/cli/commands/create-model.js +19 -81
  22. package/dist/cli/commands/create-model.js.map +1 -1
  23. package/dist/cli/commands/dev-seeds.d.ts +62 -0
  24. package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
  25. package/dist/cli/commands/dev-seeds.js +474 -0
  26. package/dist/cli/commands/dev-seeds.js.map +1 -0
  27. package/dist/cli/commands/dev.d.ts +38 -0
  28. package/dist/cli/commands/dev.d.ts.map +1 -1
  29. package/dist/cli/commands/dev.js +733 -155
  30. package/dist/cli/commands/dev.js.map +1 -1
  31. package/dist/cli/commands/index.d.ts +1 -0
  32. package/dist/cli/commands/index.d.ts.map +1 -1
  33. package/dist/cli/commands/index.js +3 -1
  34. package/dist/cli/commands/index.js.map +1 -1
  35. package/dist/cli/commands/init.d.ts +78 -0
  36. package/dist/cli/commands/init.d.ts.map +1 -1
  37. package/dist/cli/commands/init.js +1453 -898
  38. package/dist/cli/commands/init.js.map +1 -1
  39. package/dist/cli/commands/scaffold.d.ts +24 -0
  40. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  41. package/dist/cli/commands/scaffold.js +473 -90
  42. package/dist/cli/commands/scaffold.js.map +1 -1
  43. package/dist/cli/index.d.ts +5 -1
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +333 -55
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/core/config.d.ts +14 -3
  48. package/dist/core/config.d.ts.map +1 -1
  49. package/dist/core/config.js +179 -8
  50. package/dist/core/config.js.map +1 -1
  51. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  52. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  53. package/dist/core/mock/connector-mock-server.js +480 -0
  54. package/dist/core/mock/connector-mock-server.js.map +1 -0
  55. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  56. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  57. package/dist/core/mock/zod-mock-generator.js +163 -0
  58. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  59. package/dist/core/operations/auth-operations.d.ts +61 -0
  60. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  61. package/dist/core/operations/auth-operations.js +272 -0
  62. package/dist/core/operations/auth-operations.js.map +1 -0
  63. package/dist/core/operations/create-model.d.ts +15 -0
  64. package/dist/core/operations/create-model.d.ts.map +1 -0
  65. package/dist/core/operations/create-model.js +185 -0
  66. package/dist/core/operations/create-model.js.map +1 -0
  67. package/dist/core/operations/file-session.d.ts +56 -0
  68. package/dist/core/operations/file-session.d.ts.map +1 -0
  69. package/dist/core/operations/file-session.js +203 -0
  70. package/dist/core/operations/file-session.js.map +1 -0
  71. package/dist/core/operations/provision-operations.d.ts +52 -0
  72. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  73. package/dist/core/operations/provision-operations.js +211 -0
  74. package/dist/core/operations/provision-operations.js.map +1 -0
  75. package/dist/core/operations/runtime.d.ts +37 -0
  76. package/dist/core/operations/runtime.d.ts.map +1 -0
  77. package/dist/core/operations/runtime.js +248 -0
  78. package/dist/core/operations/runtime.js.map +1 -0
  79. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  80. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  81. package/dist/core/operations/scaffold-apply.js +57 -0
  82. package/dist/core/operations/scaffold-apply.js.map +1 -0
  83. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  84. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  85. package/dist/core/operations/scaffold-machine.js +51 -0
  86. package/dist/core/operations/scaffold-machine.js.map +1 -0
  87. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  88. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  89. package/dist/core/operations/scaffold-plan.js +187 -0
  90. package/dist/core/operations/scaffold-plan.js.map +1 -0
  91. package/dist/core/project/agent-instructions.d.ts +29 -0
  92. package/dist/core/project/agent-instructions.d.ts.map +1 -0
  93. package/dist/core/project/agent-instructions.js +159 -0
  94. package/dist/core/project/agent-instructions.js.map +1 -0
  95. package/dist/core/project/artifacts.d.ts +58 -0
  96. package/dist/core/project/artifacts.d.ts.map +1 -0
  97. package/dist/core/project/artifacts.js +179 -0
  98. package/dist/core/project/artifacts.js.map +1 -0
  99. package/dist/core/project/boundaries.d.ts +50 -0
  100. package/dist/core/project/boundaries.d.ts.map +1 -0
  101. package/dist/core/project/boundaries.js +117 -0
  102. package/dist/core/project/boundaries.js.map +1 -0
  103. package/dist/core/project/capabilities.d.ts +58 -0
  104. package/dist/core/project/capabilities.d.ts.map +1 -0
  105. package/dist/core/project/capabilities.js +135 -0
  106. package/dist/core/project/capabilities.js.map +1 -0
  107. package/dist/core/project/drift.d.ts +29 -0
  108. package/dist/core/project/drift.d.ts.map +1 -0
  109. package/dist/core/project/drift.js +188 -0
  110. package/dist/core/project/drift.js.map +1 -0
  111. package/dist/core/project/infra.d.ts +49 -0
  112. package/dist/core/project/infra.d.ts.map +1 -0
  113. package/dist/core/project/infra.js +167 -0
  114. package/dist/core/project/infra.js.map +1 -0
  115. package/dist/core/project/manifest.d.ts +92 -0
  116. package/dist/core/project/manifest.d.ts.map +1 -0
  117. package/dist/core/project/manifest.js +321 -0
  118. package/dist/core/project/manifest.js.map +1 -0
  119. package/dist/core/project/state.d.ts +15 -0
  120. package/dist/core/project/state.d.ts.map +1 -0
  121. package/dist/core/project/state.js +106 -0
  122. package/dist/core/project/state.js.map +1 -0
  123. package/dist/core/project/validation.d.ts +20 -0
  124. package/dist/core/project/validation.d.ts.map +1 -0
  125. package/dist/core/project/validation.js +274 -0
  126. package/dist/core/project/validation.js.map +1 -0
  127. package/dist/core/project/workflows.d.ts +43 -0
  128. package/dist/core/project/workflows.d.ts.map +1 -0
  129. package/dist/core/project/workflows.js +299 -0
  130. package/dist/core/project/workflows.js.map +1 -0
  131. package/dist/core/scaffold/auth-generator.d.ts +63 -0
  132. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  133. package/dist/core/scaffold/auth-generator.js +1774 -0
  134. package/dist/core/scaffold/auth-generator.js.map +1 -0
  135. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  136. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  137. package/dist/core/scaffold/connector-functions-generator.js +1029 -0
  138. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  139. package/dist/core/scaffold/functions-generator.d.ts +7 -1
  140. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  141. package/dist/core/scaffold/functions-generator.js +771 -69
  142. package/dist/core/scaffold/functions-generator.js.map +1 -1
  143. package/dist/core/scaffold/model-parser.d.ts +21 -1
  144. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  145. package/dist/core/scaffold/model-parser.js +243 -43
  146. package/dist/core/scaffold/model-parser.js.map +1 -1
  147. package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
  148. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  149. package/dist/core/scaffold/native-schema-generator.js +753 -0
  150. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  151. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  152. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  153. package/dist/core/scaffold/nextjs-generator.js +153 -9
  154. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  155. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  156. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  157. package/dist/core/scaffold/openapi-generator.js +190 -0
  158. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  159. package/dist/core/scaffold/ui-generator.d.ts +17 -4
  160. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  161. package/dist/core/scaffold/ui-generator.js +174 -34
  162. package/dist/core/scaffold/ui-generator.js.map +1 -1
  163. package/dist/core/verify/index.d.ts +69 -0
  164. package/dist/core/verify/index.d.ts.map +1 -0
  165. package/dist/core/verify/index.js +412 -0
  166. package/dist/core/verify/index.js.map +1 -0
  167. package/dist/database/base-model.d.ts +3 -3
  168. package/dist/database/base-model.js +3 -3
  169. package/dist/index.d.ts +2 -2
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +2 -1
  172. package/dist/index.js.map +1 -1
  173. package/dist/machine/contracts.d.ts +24 -0
  174. package/dist/machine/contracts.d.ts.map +1 -0
  175. package/dist/machine/contracts.js +3 -0
  176. package/dist/machine/contracts.js.map +1 -0
  177. package/dist/machine/errors.d.ts +14 -0
  178. package/dist/machine/errors.d.ts.map +1 -0
  179. package/dist/machine/errors.js +42 -0
  180. package/dist/machine/errors.js.map +1 -0
  181. package/dist/machine/index.d.ts +3 -0
  182. package/dist/machine/index.d.ts.map +1 -0
  183. package/dist/machine/index.js +566 -0
  184. package/dist/machine/index.js.map +1 -0
  185. package/dist/mcp/index.d.ts +25 -0
  186. package/dist/mcp/index.d.ts.map +1 -0
  187. package/dist/mcp/index.js +425 -0
  188. package/dist/mcp/index.js.map +1 -0
  189. package/dist/types/index.d.ts +109 -0
  190. package/dist/types/index.d.ts.map +1 -1
  191. package/dist/utils/package-manager.d.ts +5 -2
  192. package/dist/utils/package-manager.d.ts.map +1 -1
  193. package/dist/utils/package-manager.js +30 -9
  194. package/dist/utils/package-manager.js.map +1 -1
  195. package/dist/utils/python-uv.d.ts +21 -0
  196. package/dist/utils/python-uv.d.ts.map +1 -0
  197. package/dist/utils/python-uv.js +111 -0
  198. package/dist/utils/python-uv.js.map +1 -0
  199. package/dist/version.d.ts +2 -0
  200. package/dist/version.d.ts.map +1 -0
  201. package/dist/version.js +49 -0
  202. package/dist/version.js.map +1 -0
  203. package/package.json +9 -5
  204. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
  205. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +206 -0
  206. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
  207. package/src/__tests__/agent-instructions.test.ts +237 -0
  208. package/src/__tests__/agent-loop-phase34.test.ts +513 -0
  209. package/src/__tests__/auth.test.ts +770 -0
  210. package/src/__tests__/bff-swa-auth.test.ts +275 -0
  211. package/src/__tests__/config.test.ts +295 -0
  212. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  213. package/src/__tests__/connector-mock-server.test.ts +439 -0
  214. package/src/__tests__/connector-model-bff.test.ts +162 -0
  215. package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
  216. package/src/__tests__/dev-seeds.test.ts +236 -0
  217. package/src/__tests__/dev.test.ts +296 -0
  218. package/src/__tests__/file-session.test.ts +98 -0
  219. package/src/__tests__/fixtures.ts +184 -0
  220. package/src/__tests__/functions-generator.test.ts +237 -0
  221. package/src/__tests__/init.test.ts +411 -0
  222. package/src/__tests__/jest-globals.d.ts +8 -0
  223. package/src/__tests__/machine.test.ts +371 -0
  224. package/src/__tests__/mcp.test.ts +253 -0
  225. package/src/__tests__/model-parser.test.ts +81 -0
  226. package/src/__tests__/multi-auth.test.ts +160 -0
  227. package/src/__tests__/nextjs-generator.test.ts +105 -0
  228. package/src/__tests__/openapi-generator.test.ts +43 -0
  229. package/src/__tests__/package-manager.test.ts +227 -0
  230. package/src/__tests__/plan-apply.test.ts +499 -0
  231. package/src/__tests__/python-uv.test.ts +48 -0
  232. package/src/__tests__/scaffold.test.ts +302 -0
  233. package/src/__tests__/string-utils.test.ts +75 -0
  234. package/src/__tests__/tsconfig.json +15 -0
  235. package/src/__tests__/ui-generator.test.ts +248 -0
  236. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  237. package/src/cli/commands/add-auth.ts +853 -0
  238. package/src/cli/commands/add-connector.ts +158 -0
  239. package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
  240. package/src/cli/commands/create-model.ts +62 -0
  241. package/src/cli/commands/dev-seeds.ts +629 -0
  242. package/src/cli/commands/dev.ts +1252 -0
  243. package/src/cli/commands/index.ts +9 -0
  244. package/src/cli/commands/init.ts +3541 -0
  245. package/src/cli/commands/provision.ts +193 -0
  246. package/src/cli/commands/scaffold.ts +1081 -0
  247. package/src/cli/index.ts +350 -0
  248. package/src/core/config.ts +380 -0
  249. package/src/core/mock/connector-mock-server.ts +555 -0
  250. package/src/core/mock/zod-mock-generator.ts +205 -0
  251. package/src/core/operations/auth-operations.ts +360 -0
  252. package/src/core/operations/create-model.ts +191 -0
  253. package/src/core/operations/file-session.ts +222 -0
  254. package/src/core/operations/provision-operations.ts +297 -0
  255. package/src/core/operations/runtime.ts +258 -0
  256. package/src/core/operations/scaffold-apply.ts +106 -0
  257. package/src/core/operations/scaffold-machine.ts +77 -0
  258. package/src/core/operations/scaffold-plan.ts +215 -0
  259. package/src/core/project/agent-instructions.ts +167 -0
  260. package/src/core/project/agent-templates/AGENTS.md +502 -0
  261. package/src/core/project/agent-templates/CLAUDE.md +31 -0
  262. package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
  263. package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
  264. package/src/core/project/agent-templates/copilot-instructions.md +27 -0
  265. package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
  266. package/src/core/project/artifacts.ts +205 -0
  267. package/src/core/project/boundaries.ts +170 -0
  268. package/src/core/project/capabilities.ts +193 -0
  269. package/src/core/project/drift.ts +202 -0
  270. package/src/core/project/infra.ts +196 -0
  271. package/src/core/project/manifest.ts +402 -0
  272. package/src/core/project/state.ts +77 -0
  273. package/src/core/project/validation.ts +295 -0
  274. package/src/core/project/workflows.ts +295 -0
  275. package/src/core/scaffold/auth-generator.ts +1824 -0
  276. package/src/core/scaffold/connector-functions-generator.ts +1133 -0
  277. package/src/core/scaffold/functions-generator.ts +972 -0
  278. package/src/core/scaffold/model-parser.ts +854 -0
  279. package/src/core/scaffold/native-schema-generator.ts +904 -0
  280. package/src/core/scaffold/nextjs-generator.ts +382 -0
  281. package/src/core/scaffold/openapi-generator.ts +212 -0
  282. package/src/core/scaffold/ui-generator.ts +1100 -0
  283. package/src/core/verify/index.ts +468 -0
  284. package/src/database/base-model.ts +184 -0
  285. package/src/database/client.ts +140 -0
  286. package/src/database/repository.ts +104 -0
  287. package/src/database/runtime-check.ts +25 -0
  288. package/src/index.ts +27 -0
  289. package/src/machine/contracts.ts +33 -0
  290. package/src/machine/errors.ts +45 -0
  291. package/src/machine/index.ts +690 -0
  292. package/src/mcp/index.ts +423 -0
  293. package/src/types/index.ts +182 -0
  294. package/src/utils/package-manager.ts +246 -0
  295. package/src/utils/python-uv.ts +96 -0
  296. package/src/version.ts +13 -0
@@ -6,28 +6,66 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.generateCompactAzureFunctionsCRUD = generateCompactAzureFunctionsCRUD;
9
+ exports.generateCSharpAzureFunctionsCRUD = generateCSharpAzureFunctionsCRUD;
10
+ exports.generatePythonAzureFunctionsCRUD = generatePythonAzureFunctionsCRUD;
9
11
  const model_parser_1 = require("./model-parser");
12
+ const auth_generator_1 = require("./auth-generator");
10
13
  /**
11
14
  * Azure Functions エンティティファイルを生成(インラインハンドラー方式)
12
15
  * 各ハンドラーがベタ書きされており、ビジネスロジックの追加・変更が容易
13
16
  */
14
- function generateCompactAzureFunctionsCRUD(model, sharedPackageName) {
17
+ function generateCompactAzureFunctionsCRUD(model, sharedPackageName, authPolicy, authProvider) {
15
18
  const modelName = model.name;
16
19
  const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
17
- const modelKebab = (0, model_parser_1.toKebabCase)(modelName);
18
20
  const schemaName = model.schemaName;
21
+ const partitionKeyPath = model.partitionKey; // e.g. "/tenantId"
22
+ const partitionKeyField = partitionKeyPath.slice(1); // e.g. "tenantId"
23
+ const isIdPartition = partitionKeyField === 'id';
24
+ const hasAuth = !!authPolicy;
25
+ const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
26
+ const authImport = hasAuth ? `\n${(0, auth_generator_1.generateAuthImportTS)(namedAuth)}\n` : '';
27
+ const readGuard = hasAuth ? `\n${(0, auth_generator_1.generateAuthGuardTS)(authPolicy, 'read', authProvider)}\n` : '';
28
+ const writeGuard = hasAuth ? `\n${(0, auth_generator_1.generateAuthGuardTS)(authPolicy, 'write', authProvider)}\n` : '';
29
+ const authCatchBlock = hasAuth
30
+ ? ` const authErr = handleAuthError(error);
31
+ if (authErr) return authErr;\n`
32
+ : '';
33
+ // SDK クライアント初期化ヘルパー(PK≠/id の場合、および delete で使用)
34
+ const sdkClientInit = `const { CosmosClient } = await import('@azure/cosmos');
35
+ const endpoint = process.env.CosmosDBConnection__accountEndpoint;
36
+ let client: InstanceType<typeof CosmosClient>;
37
+ if (endpoint) {
38
+ const { DefaultAzureCredential } = await import('@azure/identity');
39
+ client = new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
40
+ } else {
41
+ client = new CosmosClient(process.env.CosmosDBConnection!);
42
+ }
43
+ const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
44
+ const container = database.container(containerName);`;
45
+ // getById ハンドラー: PK=/id の場合は input binding、それ以外は SDK
46
+ const getByIdHandler = isIdPartition
47
+ ? generateGetByIdWithBinding(modelCamel, schemaName, readGuard, authCatchBlock)
48
+ : generateGetByIdWithSdk(modelCamel, schemaName, readGuard, authCatchBlock, sdkClientInit);
49
+ // update ハンドラー: PK=/id の場合は input binding、それ以外は SDK
50
+ const updateHandler = isIdPartition
51
+ ? generateUpdateWithBinding(modelCamel, schemaName, writeGuard, authCatchBlock)
52
+ : generateUpdateWithSdk(modelCamel, schemaName, partitionKeyField, writeGuard, authCatchBlock, sdkClientInit);
53
+ // delete ハンドラー: 常に SDK(既存パターン)、PK に応じて分岐
54
+ const deleteHandler = isIdPartition
55
+ ? generateDeleteIdPartition(modelCamel, writeGuard, authCatchBlock, sdkClientInit)
56
+ : generateDeleteCustomPartition(modelCamel, partitionKeyField, writeGuard, authCatchBlock, sdkClientInit);
19
57
  return `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
20
58
  import { z } from 'zod/v4';
21
59
  import crypto from 'crypto';
22
- import { ${schemaName} } from '${sharedPackageName}';
60
+ import { ${schemaName} } from '${sharedPackageName}';${authImport}
23
61
 
24
- const containerName = '${modelName}s';
62
+ const containerName = '${modelName.endsWith('s') ? modelName : modelName + 's'}';
25
63
 
26
64
  // GET /api/${modelCamel} - 全件取得
27
65
  app.http('${modelCamel}-get-all', {
28
66
  methods: ['GET'],
29
67
  route: '${modelCamel}',
30
- authLevel: 'anonymous',
68
+ authLevel: 'function',
31
69
  extraInputs: [
32
70
  {
33
71
  type: 'cosmosDB',
@@ -39,8 +77,8 @@ app.http('${modelCamel}-get-all', {
39
77
  },
40
78
  ],
41
79
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
42
- try {
43
- const documents = context.extraInputs.get('cosmosInput') as any[];
80
+ try {${readGuard}
81
+ const documents = context.extraInputs.get('cosmosInput') as unknown[];
44
82
 
45
83
  if (!documents || !Array.isArray(documents)) {
46
84
  return { status: 200, jsonBody: [] };
@@ -51,50 +89,19 @@ app.http('${modelCamel}-get-all', {
51
89
 
52
90
  return { status: 200, jsonBody: validated };
53
91
  } catch (error) {
54
- context.error(\`Error fetching from \${containerName}:\`, error);
92
+ ${authCatchBlock} context.error(\`Error fetching from \${containerName}:\`, error);
55
93
  return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
56
94
  }
57
95
  },
58
96
  });
59
97
 
60
- // GET /api/${modelCamel}/{id} - ID指定取得
61
- app.http('${modelCamel}-get-by-id', {
62
- methods: ['GET'],
63
- route: '${modelCamel}/{id}',
64
- authLevel: 'anonymous',
65
- extraInputs: [
66
- {
67
- type: 'cosmosDB',
68
- name: 'cosmosInput',
69
- databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
70
- containerName,
71
- connection: 'CosmosDBConnection',
72
- id: '{id}',
73
- partitionKey: '{id}',
74
- },
75
- ],
76
- handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
77
- try {
78
- const document = context.extraInputs.get('cosmosInput');
79
-
80
- if (!document) {
81
- return { status: 404, jsonBody: { error: 'Item not found' } };
82
- }
83
-
84
- const validated = ${schemaName}.parse(document);
85
- return { status: 200, jsonBody: validated };
86
- } catch (error) {
87
- context.error(\`Error fetching item from \${containerName}:\`, error);
88
- return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
89
- }
90
- },
91
- });
98
+ ${getByIdHandler}
92
99
 
93
100
  // POST /api/${modelCamel} - 新規作成
94
101
  app.http('${modelCamel}-create', {
95
102
  methods: ['POST'],
96
103
  route: '${modelCamel}',
97
- authLevel: 'anonymous',
104
+ authLevel: 'function',
98
105
  extraOutputs: [
99
106
  {
100
107
  type: 'cosmosDB',
@@ -106,8 +113,8 @@ app.http('${modelCamel}-create', {
106
113
  },
107
114
  ],
108
115
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
109
- try {
110
- const body = await request.json() as any;
116
+ try {${writeGuard}
117
+ const body = await request.json() as Record<string, unknown>;
111
118
 
112
119
  const { id, createdAt, updatedAt, ...userData } = body;
113
120
  const now = new Date().toISOString();
@@ -128,17 +135,92 @@ app.http('${modelCamel}-create', {
128
135
  context.extraOutputs.set('cosmosOutput', result.data);
129
136
  return { status: 201, jsonBody: result.data };
130
137
  } catch (error) {
131
- context.error(\`Error creating item in \${containerName}:\`, error);
138
+ ${authCatchBlock} context.error(\`Error creating item in \${containerName}:\`, error);
132
139
  return { status: 500, jsonBody: { error: 'Failed to create item' } };
133
140
  }
134
141
  },
135
142
  });
136
143
 
137
- // PUT /api/${modelCamel}/{id} - 更新
144
+ ${updateHandler}
145
+
146
+ ${deleteHandler}
147
+ `;
148
+ }
149
+ // --- TS getById ハンドラー生成ヘルパー ---
150
+ function generateGetByIdWithBinding(modelCamel, schemaName, readGuard, authCatchBlock) {
151
+ return `// GET /api/${modelCamel}/{id} - ID指定取得
152
+ app.http('${modelCamel}-get-by-id', {
153
+ methods: ['GET'],
154
+ route: '${modelCamel}/{id}',
155
+ authLevel: 'function',
156
+ extraInputs: [
157
+ {
158
+ type: 'cosmosDB',
159
+ name: 'cosmosInput',
160
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
161
+ containerName,
162
+ connection: 'CosmosDBConnection',
163
+ id: '{id}',
164
+ partitionKey: '{id}',
165
+ },
166
+ ],
167
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
168
+ try {${readGuard}
169
+ const document = context.extraInputs.get('cosmosInput');
170
+
171
+ if (!document) {
172
+ return { status: 404, jsonBody: { error: 'Item not found' } };
173
+ }
174
+
175
+ const validated = ${schemaName}.parse(document);
176
+ return { status: 200, jsonBody: validated };
177
+ } catch (error) {
178
+ ${authCatchBlock} context.error(\`Error fetching item from \${containerName}:\`, error);
179
+ return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
180
+ }
181
+ },
182
+ });`;
183
+ }
184
+ function generateGetByIdWithSdk(modelCamel, schemaName, readGuard, authCatchBlock, sdkClientInit) {
185
+ return `// GET /api/${modelCamel}/{id} - ID指定取得 (custom partition key)
186
+ app.http('${modelCamel}-get-by-id', {
187
+ methods: ['GET'],
188
+ route: '${modelCamel}/{id}',
189
+ authLevel: 'function',
190
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
191
+ try {${readGuard}
192
+ const id = request.params.id;
193
+ if (!id) {
194
+ return { status: 400, jsonBody: { error: 'ID is required' } };
195
+ }
196
+
197
+ ${sdkClientInit}
198
+
199
+ const { resources } = await container.items.query({
200
+ query: 'SELECT * FROM c WHERE c.id = @id',
201
+ parameters: [{ name: '@id', value: id }],
202
+ }).fetchAll();
203
+
204
+ if (!resources || resources.length === 0) {
205
+ return { status: 404, jsonBody: { error: 'Item not found' } };
206
+ }
207
+
208
+ const validated = ${schemaName}.parse(resources[0]);
209
+ return { status: 200, jsonBody: validated };
210
+ } catch (error) {
211
+ ${authCatchBlock} context.error(\`Error fetching item from \${containerName}:\`, error);
212
+ return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
213
+ }
214
+ },
215
+ });`;
216
+ }
217
+ // --- TS update ハンドラー生成ヘルパー ---
218
+ function generateUpdateWithBinding(modelCamel, schemaName, writeGuard, authCatchBlock) {
219
+ return `// PUT /api/${modelCamel}/{id} - 更新
138
220
  app.http('${modelCamel}-update', {
139
221
  methods: ['PUT'],
140
222
  route: '${modelCamel}/{id}',
141
- authLevel: 'anonymous',
223
+ authLevel: 'function',
142
224
  extraInputs: [
143
225
  {
144
226
  type: 'cosmosDB',
@@ -160,18 +242,18 @@ app.http('${modelCamel}-update', {
160
242
  },
161
243
  ],
162
244
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
163
- try {
245
+ try {${writeGuard}
164
246
  const id = request.params.id;
165
247
  if (!id) {
166
248
  return { status: 400, jsonBody: { error: 'ID is required' } };
167
249
  }
168
250
 
169
- const existingDocument = context.extraInputs.get('cosmosInput') as any;
251
+ const existingDocument = context.extraInputs.get('cosmosInput') as Record<string, unknown> | undefined;
170
252
  if (!existingDocument) {
171
253
  return { status: 404, jsonBody: { error: 'Item not found' } };
172
254
  }
173
255
 
174
- const body = await request.json() as any;
256
+ const body = await request.json() as Record<string, unknown>;
175
257
  const { createdAt, updatedAt, ...userData } = body;
176
258
 
177
259
  const dataWithManagedFields = {
@@ -191,49 +273,669 @@ app.http('${modelCamel}-update', {
191
273
  context.extraOutputs.set('cosmosOutput', result.data);
192
274
  return { status: 200, jsonBody: result.data };
193
275
  } catch (error) {
194
- context.error(\`Error updating item in \${containerName}:\`, error);
276
+ ${authCatchBlock} context.error(\`Error updating item in \${containerName}:\`, error);
195
277
  return { status: 500, jsonBody: { error: 'Failed to update item' } };
196
278
  }
197
279
  },
198
- });
280
+ });`;
281
+ }
282
+ function generateUpdateWithSdk(modelCamel, schemaName, partitionKeyField, writeGuard, authCatchBlock, sdkClientInit) {
283
+ return `// PUT /api/${modelCamel}/{id} - 更新 (custom partition key)
284
+ app.http('${modelCamel}-update', {
285
+ methods: ['PUT'],
286
+ route: '${modelCamel}/{id}',
287
+ authLevel: 'function',
288
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
289
+ try {${writeGuard}
290
+ const id = request.params.id;
291
+ if (!id) {
292
+ return { status: 400, jsonBody: { error: 'ID is required' } };
293
+ }
294
+
295
+ ${sdkClientInit}
296
+
297
+ const { resources } = await container.items.query({
298
+ query: 'SELECT * FROM c WHERE c.id = @id',
299
+ parameters: [{ name: '@id', value: id }],
300
+ }).fetchAll();
301
+
302
+ if (!resources || resources.length === 0) {
303
+ return { status: 404, jsonBody: { error: 'Item not found' } };
304
+ }
305
+
306
+ const existingDocument = resources[0];
307
+ const body = await request.json() as Record<string, unknown>;
308
+ const { createdAt, updatedAt, ...userData } = body;
309
+
310
+ const dataWithManagedFields = {
311
+ ...userData,
312
+ id,
313
+ createdAt: existingDocument.createdAt,
314
+ updatedAt: new Date().toISOString(),
315
+ };
316
+
317
+ const result = ${schemaName}.safeParse(dataWithManagedFields);
318
+
319
+ if (!result.success) {
320
+ context.error('Validation failed:', result.error.issues);
321
+ return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
322
+ }
323
+
324
+ const pkValue = result.data.${partitionKeyField};
325
+ await container.items.upsert(result.data);
326
+ return { status: 200, jsonBody: result.data };
327
+ } catch (error) {
328
+ ${authCatchBlock} context.error(\`Error updating item in \${containerName}:\`, error);
329
+ return { status: 500, jsonBody: { error: 'Failed to update item' } };
330
+ }
331
+ },
332
+ });`;
333
+ }
334
+ // --- TS delete ハンドラー生成ヘルパー ---
335
+ function generateDeleteIdPartition(modelCamel, writeGuard, authCatchBlock, sdkClientInit) {
336
+ return `// DELETE /api/${modelCamel}/{id} - 削除
337
+ app.http('${modelCamel}-delete', {
338
+ methods: ['DELETE'],
339
+ route: '${modelCamel}/{id}',
340
+ authLevel: 'function',
341
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
342
+ try {${writeGuard}
343
+ const id = request.params.id;
344
+ if (!id) {
345
+ return { status: 400, jsonBody: { error: 'ID is required' } };
346
+ }
347
+
348
+ ${sdkClientInit}
349
+
350
+ await container.item(id, id).delete();
351
+ context.log(\`Deleted item \${id} from \${containerName}\`);
199
352
 
200
- // DELETE /api/${modelCamel}/{id} - 削除
353
+ return { status: 204 };
354
+ } catch (error) {
355
+ if ((error as { code?: number }).code === 404) {
356
+ return { status: 404, jsonBody: { error: 'Item not found' } };
357
+ }
358
+ ${authCatchBlock} context.error(\`Error deleting item from \${containerName}:\`, error);
359
+ return { status: 500, jsonBody: { error: 'Failed to delete item' } };
360
+ }
361
+ },
362
+ });`;
363
+ }
364
+ function generateDeleteCustomPartition(modelCamel, partitionKeyField, writeGuard, authCatchBlock, sdkClientInit) {
365
+ return `// DELETE /api/${modelCamel}/{id} - 削除 (custom partition key)
201
366
  app.http('${modelCamel}-delete', {
202
367
  methods: ['DELETE'],
203
368
  route: '${modelCamel}/{id}',
204
- authLevel: 'anonymous',
369
+ authLevel: 'function',
205
370
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
206
- try {
371
+ try {${writeGuard}
207
372
  const id = request.params.id;
208
373
  if (!id) {
209
374
  return { status: 400, jsonBody: { error: 'ID is required' } };
210
375
  }
211
376
 
212
- const { CosmosClient } = await import('@azure/cosmos');
213
- const endpoint = process.env.CosmosDBConnection__accountEndpoint;
214
- let client: InstanceType<typeof CosmosClient>;
215
- if (endpoint) {
216
- const { DefaultAzureCredential } = await import('@azure/identity');
217
- client = new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
218
- } else {
219
- client = new CosmosClient(process.env.CosmosDBConnection!);
377
+ ${sdkClientInit}
378
+
379
+ // パーティションキー値を取得するためにドキュメントを読み取り
380
+ const { resources } = await container.items.query({
381
+ query: 'SELECT * FROM c WHERE c.id = @id',
382
+ parameters: [{ name: '@id', value: id }],
383
+ }).fetchAll();
384
+
385
+ if (!resources || resources.length === 0) {
386
+ return { status: 404, jsonBody: { error: 'Item not found' } };
220
387
  }
221
- const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
222
- const container = database.container(containerName);
223
388
 
224
- await container.item(id, id).delete();
389
+ const pkValue = resources[0].${partitionKeyField};
390
+ await container.item(id, pkValue).delete();
225
391
  context.log(\`Deleted item \${id} from \${containerName}\`);
226
392
 
227
393
  return { status: 204 };
228
- } catch (error: any) {
229
- if (error.code === 404) {
394
+ } catch (error) {
395
+ if ((error as { code?: number }).code === 404) {
230
396
  return { status: 404, jsonBody: { error: 'Item not found' } };
231
397
  }
232
- context.error(\`Error deleting item from \${containerName}:\`, error);
398
+ ${authCatchBlock} context.error(\`Error deleting item from \${containerName}:\`, error);
233
399
  return { status: 500, jsonBody: { error: 'Failed to delete item' } };
234
400
  }
235
401
  },
236
- });
402
+ });`;
403
+ }
404
+ function generateCSharpAzureFunctionsCRUD(model, authPolicy) {
405
+ const modelName = model.name;
406
+ const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
407
+ const className = `${modelName}CrudFunctions`;
408
+ const containerName = modelName.endsWith('s') ? modelName : `${modelName}s`;
409
+ const partitionKeyPath = model.partitionKey;
410
+ const partitionKeyField = partitionKeyPath.slice(1);
411
+ const isIdPartition = partitionKeyField === 'id';
412
+ const hasAuth = !!authPolicy;
413
+ const authUsing = hasAuth ? 'using Functions.Auth;\n' : '';
414
+ const readGuard = hasAuth ? `\n${(0, auth_generator_1.generateAuthGuardCSharp)(authPolicy, 'read')}\n` : '';
415
+ const writeGuard = hasAuth ? `\n${(0, auth_generator_1.generateAuthGuardCSharp)(authPolicy, 'write')}\n` : '';
416
+ // PK値の取得ロジック(create 用)
417
+ const createPkExpr = isIdPartition
418
+ ? 'id'
419
+ : `payload["${partitionKeyField}"]?.GetValue<string>() ?? throw new InvalidOperationException("Partition key field '${partitionKeyField}' is required.")`;
420
+ // ReadCosmosItemAsync: PK=/id の場合は直接読み取り、それ以外はクエリ
421
+ const readItemMethod = isIdPartition
422
+ ? ` private static async Task<JsonObject> ReadCosmosItemAsync(Container container, string id)
423
+ {
424
+ var response = await container.ReadItemStreamAsync(id, new PartitionKey(id));
425
+ if (response.StatusCode == HttpStatusCode.NotFound)
426
+ {
427
+ throw new CosmosException("Item not found", HttpStatusCode.NotFound, 0, response.Headers.ActivityId, response.Headers.RequestCharge);
428
+ }
429
+
430
+ if (!response.IsSuccessStatusCode)
431
+ {
432
+ throw new InvalidOperationException($"Cosmos read failed with status {(int)response.StatusCode}.");
433
+ }
434
+
435
+ using var document = await JsonDocument.ParseAsync(response.Content);
436
+ return JsonNode.Parse(document.RootElement.GetRawText())?.AsObject()
437
+ ?? throw new JsonException("Cosmos item payload must be a JSON object.");
438
+ }`
439
+ : ` private static async Task<JsonObject> ReadCosmosItemAsync(Container container, string id)
440
+ {
441
+ var query = new QueryDefinition("SELECT * FROM c WHERE c.id = @id")
442
+ .WithParameter("@id", id);
443
+ using var iterator = container.GetItemQueryStreamIterator(query);
444
+
445
+ while (iterator.HasMoreResults)
446
+ {
447
+ var page = await iterator.ReadNextAsync();
448
+ if (!page.IsSuccessStatusCode)
449
+ {
450
+ throw new InvalidOperationException($"Cosmos query failed with status {(int)page.StatusCode}.");
451
+ }
452
+
453
+ using var document = await JsonDocument.ParseAsync(page.Content);
454
+ if (document.RootElement.TryGetProperty("Documents", out var documents))
455
+ {
456
+ foreach (var item in documents.EnumerateArray())
457
+ {
458
+ return JsonNode.Parse(item.GetRawText())?.AsObject()
459
+ ?? throw new JsonException("Cosmos item payload must be a JSON object.");
460
+ }
461
+ }
462
+ }
463
+
464
+ throw new CosmosException("Item not found", HttpStatusCode.NotFound, 0, "", 0);
465
+ }`;
466
+ // Replace / Delete の PK 解決ロジック
467
+ const replacePkExpr = isIdPartition
468
+ ? 'new PartitionKey(id)'
469
+ : `new PartitionKey(payload["${partitionKeyField}"]?.GetValue<string>())`;
470
+ const deleteLogic = isIdPartition
471
+ ? ` await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(id));`
472
+ : ` var existing = await ReadCosmosItemAsync(container, id);
473
+ var pkValue = existing["${partitionKeyField}"]?.GetValue<string>();
474
+ await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(pkValue));`;
475
+ return `using System.Net;
476
+ using System.Text;
477
+ using System.Text.Json;
478
+ using System.Text.Json.Nodes;
479
+ using Azure.Identity;
480
+ using Microsoft.Azure.Cosmos;
481
+ using Microsoft.Azure.Functions.Worker;
482
+ using Microsoft.Azure.Functions.Worker.Http;
483
+ using Microsoft.Extensions.Logging;
484
+ ${authUsing}
485
+ namespace SwallowKit.Functions;
486
+
487
+ public sealed class ${className}
488
+ {
489
+ private readonly ILogger<${className}> _logger;
490
+ private static readonly string ContainerName = "${containerName}";
491
+
492
+ public ${className}(ILogger<${className}> logger)
493
+ {
494
+ _logger = logger;
495
+ }
496
+
497
+ private static string DatabaseName => Environment.GetEnvironmentVariable("COSMOS_DB_DATABASE_NAME") ?? "AppDatabase";
498
+
499
+ private static bool IsLocalCosmosEndpoint(string endpoint) =>
500
+ endpoint.Contains("localhost:8081", StringComparison.OrdinalIgnoreCase) ||
501
+ endpoint.Contains("127.0.0.1:8081", StringComparison.OrdinalIgnoreCase);
502
+
503
+ private static CosmosClient CreateCosmosClient()
504
+ {
505
+ var endpoint = Environment.GetEnvironmentVariable("CosmosDBConnection__accountEndpoint");
506
+ if (!string.IsNullOrWhiteSpace(endpoint))
507
+ {
508
+ var options = IsLocalCosmosEndpoint(endpoint)
509
+ ? new CosmosClientOptions { ConnectionMode = ConnectionMode.Gateway }
510
+ : new CosmosClientOptions();
511
+ return new CosmosClient(endpoint, new DefaultAzureCredential(), options);
512
+ }
513
+
514
+ var connectionString = Environment.GetEnvironmentVariable("CosmosDBConnection");
515
+ if (string.IsNullOrWhiteSpace(connectionString))
516
+ {
517
+ throw new InvalidOperationException("Cosmos DB connection is not configured.");
518
+ }
519
+
520
+ var connectionOptions = IsLocalCosmosEndpoint(connectionString)
521
+ ? new CosmosClientOptions { ConnectionMode = ConnectionMode.Gateway }
522
+ : new CosmosClientOptions();
523
+
524
+ return new CosmosClient(connectionString, connectionOptions);
525
+ }
526
+
527
+ private static Container GetContainer(CosmosClient client) => client.GetContainer(DatabaseName, ContainerName);
528
+
529
+ private static async Task<JsonObject> ReadRequestBodyAsync(HttpRequestData request)
530
+ {
531
+ using var reader = new StreamReader(request.Body, Encoding.UTF8);
532
+ var raw = await reader.ReadToEndAsync();
533
+ var node = JsonNode.Parse(raw) as JsonObject;
534
+
535
+ if (node is null)
536
+ {
537
+ throw new JsonException("Request body must be a JSON object.");
538
+ }
539
+
540
+ return node;
541
+ }
542
+
543
+ private static JsonObject BuildManagedDocument(JsonObject source, string id, string createdAt, string updatedAt)
544
+ {
545
+ var payload = new JsonObject();
546
+
547
+ foreach (var entry in source)
548
+ {
549
+ if (entry.Key is "id" or "createdAt" or "updatedAt")
550
+ {
551
+ continue;
552
+ }
553
+
554
+ payload[entry.Key] = entry.Value?.DeepClone();
555
+ }
556
+
557
+ payload["id"] = id;
558
+ payload["createdAt"] = createdAt;
559
+ payload["updatedAt"] = updatedAt;
560
+
561
+ return payload;
562
+ }
563
+
564
+ private static Stream CreateJsonStream(JsonObject payload) =>
565
+ new MemoryStream(Encoding.UTF8.GetBytes(payload.ToJsonString()));
566
+
567
+ ${readItemMethod}
568
+
569
+ private static async Task<HttpResponseData> WriteJsonAsync(HttpRequestData request, HttpStatusCode status, object payload)
570
+ {
571
+ var response = request.CreateResponse(status);
572
+ await response.WriteAsJsonAsync(payload);
573
+ return response;
574
+ }
575
+
576
+ [Function("${modelCamel}GetAll")]
577
+ public async Task<HttpResponseData> GetAll(
578
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}")] HttpRequestData request)
579
+ {
580
+ try
581
+ {${readGuard}
582
+ using var client = CreateCosmosClient();
583
+ var container = GetContainer(client);
584
+ using var iterator = container.GetItemQueryStreamIterator("SELECT * FROM c");
585
+ var items = new List<JsonElement>();
586
+
587
+ while (iterator.HasMoreResults)
588
+ {
589
+ var page = await iterator.ReadNextAsync();
590
+ if (!page.IsSuccessStatusCode)
591
+ {
592
+ throw new InvalidOperationException($"Cosmos query failed with status {(int)page.StatusCode}.");
593
+ }
594
+
595
+ using var document = await JsonDocument.ParseAsync(page.Content);
596
+ if (!document.RootElement.TryGetProperty("Documents", out var documents))
597
+ {
598
+ continue;
599
+ }
600
+
601
+ foreach (var item in documents.EnumerateArray())
602
+ {
603
+ items.Add(item.Clone());
604
+ }
605
+ }
606
+
607
+ _logger.LogInformation("Fetched {Count} ${modelName} item(s) from {Container}.", items.Count, ContainerName);
608
+ return await WriteJsonAsync(request, HttpStatusCode.OK, items);
609
+ }
610
+ catch (Exception ex)
611
+ {
612
+ _logger.LogError(ex, "Failed to fetch ${modelName} items from Cosmos DB.");
613
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch items" });
614
+ }
615
+ }
616
+
617
+ [Function("${modelCamel}GetById")]
618
+ public async Task<HttpResponseData> GetById(
619
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
620
+ string id)
621
+ {
622
+ try
623
+ {${readGuard}
624
+ using var client = CreateCosmosClient();
625
+ var container = GetContainer(client);
626
+ var item = await ReadCosmosItemAsync(container, id);
627
+ return await WriteJsonAsync(request, HttpStatusCode.OK, item);
628
+ }
629
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
630
+ {
631
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
632
+ }
633
+ catch (Exception ex)
634
+ {
635
+ _logger.LogError(ex, "Failed to fetch ${modelName} item {Id} from Cosmos DB.", id);
636
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch item", id });
637
+ }
638
+ }
639
+
640
+ [Function("${modelCamel}Create")]
641
+ public async Task<HttpResponseData> Create(
642
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "${modelCamel}")] HttpRequestData request)
643
+ {
644
+ try
645
+ {${writeGuard}
646
+ var body = await ReadRequestBodyAsync(request);
647
+ var now = DateTimeOffset.UtcNow.ToString("O");
648
+ var id = body["id"]?.GetValue<string>() ?? Guid.NewGuid().ToString();
649
+ var payload = BuildManagedDocument(body, id, now, now);
650
+
651
+ using var client = CreateCosmosClient();
652
+ var container = GetContainer(client);
653
+ using var stream = CreateJsonStream(payload);
654
+ var pkValue = ${createPkExpr};
655
+ var response = await container.CreateItemStreamAsync(stream, new PartitionKey(pkValue));
656
+ if (!response.IsSuccessStatusCode)
657
+ {
658
+ throw new InvalidOperationException($"Cosmos create failed with status {(int)response.StatusCode}.");
659
+ }
660
+
661
+ return await WriteJsonAsync(request, HttpStatusCode.Created, payload);
662
+ }
663
+ catch (JsonException ex)
664
+ {
665
+ _logger.LogWarning(ex, "Invalid ${modelName} create payload.");
666
+ return await WriteJsonAsync(request, HttpStatusCode.BadRequest, new { error = "Request body must be a JSON object." });
667
+ }
668
+ catch (Exception ex)
669
+ {
670
+ _logger.LogError(ex, "Failed to create ${modelName} item in Cosmos DB.");
671
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to create item" });
672
+ }
673
+ }
674
+
675
+ [Function("${modelCamel}Update")]
676
+ public async Task<HttpResponseData> Update(
677
+ [HttpTrigger(AuthorizationLevel.Function, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
678
+ string id)
679
+ {
680
+ try
681
+ {${writeGuard}
682
+ using var client = CreateCosmosClient();
683
+ var container = GetContainer(client);
684
+
685
+ JsonObject existing;
686
+ try
687
+ {
688
+ existing = await ReadCosmosItemAsync(container, id);
689
+ }
690
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
691
+ {
692
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
693
+ }
694
+
695
+ var body = await ReadRequestBodyAsync(request);
696
+ var createdAt = existing["createdAt"]?.GetValue<string>() ?? DateTimeOffset.UtcNow.ToString("O");
697
+ var payload = BuildManagedDocument(body, id, createdAt, DateTimeOffset.UtcNow.ToString("O"));
698
+
699
+ using var stream = CreateJsonStream(payload);
700
+ var response = await container.ReplaceItemStreamAsync(stream, id, ${replacePkExpr});
701
+ if (!response.IsSuccessStatusCode)
702
+ {
703
+ throw new InvalidOperationException($"Cosmos replace failed with status {(int)response.StatusCode}.");
704
+ }
705
+
706
+ return await WriteJsonAsync(request, HttpStatusCode.OK, payload);
707
+ }
708
+ catch (JsonException ex)
709
+ {
710
+ _logger.LogWarning(ex, "Invalid ${modelName} update payload for {Id}.", id);
711
+ return await WriteJsonAsync(request, HttpStatusCode.BadRequest, new { error = "Request body must be a JSON object.", id });
712
+ }
713
+ catch (Exception ex)
714
+ {
715
+ _logger.LogError(ex, "Failed to update ${modelName} item {Id} in Cosmos DB.", id);
716
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to update item", id });
717
+ }
718
+ }
719
+
720
+ [Function("${modelCamel}Delete")]
721
+ public async Task<HttpResponseData> Delete(
722
+ [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
723
+ string id)
724
+ {
725
+ try
726
+ {${writeGuard}
727
+ using var client = CreateCosmosClient();
728
+ var container = GetContainer(client);
729
+ ${deleteLogic}
730
+
731
+ return request.CreateResponse(HttpStatusCode.NoContent);
732
+ }
733
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
734
+ {
735
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
736
+ }
737
+ catch (Exception ex)
738
+ {
739
+ _logger.LogError(ex, "Failed to delete ${modelName} item {Id} from Cosmos DB.", id);
740
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to delete item", id });
741
+ }
742
+ }
743
+ }
237
744
  `;
238
745
  }
746
+ function generatePythonAzureFunctionsCRUD(model, authPolicy) {
747
+ const modelName = model.name;
748
+ const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
749
+ const modelSnake = (0, model_parser_1.toKebabCase)(modelName).replace(/-/g, "_");
750
+ const containerName = modelName.endsWith('s') ? modelName : `${modelName}s`;
751
+ const partitionKeyPath = model.partitionKey;
752
+ const partitionKeyField = partitionKeyPath.slice(1);
753
+ const isIdPartition = partitionKeyField === 'id';
754
+ const hasAuth = !!authPolicy;
755
+ const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
756
+ const authImport = hasAuth ? `\nfrom auth.${namedAuth ? 'auth_router' : 'jwt_helper'} import require_auth, require_roles, handle_auth_error\n` : '';
757
+ // generateAuthGuardPython outputs at 4-space indent; inside try: we need 8-space
758
+ const readGuardRaw = hasAuth ? (0, auth_generator_1.generateAuthGuardPython)(authPolicy, 'read') : '';
759
+ const writeGuardRaw = hasAuth ? (0, auth_generator_1.generateAuthGuardPython)(authPolicy, 'write') : '';
760
+ const readGuard = hasAuth ? '\n' + readGuardRaw.split('\n').map(l => ' ' + l).join('\n') : '';
761
+ const writeGuard = hasAuth ? '\n' + writeGuardRaw.split('\n').map(l => ' ' + l).join('\n') : '';
762
+ const authCatch = hasAuth ? `\n auth_err = handle_auth_error(exc)\n if auth_err:\n return auth_err` : '';
763
+ // getById / update の読み取りロジック
764
+ const getByIdBody = isIdPartition
765
+ ? ` container = _get_container()
766
+ item = container.read_item(item=item_id, partition_key=item_id)
767
+ return _json_response(item, 200)`
768
+ : ` container = _get_container()
769
+ items = list(container.query_items(
770
+ query="SELECT * FROM c WHERE c.id = @id",
771
+ parameters=[{"name": "@id", "value": item_id}],
772
+ enable_cross_partition_query=True,
773
+ ))
774
+ if not items:
775
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
776
+ return _json_response(items[0], 200)`;
777
+ const updateBody = isIdPartition
778
+ ? ` container = _get_container()
779
+ existing = container.read_item(item=item_id, partition_key=item_id)
780
+ body = req.get_json()
781
+ payload = _build_managed_document(
782
+ body,
783
+ item_id,
784
+ existing.get("createdAt") or datetime.now(timezone.utc).isoformat(),
785
+ datetime.now(timezone.utc).isoformat(),
786
+ )
787
+ container.replace_item(item=item_id, body=payload)
788
+ return _json_response(payload, 200)`
789
+ : ` container = _get_container()
790
+ items = list(container.query_items(
791
+ query="SELECT * FROM c WHERE c.id = @id",
792
+ parameters=[{"name": "@id", "value": item_id}],
793
+ enable_cross_partition_query=True,
794
+ ))
795
+ if not items:
796
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
797
+ existing = items[0]
798
+ body = req.get_json()
799
+ payload = _build_managed_document(
800
+ body,
801
+ item_id,
802
+ existing.get("createdAt") or datetime.now(timezone.utc).isoformat(),
803
+ datetime.now(timezone.utc).isoformat(),
804
+ )
805
+ container.replace_item(item=existing, body=payload)
806
+ return _json_response(payload, 200)`;
807
+ const deleteBody = isIdPartition
808
+ ? ` container = _get_container()
809
+ container.delete_item(item=item_id, partition_key=item_id)
810
+ return func.HttpResponse(status_code=204)`
811
+ : ` container = _get_container()
812
+ items = list(container.query_items(
813
+ query="SELECT * FROM c WHERE c.id = @id",
814
+ parameters=[{"name": "@id", "value": item_id}],
815
+ enable_cross_partition_query=True,
816
+ ))
817
+ if not items:
818
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
819
+ pk_value = items[0].get("${partitionKeyField}")
820
+ container.delete_item(item=item_id, partition_key=pk_value)
821
+ return func.HttpResponse(status_code=204)`;
822
+ return {
823
+ registration: `from blueprints.${modelSnake} import bp as ${modelSnake}_bp\napp.register_blueprint(${modelSnake}_bp)`,
824
+ blueprint: `import json
825
+ from datetime import datetime, timezone
826
+ from typing import Any
827
+ from uuid import uuid4
828
+ import os
829
+
830
+ import azure.functions as func
831
+ from azure.cosmos import CosmosClient, exceptions
832
+ from azure.identity import DefaultAzureCredential
833
+ ${authImport}
834
+ bp = func.Blueprint()
835
+ CONTAINER_NAME = "${containerName}"
836
+ DATABASE_NAME = os.environ.get("COSMOS_DB_DATABASE_NAME", "AppDatabase")
837
+
838
+
839
+ def _json_response(payload: Any, status_code: int) -> func.HttpResponse:
840
+ return func.HttpResponse(
841
+ body=json.dumps(payload, ensure_ascii=False),
842
+ status_code=status_code,
843
+ mimetype="application/json",
844
+ )
845
+
846
+
847
+ def _get_container():
848
+ endpoint = os.environ.get("CosmosDBConnection__accountEndpoint")
849
+ if endpoint:
850
+ client = CosmosClient(endpoint, credential=DefaultAzureCredential())
851
+ else:
852
+ connection_string = os.environ.get("CosmosDBConnection")
853
+ if not connection_string:
854
+ raise RuntimeError("Cosmos DB connection is not configured.")
855
+ client = CosmosClient.from_connection_string(connection_string)
856
+
857
+ database = client.get_database_client(DATABASE_NAME)
858
+ return database.get_container_client(CONTAINER_NAME)
859
+
860
+
861
+ def _build_managed_document(source: dict[str, Any], item_id: str, created_at: str, updated_at: str) -> dict[str, Any]:
862
+ payload = {
863
+ key: value
864
+ for key, value in source.items()
865
+ if key not in {"id", "createdAt", "updatedAt"}
866
+ }
867
+ payload["id"] = item_id
868
+ payload["createdAt"] = created_at
869
+ payload["updatedAt"] = updated_at
870
+ return payload
871
+
872
+
873
+ @bp.route(route="${modelCamel}", methods=["GET"])
874
+ def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
875
+ try:${readGuard}
876
+ container = _get_container()
877
+ items = list(
878
+ container.query_items(
879
+ query="SELECT * FROM c",
880
+ enable_cross_partition_query=True,
881
+ )
882
+ )
883
+ return _json_response(items, 200)
884
+ except Exception as exc:${authCatch}
885
+ return _json_response({"error": "Failed to fetch items", "details": str(exc)}, 500)
886
+
887
+
888
+ @bp.route(route="${modelCamel}/{id}", methods=["GET"])
889
+ def ${modelSnake}_get_by_id(req: func.HttpRequest) -> func.HttpResponse:
890
+ item_id = req.route_params.get("id")
891
+ try:${readGuard}
892
+ ${getByIdBody}
893
+ except exceptions.CosmosResourceNotFoundError:
894
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
895
+ except Exception as exc:${authCatch}
896
+ return _json_response({"error": "Failed to fetch item", "id": item_id, "details": str(exc)}, 500)
897
+
898
+
899
+ @bp.route(route="${modelCamel}", methods=["POST"])
900
+ def ${modelSnake}_create(req: func.HttpRequest) -> func.HttpResponse:
901
+ try:${writeGuard}
902
+ body = req.get_json()
903
+ now = datetime.now(timezone.utc).isoformat()
904
+ item_id = body.get("id") or str(uuid4())
905
+ payload = _build_managed_document(body, item_id, now, now)
906
+
907
+ container = _get_container()
908
+ container.create_item(payload)
909
+ return _json_response(payload, 201)
910
+ except ValueError:
911
+ return _json_response({"error": "Request body must be a JSON object."}, 400)
912
+ except Exception as exc:${authCatch}
913
+ return _json_response({"error": "Failed to create item", "details": str(exc)}, 500)
914
+
915
+
916
+ @bp.route(route="${modelCamel}/{id}", methods=["PUT"])
917
+ def ${modelSnake}_update(req: func.HttpRequest) -> func.HttpResponse:
918
+ item_id = req.route_params.get("id")
919
+ try:${writeGuard}
920
+ ${updateBody}
921
+ except exceptions.CosmosResourceNotFoundError:
922
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
923
+ except ValueError:
924
+ return _json_response({"error": "Request body must be a JSON object.", "id": item_id}, 400)
925
+ except Exception as exc:${authCatch}
926
+ return _json_response({"error": "Failed to update item", "id": item_id, "details": str(exc)}, 500)
927
+
928
+
929
+ @bp.route(route="${modelCamel}/{id}", methods=["DELETE"])
930
+ def ${modelSnake}_delete(req: func.HttpRequest) -> func.HttpResponse:
931
+ item_id = req.route_params.get("id")
932
+ try:${writeGuard}
933
+ ${deleteBody}
934
+ except exceptions.CosmosResourceNotFoundError:
935
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
936
+ except Exception as exc:${authCatch}
937
+ return _json_response({"error": "Failed to delete item", "id": item_id, "details": str(exc)}, 500)
938
+ `,
939
+ };
940
+ }
239
941
  //# sourceMappingURL=functions-generator.js.map