swallowkit 1.0.0-beta.9 → 1.0.1

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 (272) hide show
  1. package/README.ja.md +87 -181
  2. package/README.md +87 -238
  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 +164 -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 +20 -82
  22. package/dist/cli/commands/create-model.js.map +1 -1
  23. package/dist/cli/commands/dev-seeds.d.ts +27 -0
  24. package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
  25. package/dist/cli/commands/dev-seeds.js +188 -6
  26. package/dist/cli/commands/dev-seeds.js.map +1 -1
  27. package/dist/cli/commands/dev.d.ts +30 -0
  28. package/dist/cli/commands/dev.d.ts.map +1 -1
  29. package/dist/cli/commands/dev.js +495 -129
  30. package/dist/cli/commands/dev.js.map +1 -1
  31. package/dist/cli/commands/init.d.ts +69 -1
  32. package/dist/cli/commands/init.d.ts.map +1 -1
  33. package/dist/cli/commands/init.js +788 -766
  34. package/dist/cli/commands/init.js.map +1 -1
  35. package/dist/cli/commands/scaffold.d.ts +24 -3
  36. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  37. package/dist/cli/commands/scaffold.js +408 -189
  38. package/dist/cli/commands/scaffold.js.map +1 -1
  39. package/dist/cli/index.d.ts +5 -1
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +335 -58
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/core/config.d.ts +13 -3
  44. package/dist/core/config.d.ts.map +1 -1
  45. package/dist/core/config.js +151 -8
  46. package/dist/core/config.js.map +1 -1
  47. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  48. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  49. package/dist/core/mock/connector-mock-server.js +480 -0
  50. package/dist/core/mock/connector-mock-server.js.map +1 -0
  51. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  53. package/dist/core/mock/zod-mock-generator.js +163 -0
  54. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  55. package/dist/core/operations/auth-operations.d.ts +61 -0
  56. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  57. package/dist/core/operations/auth-operations.js +272 -0
  58. package/dist/core/operations/auth-operations.js.map +1 -0
  59. package/dist/core/operations/create-model.d.ts +15 -0
  60. package/dist/core/operations/create-model.d.ts.map +1 -0
  61. package/dist/core/operations/create-model.js +185 -0
  62. package/dist/core/operations/create-model.js.map +1 -0
  63. package/dist/core/operations/file-session.d.ts +56 -0
  64. package/dist/core/operations/file-session.d.ts.map +1 -0
  65. package/dist/core/operations/file-session.js +203 -0
  66. package/dist/core/operations/file-session.js.map +1 -0
  67. package/dist/core/operations/provision-operations.d.ts +52 -0
  68. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  69. package/dist/core/operations/provision-operations.js +211 -0
  70. package/dist/core/operations/provision-operations.js.map +1 -0
  71. package/dist/core/operations/runtime.d.ts +37 -0
  72. package/dist/core/operations/runtime.d.ts.map +1 -0
  73. package/dist/core/operations/runtime.js +248 -0
  74. package/dist/core/operations/runtime.js.map +1 -0
  75. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  76. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  77. package/dist/core/operations/scaffold-apply.js +57 -0
  78. package/dist/core/operations/scaffold-apply.js.map +1 -0
  79. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  80. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  81. package/dist/core/operations/scaffold-machine.js +51 -0
  82. package/dist/core/operations/scaffold-machine.js.map +1 -0
  83. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  84. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  85. package/dist/core/operations/scaffold-plan.js +187 -0
  86. package/dist/core/operations/scaffold-plan.js.map +1 -0
  87. package/dist/core/project/agent-instructions.d.ts +29 -0
  88. package/dist/core/project/agent-instructions.d.ts.map +1 -0
  89. package/dist/core/project/agent-instructions.js +160 -0
  90. package/dist/core/project/agent-instructions.js.map +1 -0
  91. package/dist/core/project/artifacts.d.ts +58 -0
  92. package/dist/core/project/artifacts.d.ts.map +1 -0
  93. package/dist/core/project/artifacts.js +179 -0
  94. package/dist/core/project/artifacts.js.map +1 -0
  95. package/dist/core/project/boundaries.d.ts +50 -0
  96. package/dist/core/project/boundaries.d.ts.map +1 -0
  97. package/dist/core/project/boundaries.js +117 -0
  98. package/dist/core/project/boundaries.js.map +1 -0
  99. package/dist/core/project/capabilities.d.ts +58 -0
  100. package/dist/core/project/capabilities.d.ts.map +1 -0
  101. package/dist/core/project/capabilities.js +135 -0
  102. package/dist/core/project/capabilities.js.map +1 -0
  103. package/dist/core/project/drift.d.ts +29 -0
  104. package/dist/core/project/drift.d.ts.map +1 -0
  105. package/dist/core/project/drift.js +190 -0
  106. package/dist/core/project/drift.js.map +1 -0
  107. package/dist/core/project/infra.d.ts +49 -0
  108. package/dist/core/project/infra.d.ts.map +1 -0
  109. package/dist/core/project/infra.js +167 -0
  110. package/dist/core/project/infra.js.map +1 -0
  111. package/dist/core/project/manifest.d.ts +92 -0
  112. package/dist/core/project/manifest.d.ts.map +1 -0
  113. package/dist/core/project/manifest.js +321 -0
  114. package/dist/core/project/manifest.js.map +1 -0
  115. package/dist/core/project/state.d.ts +15 -0
  116. package/dist/core/project/state.d.ts.map +1 -0
  117. package/dist/core/project/state.js +106 -0
  118. package/dist/core/project/state.js.map +1 -0
  119. package/dist/core/project/validation.d.ts +20 -0
  120. package/dist/core/project/validation.d.ts.map +1 -0
  121. package/dist/core/project/validation.js +274 -0
  122. package/dist/core/project/validation.js.map +1 -0
  123. package/dist/core/project/workflows.d.ts +43 -0
  124. package/dist/core/project/workflows.d.ts.map +1 -0
  125. package/dist/core/project/workflows.js +299 -0
  126. package/dist/core/project/workflows.js.map +1 -0
  127. package/dist/core/scaffold/auth-generator.d.ts +63 -0
  128. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  129. package/dist/core/scaffold/auth-generator.js +1774 -0
  130. package/dist/core/scaffold/auth-generator.js.map +1 -0
  131. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  132. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  133. package/dist/core/scaffold/connector-functions-generator.js +1029 -0
  134. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  135. package/dist/core/scaffold/functions-generator.d.ts +4 -3
  136. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  137. package/dist/core/scaffold/functions-generator.js +405 -134
  138. package/dist/core/scaffold/functions-generator.js.map +1 -1
  139. package/dist/core/scaffold/model-parser.d.ts +20 -0
  140. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  141. package/dist/core/scaffold/model-parser.js +229 -78
  142. package/dist/core/scaffold/model-parser.js.map +1 -1
  143. package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
  144. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  145. package/dist/core/scaffold/native-schema-generator.js +753 -0
  146. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  147. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  148. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  149. package/dist/core/scaffold/nextjs-generator.js +152 -8
  150. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  151. package/dist/core/scaffold/ui-generator.d.ts +17 -4
  152. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  153. package/dist/core/scaffold/ui-generator.js +174 -34
  154. package/dist/core/scaffold/ui-generator.js.map +1 -1
  155. package/dist/core/verify/index.d.ts +69 -0
  156. package/dist/core/verify/index.d.ts.map +1 -0
  157. package/dist/core/verify/index.js +412 -0
  158. package/dist/core/verify/index.js.map +1 -0
  159. package/dist/machine/contracts.d.ts +24 -0
  160. package/dist/machine/contracts.d.ts.map +1 -0
  161. package/dist/machine/contracts.js +3 -0
  162. package/dist/machine/contracts.js.map +1 -0
  163. package/dist/machine/errors.d.ts +14 -0
  164. package/dist/machine/errors.d.ts.map +1 -0
  165. package/dist/machine/errors.js +42 -0
  166. package/dist/machine/errors.js.map +1 -0
  167. package/dist/machine/index.d.ts +3 -0
  168. package/dist/machine/index.d.ts.map +1 -0
  169. package/dist/machine/index.js +566 -0
  170. package/dist/machine/index.js.map +1 -0
  171. package/dist/mcp/index.d.ts +25 -0
  172. package/dist/mcp/index.d.ts.map +1 -0
  173. package/dist/mcp/index.js +425 -0
  174. package/dist/mcp/index.js.map +1 -0
  175. package/dist/types/index.d.ts +105 -0
  176. package/dist/types/index.d.ts.map +1 -1
  177. package/dist/utils/package-manager.d.ts +7 -11
  178. package/dist/utils/package-manager.d.ts.map +1 -1
  179. package/dist/utils/package-manager.js +42 -17
  180. package/dist/utils/package-manager.js.map +1 -1
  181. package/dist/utils/python-uv.d.ts +21 -0
  182. package/dist/utils/python-uv.d.ts.map +1 -0
  183. package/dist/utils/python-uv.js +111 -0
  184. package/dist/utils/python-uv.js.map +1 -0
  185. package/dist/version.d.ts +2 -0
  186. package/dist/version.d.ts.map +1 -0
  187. package/dist/version.js +49 -0
  188. package/dist/version.js.map +1 -0
  189. package/package.json +15 -17
  190. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
  191. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
  192. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
  193. package/src/__tests__/agent-instructions.test.ts +237 -0
  194. package/src/__tests__/agent-loop-phase34.test.ts +619 -0
  195. package/src/__tests__/auth.test.ts +770 -0
  196. package/src/__tests__/bff-swa-auth.test.ts +275 -0
  197. package/src/__tests__/config.test.ts +176 -3
  198. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  199. package/src/__tests__/connector-mock-server.test.ts +459 -0
  200. package/src/__tests__/connector-model-bff.test.ts +162 -0
  201. package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
  202. package/src/__tests__/dev-seeds.test.ts +124 -0
  203. package/src/__tests__/dev.test.ts +254 -0
  204. package/src/__tests__/file-session.test.ts +98 -0
  205. package/src/__tests__/fixtures.ts +101 -0
  206. package/src/__tests__/functions-generator.test.ts +137 -1
  207. package/src/__tests__/init.test.ts +366 -2
  208. package/src/__tests__/jest-globals.d.ts +8 -0
  209. package/src/__tests__/machine.test.ts +371 -0
  210. package/src/__tests__/mcp.test.ts +253 -0
  211. package/src/__tests__/model-parser.test.ts +142 -0
  212. package/src/__tests__/multi-auth.test.ts +160 -0
  213. package/src/__tests__/nextjs-generator.test.ts +8 -0
  214. package/src/__tests__/package-manager.test.ts +59 -4
  215. package/src/__tests__/plan-apply.test.ts +499 -0
  216. package/src/__tests__/python-uv.test.ts +48 -0
  217. package/src/__tests__/scaffold.test.ts +287 -24
  218. package/src/__tests__/tsconfig.json +15 -0
  219. package/src/__tests__/ui-generator.test.ts +104 -0
  220. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  221. package/src/cli/commands/add-auth.ts +853 -0
  222. package/src/cli/commands/add-connector.ts +159 -0
  223. package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
  224. package/src/cli/commands/create-model.ts +21 -100
  225. package/src/cli/commands/dev-seeds.ts +278 -7
  226. package/src/cli/commands/dev.ts +607 -160
  227. package/src/cli/commands/init.ts +851 -786
  228. package/src/cli/commands/scaffold.ts +531 -236
  229. package/src/cli/index.ts +338 -60
  230. package/src/core/config.ts +145 -9
  231. package/src/core/mock/connector-mock-server.ts +555 -0
  232. package/src/core/mock/zod-mock-generator.ts +205 -0
  233. package/src/core/operations/auth-operations.ts +360 -0
  234. package/src/core/operations/create-model.ts +191 -0
  235. package/src/core/operations/file-session.ts +222 -0
  236. package/src/core/operations/provision-operations.ts +297 -0
  237. package/src/core/operations/runtime.ts +258 -0
  238. package/src/core/operations/scaffold-apply.ts +106 -0
  239. package/src/core/operations/scaffold-machine.ts +77 -0
  240. package/src/core/operations/scaffold-plan.ts +215 -0
  241. package/src/core/project/agent-instructions.ts +167 -0
  242. package/src/core/project/agent-templates/AGENTS.md +504 -0
  243. package/src/core/project/agent-templates/CLAUDE.md +31 -0
  244. package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
  245. package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
  246. package/src/core/project/agent-templates/copilot-instructions.md +27 -0
  247. package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
  248. package/src/core/project/artifacts.ts +205 -0
  249. package/src/core/project/boundaries.ts +170 -0
  250. package/src/core/project/capabilities.ts +193 -0
  251. package/src/core/project/drift.ts +203 -0
  252. package/src/core/project/infra.ts +196 -0
  253. package/src/core/project/manifest.ts +402 -0
  254. package/src/core/project/state.ts +77 -0
  255. package/src/core/project/validation.ts +295 -0
  256. package/src/core/project/workflows.ts +295 -0
  257. package/src/core/scaffold/auth-generator.ts +1824 -0
  258. package/src/core/scaffold/connector-functions-generator.ts +1133 -0
  259. package/src/core/scaffold/functions-generator.ts +432 -134
  260. package/src/core/scaffold/model-parser.ts +244 -90
  261. package/src/core/scaffold/native-schema-generator.ts +904 -0
  262. package/src/core/scaffold/nextjs-generator.ts +174 -9
  263. package/src/core/scaffold/ui-generator.ts +191 -36
  264. package/src/core/verify/index.ts +468 -0
  265. package/src/machine/contracts.ts +33 -0
  266. package/src/machine/errors.ts +45 -0
  267. package/src/machine/index.ts +690 -0
  268. package/src/mcp/index.ts +423 -0
  269. package/src/types/index.ts +137 -0
  270. package/src/utils/package-manager.ts +41 -19
  271. package/src/utils/python-uv.ts +96 -0
  272. package/src/version.ts +13 -0
@@ -9,27 +9,63 @@ exports.generateCompactAzureFunctionsCRUD = generateCompactAzureFunctionsCRUD;
9
9
  exports.generateCSharpAzureFunctionsCRUD = generateCSharpAzureFunctionsCRUD;
10
10
  exports.generatePythonAzureFunctionsCRUD = generatePythonAzureFunctionsCRUD;
11
11
  const model_parser_1 = require("./model-parser");
12
+ const auth_generator_1 = require("./auth-generator");
12
13
  /**
13
14
  * Azure Functions エンティティファイルを生成(インラインハンドラー方式)
14
15
  * 各ハンドラーがベタ書きされており、ビジネスロジックの追加・変更が容易
15
16
  */
16
- function generateCompactAzureFunctionsCRUD(model, sharedPackageName) {
17
+ function generateCompactAzureFunctionsCRUD(model, sharedPackageName, authPolicy, authProvider) {
17
18
  const modelName = model.name;
18
19
  const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
19
- const modelKebab = (0, model_parser_1.toKebabCase)(modelName);
20
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);
21
57
  return `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
22
58
  import { z } from 'zod/v4';
23
59
  import crypto from 'crypto';
24
- import { ${schemaName} } from '${sharedPackageName}';
60
+ import { ${schemaName} } from '${sharedPackageName}';${authImport}
25
61
 
26
- const containerName = '${modelName}s';
62
+ const containerName = '${modelName.endsWith('s') ? modelName : modelName + 's'}';
27
63
 
28
64
  // GET /api/${modelCamel} - 全件取得
29
65
  app.http('${modelCamel}-get-all', {
30
66
  methods: ['GET'],
31
67
  route: '${modelCamel}',
32
- authLevel: 'anonymous',
68
+ authLevel: 'function',
33
69
  extraInputs: [
34
70
  {
35
71
  type: 'cosmosDB',
@@ -41,8 +77,8 @@ app.http('${modelCamel}-get-all', {
41
77
  },
42
78
  ],
43
79
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
44
- try {
45
- const documents = context.extraInputs.get('cosmosInput') as any[];
80
+ try {${readGuard}
81
+ const documents = context.extraInputs.get('cosmosInput') as unknown[];
46
82
 
47
83
  if (!documents || !Array.isArray(documents)) {
48
84
  return { status: 200, jsonBody: [] };
@@ -53,50 +89,19 @@ app.http('${modelCamel}-get-all', {
53
89
 
54
90
  return { status: 200, jsonBody: validated };
55
91
  } catch (error) {
56
- context.error(\`Error fetching from \${containerName}:\`, error);
92
+ ${authCatchBlock} context.error(\`Error fetching from \${containerName}:\`, error);
57
93
  return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
58
94
  }
59
95
  },
60
96
  });
61
97
 
62
- // GET /api/${modelCamel}/{id} - ID指定取得
63
- app.http('${modelCamel}-get-by-id', {
64
- methods: ['GET'],
65
- route: '${modelCamel}/{id}',
66
- authLevel: 'anonymous',
67
- extraInputs: [
68
- {
69
- type: 'cosmosDB',
70
- name: 'cosmosInput',
71
- databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
72
- containerName,
73
- connection: 'CosmosDBConnection',
74
- id: '{id}',
75
- partitionKey: '{id}',
76
- },
77
- ],
78
- handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
79
- try {
80
- const document = context.extraInputs.get('cosmosInput');
81
-
82
- if (!document) {
83
- return { status: 404, jsonBody: { error: 'Item not found' } };
84
- }
85
-
86
- const validated = ${schemaName}.parse(document);
87
- return { status: 200, jsonBody: validated };
88
- } catch (error) {
89
- context.error(\`Error fetching item from \${containerName}:\`, error);
90
- return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
91
- }
92
- },
93
- });
98
+ ${getByIdHandler}
94
99
 
95
100
  // POST /api/${modelCamel} - 新規作成
96
101
  app.http('${modelCamel}-create', {
97
102
  methods: ['POST'],
98
103
  route: '${modelCamel}',
99
- authLevel: 'anonymous',
104
+ authLevel: 'function',
100
105
  extraOutputs: [
101
106
  {
102
107
  type: 'cosmosDB',
@@ -108,8 +113,8 @@ app.http('${modelCamel}-create', {
108
113
  },
109
114
  ],
110
115
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
111
- try {
112
- const body = await request.json() as any;
116
+ try {${writeGuard}
117
+ const body = await request.json() as Record<string, unknown>;
113
118
 
114
119
  const { id, createdAt, updatedAt, ...userData } = body;
115
120
  const now = new Date().toISOString();
@@ -130,17 +135,92 @@ app.http('${modelCamel}-create', {
130
135
  context.extraOutputs.set('cosmosOutput', result.data);
131
136
  return { status: 201, jsonBody: result.data };
132
137
  } catch (error) {
133
- context.error(\`Error creating item in \${containerName}:\`, error);
138
+ ${authCatchBlock} context.error(\`Error creating item in \${containerName}:\`, error);
134
139
  return { status: 500, jsonBody: { error: 'Failed to create item' } };
135
140
  }
136
141
  },
137
142
  });
138
143
 
139
- // 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} - 更新
140
220
  app.http('${modelCamel}-update', {
141
221
  methods: ['PUT'],
142
222
  route: '${modelCamel}/{id}',
143
- authLevel: 'anonymous',
223
+ authLevel: 'function',
144
224
  extraInputs: [
145
225
  {
146
226
  type: 'cosmosDB',
@@ -162,18 +242,18 @@ app.http('${modelCamel}-update', {
162
242
  },
163
243
  ],
164
244
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
165
- try {
245
+ try {${writeGuard}
166
246
  const id = request.params.id;
167
247
  if (!id) {
168
248
  return { status: 400, jsonBody: { error: 'ID is required' } };
169
249
  }
170
250
 
171
- const existingDocument = context.extraInputs.get('cosmosInput') as any;
251
+ const existingDocument = context.extraInputs.get('cosmosInput') as Record<string, unknown> | undefined;
172
252
  if (!existingDocument) {
173
253
  return { status: 404, jsonBody: { error: 'Item not found' } };
174
254
  }
175
255
 
176
- const body = await request.json() as any;
256
+ const body = await request.json() as Record<string, unknown>;
177
257
  const { createdAt, updatedAt, ...userData } = body;
178
258
 
179
259
  const dataWithManagedFields = {
@@ -193,56 +273,205 @@ app.http('${modelCamel}-update', {
193
273
  context.extraOutputs.set('cosmosOutput', result.data);
194
274
  return { status: 200, jsonBody: result.data };
195
275
  } catch (error) {
196
- context.error(\`Error updating item in \${containerName}:\`, error);
276
+ ${authCatchBlock} context.error(\`Error updating item in \${containerName}:\`, error);
197
277
  return { status: 500, jsonBody: { error: 'Failed to update item' } };
198
278
  }
199
279
  },
200
- });
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}\`);
201
352
 
202
- // 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)
203
366
  app.http('${modelCamel}-delete', {
204
367
  methods: ['DELETE'],
205
368
  route: '${modelCamel}/{id}',
206
- authLevel: 'anonymous',
369
+ authLevel: 'function',
207
370
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
208
- try {
371
+ try {${writeGuard}
209
372
  const id = request.params.id;
210
373
  if (!id) {
211
374
  return { status: 400, jsonBody: { error: 'ID is required' } };
212
375
  }
213
376
 
214
- const { CosmosClient } = await import('@azure/cosmos');
215
- const endpoint = process.env.CosmosDBConnection__accountEndpoint;
216
- let client: InstanceType<typeof CosmosClient>;
217
- if (endpoint) {
218
- const { DefaultAzureCredential } = await import('@azure/identity');
219
- client = new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
220
- } else {
221
- 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' } };
222
387
  }
223
- const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
224
- const container = database.container(containerName);
225
388
 
226
- await container.item(id, id).delete();
389
+ const pkValue = resources[0].${partitionKeyField};
390
+ await container.item(id, pkValue).delete();
227
391
  context.log(\`Deleted item \${id} from \${containerName}\`);
228
392
 
229
393
  return { status: 204 };
230
- } catch (error: any) {
231
- if (error.code === 404) {
394
+ } catch (error) {
395
+ if ((error as { code?: number }).code === 404) {
232
396
  return { status: 404, jsonBody: { error: 'Item not found' } };
233
397
  }
234
- context.error(\`Error deleting item from \${containerName}:\`, error);
398
+ ${authCatchBlock} context.error(\`Error deleting item from \${containerName}:\`, error);
235
399
  return { status: 500, jsonBody: { error: 'Failed to delete item' } };
236
400
  }
237
401
  },
238
- });
239
- `;
402
+ });`;
240
403
  }
241
- function generateCSharpAzureFunctionsCRUD(model) {
404
+ function generateCSharpAzureFunctionsCRUD(model, authPolicy) {
242
405
  const modelName = model.name;
243
406
  const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
244
- const className = `${modelName}Functions`;
245
- const containerName = `${modelName}s`;
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));`;
246
475
  return `using System.Net;
247
476
  using System.Text;
248
477
  using System.Text.Json;
@@ -252,7 +481,7 @@ using Microsoft.Azure.Cosmos;
252
481
  using Microsoft.Azure.Functions.Worker;
253
482
  using Microsoft.Azure.Functions.Worker.Http;
254
483
  using Microsoft.Extensions.Logging;
255
-
484
+ ${authUsing}
256
485
  namespace SwallowKit.Functions;
257
486
 
258
487
  public sealed class ${className}
@@ -335,23 +564,7 @@ public sealed class ${className}
335
564
  private static Stream CreateJsonStream(JsonObject payload) =>
336
565
  new MemoryStream(Encoding.UTF8.GetBytes(payload.ToJsonString()));
337
566
 
338
- private static async Task<JsonObject> ReadCosmosItemAsync(Container container, string id)
339
- {
340
- var response = await container.ReadItemStreamAsync(id, new PartitionKey(id));
341
- if (response.StatusCode == HttpStatusCode.NotFound)
342
- {
343
- throw new CosmosException("Item not found", HttpStatusCode.NotFound, 0, response.Headers.ActivityId, response.Headers.RequestCharge);
344
- }
345
-
346
- if (!response.IsSuccessStatusCode)
347
- {
348
- throw new InvalidOperationException($"Cosmos read failed with status {(int)response.StatusCode}.");
349
- }
350
-
351
- using var document = await JsonDocument.ParseAsync(response.Content);
352
- return JsonNode.Parse(document.RootElement.GetRawText())?.AsObject()
353
- ?? throw new JsonException("Cosmos item payload must be a JSON object.");
354
- }
567
+ ${readItemMethod}
355
568
 
356
569
  private static async Task<HttpResponseData> WriteJsonAsync(HttpRequestData request, HttpStatusCode status, object payload)
357
570
  {
@@ -362,10 +575,10 @@ public sealed class ${className}
362
575
 
363
576
  [Function("${modelCamel}GetAll")]
364
577
  public async Task<HttpResponseData> GetAll(
365
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}")] HttpRequestData request)
578
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}")] HttpRequestData request)
366
579
  {
367
580
  try
368
- {
581
+ {${readGuard}
369
582
  using var client = CreateCosmosClient();
370
583
  var container = GetContainer(client);
371
584
  using var iterator = container.GetItemQueryStreamIterator("SELECT * FROM c");
@@ -403,11 +616,11 @@ public sealed class ${className}
403
616
 
404
617
  [Function("${modelCamel}GetById")]
405
618
  public async Task<HttpResponseData> GetById(
406
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
619
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
407
620
  string id)
408
621
  {
409
622
  try
410
- {
623
+ {${readGuard}
411
624
  using var client = CreateCosmosClient();
412
625
  var container = GetContainer(client);
413
626
  var item = await ReadCosmosItemAsync(container, id);
@@ -426,10 +639,10 @@ public sealed class ${className}
426
639
 
427
640
  [Function("${modelCamel}Create")]
428
641
  public async Task<HttpResponseData> Create(
429
- [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "${modelCamel}")] HttpRequestData request)
642
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "${modelCamel}")] HttpRequestData request)
430
643
  {
431
644
  try
432
- {
645
+ {${writeGuard}
433
646
  var body = await ReadRequestBodyAsync(request);
434
647
  var now = DateTimeOffset.UtcNow.ToString("O");
435
648
  var id = body["id"]?.GetValue<string>() ?? Guid.NewGuid().ToString();
@@ -438,7 +651,8 @@ public sealed class ${className}
438
651
  using var client = CreateCosmosClient();
439
652
  var container = GetContainer(client);
440
653
  using var stream = CreateJsonStream(payload);
441
- var response = await container.CreateItemStreamAsync(stream, new PartitionKey(id));
654
+ var pkValue = ${createPkExpr};
655
+ var response = await container.CreateItemStreamAsync(stream, new PartitionKey(pkValue));
442
656
  if (!response.IsSuccessStatusCode)
443
657
  {
444
658
  throw new InvalidOperationException($"Cosmos create failed with status {(int)response.StatusCode}.");
@@ -460,11 +674,11 @@ public sealed class ${className}
460
674
 
461
675
  [Function("${modelCamel}Update")]
462
676
  public async Task<HttpResponseData> Update(
463
- [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
677
+ [HttpTrigger(AuthorizationLevel.Function, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
464
678
  string id)
465
679
  {
466
680
  try
467
- {
681
+ {${writeGuard}
468
682
  using var client = CreateCosmosClient();
469
683
  var container = GetContainer(client);
470
684
 
@@ -483,7 +697,7 @@ public sealed class ${className}
483
697
  var payload = BuildManagedDocument(body, id, createdAt, DateTimeOffset.UtcNow.ToString("O"));
484
698
 
485
699
  using var stream = CreateJsonStream(payload);
486
- var response = await container.ReplaceItemStreamAsync(stream, id, new PartitionKey(id));
700
+ var response = await container.ReplaceItemStreamAsync(stream, id, ${replacePkExpr});
487
701
  if (!response.IsSuccessStatusCode)
488
702
  {
489
703
  throw new InvalidOperationException($"Cosmos replace failed with status {(int)response.StatusCode}.");
@@ -505,14 +719,14 @@ public sealed class ${className}
505
719
 
506
720
  [Function("${modelCamel}Delete")]
507
721
  public async Task<HttpResponseData> Delete(
508
- [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
722
+ [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
509
723
  string id)
510
724
  {
511
725
  try
512
- {
726
+ {${writeGuard}
513
727
  using var client = CreateCosmosClient();
514
728
  var container = GetContainer(client);
515
- await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(id));
729
+ ${deleteLogic}
516
730
 
517
731
  return request.CreateResponse(HttpStatusCode.NoContent);
518
732
  }
@@ -529,11 +743,82 @@ public sealed class ${className}
529
743
  }
530
744
  `;
531
745
  }
532
- function generatePythonAzureFunctionsCRUD(model) {
746
+ function generatePythonAzureFunctionsCRUD(model, authPolicy) {
533
747
  const modelName = model.name;
534
748
  const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
535
749
  const modelSnake = (0, model_parser_1.toKebabCase)(modelName).replace(/-/g, "_");
536
- const containerName = `${modelName}s`;
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)`;
537
822
  return {
538
823
  registration: `from blueprints.${modelSnake} import bp as ${modelSnake}_bp\napp.register_blueprint(${modelSnake}_bp)`,
539
824
  blueprint: `import json
@@ -545,7 +830,7 @@ import os
545
830
  import azure.functions as func
546
831
  from azure.cosmos import CosmosClient, exceptions
547
832
  from azure.identity import DefaultAzureCredential
548
-
833
+ ${authImport}
549
834
  bp = func.Blueprint()
550
835
  CONTAINER_NAME = "${containerName}"
551
836
  DATABASE_NAME = os.environ.get("COSMOS_DB_DATABASE_NAME", "AppDatabase")
@@ -587,7 +872,7 @@ def _build_managed_document(source: dict[str, Any], item_id: str, created_at: st
587
872
 
588
873
  @bp.route(route="${modelCamel}", methods=["GET"])
589
874
  def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
590
- try:
875
+ try:${readGuard}
591
876
  container = _get_container()
592
877
  items = list(
593
878
  container.query_items(
@@ -596,26 +881,24 @@ def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
596
881
  )
597
882
  )
598
883
  return _json_response(items, 200)
599
- except Exception as exc:
884
+ except Exception as exc:${authCatch}
600
885
  return _json_response({"error": "Failed to fetch items", "details": str(exc)}, 500)
601
886
 
602
887
 
603
888
  @bp.route(route="${modelCamel}/{id}", methods=["GET"])
604
889
  def ${modelSnake}_get_by_id(req: func.HttpRequest) -> func.HttpResponse:
605
890
  item_id = req.route_params.get("id")
606
- try:
607
- container = _get_container()
608
- item = container.read_item(item=item_id, partition_key=item_id)
609
- return _json_response(item, 200)
891
+ try:${readGuard}
892
+ ${getByIdBody}
610
893
  except exceptions.CosmosResourceNotFoundError:
611
894
  return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
612
- except Exception as exc:
895
+ except Exception as exc:${authCatch}
613
896
  return _json_response({"error": "Failed to fetch item", "id": item_id, "details": str(exc)}, 500)
614
897
 
615
898
 
616
899
  @bp.route(route="${modelCamel}", methods=["POST"])
617
900
  def ${modelSnake}_create(req: func.HttpRequest) -> func.HttpResponse:
618
- try:
901
+ try:${writeGuard}
619
902
  body = req.get_json()
620
903
  now = datetime.now(timezone.utc).isoformat()
621
904
  item_id = body.get("id") or str(uuid4())
@@ -626,43 +909,31 @@ def ${modelSnake}_create(req: func.HttpRequest) -> func.HttpResponse:
626
909
  return _json_response(payload, 201)
627
910
  except ValueError:
628
911
  return _json_response({"error": "Request body must be a JSON object."}, 400)
629
- except Exception as exc:
912
+ except Exception as exc:${authCatch}
630
913
  return _json_response({"error": "Failed to create item", "details": str(exc)}, 500)
631
914
 
632
915
 
633
916
  @bp.route(route="${modelCamel}/{id}", methods=["PUT"])
634
917
  def ${modelSnake}_update(req: func.HttpRequest) -> func.HttpResponse:
635
918
  item_id = req.route_params.get("id")
636
- try:
637
- container = _get_container()
638
- existing = container.read_item(item=item_id, partition_key=item_id)
639
- body = req.get_json()
640
- payload = _build_managed_document(
641
- body,
642
- item_id,
643
- existing.get("createdAt") or datetime.now(timezone.utc).isoformat(),
644
- datetime.now(timezone.utc).isoformat(),
645
- )
646
- container.replace_item(item=item_id, body=payload)
647
- return _json_response(payload, 200)
919
+ try:${writeGuard}
920
+ ${updateBody}
648
921
  except exceptions.CosmosResourceNotFoundError:
649
922
  return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
650
923
  except ValueError:
651
924
  return _json_response({"error": "Request body must be a JSON object.", "id": item_id}, 400)
652
- except Exception as exc:
925
+ except Exception as exc:${authCatch}
653
926
  return _json_response({"error": "Failed to update item", "id": item_id, "details": str(exc)}, 500)
654
927
 
655
928
 
656
929
  @bp.route(route="${modelCamel}/{id}", methods=["DELETE"])
657
930
  def ${modelSnake}_delete(req: func.HttpRequest) -> func.HttpResponse:
658
931
  item_id = req.route_params.get("id")
659
- try:
660
- container = _get_container()
661
- container.delete_item(item=item_id, partition_key=item_id)
662
- return func.HttpResponse(status_code=204)
932
+ try:${writeGuard}
933
+ ${deleteBody}
663
934
  except exceptions.CosmosResourceNotFoundError:
664
935
  return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
665
- except Exception as exc:
936
+ except Exception as exc:${authCatch}
666
937
  return _json_response({"error": "Failed to delete item", "id": item_id, "details": str(exc)}, 500)
667
938
  `,
668
939
  };