swallowkit 1.0.0-beta.11 → 1.0.0-beta.13

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.
Files changed (68) hide show
  1. package/README.ja.md +33 -9
  2. package/README.md +33 -9
  3. package/dist/__tests__/fixtures.d.ts +8 -0
  4. package/dist/__tests__/fixtures.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures.js +60 -0
  6. package/dist/__tests__/fixtures.js.map +1 -1
  7. package/dist/cli/commands/add-connector.d.ts +20 -0
  8. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  9. package/dist/cli/commands/add-connector.js +161 -0
  10. package/dist/cli/commands/add-connector.js.map +1 -0
  11. package/dist/cli/commands/create-model.d.ts +1 -0
  12. package/dist/cli/commands/create-model.d.ts.map +1 -1
  13. package/dist/cli/commands/create-model.js +65 -1
  14. package/dist/cli/commands/create-model.js.map +1 -1
  15. package/dist/cli/commands/dev.d.ts.map +1 -1
  16. package/dist/cli/commands/dev.js +49 -3
  17. package/dist/cli/commands/dev.js.map +1 -1
  18. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  19. package/dist/cli/commands/scaffold.js +130 -7
  20. package/dist/cli/commands/scaffold.js.map +1 -1
  21. package/dist/cli/index.js +15 -0
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/core/config.d.ts +3 -2
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +37 -0
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/mock/connector-mock-server.d.ts +67 -0
  28. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  29. package/dist/core/mock/connector-mock-server.js +285 -0
  30. package/dist/core/mock/connector-mock-server.js.map +1 -0
  31. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  32. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  33. package/dist/core/mock/zod-mock-generator.js +163 -0
  34. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  35. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  36. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  37. package/dist/core/scaffold/connector-functions-generator.js +1009 -0
  38. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  39. package/dist/core/scaffold/model-parser.d.ts +6 -0
  40. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  41. package/dist/core/scaffold/model-parser.js +150 -28
  42. package/dist/core/scaffold/model-parser.js.map +1 -1
  43. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  44. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  45. package/dist/core/scaffold/nextjs-generator.js +133 -0
  46. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  47. package/dist/types/index.d.ts +31 -0
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/__tests__/config.test.ts +141 -0
  51. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  52. package/src/__tests__/connector-mock-server.test.ts +252 -0
  53. package/src/__tests__/connector-model-bff.test.ts +162 -0
  54. package/src/__tests__/fixtures.ts +60 -0
  55. package/src/__tests__/scaffold.test.ts +2 -2
  56. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  57. package/src/cli/commands/add-connector.ts +157 -0
  58. package/src/cli/commands/create-model.ts +72 -2
  59. package/src/cli/commands/dev.ts +55 -4
  60. package/src/cli/commands/scaffold.ts +211 -12
  61. package/src/cli/index.ts +16 -0
  62. package/src/core/config.ts +42 -1
  63. package/src/core/mock/connector-mock-server.ts +307 -0
  64. package/src/core/mock/zod-mock-generator.ts +205 -0
  65. package/src/core/scaffold/connector-functions-generator.ts +1106 -0
  66. package/src/core/scaffold/model-parser.ts +172 -31
  67. package/src/core/scaffold/nextjs-generator.ts +154 -0
  68. package/src/types/index.ts +47 -0
@@ -0,0 +1,1009 @@
1
+ "use strict";
2
+ /**
3
+ * コネクタ用 Azure Functions CRUD コード生成
4
+ * RDB(MySQL/PostgreSQL/SQL Server)および 外部 REST API コネクタ向けのハンドラーを生成
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.generateRdbConnectorFunctionTS = generateRdbConnectorFunctionTS;
8
+ exports.generateApiConnectorFunctionTS = generateApiConnectorFunctionTS;
9
+ exports.generateRdbConnectorFunctionCSharp = generateRdbConnectorFunctionCSharp;
10
+ exports.generateApiConnectorFunctionCSharp = generateApiConnectorFunctionCSharp;
11
+ exports.generateRdbConnectorFunctionPython = generateRdbConnectorFunctionPython;
12
+ exports.generateApiConnectorFunctionPython = generateApiConnectorFunctionPython;
13
+ exports.isReadOnlyConnector = isReadOnlyConnector;
14
+ const model_parser_1 = require("./model-parser");
15
+ // ─── TypeScript Generators ────────────────────────────────────────
16
+ /**
17
+ * RDB コネクタ用 TypeScript Azure Functions を生成
18
+ */
19
+ function generateRdbConnectorFunctionTS(model, sharedPackageName, connectorDef, modelConnector) {
20
+ const modelCamel = (0, model_parser_1.toCamelCase)(model.name);
21
+ const schemaName = model.schemaName;
22
+ const ops = new Set(modelConnector.operations);
23
+ const table = modelConnector.table;
24
+ const idCol = modelConnector.idColumn || "id";
25
+ const envVar = connectorDef.connectionEnvVar;
26
+ const provider = connectorDef.provider;
27
+ const driverImport = provider === "mysql"
28
+ ? `import mysql from 'mysql2/promise';`
29
+ : provider === "postgres"
30
+ ? `import pg from 'pg';`
31
+ : `import sql from 'mssql';`;
32
+ const getConnection = provider === "mysql"
33
+ ? `async function getConnection() {
34
+ return mysql.createConnection(process.env.${envVar}!);
35
+ }`
36
+ : provider === "postgres"
37
+ ? `async function getConnection() {
38
+ const client = new pg.Client(process.env.${envVar}!);
39
+ await client.connect();
40
+ return client;
41
+ }`
42
+ : `async function getConnection() {
43
+ return sql.connect(process.env.${envVar}!);
44
+ }`;
45
+ const queryAll = provider === "mysql"
46
+ ? `const conn = await getConnection();
47
+ try {
48
+ const [rows] = await conn.query('SELECT * FROM ${table}');
49
+ const validated = z.array(${schemaName}).parse(rows);
50
+ return { status: 200, jsonBody: validated };
51
+ } finally {
52
+ await conn.end();
53
+ }`
54
+ : provider === "postgres"
55
+ ? `const client = await getConnection();
56
+ try {
57
+ const result = await client.query('SELECT * FROM ${table}');
58
+ const validated = z.array(${schemaName}).parse(result.rows);
59
+ return { status: 200, jsonBody: validated };
60
+ } finally {
61
+ await client.end();
62
+ }`
63
+ : `const pool = await getConnection();
64
+ try {
65
+ const result = await pool.request().query('SELECT * FROM ${table}');
66
+ const validated = z.array(${schemaName}).parse(result.recordset);
67
+ return { status: 200, jsonBody: validated };
68
+ } finally {
69
+ await pool.close();
70
+ }`;
71
+ const queryById = provider === "mysql"
72
+ ? `const conn = await getConnection();
73
+ try {
74
+ const [rows] = await conn.query('SELECT * FROM ${table} WHERE ${idCol} = ?', [id]);
75
+ const items = rows as any[];
76
+ if (items.length === 0) {
77
+ return { status: 404, jsonBody: { error: 'Item not found' } };
78
+ }
79
+ const validated = ${schemaName}.parse(items[0]);
80
+ return { status: 200, jsonBody: validated };
81
+ } finally {
82
+ await conn.end();
83
+ }`
84
+ : provider === "postgres"
85
+ ? `const client = await getConnection();
86
+ try {
87
+ const result = await client.query('SELECT * FROM ${table} WHERE ${idCol} = $1', [id]);
88
+ if (result.rows.length === 0) {
89
+ return { status: 404, jsonBody: { error: 'Item not found' } };
90
+ }
91
+ const validated = ${schemaName}.parse(result.rows[0]);
92
+ return { status: 200, jsonBody: validated };
93
+ } finally {
94
+ await client.end();
95
+ }`
96
+ : `const pool = await getConnection();
97
+ try {
98
+ const result = await pool.request()
99
+ .input('id', id)
100
+ .query('SELECT * FROM ${table} WHERE ${idCol} = @id');
101
+ if (result.recordset.length === 0) {
102
+ return { status: 404, jsonBody: { error: 'Item not found' } };
103
+ }
104
+ const validated = ${schemaName}.parse(result.recordset[0]);
105
+ return { status: 200, jsonBody: validated };
106
+ } finally {
107
+ await pool.close();
108
+ }`;
109
+ let code = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
110
+ import { z } from 'zod/v4';
111
+ import { ${schemaName} } from '${sharedPackageName}';
112
+ ${driverImport}
113
+
114
+ ${getConnection}
115
+ `;
116
+ if (ops.has("getAll")) {
117
+ code += `
118
+ // GET /api/${modelCamel} - 全件取得
119
+ app.http('${modelCamel}-get-all', {
120
+ methods: ['GET'],
121
+ route: '${modelCamel}',
122
+ authLevel: 'anonymous',
123
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
124
+ try {
125
+ ${queryAll}
126
+ } catch (error) {
127
+ context.error(\`Error fetching from ${table}:\`, error);
128
+ return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
129
+ }
130
+ },
131
+ });
132
+ `;
133
+ }
134
+ if (ops.has("getById")) {
135
+ code += `
136
+ // GET /api/${modelCamel}/{id} - ID指定取得
137
+ app.http('${modelCamel}-get-by-id', {
138
+ methods: ['GET'],
139
+ route: '${modelCamel}/{id}',
140
+ authLevel: 'anonymous',
141
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
142
+ try {
143
+ const id = request.params.id;
144
+ if (!id) {
145
+ return { status: 400, jsonBody: { error: 'ID is required' } };
146
+ }
147
+ ${queryById}
148
+ } catch (error) {
149
+ context.error(\`Error fetching item from ${table}:\`, error);
150
+ return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
151
+ }
152
+ },
153
+ });
154
+ `;
155
+ }
156
+ return code;
157
+ }
158
+ /**
159
+ * API コネクタ用 TypeScript Azure Functions を生成
160
+ */
161
+ function generateApiConnectorFunctionTS(model, sharedPackageName, connectorDef, modelConnector) {
162
+ const modelCamel = (0, model_parser_1.toCamelCase)(model.name);
163
+ const schemaName = model.schemaName;
164
+ const ops = new Set(modelConnector.operations);
165
+ const baseUrlEnv = connectorDef.baseUrlEnvVar;
166
+ const endpoints = modelConnector.endpoints || {};
167
+ const authSetup = generateTSAuthSetup(connectorDef);
168
+ let code = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
169
+ import { z } from 'zod/v4';
170
+ import { ${schemaName} } from '${sharedPackageName}';
171
+
172
+ function getBaseUrl(): string {
173
+ return process.env.${baseUrlEnv} || '';
174
+ }
175
+
176
+ ${authSetup.helperCode}
177
+ `;
178
+ if (ops.has("getAll")) {
179
+ const endpoint = endpoints.getAll || `GET /${modelCamel}`;
180
+ const [, epPath] = endpoint.split(" ");
181
+ code += `
182
+ // GET /api/${modelCamel} - 全件取得
183
+ app.http('${modelCamel}-get-all', {
184
+ methods: ['GET'],
185
+ route: '${modelCamel}',
186
+ authLevel: 'anonymous',
187
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
188
+ try {
189
+ const url = getBaseUrl() + '${epPath}';
190
+ const response = await fetch(url, {
191
+ method: 'GET',
192
+ headers: { ...getAuthHeaders() },
193
+ });
194
+
195
+ if (!response.ok) {
196
+ context.error(\`External API error: \${response.status}\`);
197
+ return { status: response.status, jsonBody: { error: 'External API request failed' } };
198
+ }
199
+
200
+ const data = await response.json();
201
+ const validated = z.array(${schemaName}).parse(data);
202
+ return { status: 200, jsonBody: validated };
203
+ } catch (error) {
204
+ context.error(\`Error fetching from external API:\`, error);
205
+ return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
206
+ }
207
+ },
208
+ });
209
+ `;
210
+ }
211
+ if (ops.has("getById")) {
212
+ const endpoint = endpoints.getById || `GET /${modelCamel}/{id}`;
213
+ const [, epPath] = endpoint.split(" ");
214
+ code += `
215
+ // GET /api/${modelCamel}/{id} - ID指定取得
216
+ app.http('${modelCamel}-get-by-id', {
217
+ methods: ['GET'],
218
+ route: '${modelCamel}/{id}',
219
+ authLevel: 'anonymous',
220
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
221
+ try {
222
+ const id = request.params.id;
223
+ if (!id) {
224
+ return { status: 400, jsonBody: { error: 'ID is required' } };
225
+ }
226
+ const url = getBaseUrl() + '${epPath}'.replace('{id}', id);
227
+ const response = await fetch(url, {
228
+ method: 'GET',
229
+ headers: { ...getAuthHeaders() },
230
+ });
231
+
232
+ if (!response.ok) {
233
+ if (response.status === 404) {
234
+ return { status: 404, jsonBody: { error: 'Item not found' } };
235
+ }
236
+ return { status: response.status, jsonBody: { error: 'External API request failed' } };
237
+ }
238
+
239
+ const data = await response.json();
240
+ const validated = ${schemaName}.parse(data);
241
+ return { status: 200, jsonBody: validated };
242
+ } catch (error) {
243
+ context.error(\`Error fetching item from external API:\`, error);
244
+ return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
245
+ }
246
+ },
247
+ });
248
+ `;
249
+ }
250
+ if (ops.has("create")) {
251
+ const endpoint = endpoints.create || `POST /${modelCamel}`;
252
+ const [, epPath] = endpoint.split(" ");
253
+ code += `
254
+ // POST /api/${modelCamel} - 新規作成
255
+ app.http('${modelCamel}-create', {
256
+ methods: ['POST'],
257
+ route: '${modelCamel}',
258
+ authLevel: 'anonymous',
259
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
260
+ try {
261
+ const body = await request.json();
262
+ const url = getBaseUrl() + '${epPath}';
263
+ const response = await fetch(url, {
264
+ method: 'POST',
265
+ headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
266
+ body: JSON.stringify(body),
267
+ });
268
+
269
+ if (!response.ok) {
270
+ const errorText = await response.text();
271
+ context.error(\`External API error: \${response.status} \${errorText}\`);
272
+ return { status: response.status, jsonBody: { error: 'External API request failed' } };
273
+ }
274
+
275
+ const data = await response.json();
276
+ return { status: 201, jsonBody: data };
277
+ } catch (error) {
278
+ context.error(\`Error creating item via external API:\`, error);
279
+ return { status: 500, jsonBody: { error: 'Failed to create item' } };
280
+ }
281
+ },
282
+ });
283
+ `;
284
+ }
285
+ if (ops.has("update")) {
286
+ const endpoint = endpoints.update || `PUT /${modelCamel}/{id}`;
287
+ const parts = endpoint.split(" ");
288
+ const method = parts[0];
289
+ const epPath = parts[1];
290
+ code += `
291
+ // PUT /api/${modelCamel}/{id} - 更新
292
+ app.http('${modelCamel}-update', {
293
+ methods: ['PUT'],
294
+ route: '${modelCamel}/{id}',
295
+ authLevel: 'anonymous',
296
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
297
+ try {
298
+ const id = request.params.id;
299
+ if (!id) {
300
+ return { status: 400, jsonBody: { error: 'ID is required' } };
301
+ }
302
+ const body = await request.json();
303
+ const url = getBaseUrl() + '${epPath}'.replace('{id}', id);
304
+ const response = await fetch(url, {
305
+ method: '${method}',
306
+ headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
307
+ body: JSON.stringify(body),
308
+ });
309
+
310
+ if (!response.ok) {
311
+ const errorText = await response.text();
312
+ context.error(\`External API error: \${response.status} \${errorText}\`);
313
+ return { status: response.status, jsonBody: { error: 'External API request failed' } };
314
+ }
315
+
316
+ const data = await response.json();
317
+ return { status: 200, jsonBody: data };
318
+ } catch (error) {
319
+ context.error(\`Error updating item via external API:\`, error);
320
+ return { status: 500, jsonBody: { error: 'Failed to update item' } };
321
+ }
322
+ },
323
+ });
324
+ `;
325
+ }
326
+ if (ops.has("delete")) {
327
+ const endpoint = endpoints.delete || `DELETE /${modelCamel}/{id}`;
328
+ const [, epPath] = endpoint.split(" ");
329
+ code += `
330
+ // DELETE /api/${modelCamel}/{id} - 削除
331
+ app.http('${modelCamel}-delete', {
332
+ methods: ['DELETE'],
333
+ route: '${modelCamel}/{id}',
334
+ authLevel: 'anonymous',
335
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
336
+ try {
337
+ const id = request.params.id;
338
+ if (!id) {
339
+ return { status: 400, jsonBody: { error: 'ID is required' } };
340
+ }
341
+ const url = getBaseUrl() + '${epPath}'.replace('{id}', id);
342
+ const response = await fetch(url, {
343
+ method: 'DELETE',
344
+ headers: { ...getAuthHeaders() },
345
+ });
346
+
347
+ if (!response.ok && response.status !== 404) {
348
+ return { status: response.status, jsonBody: { error: 'External API request failed' } };
349
+ }
350
+
351
+ return { status: 204 };
352
+ } catch (error) {
353
+ context.error(\`Error deleting item via external API:\`, error);
354
+ return { status: 500, jsonBody: { error: 'Failed to delete item' } };
355
+ }
356
+ },
357
+ });
358
+ `;
359
+ }
360
+ return code;
361
+ }
362
+ function generateTSAuthSetup(connectorDef) {
363
+ if (!connectorDef.auth) {
364
+ return {
365
+ helperCode: `function getAuthHeaders(): Record<string, string> {
366
+ return {};
367
+ }`,
368
+ };
369
+ }
370
+ const { type, envVar, placement, paramName } = connectorDef.auth;
371
+ if (type === "apiKey" && placement === "query") {
372
+ return {
373
+ helperCode: `function getAuthHeaders(): Record<string, string> {
374
+ return {};
375
+ }
376
+
377
+ function appendAuthQuery(url: string): string {
378
+ const sep = url.includes('?') ? '&' : '?';
379
+ return url + sep + '${paramName || "apiKey"}=' + encodeURIComponent(process.env.${envVar} || '');
380
+ }`,
381
+ };
382
+ }
383
+ if (type === "apiKey" && placement !== "query") {
384
+ return {
385
+ helperCode: `function getAuthHeaders(): Record<string, string> {
386
+ return { '${paramName || "X-Api-Key"}': process.env.${envVar} || '' };
387
+ }`,
388
+ };
389
+ }
390
+ // bearer
391
+ return {
392
+ helperCode: `function getAuthHeaders(): Record<string, string> {
393
+ return { 'Authorization': 'Bearer ' + (process.env.${envVar} || '') };
394
+ }`,
395
+ };
396
+ }
397
+ // ─── C# Generators ────────────────────────────────────────────────
398
+ /**
399
+ * RDB コネクタ用 C# Azure Functions を生成
400
+ */
401
+ function generateRdbConnectorFunctionCSharp(model, connectorDef, modelConnector) {
402
+ const modelName = model.name;
403
+ const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
404
+ const className = `${modelName}ConnectorFunctions`;
405
+ const ops = new Set(modelConnector.operations);
406
+ const table = modelConnector.table;
407
+ const idCol = modelConnector.idColumn || "id";
408
+ const envVar = connectorDef.connectionEnvVar;
409
+ const provider = connectorDef.provider;
410
+ const usingStatements = provider === "mysql"
411
+ ? `using MySqlConnector;`
412
+ : provider === "postgres"
413
+ ? `using Npgsql;`
414
+ : `using Microsoft.Data.SqlClient;`;
415
+ const connType = provider === "mysql"
416
+ ? "MySqlConnection"
417
+ : provider === "postgres"
418
+ ? "NpgsqlConnection"
419
+ : "SqlConnection";
420
+ const cmdType = provider === "mysql"
421
+ ? "MySqlCommand"
422
+ : provider === "postgres"
423
+ ? "NpgsqlCommand"
424
+ : "SqlCommand";
425
+ const paramPrefix = provider === "mysql" ? "@" : provider === "postgres" ? "@" : "@";
426
+ let methods = "";
427
+ if (ops.has("getAll")) {
428
+ methods += `
429
+ [Function("${modelCamel}GetAll")]
430
+ public async Task<HttpResponseData> GetAll(
431
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}")] HttpRequestData request)
432
+ {
433
+ try
434
+ {
435
+ using var connection = new ${connType}(Environment.GetEnvironmentVariable("${envVar}"));
436
+ await connection.OpenAsync();
437
+
438
+ using var command = new ${cmdType}("SELECT * FROM ${table}", connection);
439
+ using var reader = await command.ExecuteReaderAsync();
440
+ var items = new List<Dictionary<string, object?>>();
441
+ while (await reader.ReadAsync())
442
+ {
443
+ var item = new Dictionary<string, object?>();
444
+ for (int i = 0; i < reader.FieldCount; i++)
445
+ {
446
+ item[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
447
+ }
448
+ items.Add(item);
449
+ }
450
+
451
+ return await WriteJsonAsync(request, HttpStatusCode.OK, items);
452
+ }
453
+ catch (Exception ex)
454
+ {
455
+ _logger.LogError(ex, "Failed to fetch items from ${table}.");
456
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch items" });
457
+ }
458
+ }
459
+ `;
460
+ }
461
+ if (ops.has("getById")) {
462
+ methods += `
463
+ [Function("${modelCamel}GetById")]
464
+ public async Task<HttpResponseData> GetById(
465
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
466
+ string id)
467
+ {
468
+ try
469
+ {
470
+ using var connection = new ${connType}(Environment.GetEnvironmentVariable("${envVar}"));
471
+ await connection.OpenAsync();
472
+
473
+ using var command = new ${cmdType}("SELECT * FROM ${table} WHERE ${idCol} = ${paramPrefix}id", connection);
474
+ command.Parameters.AddWithValue("${paramPrefix}id", id);
475
+ using var reader = await command.ExecuteReaderAsync();
476
+
477
+ if (!await reader.ReadAsync())
478
+ {
479
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
480
+ }
481
+
482
+ var item = new Dictionary<string, object?>();
483
+ for (int i = 0; i < reader.FieldCount; i++)
484
+ {
485
+ item[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
486
+ }
487
+
488
+ return await WriteJsonAsync(request, HttpStatusCode.OK, item);
489
+ }
490
+ catch (Exception ex)
491
+ {
492
+ _logger.LogError(ex, "Failed to fetch ${modelName} item {Id} from ${table}.", id);
493
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch item", id });
494
+ }
495
+ }
496
+ `;
497
+ }
498
+ return `using System.Net;
499
+ using System.Text.Json;
500
+ ${usingStatements}
501
+ using Microsoft.Azure.Functions.Worker;
502
+ using Microsoft.Azure.Functions.Worker.Http;
503
+ using Microsoft.Extensions.Logging;
504
+
505
+ namespace SwallowKit.Functions;
506
+
507
+ public sealed class ${className}
508
+ {
509
+ private readonly ILogger<${className}> _logger;
510
+
511
+ public ${className}(ILogger<${className}> logger)
512
+ {
513
+ _logger = logger;
514
+ }
515
+
516
+ private static async Task<HttpResponseData> WriteJsonAsync(HttpRequestData request, HttpStatusCode status, object payload)
517
+ {
518
+ var response = request.CreateResponse(status);
519
+ await response.WriteAsJsonAsync(payload);
520
+ return response;
521
+ }
522
+ ${methods}}
523
+ `;
524
+ }
525
+ /**
526
+ * API コネクタ用 C# Azure Functions を生成
527
+ */
528
+ function generateApiConnectorFunctionCSharp(model, connectorDef, modelConnector) {
529
+ const modelName = model.name;
530
+ const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
531
+ const className = `${modelName}ConnectorFunctions`;
532
+ const ops = new Set(modelConnector.operations);
533
+ const baseUrlEnv = connectorDef.baseUrlEnvVar;
534
+ const endpoints = modelConnector.endpoints || {};
535
+ const authSetup = generateCSharpAuthSetup(connectorDef);
536
+ let methods = "";
537
+ if (ops.has("getAll")) {
538
+ const endpoint = endpoints.getAll || `GET /${modelCamel}`;
539
+ const [, epPath] = endpoint.split(" ");
540
+ methods += `
541
+ [Function("${modelCamel}GetAll")]
542
+ public async Task<HttpResponseData> GetAll(
543
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}")] HttpRequestData request)
544
+ {
545
+ try
546
+ {
547
+ var url = BaseUrl + "${epPath}";
548
+ using var httpRequest = new HttpRequestMessage(HttpMethod.Get, url);
549
+ ${authSetup.applyAuth}
550
+ var response = await _httpClient.SendAsync(httpRequest);
551
+
552
+ if (!response.IsSuccessStatusCode)
553
+ {
554
+ return await WriteJsonAsync(request, (HttpStatusCode)response.StatusCode, new { error = "External API request failed" });
555
+ }
556
+
557
+ var content = await response.Content.ReadAsStringAsync();
558
+ var data = JsonSerializer.Deserialize<JsonElement>(content);
559
+ return await WriteJsonAsync(request, HttpStatusCode.OK, data);
560
+ }
561
+ catch (Exception ex)
562
+ {
563
+ _logger.LogError(ex, "Failed to fetch ${modelName} items from external API.");
564
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch items" });
565
+ }
566
+ }
567
+ `;
568
+ }
569
+ if (ops.has("getById")) {
570
+ const endpoint = endpoints.getById || `GET /${modelCamel}/{id}`;
571
+ const [, epPath] = endpoint.split(" ");
572
+ methods += `
573
+ [Function("${modelCamel}GetById")]
574
+ public async Task<HttpResponseData> GetById(
575
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
576
+ string id)
577
+ {
578
+ try
579
+ {
580
+ var url = BaseUrl + "${epPath}".Replace("{id}", id);
581
+ using var httpRequest = new HttpRequestMessage(HttpMethod.Get, url);
582
+ ${authSetup.applyAuth}
583
+ var response = await _httpClient.SendAsync(httpRequest);
584
+
585
+ if (response.StatusCode == HttpStatusCode.NotFound)
586
+ {
587
+ return await WriteJsonAsync(request, HttpStatusCode.NotFound, new { error = "${modelName} not found", id });
588
+ }
589
+ if (!response.IsSuccessStatusCode)
590
+ {
591
+ return await WriteJsonAsync(request, (HttpStatusCode)response.StatusCode, new { error = "External API request failed" });
592
+ }
593
+
594
+ var content = await response.Content.ReadAsStringAsync();
595
+ var data = JsonSerializer.Deserialize<JsonElement>(content);
596
+ return await WriteJsonAsync(request, HttpStatusCode.OK, data);
597
+ }
598
+ catch (Exception ex)
599
+ {
600
+ _logger.LogError(ex, "Failed to fetch ${modelName} item {Id} from external API.", id);
601
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to fetch item", id });
602
+ }
603
+ }
604
+ `;
605
+ }
606
+ if (ops.has("create")) {
607
+ const endpoint = endpoints.create || `POST /${modelCamel}`;
608
+ const [, epPath] = endpoint.split(" ");
609
+ methods += `
610
+ [Function("${modelCamel}Create")]
611
+ public async Task<HttpResponseData> Create(
612
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "${modelCamel}")] HttpRequestData request)
613
+ {
614
+ try
615
+ {
616
+ using var reader = new StreamReader(request.Body);
617
+ var body = await reader.ReadToEndAsync();
618
+ var url = BaseUrl + "${epPath}";
619
+ using var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)
620
+ {
621
+ Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json")
622
+ };
623
+ ${authSetup.applyAuth}
624
+ var response = await _httpClient.SendAsync(httpRequest);
625
+
626
+ var content = await response.Content.ReadAsStringAsync();
627
+ var data = JsonSerializer.Deserialize<JsonElement>(content);
628
+ var status = response.IsSuccessStatusCode ? HttpStatusCode.Created : (HttpStatusCode)response.StatusCode;
629
+ return await WriteJsonAsync(request, status, data);
630
+ }
631
+ catch (Exception ex)
632
+ {
633
+ _logger.LogError(ex, "Failed to create ${modelName} item via external API.");
634
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to create item" });
635
+ }
636
+ }
637
+ `;
638
+ }
639
+ if (ops.has("update")) {
640
+ const endpoint = endpoints.update || `PUT /${modelCamel}/{id}`;
641
+ const parts = endpoint.split(" ");
642
+ const httpMethod = parts[0] === "PATCH" ? "Patch" : "Put";
643
+ const epPath = parts[1];
644
+ methods += `
645
+ [Function("${modelCamel}Update")]
646
+ public async Task<HttpResponseData> Update(
647
+ [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
648
+ string id)
649
+ {
650
+ try
651
+ {
652
+ using var reader = new StreamReader(request.Body);
653
+ var body = await reader.ReadToEndAsync();
654
+ var url = BaseUrl + "${epPath}".Replace("{id}", id);
655
+ using var httpRequest = new HttpRequestMessage(HttpMethod.${httpMethod}, url)
656
+ {
657
+ Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json")
658
+ };
659
+ ${authSetup.applyAuth}
660
+ var response = await _httpClient.SendAsync(httpRequest);
661
+
662
+ var content = await response.Content.ReadAsStringAsync();
663
+ var data = JsonSerializer.Deserialize<JsonElement>(content);
664
+ return await WriteJsonAsync(request, response.IsSuccessStatusCode ? HttpStatusCode.OK : (HttpStatusCode)response.StatusCode, data);
665
+ }
666
+ catch (Exception ex)
667
+ {
668
+ _logger.LogError(ex, "Failed to update ${modelName} item {Id} via external API.", id);
669
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to update item", id });
670
+ }
671
+ }
672
+ `;
673
+ }
674
+ if (ops.has("delete")) {
675
+ const endpoint = endpoints.delete || `DELETE /${modelCamel}/{id}`;
676
+ const [, epPath] = endpoint.split(" ");
677
+ methods += `
678
+ [Function("${modelCamel}Delete")]
679
+ public async Task<HttpResponseData> Delete(
680
+ [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
681
+ string id)
682
+ {
683
+ try
684
+ {
685
+ var url = BaseUrl + "${epPath}".Replace("{id}", id);
686
+ using var httpRequest = new HttpRequestMessage(HttpMethod.Delete, url);
687
+ ${authSetup.applyAuth}
688
+ var response = await _httpClient.SendAsync(httpRequest);
689
+
690
+ if (!response.IsSuccessStatusCode && response.StatusCode != HttpStatusCode.NotFound)
691
+ {
692
+ return await WriteJsonAsync(request, (HttpStatusCode)response.StatusCode, new { error = "External API request failed" });
693
+ }
694
+
695
+ return request.CreateResponse(HttpStatusCode.NoContent);
696
+ }
697
+ catch (Exception ex)
698
+ {
699
+ _logger.LogError(ex, "Failed to delete ${modelName} item {Id} via external API.", id);
700
+ return await WriteJsonAsync(request, HttpStatusCode.InternalServerError, new { error = "Failed to delete item", id });
701
+ }
702
+ }
703
+ `;
704
+ }
705
+ return `using System.Net;
706
+ using System.Text.Json;
707
+ using Microsoft.Azure.Functions.Worker;
708
+ using Microsoft.Azure.Functions.Worker.Http;
709
+ using Microsoft.Extensions.Logging;
710
+
711
+ namespace SwallowKit.Functions;
712
+
713
+ public sealed class ${className}
714
+ {
715
+ private readonly ILogger<${className}> _logger;
716
+ private readonly HttpClient _httpClient;
717
+ private static string BaseUrl => Environment.GetEnvironmentVariable("${baseUrlEnv}") ?? "";
718
+ ${authSetup.fieldDecl}
719
+
720
+ public ${className}(ILogger<${className}> logger, HttpClient httpClient)
721
+ {
722
+ _logger = logger;
723
+ _httpClient = httpClient;
724
+ }
725
+
726
+ private static async Task<HttpResponseData> WriteJsonAsync(HttpRequestData request, HttpStatusCode status, object payload)
727
+ {
728
+ var response = request.CreateResponse(status);
729
+ await response.WriteAsJsonAsync(payload);
730
+ return response;
731
+ }
732
+ ${methods}}
733
+ `;
734
+ }
735
+ function generateCSharpAuthSetup(connectorDef) {
736
+ if (!connectorDef.auth) {
737
+ return { fieldDecl: "", applyAuth: "" };
738
+ }
739
+ const { type, envVar, placement, paramName } = connectorDef.auth;
740
+ if (type === "apiKey" && placement === "query") {
741
+ return {
742
+ fieldDecl: "",
743
+ applyAuth: `// API key is appended as query parameter
744
+ var uriBuilder = new UriBuilder(httpRequest.RequestUri!);
745
+ var query = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query);
746
+ query["${paramName || "apiKey"}"] = Environment.GetEnvironmentVariable("${envVar}");
747
+ uriBuilder.Query = query.ToString();
748
+ httpRequest.RequestUri = uriBuilder.Uri;`,
749
+ };
750
+ }
751
+ if (type === "apiKey") {
752
+ return {
753
+ fieldDecl: "",
754
+ applyAuth: `httpRequest.Headers.Add("${paramName || "X-Api-Key"}", Environment.GetEnvironmentVariable("${envVar}"));`,
755
+ };
756
+ }
757
+ // bearer
758
+ return {
759
+ fieldDecl: "",
760
+ applyAuth: `httpRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("${envVar}"));`,
761
+ };
762
+ }
763
+ // ─── Python Generators ────────────────────────────────────────────
764
+ /**
765
+ * RDB コネクタ用 Python Azure Functions を生成
766
+ */
767
+ function generateRdbConnectorFunctionPython(model, connectorDef, modelConnector) {
768
+ const modelName = model.name;
769
+ const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
770
+ const modelSnake = (0, model_parser_1.toKebabCase)(modelName).replace(/-/g, "_");
771
+ const ops = new Set(modelConnector.operations);
772
+ const table = modelConnector.table;
773
+ const idCol = modelConnector.idColumn || "id";
774
+ const envVar = connectorDef.connectionEnvVar;
775
+ let handlers = "";
776
+ if (ops.has("getAll")) {
777
+ handlers += `
778
+ @bp.route(route="${modelCamel}", methods=["GET"])
779
+ def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
780
+ try:
781
+ conn = get_connection()
782
+ cursor = conn.cursor(dictionary=True)
783
+ cursor.execute("SELECT * FROM ${table}")
784
+ items = cursor.fetchall()
785
+ cursor.close()
786
+ conn.close()
787
+ return _json_response(items, 200)
788
+ except Exception as e:
789
+ logging.error(f"Error fetching from ${table}: {e}")
790
+ return _json_response({"error": "Failed to fetch items"}, 500)
791
+ `;
792
+ }
793
+ if (ops.has("getById")) {
794
+ handlers += `
795
+ @bp.route(route="${modelCamel}/{id}", methods=["GET"])
796
+ def ${modelSnake}_get_by_id(req: func.HttpRequest) -> func.HttpResponse:
797
+ item_id = req.route_params.get("id")
798
+ if not item_id:
799
+ return _json_response({"error": "ID is required"}, 400)
800
+ try:
801
+ conn = get_connection()
802
+ cursor = conn.cursor(dictionary=True)
803
+ cursor.execute("SELECT * FROM ${table} WHERE ${idCol} = %s", (item_id,))
804
+ item = cursor.fetchone()
805
+ cursor.close()
806
+ conn.close()
807
+ if item is None:
808
+ return _json_response({"error": "Item not found"}, 404)
809
+ return _json_response(item, 200)
810
+ except Exception as e:
811
+ logging.error(f"Error fetching item from ${table}: {e}")
812
+ return _json_response({"error": "Failed to fetch item"}, 500)
813
+ `;
814
+ }
815
+ const blueprint = `import json
816
+ import logging
817
+ import os
818
+
819
+ import azure.functions as func
820
+ import mysql.connector
821
+
822
+ bp = func.Blueprint()
823
+
824
+
825
+ def _json_response(payload, status_code: int) -> func.HttpResponse:
826
+ return func.HttpResponse(
827
+ body=json.dumps(payload, ensure_ascii=False, default=str),
828
+ status_code=status_code,
829
+ mimetype="application/json",
830
+ )
831
+
832
+
833
+ def get_connection():
834
+ return mysql.connector.connect(
835
+ host=os.environ.get("${envVar}_HOST", "localhost"),
836
+ user=os.environ.get("${envVar}_USER", ""),
837
+ password=os.environ.get("${envVar}_PASSWORD", ""),
838
+ database=os.environ.get("${envVar}_DATABASE", ""),
839
+ )
840
+
841
+ ${handlers}`;
842
+ return {
843
+ blueprint,
844
+ registration: `from blueprints.${modelSnake} import bp as ${modelSnake}_bp\napp.register_blueprint(${modelSnake}_bp)`,
845
+ };
846
+ }
847
+ /**
848
+ * API コネクタ用 Python Azure Functions を生成
849
+ */
850
+ function generateApiConnectorFunctionPython(model, connectorDef, modelConnector) {
851
+ const modelName = model.name;
852
+ const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
853
+ const modelSnake = (0, model_parser_1.toKebabCase)(modelName).replace(/-/g, "_");
854
+ const ops = new Set(modelConnector.operations);
855
+ const baseUrlEnv = connectorDef.baseUrlEnvVar;
856
+ const endpoints = modelConnector.endpoints || {};
857
+ const authSetup = generatePythonAuthSetup(connectorDef);
858
+ let handlers = "";
859
+ if (ops.has("getAll")) {
860
+ const endpoint = endpoints.getAll || `GET /${modelCamel}`;
861
+ const [, epPath] = endpoint.split(" ");
862
+ handlers += `
863
+ @bp.route(route="${modelCamel}", methods=["GET"])
864
+ def ${modelSnake}_get_all(req: func.HttpRequest) -> func.HttpResponse:
865
+ try:
866
+ url = BASE_URL + "${epPath}"
867
+ response = requests.get(url, ${authSetup.requestKwargs})
868
+ response.raise_for_status()
869
+ return _json_response(response.json(), 200)
870
+ except Exception as e:
871
+ logging.error(f"Error fetching from external API: {e}")
872
+ return _json_response({"error": "Failed to fetch items"}, 500)
873
+ `;
874
+ }
875
+ if (ops.has("getById")) {
876
+ const endpoint = endpoints.getById || `GET /${modelCamel}/{id}`;
877
+ const [, epPath] = endpoint.split(" ");
878
+ handlers += `
879
+ @bp.route(route="${modelCamel}/{id}", methods=["GET"])
880
+ def ${modelSnake}_get_by_id(req: func.HttpRequest) -> func.HttpResponse:
881
+ item_id = req.route_params.get("id")
882
+ if not item_id:
883
+ return _json_response({"error": "ID is required"}, 400)
884
+ try:
885
+ url = BASE_URL + "${epPath}".replace("{id}", item_id)
886
+ response = requests.get(url, ${authSetup.requestKwargs})
887
+ if response.status_code == 404:
888
+ return _json_response({"error": "Item not found"}, 404)
889
+ response.raise_for_status()
890
+ return _json_response(response.json(), 200)
891
+ except Exception as e:
892
+ logging.error(f"Error fetching item from external API: {e}")
893
+ return _json_response({"error": "Failed to fetch item"}, 500)
894
+ `;
895
+ }
896
+ if (ops.has("create")) {
897
+ const endpoint = endpoints.create || `POST /${modelCamel}`;
898
+ const [, epPath] = endpoint.split(" ");
899
+ handlers += `
900
+ @bp.route(route="${modelCamel}", methods=["POST"])
901
+ def ${modelSnake}_create(req: func.HttpRequest) -> func.HttpResponse:
902
+ try:
903
+ body = req.get_json()
904
+ url = BASE_URL + "${epPath}"
905
+ response = requests.post(url, json=body, ${authSetup.requestKwargs})
906
+ response.raise_for_status()
907
+ return _json_response(response.json(), 201)
908
+ except Exception as e:
909
+ logging.error(f"Error creating item via external API: {e}")
910
+ return _json_response({"error": "Failed to create item"}, 500)
911
+ `;
912
+ }
913
+ if (ops.has("update")) {
914
+ const endpoint = endpoints.update || `PUT /${modelCamel}/{id}`;
915
+ const parts = endpoint.split(" ");
916
+ const method = parts[0].toLowerCase();
917
+ const epPath = parts[1];
918
+ handlers += `
919
+ @bp.route(route="${modelCamel}/{id}", methods=["PUT"])
920
+ def ${modelSnake}_update(req: func.HttpRequest) -> func.HttpResponse:
921
+ item_id = req.route_params.get("id")
922
+ if not item_id:
923
+ return _json_response({"error": "ID is required"}, 400)
924
+ try:
925
+ body = req.get_json()
926
+ url = BASE_URL + "${epPath}".replace("{id}", item_id)
927
+ response = requests.${method}(url, json=body, ${authSetup.requestKwargs})
928
+ response.raise_for_status()
929
+ return _json_response(response.json(), 200)
930
+ except Exception as e:
931
+ logging.error(f"Error updating item via external API: {e}")
932
+ return _json_response({"error": "Failed to update item"}, 500)
933
+ `;
934
+ }
935
+ if (ops.has("delete")) {
936
+ const endpoint = endpoints.delete || `DELETE /${modelCamel}/{id}`;
937
+ const [, epPath] = endpoint.split(" ");
938
+ handlers += `
939
+ @bp.route(route="${modelCamel}/{id}", methods=["DELETE"])
940
+ def ${modelSnake}_delete(req: func.HttpRequest) -> func.HttpResponse:
941
+ item_id = req.route_params.get("id")
942
+ if not item_id:
943
+ return _json_response({"error": "ID is required"}, 400)
944
+ try:
945
+ url = BASE_URL + "${epPath}".replace("{id}", item_id)
946
+ response = requests.delete(url, ${authSetup.requestKwargs})
947
+ return func.HttpResponse(status_code=204)
948
+ except Exception as e:
949
+ logging.error(f"Error deleting item via external API: {e}")
950
+ return _json_response({"error": "Failed to delete item"}, 500)
951
+ `;
952
+ }
953
+ const blueprint = `import json
954
+ import logging
955
+ import os
956
+
957
+ import azure.functions as func
958
+ import requests
959
+
960
+ bp = func.Blueprint()
961
+ BASE_URL = os.environ.get("${baseUrlEnv}", "")
962
+ ${authSetup.setup}
963
+
964
+
965
+ def _json_response(payload, status_code: int) -> func.HttpResponse:
966
+ return func.HttpResponse(
967
+ body=json.dumps(payload, ensure_ascii=False, default=str),
968
+ status_code=status_code,
969
+ mimetype="application/json",
970
+ )
971
+
972
+ ${handlers}`;
973
+ return {
974
+ blueprint,
975
+ registration: `from blueprints.${modelSnake} import bp as ${modelSnake}_bp\napp.register_blueprint(${modelSnake}_bp)`,
976
+ };
977
+ }
978
+ function generatePythonAuthSetup(connectorDef) {
979
+ if (!connectorDef.auth) {
980
+ return { setup: "", requestKwargs: "timeout=30" };
981
+ }
982
+ const { type, envVar, placement, paramName } = connectorDef.auth;
983
+ if (type === "apiKey" && placement === "query") {
984
+ return {
985
+ setup: `API_KEY = os.environ.get("${envVar}", "")`,
986
+ requestKwargs: `params={"${paramName || "apiKey"}": API_KEY}, timeout=30`,
987
+ };
988
+ }
989
+ if (type === "apiKey") {
990
+ return {
991
+ setup: `API_KEY = os.environ.get("${envVar}", "")`,
992
+ requestKwargs: `headers={"${paramName || "X-Api-Key"}": API_KEY}, timeout=30`,
993
+ };
994
+ }
995
+ // bearer
996
+ return {
997
+ setup: `AUTH_TOKEN = os.environ.get("${envVar}", "")`,
998
+ requestKwargs: `headers={"Authorization": f"Bearer {AUTH_TOKEN}"}, timeout=30`,
999
+ };
1000
+ }
1001
+ // ─── Helper: check if model has write operations ──────────────────
1002
+ /**
1003
+ * モデルのコネクタ操作が読み取り専用かどうかを判定
1004
+ */
1005
+ function isReadOnlyConnector(operations) {
1006
+ const writeOps = ["create", "update", "delete"];
1007
+ return !operations.some(op => writeOps.includes(op));
1008
+ }
1009
+ //# sourceMappingURL=connector-functions-generator.js.map