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