swallowkit 1.0.0-beta.16 → 1.0.0-beta.18

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.
@@ -1,5 +1,699 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
+ exports[`generateCompactAzureFunctionsCRUD custom partition key (C#) generates snapshot for custom partition key C# 1`] = `
4
+ "using System.Net;
5
+ using System.Text;
6
+ using System.Text.Json;
7
+ using System.Text.Json.Nodes;
8
+ using Azure.Identity;
9
+ using Microsoft.Azure.Cosmos;
10
+ using Microsoft.Azure.Functions.Worker;
11
+ using Microsoft.Azure.Functions.Worker.Http;
12
+ using Microsoft.Extensions.Logging;
13
+
14
+ namespace SwallowKit.Functions;
15
+
16
+ public sealed class TodoFunctions
17
+ {
18
+ private readonly ILogger<TodoFunctions> _logger;
19
+ private static readonly string ContainerName = "Todos";
20
+
21
+ public TodoFunctions(ILogger<TodoFunctions> logger)
22
+ {
23
+ _logger = logger;
24
+ }
25
+
26
+ private static string DatabaseName => Environment.GetEnvironmentVariable("COSMOS_DB_DATABASE_NAME") ?? "AppDatabase";
27
+
28
+ private static bool IsLocalCosmosEndpoint(string endpoint) =>
29
+ endpoint.Contains("localhost:8081", StringComparison.OrdinalIgnoreCase) ||
30
+ endpoint.Contains("127.0.0.1:8081", StringComparison.OrdinalIgnoreCase);
31
+
32
+ private static CosmosClient CreateCosmosClient()
33
+ {
34
+ var endpoint = Environment.GetEnvironmentVariable("CosmosDBConnection__accountEndpoint");
35
+ if (!string.IsNullOrWhiteSpace(endpoint))
36
+ {
37
+ var options = IsLocalCosmosEndpoint(endpoint)
38
+ ? new CosmosClientOptions { ConnectionMode = ConnectionMode.Gateway }
39
+ : new CosmosClientOptions();
40
+ return new CosmosClient(endpoint, new DefaultAzureCredential(), options);
41
+ }
42
+
43
+ var connectionString = Environment.GetEnvironmentVariable("CosmosDBConnection");
44
+ if (string.IsNullOrWhiteSpace(connectionString))
45
+ {
46
+ throw new InvalidOperationException("Cosmos DB connection is not configured.");
47
+ }
48
+
49
+ var connectionOptions = IsLocalCosmosEndpoint(connectionString)
50
+ ? new CosmosClientOptions { ConnectionMode = ConnectionMode.Gateway }
51
+ : new CosmosClientOptions();
52
+
53
+ return new CosmosClient(connectionString, connectionOptions);
54
+ }
55
+
56
+ private static Container GetContainer(CosmosClient client) => client.GetContainer(DatabaseName, ContainerName);
57
+
58
+ private static async Task<JsonObject> ReadRequestBodyAsync(HttpRequestData request)
59
+ {
60
+ using var reader = new StreamReader(request.Body, Encoding.UTF8);
61
+ var raw = await reader.ReadToEndAsync();
62
+ var node = JsonNode.Parse(raw) as JsonObject;
63
+
64
+ if (node is null)
65
+ {
66
+ throw new JsonException("Request body must be a JSON object.");
67
+ }
68
+
69
+ return node;
70
+ }
71
+
72
+ private static JsonObject BuildManagedDocument(JsonObject source, string id, string createdAt, string updatedAt)
73
+ {
74
+ var payload = new JsonObject();
75
+
76
+ foreach (var entry in source)
77
+ {
78
+ if (entry.Key is "id" or "createdAt" or "updatedAt")
79
+ {
80
+ continue;
81
+ }
82
+
83
+ payload[entry.Key] = entry.Value?.DeepClone();
84
+ }
85
+
86
+ payload["id"] = id;
87
+ payload["createdAt"] = createdAt;
88
+ payload["updatedAt"] = updatedAt;
89
+
90
+ return payload;
91
+ }
92
+
93
+ private static Stream CreateJsonStream(JsonObject payload) =>
94
+ new MemoryStream(Encoding.UTF8.GetBytes(payload.ToJsonString()));
95
+
96
+ private static async Task<JsonObject> ReadCosmosItemAsync(Container container, string id)
97
+ {
98
+ var query = new QueryDefinition("SELECT * FROM c WHERE c.id = @id")
99
+ .WithParameter("@id", id);
100
+ using var iterator = container.GetItemQueryStreamIterator(query);
101
+
102
+ while (iterator.HasMoreResults)
103
+ {
104
+ var page = await iterator.ReadNextAsync();
105
+ if (!page.IsSuccessStatusCode)
106
+ {
107
+ throw new InvalidOperationException($"Cosmos query failed with status {(int)page.StatusCode}.");
108
+ }
109
+
110
+ using var document = await JsonDocument.ParseAsync(page.Content);
111
+ if (document.RootElement.TryGetProperty("Documents", out var documents))
112
+ {
113
+ foreach (var item in documents.EnumerateArray())
114
+ {
115
+ return JsonNode.Parse(item.GetRawText())?.AsObject()
116
+ ?? throw new JsonException("Cosmos item payload must be a JSON object.");
117
+ }
118
+ }
119
+ }
120
+
121
+ throw new CosmosException("Item not found", HttpStatusCode.NotFound, 0, "", 0);
122
+ }
123
+
124
+ private static async Task<HttpResponseData> WriteJsonAsync(HttpRequestData request, HttpStatusCode status, object payload)
125
+ {
126
+ var response = request.CreateResponse(status);
127
+ await response.WriteAsJsonAsync(payload);
128
+ return response;
129
+ }
130
+
131
+ [Function("todoGetAll")]
132
+ public async Task<HttpResponseData> GetAll(
133
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "todo")] HttpRequestData request)
134
+ {
135
+ try
136
+ {
137
+ using var client = CreateCosmosClient();
138
+ var container = GetContainer(client);
139
+ using var iterator = container.GetItemQueryStreamIterator("SELECT * FROM c");
140
+ var items = new List<JsonElement>();
141
+
142
+ while (iterator.HasMoreResults)
143
+ {
144
+ var page = await iterator.ReadNextAsync();
145
+ if (!page.IsSuccessStatusCode)
146
+ {
147
+ throw new InvalidOperationException($"Cosmos query failed with status {(int)page.StatusCode}.");
148
+ }
149
+
150
+ using var document = await JsonDocument.ParseAsync(page.Content);
151
+ if (!document.RootElement.TryGetProperty("Documents", out var documents))
152
+ {
153
+ continue;
154
+ }
155
+
156
+ foreach (var item in documents.EnumerateArray())
157
+ {
158
+ items.Add(item.Clone());
159
+ }
160
+ }
161
+
162
+ _logger.LogInformation("Fetched {Count} Todo item(s) from {Container}.", items.Count, ContainerName);
163
+ return await WriteJsonAsync(request, HttpStatusCode.OK, items);
164
+ }
165
+ catch (Exception ex)
166
+ {
167
+ _logger.LogError(ex, "Failed to fetch Todo items from Cosmos DB.");
168
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch items" });
169
+ }
170
+ }
171
+
172
+ [Function("todoGetById")]
173
+ public async Task<HttpResponseData> GetById(
174
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "todo/{id}")] HttpRequestData request,
175
+ string id)
176
+ {
177
+ try
178
+ {
179
+ using var client = CreateCosmosClient();
180
+ var container = GetContainer(client);
181
+ var item = await ReadCosmosItemAsync(container, id);
182
+ return await WriteJsonAsync(request, HttpStatusCode.OK, item);
183
+ }
184
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
185
+ {
186
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "Todo not found", id });
187
+ }
188
+ catch (Exception ex)
189
+ {
190
+ _logger.LogError(ex, "Failed to fetch Todo item {Id} from Cosmos DB.", id);
191
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch item", id });
192
+ }
193
+ }
194
+
195
+ [Function("todoCreate")]
196
+ public async Task<HttpResponseData> Create(
197
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "todo")] HttpRequestData request)
198
+ {
199
+ try
200
+ {
201
+ var body = await ReadRequestBodyAsync(request);
202
+ var now = DateTimeOffset.UtcNow.ToString("O");
203
+ var id = body["id"]?.GetValue<string>() ?? Guid.NewGuid().ToString();
204
+ var payload = BuildManagedDocument(body, id, now, now);
205
+
206
+ using var client = CreateCosmosClient();
207
+ var container = GetContainer(client);
208
+ using var stream = CreateJsonStream(payload);
209
+ var pkValue = payload["tenantId"]?.GetValue<string>() ?? throw new InvalidOperationException("Partition key field 'tenantId' is required.");
210
+ var response = await container.CreateItemStreamAsync(stream, new PartitionKey(pkValue));
211
+ if (!response.IsSuccessStatusCode)
212
+ {
213
+ throw new InvalidOperationException($"Cosmos create failed with status {(int)response.StatusCode}.");
214
+ }
215
+
216
+ return await WriteJsonAsync(request, HttpStatusCode.Created, payload);
217
+ }
218
+ catch (JsonException ex)
219
+ {
220
+ _logger.LogWarning(ex, "Invalid Todo create payload.");
221
+ return await WriteJsonAsync(request, HttpStatusCode.BadRequest, new { error = "Request body must be a JSON object." });
222
+ }
223
+ catch (Exception ex)
224
+ {
225
+ _logger.LogError(ex, "Failed to create Todo item in Cosmos DB.");
226
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to create item" });
227
+ }
228
+ }
229
+
230
+ [Function("todoUpdate")]
231
+ public async Task<HttpResponseData> Update(
232
+ [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "todo/{id}")] HttpRequestData request,
233
+ string id)
234
+ {
235
+ try
236
+ {
237
+ using var client = CreateCosmosClient();
238
+ var container = GetContainer(client);
239
+
240
+ JsonObject existing;
241
+ try
242
+ {
243
+ existing = await ReadCosmosItemAsync(container, id);
244
+ }
245
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
246
+ {
247
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "Todo not found", id });
248
+ }
249
+
250
+ var body = await ReadRequestBodyAsync(request);
251
+ var createdAt = existing["createdAt"]?.GetValue<string>() ?? DateTimeOffset.UtcNow.ToString("O");
252
+ var payload = BuildManagedDocument(body, id, createdAt, DateTimeOffset.UtcNow.ToString("O"));
253
+
254
+ using var stream = CreateJsonStream(payload);
255
+ var response = await container.ReplaceItemStreamAsync(stream, id, new PartitionKey(payload["tenantId"]?.GetValue<string>()));
256
+ if (!response.IsSuccessStatusCode)
257
+ {
258
+ throw new InvalidOperationException($"Cosmos replace failed with status {(int)response.StatusCode}.");
259
+ }
260
+
261
+ return await WriteJsonAsync(request, HttpStatusCode.OK, payload);
262
+ }
263
+ catch (JsonException ex)
264
+ {
265
+ _logger.LogWarning(ex, "Invalid Todo update payload for {Id}.", id);
266
+ return await WriteJsonAsync(request, HttpStatusCode.BadRequest, new { error = "Request body must be a JSON object.", id });
267
+ }
268
+ catch (Exception ex)
269
+ {
270
+ _logger.LogError(ex, "Failed to update Todo item {Id} in Cosmos DB.", id);
271
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to update item", id });
272
+ }
273
+ }
274
+
275
+ [Function("todoDelete")]
276
+ public async Task<HttpResponseData> Delete(
277
+ [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "todo/{id}")] HttpRequestData request,
278
+ string id)
279
+ {
280
+ try
281
+ {
282
+ using var client = CreateCosmosClient();
283
+ var container = GetContainer(client);
284
+ var existing = await ReadCosmosItemAsync(container, id);
285
+ var pkValue = existing["tenantId"]?.GetValue<string>();
286
+ await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(pkValue));
287
+
288
+ return request.CreateResponse(HttpStatusCode.NoContent);
289
+ }
290
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
291
+ {
292
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "Todo not found", id });
293
+ }
294
+ catch (Exception ex)
295
+ {
296
+ _logger.LogError(ex, "Failed to delete Todo item {Id} from Cosmos DB.", id);
297
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to delete item", id });
298
+ }
299
+ }
300
+ }
301
+ "
302
+ `;
303
+
304
+ exports[`generateCompactAzureFunctionsCRUD custom partition key (Python) generates snapshot for custom partition key Python 1`] = `
305
+ "import json
306
+ from datetime import datetime, timezone
307
+ from typing import Any
308
+ from uuid import uuid4
309
+ import os
310
+
311
+ import azure.functions as func
312
+ from azure.cosmos import CosmosClient, exceptions
313
+ from azure.identity import DefaultAzureCredential
314
+
315
+ bp = func.Blueprint()
316
+ CONTAINER_NAME = "Todos"
317
+ DATABASE_NAME = os.environ.get("COSMOS_DB_DATABASE_NAME", "AppDatabase")
318
+
319
+
320
+ def _json_response(payload: Any, status_code: int) -> func.HttpResponse:
321
+ return func.HttpResponse(
322
+ body=json.dumps(payload, ensure_ascii=False),
323
+ status_code=status_code,
324
+ mimetype="application/json",
325
+ )
326
+
327
+
328
+ def _get_container():
329
+ endpoint = os.environ.get("CosmosDBConnection__accountEndpoint")
330
+ if endpoint:
331
+ client = CosmosClient(endpoint, credential=DefaultAzureCredential())
332
+ else:
333
+ connection_string = os.environ.get("CosmosDBConnection")
334
+ if not connection_string:
335
+ raise RuntimeError("Cosmos DB connection is not configured.")
336
+ client = CosmosClient.from_connection_string(connection_string)
337
+
338
+ database = client.get_database_client(DATABASE_NAME)
339
+ return database.get_container_client(CONTAINER_NAME)
340
+
341
+
342
+ def _build_managed_document(source: dict[str, Any], item_id: str, created_at: str, updated_at: str) -> dict[str, Any]:
343
+ payload = {
344
+ key: value
345
+ for key, value in source.items()
346
+ if key not in {"id", "createdAt", "updatedAt"}
347
+ }
348
+ payload["id"] = item_id
349
+ payload["createdAt"] = created_at
350
+ payload["updatedAt"] = updated_at
351
+ return payload
352
+
353
+
354
+ @bp.route(route="todo", methods=["GET"])
355
+ def todo_get_all(req: func.HttpRequest) -> func.HttpResponse:
356
+ try:
357
+ container = _get_container()
358
+ items = list(
359
+ container.query_items(
360
+ query="SELECT * FROM c",
361
+ enable_cross_partition_query=True,
362
+ )
363
+ )
364
+ return _json_response(items, 200)
365
+ except Exception as exc:
366
+ return _json_response({"error": "Failed to fetch items", "details": str(exc)}, 500)
367
+
368
+
369
+ @bp.route(route="todo/{id}", methods=["GET"])
370
+ def todo_get_by_id(req: func.HttpRequest) -> func.HttpResponse:
371
+ item_id = req.route_params.get("id")
372
+ try:
373
+ container = _get_container()
374
+ items = list(container.query_items(
375
+ query="SELECT * FROM c WHERE c.id = @id",
376
+ parameters=[{"name": "@id", "value": item_id}],
377
+ enable_cross_partition_query=True,
378
+ ))
379
+ if not items:
380
+ return _json_response({"error": "Todo not found", "id": item_id}, 404)
381
+ return _json_response(items[0], 200)
382
+ except exceptions.CosmosResourceNotFoundError:
383
+ return _json_response({"error": "Todo not found", "id": item_id}, 404)
384
+ except Exception as exc:
385
+ return _json_response({"error": "Failed to fetch item", "id": item_id, "details": str(exc)}, 500)
386
+
387
+
388
+ @bp.route(route="todo", methods=["POST"])
389
+ def todo_create(req: func.HttpRequest) -> func.HttpResponse:
390
+ try:
391
+ body = req.get_json()
392
+ now = datetime.now(timezone.utc).isoformat()
393
+ item_id = body.get("id") or str(uuid4())
394
+ payload = _build_managed_document(body, item_id, now, now)
395
+
396
+ container = _get_container()
397
+ container.create_item(payload)
398
+ return _json_response(payload, 201)
399
+ except ValueError:
400
+ return _json_response({"error": "Request body must be a JSON object."}, 400)
401
+ except Exception as exc:
402
+ return _json_response({"error": "Failed to create item", "details": str(exc)}, 500)
403
+
404
+
405
+ @bp.route(route="todo/{id}", methods=["PUT"])
406
+ def todo_update(req: func.HttpRequest) -> func.HttpResponse:
407
+ item_id = req.route_params.get("id")
408
+ try:
409
+ container = _get_container()
410
+ items = list(container.query_items(
411
+ query="SELECT * FROM c WHERE c.id = @id",
412
+ parameters=[{"name": "@id", "value": item_id}],
413
+ enable_cross_partition_query=True,
414
+ ))
415
+ if not items:
416
+ return _json_response({"error": "Todo not found", "id": item_id}, 404)
417
+ existing = items[0]
418
+ body = req.get_json()
419
+ payload = _build_managed_document(
420
+ body,
421
+ item_id,
422
+ existing.get("createdAt") or datetime.now(timezone.utc).isoformat(),
423
+ datetime.now(timezone.utc).isoformat(),
424
+ )
425
+ container.replace_item(item=existing, body=payload)
426
+ return _json_response(payload, 200)
427
+ except exceptions.CosmosResourceNotFoundError:
428
+ return _json_response({"error": "Todo not found", "id": item_id}, 404)
429
+ except ValueError:
430
+ return _json_response({"error": "Request body must be a JSON object.", "id": item_id}, 400)
431
+ except Exception as exc:
432
+ return _json_response({"error": "Failed to update item", "id": item_id, "details": str(exc)}, 500)
433
+
434
+
435
+ @bp.route(route="todo/{id}", methods=["DELETE"])
436
+ def todo_delete(req: func.HttpRequest) -> func.HttpResponse:
437
+ item_id = req.route_params.get("id")
438
+ try:
439
+ container = _get_container()
440
+ items = list(container.query_items(
441
+ query="SELECT * FROM c WHERE c.id = @id",
442
+ parameters=[{"name": "@id", "value": item_id}],
443
+ enable_cross_partition_query=True,
444
+ ))
445
+ if not items:
446
+ return _json_response({"error": "Todo not found", "id": item_id}, 404)
447
+ pk_value = items[0].get("tenantId")
448
+ container.delete_item(item=item_id, partition_key=pk_value)
449
+ return func.HttpResponse(status_code=204)
450
+ except exceptions.CosmosResourceNotFoundError:
451
+ return _json_response({"error": "Todo not found", "id": item_id}, 404)
452
+ except Exception as exc:
453
+ return _json_response({"error": "Failed to delete item", "id": item_id, "details": str(exc)}, 500)
454
+ "
455
+ `;
456
+
457
+ exports[`generateCompactAzureFunctionsCRUD custom partition key (TS) generates snapshot for custom partition key TS 1`] = `
458
+ "import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
459
+ import { z } from 'zod/v4';
460
+ import crypto from 'crypto';
461
+ import { todoSchema } from '@myapp/shared';
462
+
463
+ const containerName = 'Todos';
464
+
465
+ // GET /api/todo - 全件取得
466
+ app.http('todo-get-all', {
467
+ methods: ['GET'],
468
+ route: 'todo',
469
+ authLevel: 'anonymous',
470
+ extraInputs: [
471
+ {
472
+ type: 'cosmosDB',
473
+ name: 'cosmosInput',
474
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
475
+ containerName,
476
+ connection: 'CosmosDBConnection',
477
+ sqlQuery: 'SELECT * FROM c',
478
+ },
479
+ ],
480
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
481
+ try {
482
+ const documents = context.extraInputs.get('cosmosInput') as any[];
483
+
484
+ if (!documents || !Array.isArray(documents)) {
485
+ return { status: 200, jsonBody: [] };
486
+ }
487
+
488
+ const validated = z.array(todoSchema).parse(documents);
489
+ context.log(\`Fetched \${validated.length} items from \${containerName}\`);
490
+
491
+ return { status: 200, jsonBody: validated };
492
+ } catch (error) {
493
+ context.error(\`Error fetching from \${containerName}:\`, error);
494
+ return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
495
+ }
496
+ },
497
+ });
498
+
499
+ // GET /api/todo/{id} - ID指定取得 (custom partition key)
500
+ app.http('todo-get-by-id', {
501
+ methods: ['GET'],
502
+ route: 'todo/{id}',
503
+ authLevel: 'anonymous',
504
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
505
+ try {
506
+ const id = request.params.id;
507
+ if (!id) {
508
+ return { status: 400, jsonBody: { error: 'ID is required' } };
509
+ }
510
+
511
+ const { CosmosClient } = await import('@azure/cosmos');
512
+ const endpoint = process.env.CosmosDBConnection__accountEndpoint;
513
+ let client: InstanceType<typeof CosmosClient>;
514
+ if (endpoint) {
515
+ const { DefaultAzureCredential } = await import('@azure/identity');
516
+ client = new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
517
+ } else {
518
+ client = new CosmosClient(process.env.CosmosDBConnection!);
519
+ }
520
+ const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
521
+ const container = database.container(containerName);
522
+
523
+ const { resources } = await container.items.query({
524
+ query: 'SELECT * FROM c WHERE c.id = @id',
525
+ parameters: [{ name: '@id', value: id }],
526
+ }).fetchAll();
527
+
528
+ if (!resources || resources.length === 0) {
529
+ return { status: 404, jsonBody: { error: 'Item not found' } };
530
+ }
531
+
532
+ const validated = todoSchema.parse(resources[0]);
533
+ return { status: 200, jsonBody: validated };
534
+ } catch (error) {
535
+ context.error(\`Error fetching item from \${containerName}:\`, error);
536
+ return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
537
+ }
538
+ },
539
+ });
540
+
541
+ // POST /api/todo - 新規作成
542
+ app.http('todo-create', {
543
+ methods: ['POST'],
544
+ route: 'todo',
545
+ authLevel: 'anonymous',
546
+ extraOutputs: [
547
+ {
548
+ type: 'cosmosDB',
549
+ name: 'cosmosOutput',
550
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
551
+ containerName,
552
+ connection: 'CosmosDBConnection',
553
+ createIfNotExists: true,
554
+ },
555
+ ],
556
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
557
+ try {
558
+ const body = await request.json() as any;
559
+
560
+ const { id, createdAt, updatedAt, ...userData } = body;
561
+ const now = new Date().toISOString();
562
+ const dataWithManagedFields = {
563
+ ...userData,
564
+ id: id || crypto.randomUUID(),
565
+ createdAt: now,
566
+ updatedAt: now,
567
+ };
568
+
569
+ const result = todoSchema.safeParse(dataWithManagedFields);
570
+
571
+ if (!result.success) {
572
+ context.error('Validation failed:', result.error.issues);
573
+ return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
574
+ }
575
+
576
+ context.extraOutputs.set('cosmosOutput', result.data);
577
+ return { status: 201, jsonBody: result.data };
578
+ } catch (error) {
579
+ context.error(\`Error creating item in \${containerName}:\`, error);
580
+ return { status: 500, jsonBody: { error: 'Failed to create item' } };
581
+ }
582
+ },
583
+ });
584
+
585
+ // PUT /api/todo/{id} - 更新 (custom partition key)
586
+ app.http('todo-update', {
587
+ methods: ['PUT'],
588
+ route: 'todo/{id}',
589
+ authLevel: 'anonymous',
590
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
591
+ try {
592
+ const id = request.params.id;
593
+ if (!id) {
594
+ return { status: 400, jsonBody: { error: 'ID is required' } };
595
+ }
596
+
597
+ const { CosmosClient } = await import('@azure/cosmos');
598
+ const endpoint = process.env.CosmosDBConnection__accountEndpoint;
599
+ let client: InstanceType<typeof CosmosClient>;
600
+ if (endpoint) {
601
+ const { DefaultAzureCredential } = await import('@azure/identity');
602
+ client = new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
603
+ } else {
604
+ client = new CosmosClient(process.env.CosmosDBConnection!);
605
+ }
606
+ const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
607
+ const container = database.container(containerName);
608
+
609
+ const { resources } = await container.items.query({
610
+ query: 'SELECT * FROM c WHERE c.id = @id',
611
+ parameters: [{ name: '@id', value: id }],
612
+ }).fetchAll();
613
+
614
+ if (!resources || resources.length === 0) {
615
+ return { status: 404, jsonBody: { error: 'Item not found' } };
616
+ }
617
+
618
+ const existingDocument = resources[0];
619
+ const body = await request.json() as any;
620
+ const { createdAt, updatedAt, ...userData } = body;
621
+
622
+ const dataWithManagedFields = {
623
+ ...userData,
624
+ id,
625
+ createdAt: existingDocument.createdAt,
626
+ updatedAt: new Date().toISOString(),
627
+ };
628
+
629
+ const result = todoSchema.safeParse(dataWithManagedFields);
630
+
631
+ if (!result.success) {
632
+ context.error('Validation failed:', result.error.issues);
633
+ return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
634
+ }
635
+
636
+ const pkValue = result.data.tenantId;
637
+ await container.items.upsert(result.data);
638
+ return { status: 200, jsonBody: result.data };
639
+ } catch (error) {
640
+ context.error(\`Error updating item in \${containerName}:\`, error);
641
+ return { status: 500, jsonBody: { error: 'Failed to update item' } };
642
+ }
643
+ },
644
+ });
645
+
646
+ // DELETE /api/todo/{id} - 削除 (custom partition key)
647
+ app.http('todo-delete', {
648
+ methods: ['DELETE'],
649
+ route: 'todo/{id}',
650
+ authLevel: 'anonymous',
651
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
652
+ try {
653
+ const id = request.params.id;
654
+ if (!id) {
655
+ return { status: 400, jsonBody: { error: 'ID is required' } };
656
+ }
657
+
658
+ const { CosmosClient } = await import('@azure/cosmos');
659
+ const endpoint = process.env.CosmosDBConnection__accountEndpoint;
660
+ let client: InstanceType<typeof CosmosClient>;
661
+ if (endpoint) {
662
+ const { DefaultAzureCredential } = await import('@azure/identity');
663
+ client = new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
664
+ } else {
665
+ client = new CosmosClient(process.env.CosmosDBConnection!);
666
+ }
667
+ const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
668
+ const container = database.container(containerName);
669
+
670
+ // パーティションキー値を取得するためにドキュメントを読み取り
671
+ const { resources } = await container.items.query({
672
+ query: 'SELECT * FROM c WHERE c.id = @id',
673
+ parameters: [{ name: '@id', value: id }],
674
+ }).fetchAll();
675
+
676
+ if (!resources || resources.length === 0) {
677
+ return { status: 404, jsonBody: { error: 'Item not found' } };
678
+ }
679
+
680
+ const pkValue = resources[0].tenantId;
681
+ await container.item(id, pkValue).delete();
682
+ context.log(\`Deleted item \${id} from \${containerName}\`);
683
+
684
+ return { status: 204 };
685
+ } catch (error: any) {
686
+ if (error.code === 404) {
687
+ return { status: 404, jsonBody: { error: 'Item not found' } };
688
+ }
689
+ context.error(\`Error deleting item from \${containerName}:\`, error);
690
+ return { status: 500, jsonBody: { error: 'Failed to delete item' } };
691
+ }
692
+ },
693
+ });
694
+ "
695
+ `;
696
+
3
697
  exports[`generateCompactAzureFunctionsCRUD generates correct CRUD code for a basic model 1`] = `
4
698
  "import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
5
699
  import { z } from 'zod/v4';