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.
- package/README.ja.md +87 -181
- package/README.md +87 -238
- package/dist/__tests__/fixtures.d.ts +12 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -1
- package/dist/__tests__/fixtures.js +101 -0
- package/dist/__tests__/fixtures.js.map +1 -1
- package/dist/cli/commands/add-auth.d.ts +15 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +790 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +164 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +20 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +27 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
- package/dist/cli/commands/dev-seeds.js +188 -6
- package/dist/cli/commands/dev-seeds.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +30 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +495 -129
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +69 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +788 -766
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +24 -3
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +408 -189
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +335 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +13 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +151 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/auth-operations.d.ts +61 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +272 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +185 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/file-session.d.ts +56 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +203 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +37 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +248 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +51 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +187 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/agent-instructions.d.ts +29 -0
- package/dist/core/project/agent-instructions.d.ts.map +1 -0
- package/dist/core/project/agent-instructions.js +160 -0
- package/dist/core/project/agent-instructions.js.map +1 -0
- package/dist/core/project/artifacts.d.ts +58 -0
- package/dist/core/project/artifacts.d.ts.map +1 -0
- package/dist/core/project/artifacts.js +179 -0
- package/dist/core/project/artifacts.js.map +1 -0
- package/dist/core/project/boundaries.d.ts +50 -0
- package/dist/core/project/boundaries.d.ts.map +1 -0
- package/dist/core/project/boundaries.js +117 -0
- package/dist/core/project/boundaries.js.map +1 -0
- package/dist/core/project/capabilities.d.ts +58 -0
- package/dist/core/project/capabilities.d.ts.map +1 -0
- package/dist/core/project/capabilities.js +135 -0
- package/dist/core/project/capabilities.js.map +1 -0
- package/dist/core/project/drift.d.ts +29 -0
- package/dist/core/project/drift.d.ts.map +1 -0
- package/dist/core/project/drift.js +188 -0
- package/dist/core/project/drift.js.map +1 -0
- package/dist/core/project/infra.d.ts +49 -0
- package/dist/core/project/infra.d.ts.map +1 -0
- package/dist/core/project/infra.js +167 -0
- package/dist/core/project/infra.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/state.d.ts +15 -0
- package/dist/core/project/state.d.ts.map +1 -0
- package/dist/core/project/state.js +106 -0
- package/dist/core/project/state.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +274 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/project/workflows.d.ts +43 -0
- package/dist/core/project/workflows.d.ts.map +1 -0
- package/dist/core/project/workflows.js +299 -0
- package/dist/core/project/workflows.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +63 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1774 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1029 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +4 -3
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +405 -134
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +229 -78
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +753 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +152 -8
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +17 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +174 -34
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +69 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +412 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/machine/contracts.d.ts +24 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +14 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +42 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +566 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +425 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +105 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +7 -11
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +42 -17
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +21 -0
- package/dist/utils/python-uv.d.ts.map +1 -0
- package/dist/utils/python-uv.js +111 -0
- package/dist/utils/python-uv.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +49 -0
- package/dist/version.js.map +1 -0
- package/package.json +15 -17
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
- package/src/__tests__/agent-instructions.test.ts +237 -0
- package/src/__tests__/agent-loop-phase34.test.ts +513 -0
- package/src/__tests__/auth.test.ts +770 -0
- package/src/__tests__/bff-swa-auth.test.ts +275 -0
- package/src/__tests__/config.test.ts +176 -3
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +459 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
- package/src/__tests__/dev-seeds.test.ts +124 -0
- package/src/__tests__/dev.test.ts +254 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/fixtures.ts +101 -0
- package/src/__tests__/functions-generator.test.ts +137 -1
- package/src/__tests__/init.test.ts +366 -2
- package/src/__tests__/jest-globals.d.ts +8 -0
- package/src/__tests__/machine.test.ts +371 -0
- package/src/__tests__/mcp.test.ts +253 -0
- package/src/__tests__/model-parser.test.ts +142 -0
- package/src/__tests__/multi-auth.test.ts +160 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/__tests__/package-manager.test.ts +59 -4
- package/src/__tests__/plan-apply.test.ts +499 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +287 -24
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +104 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +853 -0
- package/src/cli/commands/add-connector.ts +159 -0
- package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
- package/src/cli/commands/create-model.ts +21 -100
- package/src/cli/commands/dev-seeds.ts +278 -7
- package/src/cli/commands/dev.ts +607 -160
- package/src/cli/commands/init.ts +851 -786
- package/src/cli/commands/scaffold.ts +531 -236
- package/src/cli/index.ts +338 -60
- package/src/core/config.ts +145 -9
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/auth-operations.ts +360 -0
- package/src/core/operations/create-model.ts +191 -0
- package/src/core/operations/file-session.ts +222 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +258 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +77 -0
- package/src/core/operations/scaffold-plan.ts +215 -0
- package/src/core/project/agent-instructions.ts +167 -0
- package/src/core/project/agent-templates/AGENTS.md +504 -0
- package/src/core/project/agent-templates/CLAUDE.md +31 -0
- package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
- package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
- package/src/core/project/agent-templates/copilot-instructions.md +27 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
- package/src/core/project/artifacts.ts +205 -0
- package/src/core/project/boundaries.ts +170 -0
- package/src/core/project/capabilities.ts +193 -0
- package/src/core/project/drift.ts +202 -0
- package/src/core/project/infra.ts +196 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/state.ts +77 -0
- package/src/core/project/validation.ts +295 -0
- package/src/core/project/workflows.ts +295 -0
- package/src/core/scaffold/auth-generator.ts +1824 -0
- package/src/core/scaffold/connector-functions-generator.ts +1133 -0
- package/src/core/scaffold/functions-generator.ts +432 -134
- package/src/core/scaffold/model-parser.ts +244 -90
- package/src/core/scaffold/native-schema-generator.ts +904 -0
- package/src/core/scaffold/nextjs-generator.ts +174 -9
- package/src/core/scaffold/ui-generator.ts +191 -36
- package/src/core/verify/index.ts +468 -0
- package/src/machine/contracts.ts +33 -0
- package/src/machine/errors.ts +45 -0
- package/src/machine/index.ts +690 -0
- package/src/mcp/index.ts +423 -0
- package/src/types/index.ts +137 -0
- package/src/utils/package-manager.ts +41 -19
- package/src/utils/python-uv.ts +96 -0
- package/src/version.ts +13 -0
|
@@ -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 TodoCrudFunctions
|
|
17
|
+
{
|
|
18
|
+
private readonly ILogger<TodoCrudFunctions> _logger;
|
|
19
|
+
private static readonly string ContainerName = "Todos";
|
|
20
|
+
|
|
21
|
+
public TodoCrudFunctions(ILogger<TodoCrudFunctions> 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.Function, "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.Function, "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.Function, "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.Function, "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.Function, "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: 'function',
|
|
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 unknown[];
|
|
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: 'function',
|
|
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: 'function',
|
|
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 Record<string, unknown>;
|
|
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: 'function',
|
|
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 Record<string, unknown>;
|
|
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: 'function',
|
|
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) {
|
|
686
|
+
if ((error as { code?: number }).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';
|
|
@@ -12,7 +706,7 @@ const containerName = 'Todos';
|
|
|
12
706
|
app.http('todo-get-all', {
|
|
13
707
|
methods: ['GET'],
|
|
14
708
|
route: 'todo',
|
|
15
|
-
authLevel: '
|
|
709
|
+
authLevel: 'function',
|
|
16
710
|
extraInputs: [
|
|
17
711
|
{
|
|
18
712
|
type: 'cosmosDB',
|
|
@@ -25,7 +719,7 @@ app.http('todo-get-all', {
|
|
|
25
719
|
],
|
|
26
720
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
27
721
|
try {
|
|
28
|
-
const documents = context.extraInputs.get('cosmosInput') as
|
|
722
|
+
const documents = context.extraInputs.get('cosmosInput') as unknown[];
|
|
29
723
|
|
|
30
724
|
if (!documents || !Array.isArray(documents)) {
|
|
31
725
|
return { status: 200, jsonBody: [] };
|
|
@@ -46,7 +740,7 @@ app.http('todo-get-all', {
|
|
|
46
740
|
app.http('todo-get-by-id', {
|
|
47
741
|
methods: ['GET'],
|
|
48
742
|
route: 'todo/{id}',
|
|
49
|
-
authLevel: '
|
|
743
|
+
authLevel: 'function',
|
|
50
744
|
extraInputs: [
|
|
51
745
|
{
|
|
52
746
|
type: 'cosmosDB',
|
|
@@ -79,7 +773,7 @@ app.http('todo-get-by-id', {
|
|
|
79
773
|
app.http('todo-create', {
|
|
80
774
|
methods: ['POST'],
|
|
81
775
|
route: 'todo',
|
|
82
|
-
authLevel: '
|
|
776
|
+
authLevel: 'function',
|
|
83
777
|
extraOutputs: [
|
|
84
778
|
{
|
|
85
779
|
type: 'cosmosDB',
|
|
@@ -92,7 +786,7 @@ app.http('todo-create', {
|
|
|
92
786
|
],
|
|
93
787
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
94
788
|
try {
|
|
95
|
-
const body = await request.json() as
|
|
789
|
+
const body = await request.json() as Record<string, unknown>;
|
|
96
790
|
|
|
97
791
|
const { id, createdAt, updatedAt, ...userData } = body;
|
|
98
792
|
const now = new Date().toISOString();
|
|
@@ -123,7 +817,7 @@ app.http('todo-create', {
|
|
|
123
817
|
app.http('todo-update', {
|
|
124
818
|
methods: ['PUT'],
|
|
125
819
|
route: 'todo/{id}',
|
|
126
|
-
authLevel: '
|
|
820
|
+
authLevel: 'function',
|
|
127
821
|
extraInputs: [
|
|
128
822
|
{
|
|
129
823
|
type: 'cosmosDB',
|
|
@@ -151,12 +845,12 @@ app.http('todo-update', {
|
|
|
151
845
|
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
152
846
|
}
|
|
153
847
|
|
|
154
|
-
const existingDocument = context.extraInputs.get('cosmosInput') as
|
|
848
|
+
const existingDocument = context.extraInputs.get('cosmosInput') as Record<string, unknown> | undefined;
|
|
155
849
|
if (!existingDocument) {
|
|
156
850
|
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
157
851
|
}
|
|
158
852
|
|
|
159
|
-
const body = await request.json() as
|
|
853
|
+
const body = await request.json() as Record<string, unknown>;
|
|
160
854
|
const { createdAt, updatedAt, ...userData } = body;
|
|
161
855
|
|
|
162
856
|
const dataWithManagedFields = {
|
|
@@ -186,7 +880,7 @@ app.http('todo-update', {
|
|
|
186
880
|
app.http('todo-delete', {
|
|
187
881
|
methods: ['DELETE'],
|
|
188
882
|
route: 'todo/{id}',
|
|
189
|
-
authLevel: '
|
|
883
|
+
authLevel: 'function',
|
|
190
884
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
191
885
|
try {
|
|
192
886
|
const id = request.params.id;
|
|
@@ -210,8 +904,8 @@ app.http('todo-delete', {
|
|
|
210
904
|
context.log(\`Deleted item \${id} from \${containerName}\`);
|
|
211
905
|
|
|
212
906
|
return { status: 204 };
|
|
213
|
-
} catch (error
|
|
214
|
-
if (error.code === 404) {
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if ((error as { code?: number }).code === 404) {
|
|
215
909
|
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
216
910
|
}
|
|
217
911
|
context.error(\`Error deleting item from \${containerName}:\`, error);
|
|
@@ -234,7 +928,7 @@ const containerName = 'Issues';
|
|
|
234
928
|
app.http('issue-get-all', {
|
|
235
929
|
methods: ['GET'],
|
|
236
930
|
route: 'issue',
|
|
237
|
-
authLevel: '
|
|
931
|
+
authLevel: 'function',
|
|
238
932
|
extraInputs: [
|
|
239
933
|
{
|
|
240
934
|
type: 'cosmosDB',
|
|
@@ -247,7 +941,7 @@ app.http('issue-get-all', {
|
|
|
247
941
|
],
|
|
248
942
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
249
943
|
try {
|
|
250
|
-
const documents = context.extraInputs.get('cosmosInput') as
|
|
944
|
+
const documents = context.extraInputs.get('cosmosInput') as unknown[];
|
|
251
945
|
|
|
252
946
|
if (!documents || !Array.isArray(documents)) {
|
|
253
947
|
return { status: 200, jsonBody: [] };
|
|
@@ -268,7 +962,7 @@ app.http('issue-get-all', {
|
|
|
268
962
|
app.http('issue-get-by-id', {
|
|
269
963
|
methods: ['GET'],
|
|
270
964
|
route: 'issue/{id}',
|
|
271
|
-
authLevel: '
|
|
965
|
+
authLevel: 'function',
|
|
272
966
|
extraInputs: [
|
|
273
967
|
{
|
|
274
968
|
type: 'cosmosDB',
|
|
@@ -301,7 +995,7 @@ app.http('issue-get-by-id', {
|
|
|
301
995
|
app.http('issue-create', {
|
|
302
996
|
methods: ['POST'],
|
|
303
997
|
route: 'issue',
|
|
304
|
-
authLevel: '
|
|
998
|
+
authLevel: 'function',
|
|
305
999
|
extraOutputs: [
|
|
306
1000
|
{
|
|
307
1001
|
type: 'cosmosDB',
|
|
@@ -314,7 +1008,7 @@ app.http('issue-create', {
|
|
|
314
1008
|
],
|
|
315
1009
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
316
1010
|
try {
|
|
317
|
-
const body = await request.json() as
|
|
1011
|
+
const body = await request.json() as Record<string, unknown>;
|
|
318
1012
|
|
|
319
1013
|
const { id, createdAt, updatedAt, ...userData } = body;
|
|
320
1014
|
const now = new Date().toISOString();
|
|
@@ -345,7 +1039,7 @@ app.http('issue-create', {
|
|
|
345
1039
|
app.http('issue-update', {
|
|
346
1040
|
methods: ['PUT'],
|
|
347
1041
|
route: 'issue/{id}',
|
|
348
|
-
authLevel: '
|
|
1042
|
+
authLevel: 'function',
|
|
349
1043
|
extraInputs: [
|
|
350
1044
|
{
|
|
351
1045
|
type: 'cosmosDB',
|
|
@@ -373,12 +1067,12 @@ app.http('issue-update', {
|
|
|
373
1067
|
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
374
1068
|
}
|
|
375
1069
|
|
|
376
|
-
const existingDocument = context.extraInputs.get('cosmosInput') as
|
|
1070
|
+
const existingDocument = context.extraInputs.get('cosmosInput') as Record<string, unknown> | undefined;
|
|
377
1071
|
if (!existingDocument) {
|
|
378
1072
|
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
379
1073
|
}
|
|
380
1074
|
|
|
381
|
-
const body = await request.json() as
|
|
1075
|
+
const body = await request.json() as Record<string, unknown>;
|
|
382
1076
|
const { createdAt, updatedAt, ...userData } = body;
|
|
383
1077
|
|
|
384
1078
|
const dataWithManagedFields = {
|
|
@@ -408,7 +1102,7 @@ app.http('issue-update', {
|
|
|
408
1102
|
app.http('issue-delete', {
|
|
409
1103
|
methods: ['DELETE'],
|
|
410
1104
|
route: 'issue/{id}',
|
|
411
|
-
authLevel: '
|
|
1105
|
+
authLevel: 'function',
|
|
412
1106
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
413
1107
|
try {
|
|
414
1108
|
const id = request.params.id;
|
|
@@ -432,8 +1126,8 @@ app.http('issue-delete', {
|
|
|
432
1126
|
context.log(\`Deleted item \${id} from \${containerName}\`);
|
|
433
1127
|
|
|
434
1128
|
return { status: 204 };
|
|
435
|
-
} catch (error
|
|
436
|
-
if (error.code === 404) {
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
if ((error as { code?: number }).code === 404) {
|
|
437
1131
|
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
438
1132
|
}
|
|
439
1133
|
context.error(\`Error deleting item from \${containerName}:\`, error);
|