swallowkit 1.0.0-beta.5 → 1.0.0-beta.7

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 (97) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +251 -242
  3. package/README.md +252 -243
  4. package/dist/__tests__/fixtures.d.ts +14 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +85 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/create-model.js +14 -14
  9. package/dist/cli/commands/dev.d.ts +8 -0
  10. package/dist/cli/commands/dev.d.ts.map +1 -1
  11. package/dist/cli/commands/dev.js +238 -30
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +5 -0
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +2507 -1664
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/scaffold.d.ts +3 -0
  18. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  19. package/dist/cli/commands/scaffold.js +281 -117
  20. package/dist/cli/commands/scaffold.js.map +1 -1
  21. package/dist/cli/index.js +2 -0
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/core/config.d.ts +2 -1
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +28 -0
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/scaffold/functions-generator.d.ts +5 -0
  28. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  29. package/dist/core/scaffold/functions-generator.js +649 -218
  30. package/dist/core/scaffold/functions-generator.js.map +1 -1
  31. package/dist/core/scaffold/model-parser.d.ts +1 -1
  32. package/dist/core/scaffold/model-parser.js +99 -99
  33. package/dist/core/scaffold/nextjs-generator.js +181 -181
  34. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  35. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  36. package/dist/core/scaffold/openapi-generator.js +190 -0
  37. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  38. package/dist/core/scaffold/ui-generator.js +656 -656
  39. package/dist/database/base-model.d.ts +3 -3
  40. package/dist/database/base-model.js +3 -3
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +2 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/types/index.d.ts +4 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/dist/utils/package-manager.d.ts +2 -1
  48. package/dist/utils/package-manager.d.ts.map +1 -1
  49. package/dist/utils/package-manager.js +14 -10
  50. package/dist/utils/package-manager.js.map +1 -1
  51. package/package.json +81 -74
  52. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +445 -0
  53. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  54. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +524 -0
  55. package/src/__tests__/config.test.ts +122 -0
  56. package/src/__tests__/dev.test.ts +42 -0
  57. package/src/__tests__/fixtures.ts +83 -0
  58. package/src/__tests__/functions-generator.test.ts +101 -0
  59. package/src/__tests__/init.test.ts +59 -0
  60. package/src/__tests__/nextjs-generator.test.ts +97 -0
  61. package/src/__tests__/openapi-generator.test.ts +43 -0
  62. package/src/__tests__/package-manager.test.ts +189 -0
  63. package/src/__tests__/scaffold.test.ts +39 -0
  64. package/src/__tests__/string-utils.test.ts +75 -0
  65. package/src/__tests__/ui-generator.test.ts +144 -0
  66. package/src/cli/commands/create-model.ts +141 -0
  67. package/src/cli/commands/dev.ts +794 -0
  68. package/src/cli/commands/index.ts +8 -0
  69. package/src/cli/commands/init.ts +3363 -0
  70. package/src/cli/commands/provision.ts +193 -0
  71. package/src/cli/commands/scaffold.ts +786 -0
  72. package/src/cli/index.ts +73 -0
  73. package/src/core/config.ts +244 -0
  74. package/src/core/scaffold/functions-generator.ts +674 -0
  75. package/src/core/scaffold/model-parser.ts +627 -0
  76. package/src/core/scaffold/nextjs-generator.ts +217 -0
  77. package/src/core/scaffold/openapi-generator.ts +212 -0
  78. package/src/core/scaffold/ui-generator.ts +945 -0
  79. package/src/database/base-model.ts +184 -0
  80. package/src/database/client.ts +140 -0
  81. package/src/database/repository.ts +104 -0
  82. package/src/database/runtime-check.ts +25 -0
  83. package/src/index.ts +27 -0
  84. package/src/types/index.ts +45 -0
  85. package/src/utils/package-manager.ts +229 -0
  86. package/dist/cli/commands/build.d.ts +0 -6
  87. package/dist/cli/commands/build.d.ts.map +0 -1
  88. package/dist/cli/commands/build.js +0 -177
  89. package/dist/cli/commands/build.js.map +0 -1
  90. package/dist/cli/commands/deploy.d.ts +0 -3
  91. package/dist/cli/commands/deploy.d.ts.map +0 -1
  92. package/dist/cli/commands/deploy.js +0 -147
  93. package/dist/cli/commands/deploy.js.map +0 -1
  94. package/dist/cli/commands/setup.d.ts +0 -6
  95. package/dist/cli/commands/setup.d.ts.map +0 -1
  96. package/dist/cli/commands/setup.js +0 -254
  97. package/dist/cli/commands/setup.js.map +0 -1
@@ -0,0 +1,445 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`generateCompactAzureFunctionsCRUD generates correct CRUD code for a basic model 1`] = `
4
+ "import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
5
+ import { z } from 'zod/v4';
6
+ import crypto from 'crypto';
7
+ import { todoSchema } from '@myapp/shared';
8
+
9
+ const containerName = 'Todos';
10
+
11
+ // GET /api/todo - 全件取得
12
+ app.http('todo-get-all', {
13
+ methods: ['GET'],
14
+ route: 'todo',
15
+ authLevel: 'anonymous',
16
+ extraInputs: [
17
+ {
18
+ type: 'cosmosDB',
19
+ name: 'cosmosInput',
20
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
21
+ containerName,
22
+ connection: 'CosmosDBConnection',
23
+ sqlQuery: 'SELECT * FROM c',
24
+ },
25
+ ],
26
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
27
+ try {
28
+ const documents = context.extraInputs.get('cosmosInput') as any[];
29
+
30
+ if (!documents || !Array.isArray(documents)) {
31
+ return { status: 200, jsonBody: [] };
32
+ }
33
+
34
+ const validated = z.array(todoSchema).parse(documents);
35
+ context.log(\`Fetched \${validated.length} items from \${containerName}\`);
36
+
37
+ return { status: 200, jsonBody: validated };
38
+ } catch (error) {
39
+ context.error(\`Error fetching from \${containerName}:\`, error);
40
+ return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
41
+ }
42
+ },
43
+ });
44
+
45
+ // GET /api/todo/{id} - ID指定取得
46
+ app.http('todo-get-by-id', {
47
+ methods: ['GET'],
48
+ route: 'todo/{id}',
49
+ authLevel: 'anonymous',
50
+ extraInputs: [
51
+ {
52
+ type: 'cosmosDB',
53
+ name: 'cosmosInput',
54
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
55
+ containerName,
56
+ connection: 'CosmosDBConnection',
57
+ id: '{id}',
58
+ partitionKey: '{id}',
59
+ },
60
+ ],
61
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
62
+ try {
63
+ const document = context.extraInputs.get('cosmosInput');
64
+
65
+ if (!document) {
66
+ return { status: 404, jsonBody: { error: 'Item not found' } };
67
+ }
68
+
69
+ const validated = todoSchema.parse(document);
70
+ return { status: 200, jsonBody: validated };
71
+ } catch (error) {
72
+ context.error(\`Error fetching item from \${containerName}:\`, error);
73
+ return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
74
+ }
75
+ },
76
+ });
77
+
78
+ // POST /api/todo - 新規作成
79
+ app.http('todo-create', {
80
+ methods: ['POST'],
81
+ route: 'todo',
82
+ authLevel: 'anonymous',
83
+ extraOutputs: [
84
+ {
85
+ type: 'cosmosDB',
86
+ name: 'cosmosOutput',
87
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
88
+ containerName,
89
+ connection: 'CosmosDBConnection',
90
+ createIfNotExists: true,
91
+ },
92
+ ],
93
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
94
+ try {
95
+ const body = await request.json() as any;
96
+
97
+ const { id, createdAt, updatedAt, ...userData } = body;
98
+ const now = new Date().toISOString();
99
+ const dataWithManagedFields = {
100
+ ...userData,
101
+ id: id || crypto.randomUUID(),
102
+ createdAt: now,
103
+ updatedAt: now,
104
+ };
105
+
106
+ const result = todoSchema.safeParse(dataWithManagedFields);
107
+
108
+ if (!result.success) {
109
+ context.error('Validation failed:', result.error.issues);
110
+ return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
111
+ }
112
+
113
+ context.extraOutputs.set('cosmosOutput', result.data);
114
+ return { status: 201, jsonBody: result.data };
115
+ } catch (error) {
116
+ context.error(\`Error creating item in \${containerName}:\`, error);
117
+ return { status: 500, jsonBody: { error: 'Failed to create item' } };
118
+ }
119
+ },
120
+ });
121
+
122
+ // PUT /api/todo/{id} - 更新
123
+ app.http('todo-update', {
124
+ methods: ['PUT'],
125
+ route: 'todo/{id}',
126
+ authLevel: 'anonymous',
127
+ extraInputs: [
128
+ {
129
+ type: 'cosmosDB',
130
+ name: 'cosmosInput',
131
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
132
+ containerName,
133
+ connection: 'CosmosDBConnection',
134
+ id: '{id}',
135
+ partitionKey: '{id}',
136
+ },
137
+ ],
138
+ extraOutputs: [
139
+ {
140
+ type: 'cosmosDB',
141
+ name: 'cosmosOutput',
142
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
143
+ containerName,
144
+ connection: 'CosmosDBConnection',
145
+ },
146
+ ],
147
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
148
+ try {
149
+ const id = request.params.id;
150
+ if (!id) {
151
+ return { status: 400, jsonBody: { error: 'ID is required' } };
152
+ }
153
+
154
+ const existingDocument = context.extraInputs.get('cosmosInput') as any;
155
+ if (!existingDocument) {
156
+ return { status: 404, jsonBody: { error: 'Item not found' } };
157
+ }
158
+
159
+ const body = await request.json() as any;
160
+ const { createdAt, updatedAt, ...userData } = body;
161
+
162
+ const dataWithManagedFields = {
163
+ ...userData,
164
+ id,
165
+ createdAt: existingDocument.createdAt,
166
+ updatedAt: new Date().toISOString(),
167
+ };
168
+
169
+ const result = todoSchema.safeParse(dataWithManagedFields);
170
+
171
+ if (!result.success) {
172
+ context.error('Validation failed:', result.error.issues);
173
+ return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
174
+ }
175
+
176
+ context.extraOutputs.set('cosmosOutput', result.data);
177
+ return { status: 200, jsonBody: result.data };
178
+ } catch (error) {
179
+ context.error(\`Error updating item in \${containerName}:\`, error);
180
+ return { status: 500, jsonBody: { error: 'Failed to update item' } };
181
+ }
182
+ },
183
+ });
184
+
185
+ // DELETE /api/todo/{id} - 削除
186
+ app.http('todo-delete', {
187
+ methods: ['DELETE'],
188
+ route: 'todo/{id}',
189
+ authLevel: 'anonymous',
190
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
191
+ try {
192
+ const id = request.params.id;
193
+ if (!id) {
194
+ return { status: 400, jsonBody: { error: 'ID is required' } };
195
+ }
196
+
197
+ const { CosmosClient } = await import('@azure/cosmos');
198
+ const endpoint = process.env.CosmosDBConnection__accountEndpoint;
199
+ let client: InstanceType<typeof CosmosClient>;
200
+ if (endpoint) {
201
+ const { DefaultAzureCredential } = await import('@azure/identity');
202
+ client = new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
203
+ } else {
204
+ client = new CosmosClient(process.env.CosmosDBConnection!);
205
+ }
206
+ const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
207
+ const container = database.container(containerName);
208
+
209
+ await container.item(id, id).delete();
210
+ context.log(\`Deleted item \${id} from \${containerName}\`);
211
+
212
+ return { status: 204 };
213
+ } catch (error: any) {
214
+ if (error.code === 404) {
215
+ return { status: 404, jsonBody: { error: 'Item not found' } };
216
+ }
217
+ context.error(\`Error deleting item from \${containerName}:\`, error);
218
+ return { status: 500, jsonBody: { error: 'Failed to delete item' } };
219
+ }
220
+ },
221
+ });
222
+ "
223
+ `;
224
+
225
+ exports[`generateCompactAzureFunctionsCRUD generates correct CRUD code for a model with enum fields 1`] = `
226
+ "import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
227
+ import { z } from 'zod/v4';
228
+ import crypto from 'crypto';
229
+ import { issueSchema } from '@myapp/shared';
230
+
231
+ const containerName = 'Issues';
232
+
233
+ // GET /api/issue - 全件取得
234
+ app.http('issue-get-all', {
235
+ methods: ['GET'],
236
+ route: 'issue',
237
+ authLevel: 'anonymous',
238
+ extraInputs: [
239
+ {
240
+ type: 'cosmosDB',
241
+ name: 'cosmosInput',
242
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
243
+ containerName,
244
+ connection: 'CosmosDBConnection',
245
+ sqlQuery: 'SELECT * FROM c',
246
+ },
247
+ ],
248
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
249
+ try {
250
+ const documents = context.extraInputs.get('cosmosInput') as any[];
251
+
252
+ if (!documents || !Array.isArray(documents)) {
253
+ return { status: 200, jsonBody: [] };
254
+ }
255
+
256
+ const validated = z.array(issueSchema).parse(documents);
257
+ context.log(\`Fetched \${validated.length} items from \${containerName}\`);
258
+
259
+ return { status: 200, jsonBody: validated };
260
+ } catch (error) {
261
+ context.error(\`Error fetching from \${containerName}:\`, error);
262
+ return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
263
+ }
264
+ },
265
+ });
266
+
267
+ // GET /api/issue/{id} - ID指定取得
268
+ app.http('issue-get-by-id', {
269
+ methods: ['GET'],
270
+ route: 'issue/{id}',
271
+ authLevel: 'anonymous',
272
+ extraInputs: [
273
+ {
274
+ type: 'cosmosDB',
275
+ name: 'cosmosInput',
276
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
277
+ containerName,
278
+ connection: 'CosmosDBConnection',
279
+ id: '{id}',
280
+ partitionKey: '{id}',
281
+ },
282
+ ],
283
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
284
+ try {
285
+ const document = context.extraInputs.get('cosmosInput');
286
+
287
+ if (!document) {
288
+ return { status: 404, jsonBody: { error: 'Item not found' } };
289
+ }
290
+
291
+ const validated = issueSchema.parse(document);
292
+ return { status: 200, jsonBody: validated };
293
+ } catch (error) {
294
+ context.error(\`Error fetching item from \${containerName}:\`, error);
295
+ return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
296
+ }
297
+ },
298
+ });
299
+
300
+ // POST /api/issue - 新規作成
301
+ app.http('issue-create', {
302
+ methods: ['POST'],
303
+ route: 'issue',
304
+ authLevel: 'anonymous',
305
+ extraOutputs: [
306
+ {
307
+ type: 'cosmosDB',
308
+ name: 'cosmosOutput',
309
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
310
+ containerName,
311
+ connection: 'CosmosDBConnection',
312
+ createIfNotExists: true,
313
+ },
314
+ ],
315
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
316
+ try {
317
+ const body = await request.json() as any;
318
+
319
+ const { id, createdAt, updatedAt, ...userData } = body;
320
+ const now = new Date().toISOString();
321
+ const dataWithManagedFields = {
322
+ ...userData,
323
+ id: id || crypto.randomUUID(),
324
+ createdAt: now,
325
+ updatedAt: now,
326
+ };
327
+
328
+ const result = issueSchema.safeParse(dataWithManagedFields);
329
+
330
+ if (!result.success) {
331
+ context.error('Validation failed:', result.error.issues);
332
+ return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
333
+ }
334
+
335
+ context.extraOutputs.set('cosmosOutput', result.data);
336
+ return { status: 201, jsonBody: result.data };
337
+ } catch (error) {
338
+ context.error(\`Error creating item in \${containerName}:\`, error);
339
+ return { status: 500, jsonBody: { error: 'Failed to create item' } };
340
+ }
341
+ },
342
+ });
343
+
344
+ // PUT /api/issue/{id} - 更新
345
+ app.http('issue-update', {
346
+ methods: ['PUT'],
347
+ route: 'issue/{id}',
348
+ authLevel: 'anonymous',
349
+ extraInputs: [
350
+ {
351
+ type: 'cosmosDB',
352
+ name: 'cosmosInput',
353
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
354
+ containerName,
355
+ connection: 'CosmosDBConnection',
356
+ id: '{id}',
357
+ partitionKey: '{id}',
358
+ },
359
+ ],
360
+ extraOutputs: [
361
+ {
362
+ type: 'cosmosDB',
363
+ name: 'cosmosOutput',
364
+ databaseName: process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase',
365
+ containerName,
366
+ connection: 'CosmosDBConnection',
367
+ },
368
+ ],
369
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
370
+ try {
371
+ const id = request.params.id;
372
+ if (!id) {
373
+ return { status: 400, jsonBody: { error: 'ID is required' } };
374
+ }
375
+
376
+ const existingDocument = context.extraInputs.get('cosmosInput') as any;
377
+ if (!existingDocument) {
378
+ return { status: 404, jsonBody: { error: 'Item not found' } };
379
+ }
380
+
381
+ const body = await request.json() as any;
382
+ const { createdAt, updatedAt, ...userData } = body;
383
+
384
+ const dataWithManagedFields = {
385
+ ...userData,
386
+ id,
387
+ createdAt: existingDocument.createdAt,
388
+ updatedAt: new Date().toISOString(),
389
+ };
390
+
391
+ const result = issueSchema.safeParse(dataWithManagedFields);
392
+
393
+ if (!result.success) {
394
+ context.error('Validation failed:', result.error.issues);
395
+ return { status: 400, jsonBody: { error: 'Validation failed', details: result.error.issues } };
396
+ }
397
+
398
+ context.extraOutputs.set('cosmosOutput', result.data);
399
+ return { status: 200, jsonBody: result.data };
400
+ } catch (error) {
401
+ context.error(\`Error updating item in \${containerName}:\`, error);
402
+ return { status: 500, jsonBody: { error: 'Failed to update item' } };
403
+ }
404
+ },
405
+ });
406
+
407
+ // DELETE /api/issue/{id} - 削除
408
+ app.http('issue-delete', {
409
+ methods: ['DELETE'],
410
+ route: 'issue/{id}',
411
+ authLevel: 'anonymous',
412
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
413
+ try {
414
+ const id = request.params.id;
415
+ if (!id) {
416
+ return { status: 400, jsonBody: { error: 'ID is required' } };
417
+ }
418
+
419
+ const { CosmosClient } = await import('@azure/cosmos');
420
+ const endpoint = process.env.CosmosDBConnection__accountEndpoint;
421
+ let client: InstanceType<typeof CosmosClient>;
422
+ if (endpoint) {
423
+ const { DefaultAzureCredential } = await import('@azure/identity');
424
+ client = new CosmosClient({ endpoint, aadCredentials: new DefaultAzureCredential() });
425
+ } else {
426
+ client = new CosmosClient(process.env.CosmosDBConnection!);
427
+ }
428
+ const database = client.database(process.env.COSMOS_DB_DATABASE_NAME || 'AppDatabase');
429
+ const container = database.container(containerName);
430
+
431
+ await container.item(id, id).delete();
432
+ context.log(\`Deleted item \${id} from \${containerName}\`);
433
+
434
+ return { status: 204 };
435
+ } catch (error: any) {
436
+ if (error.code === 404) {
437
+ return { status: 404, jsonBody: { error: 'Item not found' } };
438
+ }
439
+ context.error(\`Error deleting item from \${containerName}:\`, error);
440
+ return { status: 500, jsonBody: { error: 'Failed to delete item' } };
441
+ }
442
+ },
443
+ });
444
+ "
445
+ `;
@@ -0,0 +1,194 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`generateBFFCallFunction generates call function helper code 1`] = `
4
+ "import { NextRequest, NextResponse } from 'next/server';
5
+ import { z } from 'zod/v4';
6
+
7
+ /**
8
+ * SwallowKit BFF Call Function Helper
9
+ * Azure Functions を呼び出す汎用ヘルパー
10
+ *
11
+ * @example
12
+ * // シンプルな GET
13
+ * return callFunction({ method: 'GET', path: '/api/todo', responseSchema: z.array(TodoSchema) });
14
+ *
15
+ * // バリデーション付き POST
16
+ * return callFunction({ method: 'POST', path: '/api/todo', body, inputSchema: InputSchema, responseSchema: TodoSchema, successStatus: 201 });
17
+ *
18
+ * // カスタムビジネスロジック関数の呼び出し
19
+ * return callFunction({ method: 'POST', path: '/api/todo/archive', body: { ids } });
20
+ */
21
+
22
+ function getFunctionsBaseUrl(): string {
23
+ return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
24
+ }
25
+
26
+ interface CallFunctionConfig<TInput = any, TOutput = any> {
27
+ /** HTTP メソッド */
28
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
29
+ /** Azure Functions のパス (例: '/api/todo', '/api/todo/123') */
30
+ path: string;
31
+ /** リクエストボディ (POST/PUT 用) */
32
+ body?: any;
33
+ /** 入力バリデーション用 Zod スキーマ (省略時はバリデーションなし) */
34
+ inputSchema?: z.ZodSchema<TInput>;
35
+ /** 出力バリデーション用 Zod スキーマ (省略時はそのまま返す) */
36
+ responseSchema?: z.ZodSchema<TOutput>;
37
+ /** 成功時の HTTP ステータスコード (デフォルト: 200) */
38
+ successStatus?: number;
39
+ }
40
+
41
+ export async function callFunction<TInput = any, TOutput = any>(
42
+ config: CallFunctionConfig<TInput, TOutput>
43
+ ): Promise<NextResponse> {
44
+ const { method, path, body, inputSchema, responseSchema, successStatus = 200 } = config;
45
+
46
+ try {
47
+ // 入力バリデーション
48
+ let validatedBody = body;
49
+ if (inputSchema && body !== undefined) {
50
+ const result = inputSchema.safeParse(body);
51
+ if (!result.success) {
52
+ console.error('[BFF] Validation failed:', result.error.issues);
53
+ return NextResponse.json(
54
+ { error: 'Validation failed', details: result.error.issues },
55
+ { status: 400 }
56
+ );
57
+ }
58
+ validatedBody = result.data;
59
+ }
60
+
61
+ // Azure Functions を呼び出し
62
+ const functionsBaseUrl = getFunctionsBaseUrl();
63
+ const url = functionsBaseUrl + path;
64
+ console.log(\`[BFF] \${method} \${url}\`);
65
+
66
+ const response = await fetch(url, {
67
+ method,
68
+ headers: { 'Content-Type': 'application/json' },
69
+ body: validatedBody !== undefined ? JSON.stringify(validatedBody) : undefined,
70
+ });
71
+
72
+ console.log('[BFF] Functions response status:', response.status);
73
+
74
+ // エラーレスポンスの転送
75
+ if (!response.ok) {
76
+ const text = await response.text();
77
+ console.error('[BFF] Functions error:', { status: response.status, body: text });
78
+
79
+ let errorMessage = 'Request failed';
80
+ try {
81
+ const error = JSON.parse(text);
82
+ errorMessage = error.error || errorMessage;
83
+ } catch {
84
+ errorMessage = text || errorMessage;
85
+ }
86
+ return NextResponse.json({ error: errorMessage }, { status: response.status });
87
+ }
88
+
89
+ // DELETE 204 の場合はボディなし
90
+ if (response.status === 204 || method === 'DELETE') {
91
+ return new NextResponse(null, { status: 204 });
92
+ }
93
+
94
+ // レスポンスの取得と出力バリデーション
95
+ const data = await response.json();
96
+
97
+ if (responseSchema) {
98
+ const validated = responseSchema.parse(data);
99
+ return NextResponse.json(validated, { status: successStatus });
100
+ }
101
+
102
+ return NextResponse.json(data, { status: successStatus });
103
+ } catch (error: any) {
104
+ console.error(\`[BFF] Error:\`, error);
105
+ return NextResponse.json(
106
+ { error: error.message || 'Internal server error' },
107
+ { status: 500 }
108
+ );
109
+ }
110
+ }
111
+ "
112
+ `;
113
+
114
+ exports[`generateCompactBFFRoutes generates list and detail routes 1`] = `
115
+ "import { NextRequest } from 'next/server';
116
+ import { callFunction } from '@/lib/api/call-function';
117
+ import { todoSchema } from '@myapp/shared';
118
+ import { z } from 'zod/v4';
119
+
120
+ const InputSchema = todoSchema.omit({ id: true, createdAt: true, updatedAt: true });
121
+
122
+ // GET /api/todo - 一覧取得
123
+ export async function GET() {
124
+ return callFunction({
125
+ method: 'GET',
126
+ path: '/api/todo',
127
+ responseSchema: z.array(todoSchema),
128
+ });
129
+ }
130
+
131
+ // POST /api/todo - 新規作成
132
+ export async function POST(request: NextRequest) {
133
+ const body = await request.json();
134
+ return callFunction({
135
+ method: 'POST',
136
+ path: '/api/todo',
137
+ body,
138
+ inputSchema: InputSchema,
139
+ responseSchema: todoSchema,
140
+ successStatus: 201,
141
+ });
142
+ }
143
+ "
144
+ `;
145
+
146
+ exports[`generateCompactBFFRoutes generates list and detail routes 2`] = `
147
+ "import { NextRequest } from 'next/server';
148
+ import { callFunction } from '@/lib/api/call-function';
149
+ import { todoSchema } from '@myapp/shared';
150
+
151
+ const InputSchema = todoSchema.omit({ id: true, createdAt: true, updatedAt: true });
152
+
153
+ // GET /api/todo/{id} - 詳細取得
154
+ export async function GET(
155
+ _request: NextRequest,
156
+ { params }: { params: Promise<{ id: string }> }
157
+ ) {
158
+ const { id } = await params;
159
+ return callFunction({
160
+ method: 'GET',
161
+ path: \`/api/todo/\${id}\`,
162
+ responseSchema: todoSchema,
163
+ });
164
+ }
165
+
166
+ // PUT /api/todo/{id} - 更新
167
+ export async function PUT(
168
+ request: NextRequest,
169
+ { params }: { params: Promise<{ id: string }> }
170
+ ) {
171
+ const { id } = await params;
172
+ const body = await request.json();
173
+ return callFunction({
174
+ method: 'PUT',
175
+ path: \`/api/todo/\${id}\`,
176
+ body,
177
+ inputSchema: InputSchema,
178
+ responseSchema: todoSchema,
179
+ });
180
+ }
181
+
182
+ // DELETE /api/todo/{id} - 削除
183
+ export async function DELETE(
184
+ _request: NextRequest,
185
+ { params }: { params: Promise<{ id: string }> }
186
+ ) {
187
+ const { id } = await params;
188
+ return callFunction({
189
+ method: 'DELETE',
190
+ path: \`/api/todo/\${id}\`,
191
+ });
192
+ }
193
+ "
194
+ `;