swallowkit 1.0.0-beta.5 → 1.0.0-beta.7

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 (97) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +251 -242
  3. package/README.md +252 -243
  4. package/dist/__tests__/fixtures.d.ts +14 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +85 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/create-model.js +14 -14
  9. package/dist/cli/commands/dev.d.ts +8 -0
  10. package/dist/cli/commands/dev.d.ts.map +1 -1
  11. package/dist/cli/commands/dev.js +238 -30
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +5 -0
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +2507 -1664
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/scaffold.d.ts +3 -0
  18. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  19. package/dist/cli/commands/scaffold.js +281 -117
  20. package/dist/cli/commands/scaffold.js.map +1 -1
  21. package/dist/cli/index.js +2 -0
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/core/config.d.ts +2 -1
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +28 -0
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/scaffold/functions-generator.d.ts +5 -0
  28. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  29. package/dist/core/scaffold/functions-generator.js +649 -218
  30. package/dist/core/scaffold/functions-generator.js.map +1 -1
  31. package/dist/core/scaffold/model-parser.d.ts +1 -1
  32. package/dist/core/scaffold/model-parser.js +99 -99
  33. package/dist/core/scaffold/nextjs-generator.js +181 -181
  34. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  35. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  36. package/dist/core/scaffold/openapi-generator.js +190 -0
  37. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  38. package/dist/core/scaffold/ui-generator.js +656 -656
  39. package/dist/database/base-model.d.ts +3 -3
  40. package/dist/database/base-model.js +3 -3
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +2 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/types/index.d.ts +4 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/dist/utils/package-manager.d.ts +2 -1
  48. package/dist/utils/package-manager.d.ts.map +1 -1
  49. package/dist/utils/package-manager.js +14 -10
  50. package/dist/utils/package-manager.js.map +1 -1
  51. package/package.json +81 -74
  52. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +445 -0
  53. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  54. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +524 -0
  55. package/src/__tests__/config.test.ts +122 -0
  56. package/src/__tests__/dev.test.ts +42 -0
  57. package/src/__tests__/fixtures.ts +83 -0
  58. package/src/__tests__/functions-generator.test.ts +101 -0
  59. package/src/__tests__/init.test.ts +59 -0
  60. package/src/__tests__/nextjs-generator.test.ts +97 -0
  61. package/src/__tests__/openapi-generator.test.ts +43 -0
  62. package/src/__tests__/package-manager.test.ts +189 -0
  63. package/src/__tests__/scaffold.test.ts +39 -0
  64. package/src/__tests__/string-utils.test.ts +75 -0
  65. package/src/__tests__/ui-generator.test.ts +144 -0
  66. package/src/cli/commands/create-model.ts +141 -0
  67. package/src/cli/commands/dev.ts +794 -0
  68. package/src/cli/commands/index.ts +8 -0
  69. package/src/cli/commands/init.ts +3363 -0
  70. package/src/cli/commands/provision.ts +193 -0
  71. package/src/cli/commands/scaffold.ts +786 -0
  72. package/src/cli/index.ts +73 -0
  73. package/src/core/config.ts +244 -0
  74. package/src/core/scaffold/functions-generator.ts +674 -0
  75. package/src/core/scaffold/model-parser.ts +627 -0
  76. package/src/core/scaffold/nextjs-generator.ts +217 -0
  77. package/src/core/scaffold/openapi-generator.ts +212 -0
  78. package/src/core/scaffold/ui-generator.ts +945 -0
  79. package/src/database/base-model.ts +184 -0
  80. package/src/database/client.ts +140 -0
  81. package/src/database/repository.ts +104 -0
  82. package/src/database/runtime-check.ts +25 -0
  83. package/src/index.ts +27 -0
  84. package/src/types/index.ts +45 -0
  85. package/src/utils/package-manager.ts +229 -0
  86. package/dist/cli/commands/build.d.ts +0 -6
  87. package/dist/cli/commands/build.d.ts.map +0 -1
  88. package/dist/cli/commands/build.js +0 -177
  89. package/dist/cli/commands/build.js.map +0 -1
  90. package/dist/cli/commands/deploy.d.ts +0 -3
  91. package/dist/cli/commands/deploy.d.ts.map +0 -1
  92. package/dist/cli/commands/deploy.js +0 -147
  93. package/dist/cli/commands/deploy.js.map +0 -1
  94. package/dist/cli/commands/setup.d.ts +0 -6
  95. package/dist/cli/commands/setup.d.ts.map +0 -1
  96. package/dist/cli/commands/setup.js +0 -254
  97. package/dist/cli/commands/setup.js.map +0 -1
@@ -0,0 +1,674 @@
1
+ /**
2
+ * Azure Functions CRUD コード生成
3
+ * Cosmos DB 入出力バインディングを使用した Azure Functions のベストプラクティスに従う
4
+ * インラインハンドラー方式(ファクトリー不使用)
5
+ */
6
+
7
+ import { ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
8
+
9
+ /**
10
+ * Azure Functions エンティティファイルを生成(インラインハンドラー方式)
11
+ * 各ハンドラーがベタ書きされており、ビジネスロジックの追加・変更が容易
12
+ */
13
+ export function generateCompactAzureFunctionsCRUD(model: ModelInfo, sharedPackageName: string): string {
14
+ const modelName = model.name;
15
+ const modelCamel = toCamelCase(modelName);
16
+ const modelKebab = toKebabCase(modelName);
17
+ const schemaName = model.schemaName;
18
+
19
+ return `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
20
+ import { z } from 'zod/v4';
21
+ import crypto from 'crypto';
22
+ import { ${schemaName} } from '${sharedPackageName}';
23
+
24
+ const containerName = '${modelName}s';
25
+
26
+ // GET /api/${modelCamel} - 全件取得
27
+ app.http('${modelCamel}-get-all', {
28
+ methods: ['GET'],
29
+ route: '${modelCamel}',
30
+ authLevel: 'anonymous',
31
+ extraInputs: [
32
+ {
33
+ type: 'cosmosDB',
34
+ name: 'cosmosInput',
35
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
36
+ containerName,
37
+ connection: 'CosmosDBConnection',
38
+ sqlQuery: 'SELECT * FROM c',
39
+ },
40
+ ],
41
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
42
+ try {
43
+ const documents = context.extraInputs.get('cosmosInput') as any[];
44
+
45
+ if (!documents || !Array.isArray(documents)) {
46
+ return { status: 200, jsonBody: [] };
47
+ }
48
+
49
+ const validated = z.array(${schemaName}).parse(documents);
50
+ context.log(\`Fetched \${validated.length} items from \${containerName}\`);
51
+
52
+ return { status: 200, jsonBody: validated };
53
+ } catch (error) {
54
+ context.error(\`Error fetching from \${containerName}:\`, error);
55
+ return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
56
+ }
57
+ },
58
+ });
59
+
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
+ });
92
+
93
+ // POST /api/${modelCamel} - 新規作成
94
+ app.http('${modelCamel}-create', {
95
+ methods: ['POST'],
96
+ route: '${modelCamel}',
97
+ authLevel: 'anonymous',
98
+ extraOutputs: [
99
+ {
100
+ type: 'cosmosDB',
101
+ name: 'cosmosOutput',
102
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
103
+ containerName,
104
+ connection: 'CosmosDBConnection',
105
+ createIfNotExists: true,
106
+ },
107
+ ],
108
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
109
+ try {
110
+ const body = await request.json() as any;
111
+
112
+ const { id, createdAt, updatedAt, ...userData } = body;
113
+ const now = new Date().toISOString();
114
+ const dataWithManagedFields = {
115
+ ...userData,
116
+ id: id || crypto.randomUUID(),
117
+ createdAt: now,
118
+ updatedAt: now,
119
+ };
120
+
121
+ const result = ${schemaName}.safeParse(dataWithManagedFields);
122
+
123
+ if (!result.success) {
124
+ context.error('Validation failed:', result.error.issues);
125
+ return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
126
+ }
127
+
128
+ context.extraOutputs.set('cosmosOutput', result.data);
129
+ return { status: 201, jsonBody: result.data };
130
+ } catch (error) {
131
+ context.error(\`Error creating item in \${containerName}:\`, error);
132
+ return { status: 500, jsonBody: { error: 'Failed to create item' } };
133
+ }
134
+ },
135
+ });
136
+
137
+ // PUT /api/${modelCamel}/{id} - 更新
138
+ app.http('${modelCamel}-update', {
139
+ methods: ['PUT'],
140
+ route: '${modelCamel}/{id}',
141
+ authLevel: 'anonymous',
142
+ extraInputs: [
143
+ {
144
+ type: 'cosmosDB',
145
+ name: 'cosmosInput',
146
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
147
+ containerName,
148
+ connection: 'CosmosDBConnection',
149
+ id: '{id}',
150
+ partitionKey: '{id}',
151
+ },
152
+ ],
153
+ extraOutputs: [
154
+ {
155
+ type: 'cosmosDB',
156
+ name: 'cosmosOutput',
157
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
158
+ containerName,
159
+ connection: 'CosmosDBConnection',
160
+ },
161
+ ],
162
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
163
+ try {
164
+ const id = request.params.id;
165
+ if (!id) {
166
+ return { status: 400, jsonBody: { error: 'ID is required' } };
167
+ }
168
+
169
+ const existingDocument = context.extraInputs.get('cosmosInput') as any;
170
+ if (!existingDocument) {
171
+ return { status: 404, jsonBody: { error: 'Item not found' } };
172
+ }
173
+
174
+ const body = await request.json() as any;
175
+ const { createdAt, updatedAt, ...userData } = body;
176
+
177
+ const dataWithManagedFields = {
178
+ ...userData,
179
+ id,
180
+ createdAt: existingDocument.createdAt,
181
+ updatedAt: new Date().toISOString(),
182
+ };
183
+
184
+ const result = ${schemaName}.safeParse(dataWithManagedFields);
185
+
186
+ if (!result.success) {
187
+ context.error('Validation failed:', result.error.issues);
188
+ return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
189
+ }
190
+
191
+ context.extraOutputs.set('cosmosOutput', result.data);
192
+ return { status: 200, jsonBody: result.data };
193
+ } catch (error) {
194
+ context.error(\`Error updating item in \${containerName}:\`, error);
195
+ return { status: 500, jsonBody: { error: 'Failed to update item' } };
196
+ }
197
+ },
198
+ });
199
+
200
+ // DELETE /api/${modelCamel}/{id} - 削除
201
+ app.http('${modelCamel}-delete', {
202
+ methods: ['DELETE'],
203
+ route: '${modelCamel}/{id}',
204
+ authLevel: 'anonymous',
205
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
206
+ try {
207
+ const id = request.params.id;
208
+ if (!id) {
209
+ return { status: 400, jsonBody: { error: 'ID is required' } };
210
+ }
211
+
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);
223
+
224
+ await container.item(id, id).delete();
225
+ context.log(\`Deleted item \${id} from \${containerName}\`);
226
+
227
+ return { status: 204 };
228
+ } catch (error: any) {
229
+ if (error.code === 404) {
230
+ return { status: 404, jsonBody: { error: 'Item not found' } };
231
+ }
232
+ context.error(\`Error deleting item from \${containerName}:\`, error);
233
+ return { status: 500, jsonBody: { error: 'Failed to delete item' } };
234
+ }
235
+ },
236
+ });
237
+ `;
238
+ }
239
+
240
+ export function generateCSharpAzureFunctionsCRUD(model: ModelInfo): string {
241
+ const modelName = model.name;
242
+ const modelCamel = toCamelCase(modelName);
243
+ const className = `${modelName}Functions`;
244
+ const containerName = `${modelName}s`;
245
+
246
+ return `using System.Net;
247
+ using System.Text;
248
+ using System.Text.Json;
249
+ using System.Text.Json.Nodes;
250
+ using Azure.Identity;
251
+ using Microsoft.Azure.Cosmos;
252
+ using Microsoft.Azure.Functions.Worker;
253
+ using Microsoft.Azure.Functions.Worker.Http;
254
+ using Microsoft.Extensions.Logging;
255
+
256
+ namespace SwallowKit.Functions;
257
+
258
+ public sealed class ${className}
259
+ {
260
+ private readonly ILogger<${className}> _logger;
261
+ private static readonly string ContainerName = "${containerName}";
262
+
263
+ public ${className}(ILogger<${className}> logger)
264
+ {
265
+ _logger = logger;
266
+ }
267
+
268
+ private static string DatabaseName => Environment.GetEnvironmentVariable("COSMOS_DB_DATABASE_NAME") ?? "AppDatabase";
269
+
270
+ private static bool IsLocalCosmosEndpoint(string endpoint) =>
271
+ endpoint.Contains("localhost:8081", StringComparison.OrdinalIgnoreCase) ||
272
+ endpoint.Contains("127.0.0.1:8081", StringComparison.OrdinalIgnoreCase);
273
+
274
+ private static CosmosClient CreateCosmosClient()
275
+ {
276
+ var endpoint = Environment.GetEnvironmentVariable("CosmosDBConnection__accountEndpoint");
277
+ if (!string.IsNullOrWhiteSpace(endpoint))
278
+ {
279
+ var options = IsLocalCosmosEndpoint(endpoint)
280
+ ? new CosmosClientOptions { ConnectionMode = ConnectionMode.Gateway }
281
+ : new CosmosClientOptions();
282
+ return new CosmosClient(endpoint, new DefaultAzureCredential(), options);
283
+ }
284
+
285
+ var connectionString = Environment.GetEnvironmentVariable("CosmosDBConnection");
286
+ if (string.IsNullOrWhiteSpace(connectionString))
287
+ {
288
+ throw new InvalidOperationException("Cosmos DB connection is not configured.");
289
+ }
290
+
291
+ var connectionOptions = IsLocalCosmosEndpoint(connectionString)
292
+ ? new CosmosClientOptions { ConnectionMode = ConnectionMode.Gateway }
293
+ : new CosmosClientOptions();
294
+
295
+ return new CosmosClient(connectionString, connectionOptions);
296
+ }
297
+
298
+ private static Container GetContainer(CosmosClient client) => client.GetContainer(DatabaseName, ContainerName);
299
+
300
+ private static async Task<JsonObject> ReadRequestBodyAsync(HttpRequestData request)
301
+ {
302
+ using var reader = new StreamReader(request.Body, Encoding.UTF8);
303
+ var raw = await reader.ReadToEndAsync();
304
+ var node = JsonNode.Parse(raw) as JsonObject;
305
+
306
+ if (node is null)
307
+ {
308
+ throw new JsonException("Request body must be a JSON object.");
309
+ }
310
+
311
+ return node;
312
+ }
313
+
314
+ private static JsonObject BuildManagedDocument(JsonObject source, string id, string createdAt, string updatedAt)
315
+ {
316
+ var payload = new JsonObject();
317
+
318
+ foreach (var entry in source)
319
+ {
320
+ if (entry.Key is "id" or "createdAt" or "updatedAt")
321
+ {
322
+ continue;
323
+ }
324
+
325
+ payload[entry.Key] = entry.Value?.DeepClone();
326
+ }
327
+
328
+ payload["id"] = id;
329
+ payload["createdAt"] = createdAt;
330
+ payload["updatedAt"] = updatedAt;
331
+
332
+ return payload;
333
+ }
334
+
335
+ private static Stream CreateJsonStream(JsonObject payload) =>
336
+ new MemoryStream(Encoding.UTF8.GetBytes(payload.ToJsonString()));
337
+
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
+ }
355
+
356
+ private static async Task<HttpResponseData> WriteJsonAsync(HttpRequestData request, HttpStatusCode status, object payload)
357
+ {
358
+ var response = request.CreateResponse(status);
359
+ await response.WriteAsJsonAsync(payload);
360
+ return response;
361
+ }
362
+
363
+ [Function("${modelCamel}GetAll")]
364
+ public async Task<HttpResponseData> GetAll(
365
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}")] HttpRequestData request)
366
+ {
367
+ try
368
+ {
369
+ using var client = CreateCosmosClient();
370
+ var container = GetContainer(client);
371
+ using var iterator = container.GetItemQueryStreamIterator("SELECT * FROM c");
372
+ var items = new List<JsonElement>();
373
+
374
+ while (iterator.HasMoreResults)
375
+ {
376
+ var page = await iterator.ReadNextAsync();
377
+ if (!page.IsSuccessStatusCode)
378
+ {
379
+ throw new InvalidOperationException($"Cosmos query failed with status {(int)page.StatusCode}.");
380
+ }
381
+
382
+ using var document = await JsonDocument.ParseAsync(page.Content);
383
+ if (!document.RootElement.TryGetProperty("Documents", out var documents))
384
+ {
385
+ continue;
386
+ }
387
+
388
+ foreach (var item in documents.EnumerateArray())
389
+ {
390
+ items.Add(item.Clone());
391
+ }
392
+ }
393
+
394
+ _logger.LogInformation("Fetched {Count} ${modelName} item(s) from {Container}.", items.Count, ContainerName);
395
+ return await WriteJsonAsync(request, HttpStatusCode.OK, items);
396
+ }
397
+ catch (Exception ex)
398
+ {
399
+ _logger.LogError(ex, "Failed to fetch ${modelName} items from Cosmos DB.");
400
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch items" });
401
+ }
402
+ }
403
+
404
+ [Function("${modelCamel}GetById")]
405
+ public async Task<HttpResponseData> GetById(
406
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
407
+ string id)
408
+ {
409
+ try
410
+ {
411
+ using var client = CreateCosmosClient();
412
+ var container = GetContainer(client);
413
+ var item = await ReadCosmosItemAsync(container, id);
414
+ return await WriteJsonAsync(request, HttpStatusCode.OK, item);
415
+ }
416
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
417
+ {
418
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
419
+ }
420
+ catch (Exception ex)
421
+ {
422
+ _logger.LogError(ex, "Failed to fetch ${modelName} item {Id} from Cosmos DB.", id);
423
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch item", id });
424
+ }
425
+ }
426
+
427
+ [Function("${modelCamel}Create")]
428
+ public async Task<HttpResponseData> Create(
429
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "${modelCamel}")] HttpRequestData request)
430
+ {
431
+ try
432
+ {
433
+ var body = await ReadRequestBodyAsync(request);
434
+ var now = DateTimeOffset.UtcNow.ToString("O");
435
+ var id = body["id"]?.GetValue<string>() ?? Guid.NewGuid().ToString();
436
+ var payload = BuildManagedDocument(body, id, now, now);
437
+
438
+ using var client = CreateCosmosClient();
439
+ var container = GetContainer(client);
440
+ using var stream = CreateJsonStream(payload);
441
+ var response = await container.CreateItemStreamAsync(stream, new PartitionKey(id));
442
+ if (!response.IsSuccessStatusCode)
443
+ {
444
+ throw new InvalidOperationException($"Cosmos create failed with status {(int)response.StatusCode}.");
445
+ }
446
+
447
+ return await WriteJsonAsync(request, HttpStatusCode.Created, payload);
448
+ }
449
+ catch (JsonException ex)
450
+ {
451
+ _logger.LogWarning(ex, "Invalid ${modelName} create payload.");
452
+ return await WriteJsonAsync(request, HttpStatusCode.BadRequest, new { error = "Request body must be a JSON object." });
453
+ }
454
+ catch (Exception ex)
455
+ {
456
+ _logger.LogError(ex, "Failed to create ${modelName} item in Cosmos DB.");
457
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to create item" });
458
+ }
459
+ }
460
+
461
+ [Function("${modelCamel}Update")]
462
+ public async Task<HttpResponseData> Update(
463
+ [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
464
+ string id)
465
+ {
466
+ try
467
+ {
468
+ using var client = CreateCosmosClient();
469
+ var container = GetContainer(client);
470
+
471
+ JsonObject existing;
472
+ try
473
+ {
474
+ existing = await ReadCosmosItemAsync(container, id);
475
+ }
476
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
477
+ {
478
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
479
+ }
480
+
481
+ var body = await ReadRequestBodyAsync(request);
482
+ var createdAt = existing["createdAt"]?.GetValue<string>() ?? DateTimeOffset.UtcNow.ToString("O");
483
+ var payload = BuildManagedDocument(body, id, createdAt, DateTimeOffset.UtcNow.ToString("O"));
484
+
485
+ using var stream = CreateJsonStream(payload);
486
+ var response = await container.ReplaceItemStreamAsync(stream, id, new PartitionKey(id));
487
+ if (!response.IsSuccessStatusCode)
488
+ {
489
+ throw new InvalidOperationException($"Cosmos replace failed with status {(int)response.StatusCode}.");
490
+ }
491
+
492
+ return await WriteJsonAsync(request, HttpStatusCode.OK, payload);
493
+ }
494
+ catch (JsonException ex)
495
+ {
496
+ _logger.LogWarning(ex, "Invalid ${modelName} update payload for {Id}.", id);
497
+ return await WriteJsonAsync(request, HttpStatusCode.BadRequest, new { error = "Request body must be a JSON object.", id });
498
+ }
499
+ catch (Exception ex)
500
+ {
501
+ _logger.LogError(ex, "Failed to update ${modelName} item {Id} in Cosmos DB.", id);
502
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to update item", id });
503
+ }
504
+ }
505
+
506
+ [Function("${modelCamel}Delete")]
507
+ public async Task<HttpResponseData> Delete(
508
+ [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
509
+ string id)
510
+ {
511
+ try
512
+ {
513
+ using var client = CreateCosmosClient();
514
+ var container = GetContainer(client);
515
+ await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(id));
516
+
517
+ return request.CreateResponse(HttpStatusCode.NoContent);
518
+ }
519
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
520
+ {
521
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
522
+ }
523
+ catch (Exception ex)
524
+ {
525
+ _logger.LogError(ex, "Failed to delete ${modelName} item {Id} from Cosmos DB.", id);
526
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to delete item", id });
527
+ }
528
+ }
529
+ }
530
+ `;
531
+ }
532
+
533
+ export function generatePythonAzureFunctionsCRUD(model: ModelInfo): {
534
+ blueprint: string;
535
+ registration: string;
536
+ } {
537
+ const modelName = model.name;
538
+ const modelCamel = toCamelCase(modelName);
539
+ const modelSnake = toKebabCase(modelName).replace(/-/g, "_");
540
+ const containerName = `${modelName}s`;
541
+
542
+ return {
543
+ registration: `from blueprints.${modelSnake} import bp as ${modelSnake}_bp\napp.register_blueprint(${modelSnake}_bp)`,
544
+ blueprint: `import json
545
+ from datetime import datetime, timezone
546
+ from typing import Any
547
+ from uuid import uuid4
548
+ import os
549
+
550
+ import azure.functions as func
551
+ from azure.cosmos import CosmosClient, exceptions
552
+ from azure.identity import DefaultAzureCredential
553
+
554
+ bp = func.Blueprint()
555
+ CONTAINER_NAME = "${containerName}"
556
+ DATABASE_NAME = os.environ.get("COSMOS_DB_DATABASE_NAME", "AppDatabase")
557
+
558
+
559
+ def _json_response(payload: Any, status_code: int) -> func.HttpResponse:
560
+ return func.HttpResponse(
561
+ body=json.dumps(payload, ensure_ascii=False),
562
+ status_code=status_code,
563
+ mimetype="application/json",
564
+ )
565
+
566
+
567
+ def _get_container():
568
+ endpoint = os.environ.get("CosmosDBConnection__accountEndpoint")
569
+ if endpoint:
570
+ client = CosmosClient(endpoint, credential=DefaultAzureCredential())
571
+ else:
572
+ connection_string = os.environ.get("CosmosDBConnection")
573
+ if not connection_string:
574
+ raise RuntimeError("Cosmos DB connection is not configured.")
575
+ client = CosmosClient.from_connection_string(connection_string)
576
+
577
+ database = client.get_database_client(DATABASE_NAME)
578
+ return database.get_container_client(CONTAINER_NAME)
579
+
580
+
581
+ def _build_managed_document(source: dict[str, Any], item_id: str, created_at: str, updated_at: str) -> dict[str, Any]:
582
+ payload = {
583
+ key: value
584
+ for key, value in source.items()
585
+ if key not in {"id", "createdAt", "updatedAt"}
586
+ }
587
+ payload["id"] = item_id
588
+ payload["createdAt"] = created_at
589
+ payload["updatedAt"] = updated_at
590
+ return payload
591
+
592
+
593
+ @bp.route(route="${modelCamel}", methods=["GET"])
594
+ def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
595
+ try:
596
+ container = _get_container()
597
+ items = list(
598
+ container.query_items(
599
+ query="SELECT * FROM c",
600
+ enable_cross_partition_query=True,
601
+ )
602
+ )
603
+ return _json_response(items, 200)
604
+ except Exception as exc:
605
+ return _json_response({"error": "Failed to fetch items", "details": str(exc)}, 500)
606
+
607
+
608
+ @bp.route(route="${modelCamel}/{id}", methods=["GET"])
609
+ def ${modelSnake}_get_by_id(req: func.HttpRequest) -> func.HttpResponse:
610
+ 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)
615
+ except exceptions.CosmosResourceNotFoundError:
616
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
617
+ except Exception as exc:
618
+ return _json_response({"error": "Failed to fetch item", "id": item_id, "details": str(exc)}, 500)
619
+
620
+
621
+ @bp.route(route="${modelCamel}", methods=["POST"])
622
+ def ${modelSnake}_create(req: func.HttpRequest) -> func.HttpResponse:
623
+ try:
624
+ body = req.get_json()
625
+ now = datetime.now(timezone.utc).isoformat()
626
+ item_id = body.get("id") or str(uuid4())
627
+ payload = _build_managed_document(body, item_id, now, now)
628
+
629
+ container = _get_container()
630
+ container.create_item(payload)
631
+ return _json_response(payload, 201)
632
+ except ValueError:
633
+ return _json_response({"error": "Request body must be a JSON object."}, 400)
634
+ except Exception as exc:
635
+ return _json_response({"error": "Failed to create item", "details": str(exc)}, 500)
636
+
637
+
638
+ @bp.route(route="${modelCamel}/{id}", methods=["PUT"])
639
+ def ${modelSnake}_update(req: func.HttpRequest) -> func.HttpResponse:
640
+ 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)
653
+ except exceptions.CosmosResourceNotFoundError:
654
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
655
+ except ValueError:
656
+ return _json_response({"error": "Request body must be a JSON object.", "id": item_id}, 400)
657
+ except Exception as exc:
658
+ return _json_response({"error": "Failed to update item", "id": item_id, "details": str(exc)}, 500)
659
+
660
+
661
+ @bp.route(route="${modelCamel}/{id}", methods=["DELETE"])
662
+ def ${modelSnake}_delete(req: func.HttpRequest) -> func.HttpResponse:
663
+ 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)
668
+ except exceptions.CosmosResourceNotFoundError:
669
+ return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
670
+ except Exception as exc:
671
+ return _json_response({"error": "Failed to delete item", "id": item_id, "details": str(exc)}, 500)
672
+ `,
673
+ };
674
+ }