swallowkit 1.0.0-beta.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +188 -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 +513 -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 +202 -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
@@ -5,29 +5,72 @@
5
5
  */
6
6
 
7
7
  import { ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
8
+ import { AuthProvider, ModelAuthPolicy } from "../../types";
9
+ import { generateAuthImportTS, generateAuthGuardTS, generateAuthGuardCSharp, generateAuthGuardPython } from "./auth-generator";
8
10
 
9
11
  /**
10
12
  * Azure Functions エンティティファイルを生成(インラインハンドラー方式)
11
13
  * 各ハンドラーがベタ書きされており、ビジネスロジックの追加・変更が容易
12
14
  */
13
- export function generateCompactAzureFunctionsCRUD(model: ModelInfo, sharedPackageName: string): string {
15
+ export function generateCompactAzureFunctionsCRUD(model: ModelInfo, sharedPackageName: string, authPolicy?: ModelAuthPolicy, authProvider?: AuthProvider): string {
14
16
  const modelName = model.name;
15
17
  const modelCamel = toCamelCase(modelName);
16
- const modelKebab = toKebabCase(modelName);
17
18
  const schemaName = model.schemaName;
18
19
 
20
+ const partitionKeyPath = model.partitionKey; // e.g. "/tenantId"
21
+ const partitionKeyField = partitionKeyPath.slice(1); // e.g. "tenantId"
22
+ const isIdPartition = partitionKeyField === 'id';
23
+
24
+ const hasAuth = !!authPolicy;
25
+ const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
26
+ const authImport = hasAuth ? `\n${generateAuthImportTS(namedAuth)}\n` : '';
27
+ const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
28
+ const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write', authProvider)}\n` : '';
29
+ const authCatchBlock = hasAuth
30
+ ? ` const authErr = handleAuthError(error);
31
+ if (authErr) return authErr;\n`
32
+ : '';
33
+
34
+ // SDK クライアント初期化ヘルパー(PK≠/id の場合、および delete で使用)
35
+ const sdkClientInit = `const { CosmosClient } = await import('@azure/cosmos');
36
+ const endpoint = process.env.CosmosDBConnection__accountEndpoint;
37
+ let client: InstanceType<typeof CosmosClient>;
38
+ if (endpoint) {
39
+ const { DefaultAzureCredential } = await import('@azure/identity');
40
+ client = new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
41
+ } else {
42
+ client = new CosmosClient(process.env.CosmosDBConnection!);
43
+ }
44
+ const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
45
+ const container = database.container(containerName);`;
46
+
47
+ // getById ハンドラー: PK=/id の場合は input binding、それ以外は SDK
48
+ const getByIdHandler = isIdPartition
49
+ ? generateGetByIdWithBinding(modelCamel, schemaName, readGuard, authCatchBlock)
50
+ : generateGetByIdWithSdk(modelCamel, schemaName, readGuard, authCatchBlock, sdkClientInit);
51
+
52
+ // update ハンドラー: PK=/id の場合は input binding、それ以外は SDK
53
+ const updateHandler = isIdPartition
54
+ ? generateUpdateWithBinding(modelCamel, schemaName, writeGuard, authCatchBlock)
55
+ : generateUpdateWithSdk(modelCamel, schemaName, partitionKeyField, writeGuard, authCatchBlock, sdkClientInit);
56
+
57
+ // delete ハンドラー: 常に SDK(既存パターン)、PK に応じて分岐
58
+ const deleteHandler = isIdPartition
59
+ ? generateDeleteIdPartition(modelCamel, writeGuard, authCatchBlock, sdkClientInit)
60
+ : generateDeleteCustomPartition(modelCamel, partitionKeyField, writeGuard, authCatchBlock, sdkClientInit);
61
+
19
62
  return `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
20
63
  import { z } from 'zod/v4';
21
64
  import crypto from 'crypto';
22
- import { ${schemaName} } from '${sharedPackageName}';
65
+ import { ${schemaName} } from '${sharedPackageName}';${authImport}
23
66
 
24
- const containerName = '${modelName}s';
67
+ const containerName = '${modelName.endsWith('s') ? modelName : modelName + 's'}';
25
68
 
26
69
  // GET /api/${modelCamel} - 全件取得
27
70
  app.http('${modelCamel}-get-all', {
28
71
  methods: ['GET'],
29
72
  route: '${modelCamel}',
30
- authLevel: 'anonymous',
73
+ authLevel: 'function',
31
74
  extraInputs: [
32
75
  {
33
76
  type: 'cosmosDB',
@@ -39,8 +82,8 @@ app.http('${modelCamel}-get-all', {
39
82
  },
40
83
  ],
41
84
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
42
- try {
43
- const documents = context.extraInputs.get('cosmosInput') as any[];
85
+ try {${readGuard}
86
+ const documents = context.extraInputs.get('cosmosInput') as unknown[];
44
87
 
45
88
  if (!documents || !Array.isArray(documents)) {
46
89
  return { status: 200, jsonBody: [] };
@@ -51,50 +94,19 @@ app.http('${modelCamel}-get-all', {
51
94
 
52
95
  return { status: 200, jsonBody: validated };
53
96
  } catch (error) {
54
- context.error(\`Error fetching from \${containerName}:\`, error);
97
+ ${authCatchBlock} context.error(\`Error fetching from \${containerName}:\`, error);
55
98
  return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
56
99
  }
57
100
  },
58
101
  });
59
102
 
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
- });
103
+ ${getByIdHandler}
92
104
 
93
105
  // POST /api/${modelCamel} - 新規作成
94
106
  app.http('${modelCamel}-create', {
95
107
  methods: ['POST'],
96
108
  route: '${modelCamel}',
97
- authLevel: 'anonymous',
109
+ authLevel: 'function',
98
110
  extraOutputs: [
99
111
  {
100
112
  type: 'cosmosDB',
@@ -106,8 +118,8 @@ app.http('${modelCamel}-create', {
106
118
  },
107
119
  ],
108
120
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
109
- try {
110
- const body = await request.json() as any;
121
+ try {${writeGuard}
122
+ const body = await request.json() as Record<string, unknown>;
111
123
 
112
124
  const { id, createdAt, updatedAt, ...userData } = body;
113
125
  const now = new Date().toISOString();
@@ -128,17 +140,97 @@ app.http('${modelCamel}-create', {
128
140
  context.extraOutputs.set('cosmosOutput', result.data);
129
141
  return { status: 201, jsonBody: result.data };
130
142
  } catch (error) {
131
- context.error(\`Error creating item in \${containerName}:\`, error);
143
+ ${authCatchBlock} context.error(\`Error creating item in \${containerName}:\`, error);
132
144
  return { status: 500, jsonBody: { error: 'Failed to create item' } };
133
145
  }
134
146
  },
135
147
  });
136
148
 
137
- // PUT /api/${modelCamel}/{id} - 更新
149
+ ${updateHandler}
150
+
151
+ ${deleteHandler}
152
+ `;
153
+ }
154
+
155
+ // --- TS getById ハンドラー生成ヘルパー ---
156
+
157
+ function generateGetByIdWithBinding(modelCamel: string, schemaName: string, readGuard: string, authCatchBlock: string): string {
158
+ return `// GET /api/${modelCamel}/{id} - ID指定取得
159
+ app.http('${modelCamel}-get-by-id', {
160
+ methods: ['GET'],
161
+ route: '${modelCamel}/{id}',
162
+ authLevel: 'function',
163
+ extraInputs: [
164
+ {
165
+ type: 'cosmosDB',
166
+ name: 'cosmosInput',
167
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
168
+ containerName,
169
+ connection: 'CosmosDBConnection',
170
+ id: '{id}',
171
+ partitionKey: '{id}',
172
+ },
173
+ ],
174
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
175
+ try {${readGuard}
176
+ const document = context.extraInputs.get('cosmosInput');
177
+
178
+ if (!document) {
179
+ return { status: 404, jsonBody: { error: 'Item not found' } };
180
+ }
181
+
182
+ const validated = ${schemaName}.parse(document);
183
+ return { status: 200, jsonBody: validated };
184
+ } catch (error) {
185
+ ${authCatchBlock} context.error(\`Error fetching item from \${containerName}:\`, error);
186
+ return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
187
+ }
188
+ },
189
+ });`;
190
+ }
191
+
192
+ function generateGetByIdWithSdk(modelCamel: string, schemaName: string, readGuard: string, authCatchBlock: string, sdkClientInit: string): string {
193
+ return `// GET /api/${modelCamel}/{id} - ID指定取得 (custom partition key)
194
+ app.http('${modelCamel}-get-by-id', {
195
+ methods: ['GET'],
196
+ route: '${modelCamel}/{id}',
197
+ authLevel: 'function',
198
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
199
+ try {${readGuard}
200
+ const id = request.params.id;
201
+ if (!id) {
202
+ return { status: 400, jsonBody: { error: 'ID is required' } };
203
+ }
204
+
205
+ ${sdkClientInit}
206
+
207
+ const { resources } = await container.items.query({
208
+ query: 'SELECT * FROM c WHERE c.id = @id',
209
+ parameters: [{ name: '@id', value: id }],
210
+ }).fetchAll();
211
+
212
+ if (!resources || resources.length === 0) {
213
+ return { status: 404, jsonBody: { error: 'Item not found' } };
214
+ }
215
+
216
+ const validated = ${schemaName}.parse(resources[0]);
217
+ return { status: 200, jsonBody: validated };
218
+ } catch (error) {
219
+ ${authCatchBlock} context.error(\`Error fetching item from \${containerName}:\`, error);
220
+ return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
221
+ }
222
+ },
223
+ });`;
224
+ }
225
+
226
+ // --- TS update ハンドラー生成ヘルパー ---
227
+
228
+ function generateUpdateWithBinding(modelCamel: string, schemaName: string, writeGuard: string, authCatchBlock: string): string {
229
+ return `// PUT /api/${modelCamel}/{id} - 更新
138
230
  app.http('${modelCamel}-update', {
139
231
  methods: ['PUT'],
140
232
  route: '${modelCamel}/{id}',
141
- authLevel: 'anonymous',
233
+ authLevel: 'function',
142
234
  extraInputs: [
143
235
  {
144
236
  type: 'cosmosDB',
@@ -160,18 +252,18 @@ app.http('${modelCamel}-update', {
160
252
  },
161
253
  ],
162
254
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
163
- try {
255
+ try {${writeGuard}
164
256
  const id = request.params.id;
165
257
  if (!id) {
166
258
  return { status: 400, jsonBody: { error: 'ID is required' } };
167
259
  }
168
260
 
169
- const existingDocument = context.extraInputs.get('cosmosInput') as any;
261
+ const existingDocument = context.extraInputs.get('cosmosInput') as Record<string, unknown> | undefined;
170
262
  if (!existingDocument) {
171
263
  return { status: 404, jsonBody: { error: 'Item not found' } };
172
264
  }
173
265
 
174
- const body = await request.json() as any;
266
+ const body = await request.json() as Record<string, unknown>;
175
267
  const { createdAt, updatedAt, ...userData } = body;
176
268
 
177
269
  const dataWithManagedFields = {
@@ -191,57 +283,216 @@ app.http('${modelCamel}-update', {
191
283
  context.extraOutputs.set('cosmosOutput', result.data);
192
284
  return { status: 200, jsonBody: result.data };
193
285
  } catch (error) {
194
- context.error(\`Error updating item in \${containerName}:\`, error);
286
+ ${authCatchBlock} context.error(\`Error updating item in \${containerName}:\`, error);
195
287
  return { status: 500, jsonBody: { error: 'Failed to update item' } };
196
288
  }
197
289
  },
198
- });
290
+ });`;
291
+ }
292
+
293
+ function generateUpdateWithSdk(modelCamel: string, schemaName: string, partitionKeyField: string, writeGuard: string, authCatchBlock: string, sdkClientInit: string): string {
294
+ return `// PUT /api/${modelCamel}/{id} - 更新 (custom partition key)
295
+ app.http('${modelCamel}-update', {
296
+ methods: ['PUT'],
297
+ route: '${modelCamel}/{id}',
298
+ authLevel: 'function',
299
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
300
+ try {${writeGuard}
301
+ const id = request.params.id;
302
+ if (!id) {
303
+ return { status: 400, jsonBody: { error: 'ID is required' } };
304
+ }
305
+
306
+ ${sdkClientInit}
307
+
308
+ const { resources } = await container.items.query({
309
+ query: 'SELECT * FROM c WHERE c.id = @id',
310
+ parameters: [{ name: '@id', value: id }],
311
+ }).fetchAll();
312
+
313
+ if (!resources || resources.length === 0) {
314
+ return { status: 404, jsonBody: { error: 'Item not found' } };
315
+ }
316
+
317
+ const existingDocument = resources[0];
318
+ const body = await request.json() as Record<string, unknown>;
319
+ const { createdAt, updatedAt, ...userData } = body;
320
+
321
+ const dataWithManagedFields = {
322
+ ...userData,
323
+ id,
324
+ createdAt: existingDocument.createdAt,
325
+ updatedAt: new Date().toISOString(),
326
+ };
327
+
328
+ const result = ${schemaName}.safeParse(dataWithManagedFields);
329
+
330
+ if (!result.success) {
331
+ context.error('Validation failed:', result.error.issues);
332
+ return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
333
+ }
334
+
335
+ const pkValue = result.data.${partitionKeyField};
336
+ await container.items.upsert(result.data);
337
+ return { status: 200, jsonBody: result.data };
338
+ } catch (error) {
339
+ ${authCatchBlock} context.error(\`Error updating item in \${containerName}:\`, error);
340
+ return { status: 500, jsonBody: { error: 'Failed to update item' } };
341
+ }
342
+ },
343
+ });`;
344
+ }
345
+
346
+ // --- TS delete ハンドラー生成ヘルパー ---
199
347
 
200
- // DELETE /api/${modelCamel}/{id} - 削除
348
+ function generateDeleteIdPartition(modelCamel: string, writeGuard: string, authCatchBlock: string, sdkClientInit: string): string {
349
+ return `// DELETE /api/${modelCamel}/{id} - 削除
201
350
  app.http('${modelCamel}-delete', {
202
351
  methods: ['DELETE'],
203
352
  route: '${modelCamel}/{id}',
204
- authLevel: 'anonymous',
353
+ authLevel: 'function',
205
354
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
206
- try {
355
+ try {${writeGuard}
207
356
  const id = request.params.id;
208
357
  if (!id) {
209
358
  return { status: 400, jsonBody: { error: 'ID is required' } };
210
359
  }
211
360
 
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!);
220
- }
221
- const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
222
- const container = database.container(containerName);
361
+ ${sdkClientInit}
223
362
 
224
363
  await container.item(id, id).delete();
225
364
  context.log(\`Deleted item \${id} from \${containerName}\`);
226
365
 
227
366
  return { status: 204 };
228
- } catch (error: any) {
229
- if (error.code === 404) {
367
+ } catch (error) {
368
+ if ((error as { code?: number }).code === 404) {
230
369
  return { status: 404, jsonBody: { error: 'Item not found' } };
231
370
  }
232
- context.error(\`Error deleting item from \${containerName}:\`, error);
371
+ ${authCatchBlock} context.error(\`Error deleting item from \${containerName}:\`, error);
233
372
  return { status: 500, jsonBody: { error: 'Failed to delete item' } };
234
373
  }
235
374
  },
236
- });
237
- `;
375
+ });`;
238
376
  }
239
377
 
240
- export function generateCSharpAzureFunctionsCRUD(model: ModelInfo): string {
378
+ function generateDeleteCustomPartition(modelCamel: string, partitionKeyField: string, writeGuard: string, authCatchBlock: string, sdkClientInit: string): string {
379
+ return `// DELETE /api/${modelCamel}/{id} - 削除 (custom partition key)
380
+ app.http('${modelCamel}-delete', {
381
+ methods: ['DELETE'],
382
+ route: '${modelCamel}/{id}',
383
+ authLevel: 'function',
384
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
385
+ try {${writeGuard}
386
+ const id = request.params.id;
387
+ if (!id) {
388
+ return { status: 400, jsonBody: { error: 'ID is required' } };
389
+ }
390
+
391
+ ${sdkClientInit}
392
+
393
+ // パーティションキー値を取得するためにドキュメントを読み取り
394
+ const { resources } = await container.items.query({
395
+ query: 'SELECT * FROM c WHERE c.id = @id',
396
+ parameters: [{ name: '@id', value: id }],
397
+ }).fetchAll();
398
+
399
+ if (!resources || resources.length === 0) {
400
+ return { status: 404, jsonBody: { error: 'Item not found' } };
401
+ }
402
+
403
+ const pkValue = resources[0].${partitionKeyField};
404
+ await container.item(id, pkValue).delete();
405
+ context.log(\`Deleted item \${id} from \${containerName}\`);
406
+
407
+ return { status: 204 };
408
+ } catch (error) {
409
+ if ((error as { code?: number }).code === 404) {
410
+ return { status: 404, jsonBody: { error: 'Item not found' } };
411
+ }
412
+ ${authCatchBlock} context.error(\`Error deleting item from \${containerName}:\`, error);
413
+ return { status: 500, jsonBody: { error: 'Failed to delete item' } };
414
+ }
415
+ },
416
+ });`;
417
+ }
418
+
419
+ export function generateCSharpAzureFunctionsCRUD(model: ModelInfo, authPolicy?: ModelAuthPolicy): string {
241
420
  const modelName = model.name;
242
421
  const modelCamel = toCamelCase(modelName);
243
- const className = `${modelName}Functions`;
244
- const containerName = `${modelName}s`;
422
+ const className = `${modelName}CrudFunctions`;
423
+ const containerName = modelName.endsWith('s') ? modelName : `${modelName}s`;
424
+
425
+ const partitionKeyPath = model.partitionKey;
426
+ const partitionKeyField = partitionKeyPath.slice(1);
427
+ const isIdPartition = partitionKeyField === 'id';
428
+
429
+ const hasAuth = !!authPolicy;
430
+ const authUsing = hasAuth ? 'using Functions.Auth;\n' : '';
431
+ const readGuard = hasAuth ? `\n${generateAuthGuardCSharp(authPolicy!, 'read')}\n` : '';
432
+ const writeGuard = hasAuth ? `\n${generateAuthGuardCSharp(authPolicy!, 'write')}\n` : '';
433
+
434
+ // PK値の取得ロジック(create 用)
435
+ const createPkExpr = isIdPartition
436
+ ? 'id'
437
+ : `payload["${partitionKeyField}"]?.GetValue<string>() ?? throw new InvalidOperationException("Partition key field '${partitionKeyField}' is required.")`;
438
+
439
+ // ReadCosmosItemAsync: PK=/id の場合は直接読み取り、それ以外はクエリ
440
+ const readItemMethod = isIdPartition
441
+ ? ` private static async Task<JsonObject> ReadCosmosItemAsync(Container container, string id)
442
+ {
443
+ var response = await container.ReadItemStreamAsync(id, new PartitionKey(id));
444
+ if (response.StatusCode == HttpStatusCode.NotFound)
445
+ {
446
+ throw new CosmosException("Item not found", HttpStatusCode.NotFound, 0, response.Headers.ActivityId, response.Headers.RequestCharge);
447
+ }
448
+
449
+ if (!response.IsSuccessStatusCode)
450
+ {
451
+ throw new InvalidOperationException($"Cosmos read failed with status {(int)response.StatusCode}.");
452
+ }
453
+
454
+ using var document = await JsonDocument.ParseAsync(response.Content);
455
+ return JsonNode.Parse(document.RootElement.GetRawText())?.AsObject()
456
+ ?? throw new JsonException("Cosmos item payload must be a JSON object.");
457
+ }`
458
+ : ` private static async Task<JsonObject> ReadCosmosItemAsync(Container container, string id)
459
+ {
460
+ var query = new QueryDefinition("SELECT * FROM c WHERE c.id = @id")
461
+ .WithParameter("@id", id);
462
+ using var iterator = container.GetItemQueryStreamIterator(query);
463
+
464
+ while (iterator.HasMoreResults)
465
+ {
466
+ var page = await iterator.ReadNextAsync();
467
+ if (!page.IsSuccessStatusCode)
468
+ {
469
+ throw new InvalidOperationException($"Cosmos query failed with status {(int)page.StatusCode}.");
470
+ }
471
+
472
+ using var document = await JsonDocument.ParseAsync(page.Content);
473
+ if (document.RootElement.TryGetProperty("Documents", out var documents))
474
+ {
475
+ foreach (var item in documents.EnumerateArray())
476
+ {
477
+ return JsonNode.Parse(item.GetRawText())?.AsObject()
478
+ ?? throw new JsonException("Cosmos item payload must be a JSON object.");
479
+ }
480
+ }
481
+ }
482
+
483
+ throw new CosmosException("Item not found", HttpStatusCode.NotFound, 0, "", 0);
484
+ }`;
485
+
486
+ // Replace / Delete の PK 解決ロジック
487
+ const replacePkExpr = isIdPartition
488
+ ? 'new PartitionKey(id)'
489
+ : `new PartitionKey(payload["${partitionKeyField}"]?.GetValue<string>())`;
490
+
491
+ const deleteLogic = isIdPartition
492
+ ? ` await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(id));`
493
+ : ` var existing = await ReadCosmosItemAsync(container, id);
494
+ var pkValue = existing["${partitionKeyField}"]?.GetValue<string>();
495
+ await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(pkValue));`;
245
496
 
246
497
  return `using System.Net;
247
498
  using System.Text;
@@ -252,7 +503,7 @@ using Microsoft.Azure.Cosmos;
252
503
  using Microsoft.Azure.Functions.Worker;
253
504
  using Microsoft.Azure.Functions.Worker.Http;
254
505
  using Microsoft.Extensions.Logging;
255
-
506
+ ${authUsing}
256
507
  namespace SwallowKit.Functions;
257
508
 
258
509
  public sealed class ${className}
@@ -335,23 +586,7 @@ public sealed class ${className}
335
586
  private static Stream CreateJsonStream(JsonObject payload) =>
336
587
  new MemoryStream(Encoding.UTF8.GetBytes(payload.ToJsonString()));
337
588
 
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
- }
589
+ ${readItemMethod}
355
590
 
356
591
  private static async Task<HttpResponseData> WriteJsonAsync(HttpRequestData request, HttpStatusCode status, object payload)
357
592
  {
@@ -362,10 +597,10 @@ public sealed class ${className}
362
597
 
363
598
  [Function("${modelCamel}GetAll")]
364
599
  public async Task<HttpResponseData> GetAll(
365
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}")] HttpRequestData request)
600
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}")] HttpRequestData request)
366
601
  {
367
602
  try
368
- {
603
+ {${readGuard}
369
604
  using var client = CreateCosmosClient();
370
605
  var container = GetContainer(client);
371
606
  using var iterator = container.GetItemQueryStreamIterator("SELECT * FROM c");
@@ -403,11 +638,11 @@ public sealed class ${className}
403
638
 
404
639
  [Function("${modelCamel}GetById")]
405
640
  public async Task<HttpResponseData> GetById(
406
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
641
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
407
642
  string id)
408
643
  {
409
644
  try
410
- {
645
+ {${readGuard}
411
646
  using var client = CreateCosmosClient();
412
647
  var container = GetContainer(client);
413
648
  var item = await ReadCosmosItemAsync(container, id);
@@ -426,10 +661,10 @@ public sealed class ${className}
426
661
 
427
662
  [Function("${modelCamel}Create")]
428
663
  public async Task<HttpResponseData> Create(
429
- [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "${modelCamel}")] HttpRequestData request)
664
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "${modelCamel}")] HttpRequestData request)
430
665
  {
431
666
  try
432
- {
667
+ {${writeGuard}
433
668
  var body = await ReadRequestBodyAsync(request);
434
669
  var now = DateTimeOffset.UtcNow.ToString("O");
435
670
  var id = body["id"]?.GetValue<string>() ?? Guid.NewGuid().ToString();
@@ -438,7 +673,8 @@ public sealed class ${className}
438
673
  using var client = CreateCosmosClient();
439
674
  var container = GetContainer(client);
440
675
  using var stream = CreateJsonStream(payload);
441
- var response = await container.CreateItemStreamAsync(stream, new PartitionKey(id));
676
+ var pkValue = ${createPkExpr};
677
+ var response = await container.CreateItemStreamAsync(stream, new PartitionKey(pkValue));
442
678
  if (!response.IsSuccessStatusCode)
443
679
  {
444
680
  throw new InvalidOperationException($"Cosmos create failed with status {(int)response.StatusCode}.");
@@ -460,11 +696,11 @@ public sealed class ${className}
460
696
 
461
697
  [Function("${modelCamel}Update")]
462
698
  public async Task<HttpResponseData> Update(
463
- [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
699
+ [HttpTrigger(AuthorizationLevel.Function, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
464
700
  string id)
465
701
  {
466
702
  try
467
- {
703
+ {${writeGuard}
468
704
  using var client = CreateCosmosClient();
469
705
  var container = GetContainer(client);
470
706
 
@@ -483,7 +719,7 @@ public sealed class ${className}
483
719
  var payload = BuildManagedDocument(body, id, createdAt, DateTimeOffset.UtcNow.ToString("O"));
484
720
 
485
721
  using var stream = CreateJsonStream(payload);
486
- var response = await container.ReplaceItemStreamAsync(stream, id, new PartitionKey(id));
722
+ var response = await container.ReplaceItemStreamAsync(stream, id, ${replacePkExpr});
487
723
  if (!response.IsSuccessStatusCode)
488
724
  {
489
725
  throw new InvalidOperationException($"Cosmos replace failed with status {(int)response.StatusCode}.");
@@ -505,14 +741,14 @@ public sealed class ${className}
505
741
 
506
742
  [Function("${modelCamel}Delete")]
507
743
  public async Task<HttpResponseData> Delete(
508
- [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
744
+ [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
509
745
  string id)
510
746
  {
511
747
  try
512
- {
748
+ {${writeGuard}
513
749
  using var client = CreateCosmosClient();
514
750
  var container = GetContainer(client);
515
- await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(id));
751
+ ${deleteLogic}
516
752
 
517
753
  return request.CreateResponse(HttpStatusCode.NoContent);
518
754
  }
@@ -530,14 +766,90 @@ public sealed class ${className}
530
766
  `;
531
767
  }
532
768
 
533
- export function generatePythonAzureFunctionsCRUD(model: ModelInfo): {
769
+ export function generatePythonAzureFunctionsCRUD(model: ModelInfo, authPolicy?: ModelAuthPolicy): {
534
770
  blueprint: string;
535
771
  registration: string;
536
772
  } {
537
773
  const modelName = model.name;
538
774
  const modelCamel = toCamelCase(modelName);
539
775
  const modelSnake = toKebabCase(modelName).replace(/-/g, "_");
540
- const containerName = `${modelName}s`;
776
+ const containerName = modelName.endsWith('s') ? modelName : `${modelName}s`;
777
+
778
+ const partitionKeyPath = model.partitionKey;
779
+ const partitionKeyField = partitionKeyPath.slice(1);
780
+ const isIdPartition = partitionKeyField === 'id';
781
+
782
+ const hasAuth = !!authPolicy;
783
+ const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
784
+ const authImport = hasAuth ? `\nfrom auth.${namedAuth ? 'auth_router' : 'jwt_helper'} import require_auth, require_roles, handle_auth_error\n` : '';
785
+ // generateAuthGuardPython outputs at 4-space indent; inside try: we need 8-space
786
+ const readGuardRaw = hasAuth ? generateAuthGuardPython(authPolicy!, 'read') : '';
787
+ const writeGuardRaw = hasAuth ? generateAuthGuardPython(authPolicy!, 'write') : '';
788
+ const readGuard = hasAuth ? '\n' + readGuardRaw.split('\n').map(l => ' ' + l).join('\n') : '';
789
+ const writeGuard = hasAuth ? '\n' + writeGuardRaw.split('\n').map(l => ' ' + l).join('\n') : '';
790
+ const authCatch = hasAuth ? `\n auth_err = handle_auth_error(exc)\n if auth_err:\n return auth_err` : '';
791
+
792
+ // getById / update の読み取りロジック
793
+ const getByIdBody = isIdPartition
794
+ ? ` container = _get_container()
795
+ item = container.read_item(item=item_id, partition_key=item_id)
796
+ return _json_response(item, 200)`
797
+ : ` container = _get_container()
798
+ items = list(container.query_items(
799
+ query="SELECT * FROM c WHERE c.id = @id",
800
+ parameters=[{"name": "@id", "value": item_id}],
801
+ enable_cross_partition_query=True,
802
+ ))
803
+ if not items:
804
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
805
+ return _json_response(items[0], 200)`;
806
+
807
+ const updateBody = isIdPartition
808
+ ? ` container = _get_container()
809
+ existing = container.read_item(item=item_id, partition_key=item_id)
810
+ body = req.get_json()
811
+ payload = _build_managed_document(
812
+ body,
813
+ item_id,
814
+ existing.get("createdAt") or datetime.now(timezone.utc).isoformat(),
815
+ datetime.now(timezone.utc).isoformat(),
816
+ )
817
+ container.replace_item(item=item_id, body=payload)
818
+ return _json_response(payload, 200)`
819
+ : ` container = _get_container()
820
+ items = list(container.query_items(
821
+ query="SELECT * FROM c WHERE c.id = @id",
822
+ parameters=[{"name": "@id", "value": item_id}],
823
+ enable_cross_partition_query=True,
824
+ ))
825
+ if not items:
826
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
827
+ existing = items[0]
828
+ body = req.get_json()
829
+ payload = _build_managed_document(
830
+ body,
831
+ item_id,
832
+ existing.get("createdAt") or datetime.now(timezone.utc).isoformat(),
833
+ datetime.now(timezone.utc).isoformat(),
834
+ )
835
+ container.replace_item(item=existing, body=payload)
836
+ return _json_response(payload, 200)`;
837
+
838
+ const deleteBody = isIdPartition
839
+ ? ` container = _get_container()
840
+ container.delete_item(item=item_id, partition_key=item_id)
841
+ return func.HttpResponse(status_code=204)`
842
+ : ` container = _get_container()
843
+ items = list(container.query_items(
844
+ query="SELECT * FROM c WHERE c.id = @id",
845
+ parameters=[{"name": "@id", "value": item_id}],
846
+ enable_cross_partition_query=True,
847
+ ))
848
+ if not items:
849
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
850
+ pk_value = items[0].get("${partitionKeyField}")
851
+ container.delete_item(item=item_id, partition_key=pk_value)
852
+ return func.HttpResponse(status_code=204)`;
541
853
 
542
854
  return {
543
855
  registration: `from blueprints.${modelSnake} import bp as ${modelSnake}_bp\napp.register_blueprint(${modelSnake}_bp)`,
@@ -550,7 +862,7 @@ import os
550
862
  import azure.functions as func
551
863
  from azure.cosmos import CosmosClient, exceptions
552
864
  from azure.identity import DefaultAzureCredential
553
-
865
+ ${authImport}
554
866
  bp = func.Blueprint()
555
867
  CONTAINER_NAME = "${containerName}"
556
868
  DATABASE_NAME = os.environ.get("COSMOS_DB_DATABASE_NAME", "AppDatabase")
@@ -592,7 +904,7 @@ def _build_managed_document(source: dict[str, Any], item_id: str, created_at: st
592
904
 
593
905
  @bp.route(route="${modelCamel}", methods=["GET"])
594
906
  def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
595
- try:
907
+ try:${readGuard}
596
908
  container = _get_container()
597
909
  items = list(
598
910
  container.query_items(
@@ -601,26 +913,24 @@ def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
601
913
  )
602
914
  )
603
915
  return _json_response(items, 200)
604
- except Exception as exc:
916
+ except Exception as exc:${authCatch}
605
917
  return _json_response({"error": "Failed to fetch items", "details": str(exc)}, 500)
606
918
 
607
919
 
608
920
  @bp.route(route="${modelCamel}/{id}", methods=["GET"])
609
921
  def ${modelSnake}_get_by_id(req: func.HttpRequest) -> func.HttpResponse:
610
922
  item_id = req.route_params.get("id")
611
- try:
612
- container = _get_container()
613
- item = container.read_item(item=item_id, partition_key=item_id)
614
- return _json_response(item, 200)
923
+ try:${readGuard}
924
+ ${getByIdBody}
615
925
  except exceptions.CosmosResourceNotFoundError:
616
926
  return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
617
- except Exception as exc:
927
+ except Exception as exc:${authCatch}
618
928
  return _json_response({"error": "Failed to fetch item", "id": item_id, "details": str(exc)}, 500)
619
929
 
620
930
 
621
931
  @bp.route(route="${modelCamel}", methods=["POST"])
622
932
  def ${modelSnake}_create(req: func.HttpRequest) -> func.HttpResponse:
623
- try:
933
+ try:${writeGuard}
624
934
  body = req.get_json()
625
935
  now = datetime.now(timezone.utc).isoformat()
626
936
  item_id = body.get("id") or str(uuid4())
@@ -631,43 +941,31 @@ def ${modelSnake}_create(req: func.HttpRequest) -> func.HttpResponse:
631
941
  return _json_response(payload, 201)
632
942
  except ValueError:
633
943
  return _json_response({"error": "Request body must be a JSON object."}, 400)
634
- except Exception as exc:
944
+ except Exception as exc:${authCatch}
635
945
  return _json_response({"error": "Failed to create item", "details": str(exc)}, 500)
636
946
 
637
947
 
638
948
  @bp.route(route="${modelCamel}/{id}", methods=["PUT"])
639
949
  def ${modelSnake}_update(req: func.HttpRequest) -> func.HttpResponse:
640
950
  item_id = req.route_params.get("id")
641
- try:
642
- container = _get_container()
643
- existing = container.read_item(item=item_id, partition_key=item_id)
644
- body = req.get_json()
645
- payload = _build_managed_document(
646
- body,
647
- item_id,
648
- existing.get("createdAt") or datetime.now(timezone.utc).isoformat(),
649
- datetime.now(timezone.utc).isoformat(),
650
- )
651
- container.replace_item(item=item_id, body=payload)
652
- return _json_response(payload, 200)
951
+ try:${writeGuard}
952
+ ${updateBody}
653
953
  except exceptions.CosmosResourceNotFoundError:
654
954
  return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
655
955
  except ValueError:
656
956
  return _json_response({"error": "Request body must be a JSON object.", "id": item_id}, 400)
657
- except Exception as exc:
957
+ except Exception as exc:${authCatch}
658
958
  return _json_response({"error": "Failed to update item", "id": item_id, "details": str(exc)}, 500)
659
959
 
660
960
 
661
961
  @bp.route(route="${modelCamel}/{id}", methods=["DELETE"])
662
962
  def ${modelSnake}_delete(req: func.HttpRequest) -> func.HttpResponse:
663
963
  item_id = req.route_params.get("id")
664
- try:
665
- container = _get_container()
666
- container.delete_item(item=item_id, partition_key=item_id)
667
- return func.HttpResponse(status_code=204)
964
+ try:${writeGuard}
965
+ ${deleteBody}
668
966
  except exceptions.CosmosResourceNotFoundError:
669
967
  return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
670
- except Exception as exc:
968
+ except Exception as exc:${authCatch}
671
969
  return _json_response({"error": "Failed to delete item", "id": item_id, "details": str(exc)}, 500)
672
970
  `,
673
971
  };