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