swallowkit 1.0.0-beta.2 → 1.0.0-beta.20
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/LICENSE +21 -21
- package/README.ja.md +312 -215
- package/README.md +369 -216
- package/dist/__tests__/fixtures.d.ts +22 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +146 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/cli/commands/add-auth.d.ts +10 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +444 -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 +163 -0
- package/dist/cli/commands/add-connector.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 +21 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +35 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
- package/dist/cli/commands/dev-seeds.js +292 -0
- package/dist/cli/commands/dev-seeds.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +19 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +476 -117
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +13 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2627 -1708
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +3 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +617 -129
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +162 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +8 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +90 -4
- 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/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +171 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +32 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +225 -0
- package/dist/core/operations/runtime.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 +63 -0
- package/dist/core/operations/scaffold-machine.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/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +204 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +38 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1244 -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 +1027 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +7 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +920 -213
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +329 -135
- package/dist/core/scaffold/model-parser.js.map +1 -1
- 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 +314 -182
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +3 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
- package/dist/core/scaffold/openapi-generator.js +190 -0
- package/dist/core/scaffold/openapi-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +10 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +768 -663
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/database/base-model.d.ts +3 -3
- package/dist/database/base-model.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +16 -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 +11 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +34 -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 +156 -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 +184 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +109 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +215 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/package.json +85 -73
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
- package/src/__tests__/auth.test.ts +654 -0
- package/src/__tests__/config.test.ts +263 -0
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +439 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/dev-seeds.test.ts +112 -0
- package/src/__tests__/dev.test.ts +148 -0
- package/src/__tests__/fixtures.ts +144 -0
- package/src/__tests__/functions-generator.test.ts +237 -0
- package/src/__tests__/init.test.ts +80 -0
- package/src/__tests__/machine.test.ts +212 -0
- package/src/__tests__/mcp.test.ts +56 -0
- package/src/__tests__/model-parser.test.ts +72 -0
- package/src/__tests__/nextjs-generator.test.ts +97 -0
- package/src/__tests__/openapi-generator.test.ts +43 -0
- package/src/__tests__/package-manager.test.ts +189 -0
- package/src/__tests__/scaffold.test.ts +39 -0
- package/src/__tests__/string-utils.test.ts +75 -0
- package/src/__tests__/ui-generator.test.ts +144 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +500 -0
- package/src/cli/commands/add-connector.ts +158 -0
- package/src/cli/commands/create-model.ts +62 -0
- package/src/cli/commands/dev-seeds.ts +358 -0
- package/src/cli/commands/dev.ts +962 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/init.ts +3371 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +1211 -0
- package/src/cli/index.ts +191 -0
- package/src/core/config.ts +308 -0
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/create-model.ts +174 -0
- package/src/core/operations/runtime.ts +235 -0
- package/src/core/operations/scaffold-machine.ts +91 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/validation.ts +221 -0
- package/src/core/scaffold/auth-generator.ts +1284 -0
- package/src/core/scaffold/connector-functions-generator.ts +1128 -0
- package/src/core/scaffold/functions-generator.ts +970 -0
- package/src/core/scaffold/model-parser.ts +841 -0
- package/src/core/scaffold/nextjs-generator.ts +370 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +1061 -0
- package/src/database/base-model.ts +184 -0
- package/src/database/client.ts +140 -0
- package/src/database/repository.ts +104 -0
- package/src/database/runtime-check.ts +25 -0
- package/src/index.ts +27 -0
- package/src/machine/contracts.ts +17 -0
- package/src/machine/errors.ts +34 -0
- package/src/machine/index.ts +173 -0
- package/src/mcp/index.ts +185 -0
- package/src/types/index.ts +134 -0
- package/src/utils/package-manager.ts +229 -0
|
@@ -6,227 +6,934 @@
|
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.generateCompactAzureFunctionsCRUD = generateCompactAzureFunctionsCRUD;
|
|
9
|
+
exports.generateCSharpAzureFunctionsCRUD = generateCSharpAzureFunctionsCRUD;
|
|
10
|
+
exports.generatePythonAzureFunctionsCRUD = generatePythonAzureFunctionsCRUD;
|
|
9
11
|
const model_parser_1 = require("./model-parser");
|
|
12
|
+
const auth_generator_1 = require("./auth-generator");
|
|
10
13
|
/**
|
|
11
14
|
* Azure Functions エンティティファイルを生成(インラインハンドラー方式)
|
|
12
15
|
* 各ハンドラーがベタ書きされており、ビジネスロジックの追加・変更が容易
|
|
13
16
|
*/
|
|
14
|
-
function generateCompactAzureFunctionsCRUD(model, sharedPackageName) {
|
|
17
|
+
function generateCompactAzureFunctionsCRUD(model, sharedPackageName, authPolicy) {
|
|
15
18
|
const modelName = model.name;
|
|
16
19
|
const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
|
|
17
|
-
const modelKebab = (0, model_parser_1.toKebabCase)(modelName);
|
|
18
20
|
const schemaName = model.schemaName;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
77
|
-
try {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
if (!
|
|
81
|
-
return { status:
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const validated = ${schemaName}.parse(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
type: 'cosmosDB',
|
|
145
|
-
name: 'cosmosInput',
|
|
146
|
-
databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
|
|
147
|
-
containerName,
|
|
148
|
-
connection: 'CosmosDBConnection',
|
|
149
|
-
id: '{id}',
|
|
150
|
-
partitionKey: '{id}',
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
extraOutputs: [
|
|
154
|
-
{
|
|
155
|
-
type: 'cosmosDB',
|
|
156
|
-
name: 'cosmosOutput',
|
|
157
|
-
databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
|
|
158
|
-
containerName,
|
|
159
|
-
connection: 'CosmosDBConnection',
|
|
160
|
-
},
|
|
161
|
-
],
|
|
162
|
-
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
163
|
-
try {
|
|
164
|
-
const id = request.params.id;
|
|
165
|
-
if (!id) {
|
|
166
|
-
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const existingDocument = context.extraInputs.get('cosmosInput') as any;
|
|
170
|
-
if (!existingDocument) {
|
|
171
|
-
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const body = await request.json() as any;
|
|
175
|
-
const { createdAt, updatedAt, ...userData } = body;
|
|
176
|
-
|
|
177
|
-
const dataWithManagedFields = {
|
|
178
|
-
...userData,
|
|
179
|
-
id,
|
|
180
|
-
createdAt: existingDocument.createdAt,
|
|
181
|
-
updatedAt: new Date().toISOString(),
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const result = ${schemaName}.safeParse(dataWithManagedFields);
|
|
185
|
-
|
|
186
|
-
if (!result.success) {
|
|
187
|
-
context.error('Validation failed:', result.error.issues);
|
|
188
|
-
return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
context.extraOutputs.set('cosmosOutput', result.data);
|
|
192
|
-
return { status: 200, jsonBody: result.data };
|
|
193
|
-
} catch (error) {
|
|
194
|
-
context.error(\`Error updating item in \${containerName}:\`, error);
|
|
195
|
-
return { status: 500, jsonBody: { error: 'Failed to update item' } };
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// DELETE /api/${modelCamel}/{id} - 削除
|
|
201
|
-
app.http('${modelCamel}-delete', {
|
|
202
|
-
methods: ['DELETE'],
|
|
203
|
-
route: '${modelCamel}/{id}',
|
|
204
|
-
authLevel: 'anonymous',
|
|
205
|
-
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
206
|
-
try {
|
|
207
|
-
const id = request.params.id;
|
|
208
|
-
if (!id) {
|
|
209
|
-
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const { CosmosClient } = await import('@azure/cosmos');
|
|
213
|
-
const client = new CosmosClient(process.env.CosmosDBConnection!);
|
|
214
|
-
const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
|
|
215
|
-
const container = database.container(containerName);
|
|
216
|
-
|
|
217
|
-
await container.item(id, id).delete();
|
|
218
|
-
context.log(\`Deleted item \${id} from \${containerName}\`);
|
|
219
|
-
|
|
220
|
-
return { status: 204 };
|
|
221
|
-
} catch (error: any) {
|
|
222
|
-
if (error.code === 404) {
|
|
223
|
-
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
224
|
-
}
|
|
225
|
-
context.error(\`Error deleting item from \${containerName}:\`, error);
|
|
226
|
-
return { status: 500, jsonBody: { error: 'Failed to delete item' } };
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
});
|
|
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 authImport = hasAuth ? `\n${(0, auth_generator_1.generateAuthImportTS)()}\n` : '';
|
|
26
|
+
const readGuard = hasAuth ? `\n${(0, auth_generator_1.generateAuthGuardTS)(authPolicy, 'read')}\n` : '';
|
|
27
|
+
const writeGuard = hasAuth ? `\n${(0, auth_generator_1.generateAuthGuardTS)(authPolicy, 'write')}\n` : '';
|
|
28
|
+
const authCatchBlock = hasAuth
|
|
29
|
+
? ` const authErr = handleAuthError(error);
|
|
30
|
+
if (authErr) return authErr;\n`
|
|
31
|
+
: '';
|
|
32
|
+
// SDK クライアント初期化ヘルパー(PK≠/id の場合、および delete で使用)
|
|
33
|
+
const sdkClientInit = `const { CosmosClient } = await import('@azure/cosmos');
|
|
34
|
+
const endpoint = process.env.CosmosDBConnection__accountEndpoint;
|
|
35
|
+
let client: InstanceType<typeof CosmosClient>;
|
|
36
|
+
if (endpoint) {
|
|
37
|
+
const { DefaultAzureCredential } = await import('@azure/identity');
|
|
38
|
+
client = new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
|
|
39
|
+
} else {
|
|
40
|
+
client = new CosmosClient(process.env.CosmosDBConnection!);
|
|
41
|
+
}
|
|
42
|
+
const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
|
|
43
|
+
const container = database.container(containerName);`;
|
|
44
|
+
// getById ハンドラー: PK=/id の場合は input binding、それ以外は SDK
|
|
45
|
+
const getByIdHandler = isIdPartition
|
|
46
|
+
? generateGetByIdWithBinding(modelCamel, schemaName, readGuard, authCatchBlock)
|
|
47
|
+
: generateGetByIdWithSdk(modelCamel, schemaName, readGuard, authCatchBlock, sdkClientInit);
|
|
48
|
+
// update ハンドラー: PK=/id の場合は input binding、それ以外は SDK
|
|
49
|
+
const updateHandler = isIdPartition
|
|
50
|
+
? generateUpdateWithBinding(modelCamel, schemaName, writeGuard, authCatchBlock)
|
|
51
|
+
: generateUpdateWithSdk(modelCamel, schemaName, partitionKeyField, writeGuard, authCatchBlock, sdkClientInit);
|
|
52
|
+
// delete ハンドラー: 常に SDK(既存パターン)、PK に応じて分岐
|
|
53
|
+
const deleteHandler = isIdPartition
|
|
54
|
+
? generateDeleteIdPartition(modelCamel, writeGuard, authCatchBlock, sdkClientInit)
|
|
55
|
+
: generateDeleteCustomPartition(modelCamel, partitionKeyField, writeGuard, authCatchBlock, sdkClientInit);
|
|
56
|
+
return `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
57
|
+
import { z } from 'zod/v4';
|
|
58
|
+
import crypto from 'crypto';
|
|
59
|
+
import { ${schemaName} } from '${sharedPackageName}';${authImport}
|
|
60
|
+
|
|
61
|
+
const containerName = '${modelName.endsWith('s') ? modelName : modelName + 's'}';
|
|
62
|
+
|
|
63
|
+
// GET /api/${modelCamel} - 全件取得
|
|
64
|
+
app.http('${modelCamel}-get-all', {
|
|
65
|
+
methods: ['GET'],
|
|
66
|
+
route: '${modelCamel}',
|
|
67
|
+
authLevel: 'anonymous',
|
|
68
|
+
extraInputs: [
|
|
69
|
+
{
|
|
70
|
+
type: 'cosmosDB',
|
|
71
|
+
name: 'cosmosInput',
|
|
72
|
+
databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
|
|
73
|
+
containerName,
|
|
74
|
+
connection: 'CosmosDBConnection',
|
|
75
|
+
sqlQuery: 'SELECT * FROM c',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
79
|
+
try {${readGuard}
|
|
80
|
+
const documents = context.extraInputs.get('cosmosInput') as any[];
|
|
81
|
+
|
|
82
|
+
if (!documents || !Array.isArray(documents)) {
|
|
83
|
+
return { status: 200, jsonBody: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const validated = z.array(${schemaName}).parse(documents);
|
|
87
|
+
context.log(\`Fetched \${validated.length} items from \${containerName}\`);
|
|
88
|
+
|
|
89
|
+
return { status: 200, jsonBody: validated };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
${authCatchBlock} context.error(\`Error fetching from \${containerName}:\`, error);
|
|
92
|
+
return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
${getByIdHandler}
|
|
98
|
+
|
|
99
|
+
// POST /api/${modelCamel} - 新規作成
|
|
100
|
+
app.http('${modelCamel}-create', {
|
|
101
|
+
methods: ['POST'],
|
|
102
|
+
route: '${modelCamel}',
|
|
103
|
+
authLevel: 'anonymous',
|
|
104
|
+
extraOutputs: [
|
|
105
|
+
{
|
|
106
|
+
type: 'cosmosDB',
|
|
107
|
+
name: 'cosmosOutput',
|
|
108
|
+
databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
|
|
109
|
+
containerName,
|
|
110
|
+
connection: 'CosmosDBConnection',
|
|
111
|
+
createIfNotExists: true,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
115
|
+
try {${writeGuard}
|
|
116
|
+
const body = await request.json() as any;
|
|
117
|
+
|
|
118
|
+
const { id, createdAt, updatedAt, ...userData } = body;
|
|
119
|
+
const now = new Date().toISOString();
|
|
120
|
+
const dataWithManagedFields = {
|
|
121
|
+
...userData,
|
|
122
|
+
id: id || crypto.randomUUID(),
|
|
123
|
+
createdAt: now,
|
|
124
|
+
updatedAt: now,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = ${schemaName}.safeParse(dataWithManagedFields);
|
|
128
|
+
|
|
129
|
+
if (!result.success) {
|
|
130
|
+
context.error('Validation failed:', result.error.issues);
|
|
131
|
+
return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
context.extraOutputs.set('cosmosOutput', result.data);
|
|
135
|
+
return { status: 201, jsonBody: result.data };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
${authCatchBlock} context.error(\`Error creating item in \${containerName}:\`, error);
|
|
138
|
+
return { status: 500, jsonBody: { error: 'Failed to create item' } };
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
${updateHandler}
|
|
144
|
+
|
|
145
|
+
${deleteHandler}
|
|
230
146
|
`;
|
|
231
147
|
}
|
|
148
|
+
// --- TS getById ハンドラー生成ヘルパー ---
|
|
149
|
+
function generateGetByIdWithBinding(modelCamel, schemaName, readGuard, authCatchBlock) {
|
|
150
|
+
return `// GET /api/${modelCamel}/{id} - ID指定取得
|
|
151
|
+
app.http('${modelCamel}-get-by-id', {
|
|
152
|
+
methods: ['GET'],
|
|
153
|
+
route: '${modelCamel}/{id}',
|
|
154
|
+
authLevel: 'anonymous',
|
|
155
|
+
extraInputs: [
|
|
156
|
+
{
|
|
157
|
+
type: 'cosmosDB',
|
|
158
|
+
name: 'cosmosInput',
|
|
159
|
+
databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
|
|
160
|
+
containerName,
|
|
161
|
+
connection: 'CosmosDBConnection',
|
|
162
|
+
id: '{id}',
|
|
163
|
+
partitionKey: '{id}',
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
167
|
+
try {${readGuard}
|
|
168
|
+
const document = context.extraInputs.get('cosmosInput');
|
|
169
|
+
|
|
170
|
+
if (!document) {
|
|
171
|
+
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const validated = ${schemaName}.parse(document);
|
|
175
|
+
return { status: 200, jsonBody: validated };
|
|
176
|
+
} catch (error) {
|
|
177
|
+
${authCatchBlock} context.error(\`Error fetching item from \${containerName}:\`, error);
|
|
178
|
+
return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
});`;
|
|
182
|
+
}
|
|
183
|
+
function generateGetByIdWithSdk(modelCamel, schemaName, readGuard, authCatchBlock, sdkClientInit) {
|
|
184
|
+
return `// GET /api/${modelCamel}/{id} - ID指定取得 (custom partition key)
|
|
185
|
+
app.http('${modelCamel}-get-by-id', {
|
|
186
|
+
methods: ['GET'],
|
|
187
|
+
route: '${modelCamel}/{id}',
|
|
188
|
+
authLevel: 'anonymous',
|
|
189
|
+
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
190
|
+
try {${readGuard}
|
|
191
|
+
const id = request.params.id;
|
|
192
|
+
if (!id) {
|
|
193
|
+
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
${sdkClientInit}
|
|
197
|
+
|
|
198
|
+
const { resources } = await container.items.query({
|
|
199
|
+
query: 'SELECT * FROM c WHERE c.id = @id',
|
|
200
|
+
parameters: [{ name: '@id', value: id }],
|
|
201
|
+
}).fetchAll();
|
|
202
|
+
|
|
203
|
+
if (!resources || resources.length === 0) {
|
|
204
|
+
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const validated = ${schemaName}.parse(resources[0]);
|
|
208
|
+
return { status: 200, jsonBody: validated };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
${authCatchBlock} context.error(\`Error fetching item from \${containerName}:\`, error);
|
|
211
|
+
return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});`;
|
|
215
|
+
}
|
|
216
|
+
// --- TS update ハンドラー生成ヘルパー ---
|
|
217
|
+
function generateUpdateWithBinding(modelCamel, schemaName, writeGuard, authCatchBlock) {
|
|
218
|
+
return `// PUT /api/${modelCamel}/{id} - 更新
|
|
219
|
+
app.http('${modelCamel}-update', {
|
|
220
|
+
methods: ['PUT'],
|
|
221
|
+
route: '${modelCamel}/{id}',
|
|
222
|
+
authLevel: 'anonymous',
|
|
223
|
+
extraInputs: [
|
|
224
|
+
{
|
|
225
|
+
type: 'cosmosDB',
|
|
226
|
+
name: 'cosmosInput',
|
|
227
|
+
databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
|
|
228
|
+
containerName,
|
|
229
|
+
connection: 'CosmosDBConnection',
|
|
230
|
+
id: '{id}',
|
|
231
|
+
partitionKey: '{id}',
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
extraOutputs: [
|
|
235
|
+
{
|
|
236
|
+
type: 'cosmosDB',
|
|
237
|
+
name: 'cosmosOutput',
|
|
238
|
+
databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
|
|
239
|
+
containerName,
|
|
240
|
+
connection: 'CosmosDBConnection',
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
244
|
+
try {${writeGuard}
|
|
245
|
+
const id = request.params.id;
|
|
246
|
+
if (!id) {
|
|
247
|
+
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const existingDocument = context.extraInputs.get('cosmosInput') as any;
|
|
251
|
+
if (!existingDocument) {
|
|
252
|
+
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const body = await request.json() as any;
|
|
256
|
+
const { createdAt, updatedAt, ...userData } = body;
|
|
257
|
+
|
|
258
|
+
const dataWithManagedFields = {
|
|
259
|
+
...userData,
|
|
260
|
+
id,
|
|
261
|
+
createdAt: existingDocument.createdAt,
|
|
262
|
+
updatedAt: new Date().toISOString(),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const result = ${schemaName}.safeParse(dataWithManagedFields);
|
|
266
|
+
|
|
267
|
+
if (!result.success) {
|
|
268
|
+
context.error('Validation failed:', result.error.issues);
|
|
269
|
+
return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
context.extraOutputs.set('cosmosOutput', result.data);
|
|
273
|
+
return { status: 200, jsonBody: result.data };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
${authCatchBlock} context.error(\`Error updating item in \${containerName}:\`, error);
|
|
276
|
+
return { status: 500, jsonBody: { error: 'Failed to update item' } };
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
});`;
|
|
280
|
+
}
|
|
281
|
+
function generateUpdateWithSdk(modelCamel, schemaName, partitionKeyField, writeGuard, authCatchBlock, sdkClientInit) {
|
|
282
|
+
return `// PUT /api/${modelCamel}/{id} - 更新 (custom partition key)
|
|
283
|
+
app.http('${modelCamel}-update', {
|
|
284
|
+
methods: ['PUT'],
|
|
285
|
+
route: '${modelCamel}/{id}',
|
|
286
|
+
authLevel: 'anonymous',
|
|
287
|
+
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
288
|
+
try {${writeGuard}
|
|
289
|
+
const id = request.params.id;
|
|
290
|
+
if (!id) {
|
|
291
|
+
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
${sdkClientInit}
|
|
295
|
+
|
|
296
|
+
const { resources } = await container.items.query({
|
|
297
|
+
query: 'SELECT * FROM c WHERE c.id = @id',
|
|
298
|
+
parameters: [{ name: '@id', value: id }],
|
|
299
|
+
}).fetchAll();
|
|
300
|
+
|
|
301
|
+
if (!resources || resources.length === 0) {
|
|
302
|
+
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const existingDocument = resources[0];
|
|
306
|
+
const body = await request.json() as any;
|
|
307
|
+
const { createdAt, updatedAt, ...userData } = body;
|
|
308
|
+
|
|
309
|
+
const dataWithManagedFields = {
|
|
310
|
+
...userData,
|
|
311
|
+
id,
|
|
312
|
+
createdAt: existingDocument.createdAt,
|
|
313
|
+
updatedAt: new Date().toISOString(),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const result = ${schemaName}.safeParse(dataWithManagedFields);
|
|
317
|
+
|
|
318
|
+
if (!result.success) {
|
|
319
|
+
context.error('Validation failed:', result.error.issues);
|
|
320
|
+
return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const pkValue = result.data.${partitionKeyField};
|
|
324
|
+
await container.items.upsert(result.data);
|
|
325
|
+
return { status: 200, jsonBody: result.data };
|
|
326
|
+
} catch (error) {
|
|
327
|
+
${authCatchBlock} context.error(\`Error updating item in \${containerName}:\`, error);
|
|
328
|
+
return { status: 500, jsonBody: { error: 'Failed to update item' } };
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
});`;
|
|
332
|
+
}
|
|
333
|
+
// --- TS delete ハンドラー生成ヘルパー ---
|
|
334
|
+
function generateDeleteIdPartition(modelCamel, writeGuard, authCatchBlock, sdkClientInit) {
|
|
335
|
+
return `// DELETE /api/${modelCamel}/{id} - 削除
|
|
336
|
+
app.http('${modelCamel}-delete', {
|
|
337
|
+
methods: ['DELETE'],
|
|
338
|
+
route: '${modelCamel}/{id}',
|
|
339
|
+
authLevel: 'anonymous',
|
|
340
|
+
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
341
|
+
try {${writeGuard}
|
|
342
|
+
const id = request.params.id;
|
|
343
|
+
if (!id) {
|
|
344
|
+
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
${sdkClientInit}
|
|
348
|
+
|
|
349
|
+
await container.item(id, id).delete();
|
|
350
|
+
context.log(\`Deleted item \${id} from \${containerName}\`);
|
|
351
|
+
|
|
352
|
+
return { status: 204 };
|
|
353
|
+
} catch (error: any) {
|
|
354
|
+
if (error.code === 404) {
|
|
355
|
+
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
356
|
+
}
|
|
357
|
+
${authCatchBlock} context.error(\`Error deleting item from \${containerName}:\`, error);
|
|
358
|
+
return { status: 500, jsonBody: { error: 'Failed to delete item' } };
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
});`;
|
|
362
|
+
}
|
|
363
|
+
function generateDeleteCustomPartition(modelCamel, partitionKeyField, writeGuard, authCatchBlock, sdkClientInit) {
|
|
364
|
+
return `// DELETE /api/${modelCamel}/{id} - 削除 (custom partition key)
|
|
365
|
+
app.http('${modelCamel}-delete', {
|
|
366
|
+
methods: ['DELETE'],
|
|
367
|
+
route: '${modelCamel}/{id}',
|
|
368
|
+
authLevel: 'anonymous',
|
|
369
|
+
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
370
|
+
try {${writeGuard}
|
|
371
|
+
const id = request.params.id;
|
|
372
|
+
if (!id) {
|
|
373
|
+
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
${sdkClientInit}
|
|
377
|
+
|
|
378
|
+
// パーティションキー値を取得するためにドキュメントを読み取り
|
|
379
|
+
const { resources } = await container.items.query({
|
|
380
|
+
query: 'SELECT * FROM c WHERE c.id = @id',
|
|
381
|
+
parameters: [{ name: '@id', value: id }],
|
|
382
|
+
}).fetchAll();
|
|
383
|
+
|
|
384
|
+
if (!resources || resources.length === 0) {
|
|
385
|
+
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const pkValue = resources[0].${partitionKeyField};
|
|
389
|
+
await container.item(id, pkValue).delete();
|
|
390
|
+
context.log(\`Deleted item \${id} from \${containerName}\`);
|
|
391
|
+
|
|
392
|
+
return { status: 204 };
|
|
393
|
+
} catch (error: any) {
|
|
394
|
+
if (error.code === 404) {
|
|
395
|
+
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
396
|
+
}
|
|
397
|
+
${authCatchBlock} context.error(\`Error deleting item from \${containerName}:\`, error);
|
|
398
|
+
return { status: 500, jsonBody: { error: 'Failed to delete item' } };
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
});`;
|
|
402
|
+
}
|
|
403
|
+
function generateCSharpAzureFunctionsCRUD(model, authPolicy) {
|
|
404
|
+
const modelName = model.name;
|
|
405
|
+
const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
|
|
406
|
+
const className = `${modelName}Functions`;
|
|
407
|
+
const containerName = modelName.endsWith('s') ? modelName : `${modelName}s`;
|
|
408
|
+
const partitionKeyPath = model.partitionKey;
|
|
409
|
+
const partitionKeyField = partitionKeyPath.slice(1);
|
|
410
|
+
const isIdPartition = partitionKeyField === 'id';
|
|
411
|
+
const hasAuth = !!authPolicy;
|
|
412
|
+
const authUsing = hasAuth ? 'using Functions.Auth;\n' : '';
|
|
413
|
+
const readGuard = hasAuth ? `\n${(0, auth_generator_1.generateAuthGuardCSharp)(authPolicy, 'read')}\n` : '';
|
|
414
|
+
const writeGuard = hasAuth ? `\n${(0, auth_generator_1.generateAuthGuardCSharp)(authPolicy, 'write')}\n` : '';
|
|
415
|
+
// PK値の取得ロジック(create 用)
|
|
416
|
+
const createPkExpr = isIdPartition
|
|
417
|
+
? 'id'
|
|
418
|
+
: `payload["${partitionKeyField}"]?.GetValue<string>() ?? throw new InvalidOperationException("Partition key field '${partitionKeyField}' is required.")`;
|
|
419
|
+
// ReadCosmosItemAsync: PK=/id の場合は直接読み取り、それ以外はクエリ
|
|
420
|
+
const readItemMethod = isIdPartition
|
|
421
|
+
? ` private static async Task<JsonObject> ReadCosmosItemAsync(Container container, string id)
|
|
422
|
+
{
|
|
423
|
+
var response = await container.ReadItemStreamAsync(id, new PartitionKey(id));
|
|
424
|
+
if (response.StatusCode == HttpStatusCode.NotFound)
|
|
425
|
+
{
|
|
426
|
+
throw new CosmosException("Item not found", HttpStatusCode.NotFound, 0, response.Headers.ActivityId, response.Headers.RequestCharge);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!response.IsSuccessStatusCode)
|
|
430
|
+
{
|
|
431
|
+
throw new InvalidOperationException($"Cosmos read failed with status {(int)response.StatusCode}.");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
using var document = await JsonDocument.ParseAsync(response.Content);
|
|
435
|
+
return JsonNode.Parse(document.RootElement.GetRawText())?.AsObject()
|
|
436
|
+
?? throw new JsonException("Cosmos item payload must be a JSON object.");
|
|
437
|
+
}`
|
|
438
|
+
: ` private static async Task<JsonObject> ReadCosmosItemAsync(Container container, string id)
|
|
439
|
+
{
|
|
440
|
+
var query = new QueryDefinition("SELECT * FROM c WHERE c.id = @id")
|
|
441
|
+
.WithParameter("@id", id);
|
|
442
|
+
using var iterator = container.GetItemQueryStreamIterator(query);
|
|
443
|
+
|
|
444
|
+
while (iterator.HasMoreResults)
|
|
445
|
+
{
|
|
446
|
+
var page = await iterator.ReadNextAsync();
|
|
447
|
+
if (!page.IsSuccessStatusCode)
|
|
448
|
+
{
|
|
449
|
+
throw new InvalidOperationException($"Cosmos query failed with status {(int)page.StatusCode}.");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
using var document = await JsonDocument.ParseAsync(page.Content);
|
|
453
|
+
if (document.RootElement.TryGetProperty("Documents", out var documents))
|
|
454
|
+
{
|
|
455
|
+
foreach (var item in documents.EnumerateArray())
|
|
456
|
+
{
|
|
457
|
+
return JsonNode.Parse(item.GetRawText())?.AsObject()
|
|
458
|
+
?? throw new JsonException("Cosmos item payload must be a JSON object.");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
throw new CosmosException("Item not found", HttpStatusCode.NotFound, 0, "", 0);
|
|
464
|
+
}`;
|
|
465
|
+
// Replace / Delete の PK 解決ロジック
|
|
466
|
+
const replacePkExpr = isIdPartition
|
|
467
|
+
? 'new PartitionKey(id)'
|
|
468
|
+
: `new PartitionKey(payload["${partitionKeyField}"]?.GetValue<string>())`;
|
|
469
|
+
const deleteLogic = isIdPartition
|
|
470
|
+
? ` await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(id));`
|
|
471
|
+
: ` var existing = await ReadCosmosItemAsync(container, id);
|
|
472
|
+
var pkValue = existing["${partitionKeyField}"]?.GetValue<string>();
|
|
473
|
+
await container.DeleteItemAsync<JsonObject>(id, new PartitionKey(pkValue));`;
|
|
474
|
+
return `using System.Net;
|
|
475
|
+
using System.Text;
|
|
476
|
+
using System.Text.Json;
|
|
477
|
+
using System.Text.Json.Nodes;
|
|
478
|
+
using Azure.Identity;
|
|
479
|
+
using Microsoft.Azure.Cosmos;
|
|
480
|
+
using Microsoft.Azure.Functions.Worker;
|
|
481
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
482
|
+
using Microsoft.Extensions.Logging;
|
|
483
|
+
${authUsing}
|
|
484
|
+
namespace SwallowKit.Functions;
|
|
485
|
+
|
|
486
|
+
public sealed class ${className}
|
|
487
|
+
{
|
|
488
|
+
private readonly ILogger<${className}> _logger;
|
|
489
|
+
private static readonly string ContainerName = "${containerName}";
|
|
490
|
+
|
|
491
|
+
public ${className}(ILogger<${className}> logger)
|
|
492
|
+
{
|
|
493
|
+
_logger = logger;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private static string DatabaseName => Environment.GetEnvironmentVariable("COSMOS_DB_DATABASE_NAME") ?? "AppDatabase";
|
|
497
|
+
|
|
498
|
+
private static bool IsLocalCosmosEndpoint(string endpoint) =>
|
|
499
|
+
endpoint.Contains("localhost:8081", StringComparison.OrdinalIgnoreCase) ||
|
|
500
|
+
endpoint.Contains("127.0.0.1:8081", StringComparison.OrdinalIgnoreCase);
|
|
501
|
+
|
|
502
|
+
private static CosmosClient CreateCosmosClient()
|
|
503
|
+
{
|
|
504
|
+
var endpoint = Environment.GetEnvironmentVariable("CosmosDBConnection__accountEndpoint");
|
|
505
|
+
if (!string.IsNullOrWhiteSpace(endpoint))
|
|
506
|
+
{
|
|
507
|
+
var options = IsLocalCosmosEndpoint(endpoint)
|
|
508
|
+
? new CosmosClientOptions { ConnectionMode = ConnectionMode.Gateway }
|
|
509
|
+
: new CosmosClientOptions();
|
|
510
|
+
return new CosmosClient(endpoint, new DefaultAzureCredential(), options);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
var connectionString = Environment.GetEnvironmentVariable("CosmosDBConnection");
|
|
514
|
+
if (string.IsNullOrWhiteSpace(connectionString))
|
|
515
|
+
{
|
|
516
|
+
throw new InvalidOperationException("Cosmos DB connection is not configured.");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
var connectionOptions = IsLocalCosmosEndpoint(connectionString)
|
|
520
|
+
? new CosmosClientOptions { ConnectionMode = ConnectionMode.Gateway }
|
|
521
|
+
: new CosmosClientOptions();
|
|
522
|
+
|
|
523
|
+
return new CosmosClient(connectionString, connectionOptions);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private static Container GetContainer(CosmosClient client) => client.GetContainer(DatabaseName, ContainerName);
|
|
527
|
+
|
|
528
|
+
private static async Task<JsonObject> ReadRequestBodyAsync(HttpRequestData request)
|
|
529
|
+
{
|
|
530
|
+
using var reader = new StreamReader(request.Body, Encoding.UTF8);
|
|
531
|
+
var raw = await reader.ReadToEndAsync();
|
|
532
|
+
var node = JsonNode.Parse(raw) as JsonObject;
|
|
533
|
+
|
|
534
|
+
if (node is null)
|
|
535
|
+
{
|
|
536
|
+
throw new JsonException("Request body must be a JSON object.");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return node;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private static JsonObject BuildManagedDocument(JsonObject source, string id, string createdAt, string updatedAt)
|
|
543
|
+
{
|
|
544
|
+
var payload = new JsonObject();
|
|
545
|
+
|
|
546
|
+
foreach (var entry in source)
|
|
547
|
+
{
|
|
548
|
+
if (entry.Key is "id" or "createdAt" or "updatedAt")
|
|
549
|
+
{
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
payload[entry.Key] = entry.Value?.DeepClone();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
payload["id"] = id;
|
|
557
|
+
payload["createdAt"] = createdAt;
|
|
558
|
+
payload["updatedAt"] = updatedAt;
|
|
559
|
+
|
|
560
|
+
return payload;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private static Stream CreateJsonStream(JsonObject payload) =>
|
|
564
|
+
new MemoryStream(Encoding.UTF8.GetBytes(payload.ToJsonString()));
|
|
565
|
+
|
|
566
|
+
${readItemMethod}
|
|
567
|
+
|
|
568
|
+
private static async Task<HttpResponseData> WriteJsonAsync(HttpRequestData request, HttpStatusCode status, object payload)
|
|
569
|
+
{
|
|
570
|
+
var response = request.CreateResponse(status);
|
|
571
|
+
await response.WriteAsJsonAsync(payload);
|
|
572
|
+
return response;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
[Function("${modelCamel}GetAll")]
|
|
576
|
+
public async Task<HttpResponseData> GetAll(
|
|
577
|
+
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}")] HttpRequestData request)
|
|
578
|
+
{
|
|
579
|
+
try
|
|
580
|
+
{${readGuard}
|
|
581
|
+
using var client = CreateCosmosClient();
|
|
582
|
+
var container = GetContainer(client);
|
|
583
|
+
using var iterator = container.GetItemQueryStreamIterator("SELECT * FROM c");
|
|
584
|
+
var items = new List<JsonElement>();
|
|
585
|
+
|
|
586
|
+
while (iterator.HasMoreResults)
|
|
587
|
+
{
|
|
588
|
+
var page = await iterator.ReadNextAsync();
|
|
589
|
+
if (!page.IsSuccessStatusCode)
|
|
590
|
+
{
|
|
591
|
+
throw new InvalidOperationException($"Cosmos query failed with status {(int)page.StatusCode}.");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
using var document = await JsonDocument.ParseAsync(page.Content);
|
|
595
|
+
if (!document.RootElement.TryGetProperty("Documents", out var documents))
|
|
596
|
+
{
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
foreach (var item in documents.EnumerateArray())
|
|
601
|
+
{
|
|
602
|
+
items.Add(item.Clone());
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
_logger.LogInformation("Fetched {Count} ${modelName} item(s) from {Container}.", items.Count, ContainerName);
|
|
607
|
+
return await WriteJsonAsync(request, HttpStatusCode.OK, items);
|
|
608
|
+
}
|
|
609
|
+
catch (Exception ex)
|
|
610
|
+
{
|
|
611
|
+
_logger.LogError(ex, "Failed to fetch ${modelName} items from Cosmos DB.");
|
|
612
|
+
return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch items" });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
[Function("${modelCamel}GetById")]
|
|
617
|
+
public async Task<HttpResponseData> GetById(
|
|
618
|
+
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
|
|
619
|
+
string id)
|
|
620
|
+
{
|
|
621
|
+
try
|
|
622
|
+
{${readGuard}
|
|
623
|
+
using var client = CreateCosmosClient();
|
|
624
|
+
var container = GetContainer(client);
|
|
625
|
+
var item = await ReadCosmosItemAsync(container, id);
|
|
626
|
+
return await WriteJsonAsync(request, HttpStatusCode.OK, item);
|
|
627
|
+
}
|
|
628
|
+
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
|
|
629
|
+
{
|
|
630
|
+
return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
|
|
631
|
+
}
|
|
632
|
+
catch (Exception ex)
|
|
633
|
+
{
|
|
634
|
+
_logger.LogError(ex, "Failed to fetch ${modelName} item {Id} from Cosmos DB.", id);
|
|
635
|
+
return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch item", id });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
[Function("${modelCamel}Create")]
|
|
640
|
+
public async Task<HttpResponseData> Create(
|
|
641
|
+
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "${modelCamel}")] HttpRequestData request)
|
|
642
|
+
{
|
|
643
|
+
try
|
|
644
|
+
{${writeGuard}
|
|
645
|
+
var body = await ReadRequestBodyAsync(request);
|
|
646
|
+
var now = DateTimeOffset.UtcNow.ToString("O");
|
|
647
|
+
var id = body["id"]?.GetValue<string>() ?? Guid.NewGuid().ToString();
|
|
648
|
+
var payload = BuildManagedDocument(body, id, now, now);
|
|
649
|
+
|
|
650
|
+
using var client = CreateCosmosClient();
|
|
651
|
+
var container = GetContainer(client);
|
|
652
|
+
using var stream = CreateJsonStream(payload);
|
|
653
|
+
var pkValue = ${createPkExpr};
|
|
654
|
+
var response = await container.CreateItemStreamAsync(stream, new PartitionKey(pkValue));
|
|
655
|
+
if (!response.IsSuccessStatusCode)
|
|
656
|
+
{
|
|
657
|
+
throw new InvalidOperationException($"Cosmos create failed with status {(int)response.StatusCode}.");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return await WriteJsonAsync(request, HttpStatusCode.Created, payload);
|
|
661
|
+
}
|
|
662
|
+
catch (JsonException ex)
|
|
663
|
+
{
|
|
664
|
+
_logger.LogWarning(ex, "Invalid ${modelName} create payload.");
|
|
665
|
+
return await WriteJsonAsync(request, HttpStatusCode.BadRequest, new { error = "Request body must be a JSON object." });
|
|
666
|
+
}
|
|
667
|
+
catch (Exception ex)
|
|
668
|
+
{
|
|
669
|
+
_logger.LogError(ex, "Failed to create ${modelName} item in Cosmos DB.");
|
|
670
|
+
return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to create item" });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
[Function("${modelCamel}Update")]
|
|
675
|
+
public async Task<HttpResponseData> Update(
|
|
676
|
+
[HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
|
|
677
|
+
string id)
|
|
678
|
+
{
|
|
679
|
+
try
|
|
680
|
+
{${writeGuard}
|
|
681
|
+
using var client = CreateCosmosClient();
|
|
682
|
+
var container = GetContainer(client);
|
|
683
|
+
|
|
684
|
+
JsonObject existing;
|
|
685
|
+
try
|
|
686
|
+
{
|
|
687
|
+
existing = await ReadCosmosItemAsync(container, id);
|
|
688
|
+
}
|
|
689
|
+
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
|
|
690
|
+
{
|
|
691
|
+
return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
var body = await ReadRequestBodyAsync(request);
|
|
695
|
+
var createdAt = existing["createdAt"]?.GetValue<string>() ?? DateTimeOffset.UtcNow.ToString("O");
|
|
696
|
+
var payload = BuildManagedDocument(body, id, createdAt, DateTimeOffset.UtcNow.ToString("O"));
|
|
697
|
+
|
|
698
|
+
using var stream = CreateJsonStream(payload);
|
|
699
|
+
var response = await container.ReplaceItemStreamAsync(stream, id, ${replacePkExpr});
|
|
700
|
+
if (!response.IsSuccessStatusCode)
|
|
701
|
+
{
|
|
702
|
+
throw new InvalidOperationException($"Cosmos replace failed with status {(int)response.StatusCode}.");
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return await WriteJsonAsync(request, HttpStatusCode.OK, payload);
|
|
706
|
+
}
|
|
707
|
+
catch (JsonException ex)
|
|
708
|
+
{
|
|
709
|
+
_logger.LogWarning(ex, "Invalid ${modelName} update payload for {Id}.", id);
|
|
710
|
+
return await WriteJsonAsync(request, HttpStatusCode.BadRequest, new { error = "Request body must be a JSON object.", id });
|
|
711
|
+
}
|
|
712
|
+
catch (Exception ex)
|
|
713
|
+
{
|
|
714
|
+
_logger.LogError(ex, "Failed to update ${modelName} item {Id} in Cosmos DB.", id);
|
|
715
|
+
return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to update item", id });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
[Function("${modelCamel}Delete")]
|
|
720
|
+
public async Task<HttpResponseData> Delete(
|
|
721
|
+
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
|
|
722
|
+
string id)
|
|
723
|
+
{
|
|
724
|
+
try
|
|
725
|
+
{${writeGuard}
|
|
726
|
+
using var client = CreateCosmosClient();
|
|
727
|
+
var container = GetContainer(client);
|
|
728
|
+
${deleteLogic}
|
|
729
|
+
|
|
730
|
+
return request.CreateResponse(HttpStatusCode.NoContent);
|
|
731
|
+
}
|
|
732
|
+
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
|
|
733
|
+
{
|
|
734
|
+
return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
|
|
735
|
+
}
|
|
736
|
+
catch (Exception ex)
|
|
737
|
+
{
|
|
738
|
+
_logger.LogError(ex, "Failed to delete ${modelName} item {Id} from Cosmos DB.", id);
|
|
739
|
+
return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to delete item", id });
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
`;
|
|
744
|
+
}
|
|
745
|
+
function generatePythonAzureFunctionsCRUD(model, authPolicy) {
|
|
746
|
+
const modelName = model.name;
|
|
747
|
+
const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
|
|
748
|
+
const modelSnake = (0, model_parser_1.toKebabCase)(modelName).replace(/-/g, "_");
|
|
749
|
+
const containerName = modelName.endsWith('s') ? modelName : `${modelName}s`;
|
|
750
|
+
const partitionKeyPath = model.partitionKey;
|
|
751
|
+
const partitionKeyField = partitionKeyPath.slice(1);
|
|
752
|
+
const isIdPartition = partitionKeyField === 'id';
|
|
753
|
+
const hasAuth = !!authPolicy;
|
|
754
|
+
const authImport = hasAuth ? '\nfrom auth.jwt_helper import require_auth, require_roles, handle_auth_error\n' : '';
|
|
755
|
+
// generateAuthGuardPython outputs at 4-space indent; inside try: we need 8-space
|
|
756
|
+
const readGuardRaw = hasAuth ? (0, auth_generator_1.generateAuthGuardPython)(authPolicy, 'read') : '';
|
|
757
|
+
const writeGuardRaw = hasAuth ? (0, auth_generator_1.generateAuthGuardPython)(authPolicy, 'write') : '';
|
|
758
|
+
const readGuard = hasAuth ? '\n' + readGuardRaw.split('\n').map(l => ' ' + l).join('\n') : '';
|
|
759
|
+
const writeGuard = hasAuth ? '\n' + writeGuardRaw.split('\n').map(l => ' ' + l).join('\n') : '';
|
|
760
|
+
const authCatch = hasAuth ? `\n auth_err = handle_auth_error(exc)\n if auth_err:\n return auth_err` : '';
|
|
761
|
+
// getById / update の読み取りロジック
|
|
762
|
+
const getByIdBody = isIdPartition
|
|
763
|
+
? ` container = _get_container()
|
|
764
|
+
item = container.read_item(item=item_id, partition_key=item_id)
|
|
765
|
+
return _json_response(item, 200)`
|
|
766
|
+
: ` container = _get_container()
|
|
767
|
+
items = list(container.query_items(
|
|
768
|
+
query="SELECT * FROM c WHERE c.id = @id",
|
|
769
|
+
parameters=[{"name": "@id", "value": item_id}],
|
|
770
|
+
enable_cross_partition_query=True,
|
|
771
|
+
))
|
|
772
|
+
if not items:
|
|
773
|
+
return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
|
|
774
|
+
return _json_response(items[0], 200)`;
|
|
775
|
+
const updateBody = isIdPartition
|
|
776
|
+
? ` container = _get_container()
|
|
777
|
+
existing = container.read_item(item=item_id, partition_key=item_id)
|
|
778
|
+
body = req.get_json()
|
|
779
|
+
payload = _build_managed_document(
|
|
780
|
+
body,
|
|
781
|
+
item_id,
|
|
782
|
+
existing.get("createdAt") or datetime.now(timezone.utc).isoformat(),
|
|
783
|
+
datetime.now(timezone.utc).isoformat(),
|
|
784
|
+
)
|
|
785
|
+
container.replace_item(item=item_id, body=payload)
|
|
786
|
+
return _json_response(payload, 200)`
|
|
787
|
+
: ` container = _get_container()
|
|
788
|
+
items = list(container.query_items(
|
|
789
|
+
query="SELECT * FROM c WHERE c.id = @id",
|
|
790
|
+
parameters=[{"name": "@id", "value": item_id}],
|
|
791
|
+
enable_cross_partition_query=True,
|
|
792
|
+
))
|
|
793
|
+
if not items:
|
|
794
|
+
return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
|
|
795
|
+
existing = items[0]
|
|
796
|
+
body = req.get_json()
|
|
797
|
+
payload = _build_managed_document(
|
|
798
|
+
body,
|
|
799
|
+
item_id,
|
|
800
|
+
existing.get("createdAt") or datetime.now(timezone.utc).isoformat(),
|
|
801
|
+
datetime.now(timezone.utc).isoformat(),
|
|
802
|
+
)
|
|
803
|
+
container.replace_item(item=existing, body=payload)
|
|
804
|
+
return _json_response(payload, 200)`;
|
|
805
|
+
const deleteBody = isIdPartition
|
|
806
|
+
? ` container = _get_container()
|
|
807
|
+
container.delete_item(item=item_id, partition_key=item_id)
|
|
808
|
+
return func.HttpResponse(status_code=204)`
|
|
809
|
+
: ` container = _get_container()
|
|
810
|
+
items = list(container.query_items(
|
|
811
|
+
query="SELECT * FROM c WHERE c.id = @id",
|
|
812
|
+
parameters=[{"name": "@id", "value": item_id}],
|
|
813
|
+
enable_cross_partition_query=True,
|
|
814
|
+
))
|
|
815
|
+
if not items:
|
|
816
|
+
return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
|
|
817
|
+
pk_value = items[0].get("${partitionKeyField}")
|
|
818
|
+
container.delete_item(item=item_id, partition_key=pk_value)
|
|
819
|
+
return func.HttpResponse(status_code=204)`;
|
|
820
|
+
return {
|
|
821
|
+
registration: `from blueprints.${modelSnake} import bp as ${modelSnake}_bp\napp.register_blueprint(${modelSnake}_bp)`,
|
|
822
|
+
blueprint: `import json
|
|
823
|
+
from datetime import datetime, timezone
|
|
824
|
+
from typing import Any
|
|
825
|
+
from uuid import uuid4
|
|
826
|
+
import os
|
|
827
|
+
|
|
828
|
+
import azure.functions as func
|
|
829
|
+
from azure.cosmos import CosmosClient, exceptions
|
|
830
|
+
from azure.identity import DefaultAzureCredential
|
|
831
|
+
${authImport}
|
|
832
|
+
bp = func.Blueprint()
|
|
833
|
+
CONTAINER_NAME = "${containerName}"
|
|
834
|
+
DATABASE_NAME = os.environ.get("COSMOS_DB_DATABASE_NAME", "AppDatabase")
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def _json_response(payload: Any, status_code: int) -> func.HttpResponse:
|
|
838
|
+
return func.HttpResponse(
|
|
839
|
+
body=json.dumps(payload, ensure_ascii=False),
|
|
840
|
+
status_code=status_code,
|
|
841
|
+
mimetype="application/json",
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _get_container():
|
|
846
|
+
endpoint = os.environ.get("CosmosDBConnection__accountEndpoint")
|
|
847
|
+
if endpoint:
|
|
848
|
+
client = CosmosClient(endpoint, credential=DefaultAzureCredential())
|
|
849
|
+
else:
|
|
850
|
+
connection_string = os.environ.get("CosmosDBConnection")
|
|
851
|
+
if not connection_string:
|
|
852
|
+
raise RuntimeError("Cosmos DB connection is not configured.")
|
|
853
|
+
client = CosmosClient.from_connection_string(connection_string)
|
|
854
|
+
|
|
855
|
+
database = client.get_database_client(DATABASE_NAME)
|
|
856
|
+
return database.get_container_client(CONTAINER_NAME)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _build_managed_document(source: dict[str, Any], item_id: str, created_at: str, updated_at: str) -> dict[str, Any]:
|
|
860
|
+
payload = {
|
|
861
|
+
key: value
|
|
862
|
+
for key, value in source.items()
|
|
863
|
+
if key not in {"id", "createdAt", "updatedAt"}
|
|
864
|
+
}
|
|
865
|
+
payload["id"] = item_id
|
|
866
|
+
payload["createdAt"] = created_at
|
|
867
|
+
payload["updatedAt"] = updated_at
|
|
868
|
+
return payload
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@bp.route(route="${modelCamel}", methods=["GET"])
|
|
872
|
+
def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
|
|
873
|
+
try:${readGuard}
|
|
874
|
+
container = _get_container()
|
|
875
|
+
items = list(
|
|
876
|
+
container.query_items(
|
|
877
|
+
query="SELECT * FROM c",
|
|
878
|
+
enable_cross_partition_query=True,
|
|
879
|
+
)
|
|
880
|
+
)
|
|
881
|
+
return _json_response(items, 200)
|
|
882
|
+
except Exception as exc:${authCatch}
|
|
883
|
+
return _json_response({"error": "Failed to fetch items", "details": str(exc)}, 500)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
@bp.route(route="${modelCamel}/{id}", methods=["GET"])
|
|
887
|
+
def ${modelSnake}_get_by_id(req: func.HttpRequest) -> func.HttpResponse:
|
|
888
|
+
item_id = req.route_params.get("id")
|
|
889
|
+
try:${readGuard}
|
|
890
|
+
${getByIdBody}
|
|
891
|
+
except exceptions.CosmosResourceNotFoundError:
|
|
892
|
+
return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
|
|
893
|
+
except Exception as exc:${authCatch}
|
|
894
|
+
return _json_response({"error": "Failed to fetch item", "id": item_id, "details": str(exc)}, 500)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
@bp.route(route="${modelCamel}", methods=["POST"])
|
|
898
|
+
def ${modelSnake}_create(req: func.HttpRequest) -> func.HttpResponse:
|
|
899
|
+
try:${writeGuard}
|
|
900
|
+
body = req.get_json()
|
|
901
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
902
|
+
item_id = body.get("id") or str(uuid4())
|
|
903
|
+
payload = _build_managed_document(body, item_id, now, now)
|
|
904
|
+
|
|
905
|
+
container = _get_container()
|
|
906
|
+
container.create_item(payload)
|
|
907
|
+
return _json_response(payload, 201)
|
|
908
|
+
except ValueError:
|
|
909
|
+
return _json_response({"error": "Request body must be a JSON object."}, 400)
|
|
910
|
+
except Exception as exc:${authCatch}
|
|
911
|
+
return _json_response({"error": "Failed to create item", "details": str(exc)}, 500)
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
@bp.route(route="${modelCamel}/{id}", methods=["PUT"])
|
|
915
|
+
def ${modelSnake}_update(req: func.HttpRequest) -> func.HttpResponse:
|
|
916
|
+
item_id = req.route_params.get("id")
|
|
917
|
+
try:${writeGuard}
|
|
918
|
+
${updateBody}
|
|
919
|
+
except exceptions.CosmosResourceNotFoundError:
|
|
920
|
+
return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
|
|
921
|
+
except ValueError:
|
|
922
|
+
return _json_response({"error": "Request body must be a JSON object.", "id": item_id}, 400)
|
|
923
|
+
except Exception as exc:${authCatch}
|
|
924
|
+
return _json_response({"error": "Failed to update item", "id": item_id, "details": str(exc)}, 500)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
@bp.route(route="${modelCamel}/{id}", methods=["DELETE"])
|
|
928
|
+
def ${modelSnake}_delete(req: func.HttpRequest) -> func.HttpResponse:
|
|
929
|
+
item_id = req.route_params.get("id")
|
|
930
|
+
try:${writeGuard}
|
|
931
|
+
${deleteBody}
|
|
932
|
+
except exceptions.CosmosResourceNotFoundError:
|
|
933
|
+
return _json_response({"error": "${modelName} not found", "id": item_id}, 404)
|
|
934
|
+
except Exception as exc:${authCatch}
|
|
935
|
+
return _json_response({"error": "Failed to delete item", "id": item_id, "details": str(exc)}, 500)
|
|
936
|
+
`,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
232
939
|
//# sourceMappingURL=functions-generator.js.map
|