swallowkit 1.0.0-beta.13 → 1.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +2 -0
- package/README.md +2 -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 +365 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/dev.js +33 -3
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +145 -10
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +5 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +28 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -1
- package/dist/core/mock/connector-mock-server.js +195 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -1
- 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 +1246 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.js +37 -18
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts +2 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +21 -12
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +7 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +45 -0
- package/dist/core/scaffold/model-parser.js.map +1 -1
- 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 +120 -12
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/types/index.d.ts +30 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +9 -1
- package/src/__tests__/auth.test.ts +654 -0
- package/src/__tests__/connector-mock-server.test.ts +151 -2
- package/src/cli/commands/add-auth.ts +413 -0
- package/src/cli/commands/dev.ts +34 -4
- package/src/cli/commands/scaffold.ts +165 -11
- package/src/cli/index.ts +11 -0
- package/src/core/config.ts +13 -2
- package/src/core/mock/connector-mock-server.ts +248 -0
- package/src/core/scaffold/auth-generator.ts +1286 -0
- package/src/core/scaffold/connector-functions-generator.ts +42 -18
- package/src/core/scaffold/functions-generator.ts +23 -12
- package/src/core/scaffold/model-parser.ts +50 -1
- package/src/core/scaffold/ui-generator.ts +132 -12
- package/src/types/index.ts +42 -0
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
RdbModelConnectorConfig,
|
|
12
12
|
ApiModelConnectorConfig,
|
|
13
13
|
ConnectorOperation,
|
|
14
|
+
ModelAuthPolicy,
|
|
14
15
|
} from "../../types";
|
|
16
|
+
import { generateAuthImportTS, generateAuthGuardTS } from "./auth-generator";
|
|
15
17
|
|
|
16
18
|
// ─── TypeScript Generators ────────────────────────────────────────
|
|
17
19
|
|
|
@@ -22,7 +24,8 @@ export function generateRdbConnectorFunctionTS(
|
|
|
22
24
|
model: ModelInfo,
|
|
23
25
|
sharedPackageName: string,
|
|
24
26
|
connectorDef: RdbConnectorConfig,
|
|
25
|
-
modelConnector: RdbModelConnectorConfig
|
|
27
|
+
modelConnector: RdbModelConnectorConfig,
|
|
28
|
+
authPolicy?: ModelAuthPolicy
|
|
26
29
|
): string {
|
|
27
30
|
const modelCamel = toCamelCase(model.name);
|
|
28
31
|
const schemaName = model.schemaName;
|
|
@@ -118,10 +121,20 @@ export function generateRdbConnectorFunctionTS(
|
|
|
118
121
|
await pool.close();
|
|
119
122
|
}`;
|
|
120
123
|
|
|
124
|
+
// Auth guard setup
|
|
125
|
+
const hasAuth = !!authPolicy;
|
|
126
|
+
const authImport = hasAuth ? `\n${generateAuthImportTS()}\n` : '';
|
|
127
|
+
const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read')}\n` : '';
|
|
128
|
+
const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write')}\n` : '';
|
|
129
|
+
const authCatchBlock = hasAuth
|
|
130
|
+
? ` const authErr = handleAuthError(error);
|
|
131
|
+
if (authErr) return authErr;\n `
|
|
132
|
+
: ` `;
|
|
133
|
+
|
|
121
134
|
let code = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
122
135
|
import { z } from 'zod/v4';
|
|
123
136
|
import { ${schemaName} } from '${sharedPackageName}';
|
|
124
|
-
${driverImport}
|
|
137
|
+
${driverImport}${authImport}
|
|
125
138
|
|
|
126
139
|
${getConnection}
|
|
127
140
|
`;
|
|
@@ -134,10 +147,10 @@ app.http('${modelCamel}-get-all', {
|
|
|
134
147
|
route: '${modelCamel}',
|
|
135
148
|
authLevel: 'anonymous',
|
|
136
149
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
137
|
-
try {
|
|
150
|
+
try {${readGuard}
|
|
138
151
|
${queryAll}
|
|
139
152
|
} catch (error) {
|
|
140
|
-
context.error(\`Error fetching from ${table}:\`, error);
|
|
153
|
+
${authCatchBlock}context.error(\`Error fetching from ${table}:\`, error);
|
|
141
154
|
return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
|
|
142
155
|
}
|
|
143
156
|
},
|
|
@@ -153,14 +166,14 @@ app.http('${modelCamel}-get-by-id', {
|
|
|
153
166
|
route: '${modelCamel}/{id}',
|
|
154
167
|
authLevel: 'anonymous',
|
|
155
168
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
156
|
-
try {
|
|
169
|
+
try {${readGuard}
|
|
157
170
|
const id = request.params.id;
|
|
158
171
|
if (!id) {
|
|
159
172
|
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
160
173
|
}
|
|
161
174
|
${queryById}
|
|
162
175
|
} catch (error) {
|
|
163
|
-
context.error(\`Error fetching item from ${table}:\`, error);
|
|
176
|
+
${authCatchBlock}context.error(\`Error fetching item from ${table}:\`, error);
|
|
164
177
|
return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
|
|
165
178
|
}
|
|
166
179
|
},
|
|
@@ -178,7 +191,8 @@ export function generateApiConnectorFunctionTS(
|
|
|
178
191
|
model: ModelInfo,
|
|
179
192
|
sharedPackageName: string,
|
|
180
193
|
connectorDef: ApiConnectorConfig,
|
|
181
|
-
modelConnector: ApiModelConnectorConfig
|
|
194
|
+
modelConnector: ApiModelConnectorConfig,
|
|
195
|
+
authPolicy?: ModelAuthPolicy
|
|
182
196
|
): string {
|
|
183
197
|
const modelCamel = toCamelCase(model.name);
|
|
184
198
|
const schemaName = model.schemaName;
|
|
@@ -188,9 +202,19 @@ export function generateApiConnectorFunctionTS(
|
|
|
188
202
|
|
|
189
203
|
const authSetup = generateTSAuthSetup(connectorDef);
|
|
190
204
|
|
|
205
|
+
// Auth guard setup (user access control, separate from connector auth to external APIs)
|
|
206
|
+
const hasAuth = !!authPolicy;
|
|
207
|
+
const authImportLine = hasAuth ? `\n${generateAuthImportTS()}\n` : '';
|
|
208
|
+
const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read')}\n` : '';
|
|
209
|
+
const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write')}\n` : '';
|
|
210
|
+
const authCatchBlock = hasAuth
|
|
211
|
+
? ` const authErr = handleAuthError(error);
|
|
212
|
+
if (authErr) return authErr;\n `
|
|
213
|
+
: ` `;
|
|
214
|
+
|
|
191
215
|
let code = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
192
216
|
import { z } from 'zod/v4';
|
|
193
|
-
import { ${schemaName} } from '${sharedPackageName}'
|
|
217
|
+
import { ${schemaName} } from '${sharedPackageName}';${authImportLine}
|
|
194
218
|
|
|
195
219
|
function getBaseUrl(): string {
|
|
196
220
|
return process.env.${baseUrlEnv} || '';
|
|
@@ -209,7 +233,7 @@ app.http('${modelCamel}-get-all', {
|
|
|
209
233
|
route: '${modelCamel}',
|
|
210
234
|
authLevel: 'anonymous',
|
|
211
235
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
212
|
-
try {
|
|
236
|
+
try {${readGuard}
|
|
213
237
|
const url = getBaseUrl() + '${epPath}';
|
|
214
238
|
const response = await fetch(url, {
|
|
215
239
|
method: 'GET',
|
|
@@ -225,7 +249,7 @@ app.http('${modelCamel}-get-all', {
|
|
|
225
249
|
const validated = z.array(${schemaName}).parse(data);
|
|
226
250
|
return { status: 200, jsonBody: validated };
|
|
227
251
|
} catch (error) {
|
|
228
|
-
context.error(\`Error fetching from external API:\`, error);
|
|
252
|
+
${authCatchBlock}context.error(\`Error fetching from external API:\`, error);
|
|
229
253
|
return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
|
|
230
254
|
}
|
|
231
255
|
},
|
|
@@ -243,7 +267,7 @@ app.http('${modelCamel}-get-by-id', {
|
|
|
243
267
|
route: '${modelCamel}/{id}',
|
|
244
268
|
authLevel: 'anonymous',
|
|
245
269
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
246
|
-
try {
|
|
270
|
+
try {${readGuard}
|
|
247
271
|
const id = request.params.id;
|
|
248
272
|
if (!id) {
|
|
249
273
|
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
@@ -265,7 +289,7 @@ app.http('${modelCamel}-get-by-id', {
|
|
|
265
289
|
const validated = ${schemaName}.parse(data);
|
|
266
290
|
return { status: 200, jsonBody: validated };
|
|
267
291
|
} catch (error) {
|
|
268
|
-
context.error(\`Error fetching item from external API:\`, error);
|
|
292
|
+
${authCatchBlock}context.error(\`Error fetching item from external API:\`, error);
|
|
269
293
|
return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
|
|
270
294
|
}
|
|
271
295
|
},
|
|
@@ -283,7 +307,7 @@ app.http('${modelCamel}-create', {
|
|
|
283
307
|
route: '${modelCamel}',
|
|
284
308
|
authLevel: 'anonymous',
|
|
285
309
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
286
|
-
try {
|
|
310
|
+
try {${writeGuard}
|
|
287
311
|
const body = await request.json();
|
|
288
312
|
const url = getBaseUrl() + '${epPath}';
|
|
289
313
|
const response = await fetch(url, {
|
|
@@ -301,7 +325,7 @@ app.http('${modelCamel}-create', {
|
|
|
301
325
|
const data = await response.json();
|
|
302
326
|
return { status: 201, jsonBody: data };
|
|
303
327
|
} catch (error) {
|
|
304
|
-
context.error(\`Error creating item via external API:\`, error);
|
|
328
|
+
${authCatchBlock}context.error(\`Error creating item via external API:\`, error);
|
|
305
329
|
return { status: 500, jsonBody: { error: 'Failed to create item' } };
|
|
306
330
|
}
|
|
307
331
|
},
|
|
@@ -321,7 +345,7 @@ app.http('${modelCamel}-update', {
|
|
|
321
345
|
route: '${modelCamel}/{id}',
|
|
322
346
|
authLevel: 'anonymous',
|
|
323
347
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
324
|
-
try {
|
|
348
|
+
try {${writeGuard}
|
|
325
349
|
const id = request.params.id;
|
|
326
350
|
if (!id) {
|
|
327
351
|
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
@@ -343,7 +367,7 @@ app.http('${modelCamel}-update', {
|
|
|
343
367
|
const data = await response.json();
|
|
344
368
|
return { status: 200, jsonBody: data };
|
|
345
369
|
} catch (error) {
|
|
346
|
-
context.error(\`Error updating item via external API:\`, error);
|
|
370
|
+
${authCatchBlock}context.error(\`Error updating item via external API:\`, error);
|
|
347
371
|
return { status: 500, jsonBody: { error: 'Failed to update item' } };
|
|
348
372
|
}
|
|
349
373
|
},
|
|
@@ -361,7 +385,7 @@ app.http('${modelCamel}-delete', {
|
|
|
361
385
|
route: '${modelCamel}/{id}',
|
|
362
386
|
authLevel: 'anonymous',
|
|
363
387
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
364
|
-
try {
|
|
388
|
+
try {${writeGuard}
|
|
365
389
|
const id = request.params.id;
|
|
366
390
|
if (!id) {
|
|
367
391
|
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
@@ -378,7 +402,7 @@ app.http('${modelCamel}-delete', {
|
|
|
378
402
|
|
|
379
403
|
return { status: 204 };
|
|
380
404
|
} catch (error) {
|
|
381
|
-
context.error(\`Error deleting item via external API:\`, error);
|
|
405
|
+
${authCatchBlock}context.error(\`Error deleting item via external API:\`, error);
|
|
382
406
|
return { status: 500, jsonBody: { error: 'Failed to delete item' } };
|
|
383
407
|
}
|
|
384
408
|
},
|
|
@@ -5,21 +5,32 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
|
|
8
|
+
import { ModelAuthPolicy } from "../../types";
|
|
9
|
+
import { generateAuthImportTS, generateAuthGuardTS } from "./auth-generator";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Azure Functions エンティティファイルを生成(インラインハンドラー方式)
|
|
11
13
|
* 各ハンドラーがベタ書きされており、ビジネスロジックの追加・変更が容易
|
|
12
14
|
*/
|
|
13
|
-
export function generateCompactAzureFunctionsCRUD(model: ModelInfo, sharedPackageName: string): string {
|
|
15
|
+
export function generateCompactAzureFunctionsCRUD(model: ModelInfo, sharedPackageName: string, authPolicy?: ModelAuthPolicy): string {
|
|
14
16
|
const modelName = model.name;
|
|
15
17
|
const modelCamel = toCamelCase(modelName);
|
|
16
18
|
const modelKebab = toKebabCase(modelName);
|
|
17
19
|
const schemaName = model.schemaName;
|
|
18
20
|
|
|
21
|
+
const hasAuth = !!authPolicy;
|
|
22
|
+
const authImport = hasAuth ? `\n${generateAuthImportTS()}\n` : '';
|
|
23
|
+
const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read')}\n` : '';
|
|
24
|
+
const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write')}\n` : '';
|
|
25
|
+
const authCatchBlock = hasAuth
|
|
26
|
+
? ` const authErr = handleAuthError(error);
|
|
27
|
+
if (authErr) return authErr;\n`
|
|
28
|
+
: '';
|
|
29
|
+
|
|
19
30
|
return `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
20
31
|
import { z } from 'zod/v4';
|
|
21
32
|
import crypto from 'crypto';
|
|
22
|
-
import { ${schemaName} } from '${sharedPackageName}'
|
|
33
|
+
import { ${schemaName} } from '${sharedPackageName}';${authImport}
|
|
23
34
|
|
|
24
35
|
const containerName = '${modelName}s';
|
|
25
36
|
|
|
@@ -39,7 +50,7 @@ app.http('${modelCamel}-get-all', {
|
|
|
39
50
|
},
|
|
40
51
|
],
|
|
41
52
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
42
|
-
try {
|
|
53
|
+
try {${readGuard}
|
|
43
54
|
const documents = context.extraInputs.get('cosmosInput') as any[];
|
|
44
55
|
|
|
45
56
|
if (!documents || !Array.isArray(documents)) {
|
|
@@ -51,7 +62,7 @@ app.http('${modelCamel}-get-all', {
|
|
|
51
62
|
|
|
52
63
|
return { status: 200, jsonBody: validated };
|
|
53
64
|
} catch (error) {
|
|
54
|
-
context.error(\`Error fetching from \${containerName}:\`, error);
|
|
65
|
+
${authCatchBlock} context.error(\`Error fetching from \${containerName}:\`, error);
|
|
55
66
|
return { status: 500, jsonBody: { error: 'Failed to fetch items' } };
|
|
56
67
|
}
|
|
57
68
|
},
|
|
@@ -74,7 +85,7 @@ app.http('${modelCamel}-get-by-id', {
|
|
|
74
85
|
},
|
|
75
86
|
],
|
|
76
87
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
77
|
-
try {
|
|
88
|
+
try {${readGuard}
|
|
78
89
|
const document = context.extraInputs.get('cosmosInput');
|
|
79
90
|
|
|
80
91
|
if (!document) {
|
|
@@ -84,7 +95,7 @@ app.http('${modelCamel}-get-by-id', {
|
|
|
84
95
|
const validated = ${schemaName}.parse(document);
|
|
85
96
|
return { status: 200, jsonBody: validated };
|
|
86
97
|
} catch (error) {
|
|
87
|
-
context.error(\`Error fetching item from \${containerName}:\`, error);
|
|
98
|
+
${authCatchBlock} context.error(\`Error fetching item from \${containerName}:\`, error);
|
|
88
99
|
return { status: 500, jsonBody: { error: 'Failed to fetch item' } };
|
|
89
100
|
}
|
|
90
101
|
},
|
|
@@ -106,7 +117,7 @@ app.http('${modelCamel}-create', {
|
|
|
106
117
|
},
|
|
107
118
|
],
|
|
108
119
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
109
|
-
try {
|
|
120
|
+
try {${writeGuard}
|
|
110
121
|
const body = await request.json() as any;
|
|
111
122
|
|
|
112
123
|
const { id, createdAt, updatedAt, ...userData } = body;
|
|
@@ -128,7 +139,7 @@ app.http('${modelCamel}-create', {
|
|
|
128
139
|
context.extraOutputs.set('cosmosOutput', result.data);
|
|
129
140
|
return { status: 201, jsonBody: result.data };
|
|
130
141
|
} catch (error) {
|
|
131
|
-
context.error(\`Error creating item in \${containerName}:\`, error);
|
|
142
|
+
${authCatchBlock} context.error(\`Error creating item in \${containerName}:\`, error);
|
|
132
143
|
return { status: 500, jsonBody: { error: 'Failed to create item' } };
|
|
133
144
|
}
|
|
134
145
|
},
|
|
@@ -160,7 +171,7 @@ app.http('${modelCamel}-update', {
|
|
|
160
171
|
},
|
|
161
172
|
],
|
|
162
173
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
163
|
-
try {
|
|
174
|
+
try {${writeGuard}
|
|
164
175
|
const id = request.params.id;
|
|
165
176
|
if (!id) {
|
|
166
177
|
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
@@ -191,7 +202,7 @@ app.http('${modelCamel}-update', {
|
|
|
191
202
|
context.extraOutputs.set('cosmosOutput', result.data);
|
|
192
203
|
return { status: 200, jsonBody: result.data };
|
|
193
204
|
} catch (error) {
|
|
194
|
-
context.error(\`Error updating item in \${containerName}:\`, error);
|
|
205
|
+
${authCatchBlock} context.error(\`Error updating item in \${containerName}:\`, error);
|
|
195
206
|
return { status: 500, jsonBody: { error: 'Failed to update item' } };
|
|
196
207
|
}
|
|
197
208
|
},
|
|
@@ -203,7 +214,7 @@ app.http('${modelCamel}-delete', {
|
|
|
203
214
|
route: '${modelCamel}/{id}',
|
|
204
215
|
authLevel: 'anonymous',
|
|
205
216
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
206
|
-
try {
|
|
217
|
+
try {${writeGuard}
|
|
207
218
|
const id = request.params.id;
|
|
208
219
|
if (!id) {
|
|
209
220
|
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
@@ -229,7 +240,7 @@ app.http('${modelCamel}-delete', {
|
|
|
229
240
|
if (error.code === 404) {
|
|
230
241
|
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
231
242
|
}
|
|
232
|
-
context.error(\`Error deleting item from \${containerName}:\`, error);
|
|
243
|
+
${authCatchBlock} context.error(\`Error deleting item from \${containerName}:\`, error);
|
|
233
244
|
return { status: 500, jsonBody: { error: 'Failed to delete item' } };
|
|
234
245
|
}
|
|
235
246
|
},
|
|
@@ -6,7 +6,7 @@ import * as fs from "fs";
|
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { pathToFileURL } from "url";
|
|
8
8
|
|
|
9
|
-
import { ModelConnectorConfig } from "../../types";
|
|
9
|
+
import { ModelConnectorConfig, ModelAuthPolicy } from "../../types";
|
|
10
10
|
|
|
11
11
|
export interface ModelInfo {
|
|
12
12
|
name: string; // モデル名(例: "Todo")
|
|
@@ -19,6 +19,7 @@ export interface ModelInfo {
|
|
|
19
19
|
hasUpdatedAt: boolean; // updatedAt フィールドがあるか
|
|
20
20
|
nestedSchemaRefs: NestedSchemaRef[]; // ネストしたスキーマ参照
|
|
21
21
|
connectorConfig?: ModelConnectorConfig; // コネクタメタデータ(外部データソース用)
|
|
22
|
+
authPolicy?: ModelAuthPolicy; // 認可ポリシー(ロールベースアクセス制御用)
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface FieldInfo {
|
|
@@ -85,6 +86,9 @@ export async function parseModelFile(modelPath: string): Promise<ModelInfo> {
|
|
|
85
86
|
|
|
86
87
|
// connectorConfig を抽出(外部データソース用メタデータ)
|
|
87
88
|
const connectorConfig = parseConnectorConfig(content);
|
|
89
|
+
|
|
90
|
+
// authPolicy を抽出(ロールベースアクセス制御用メタデータ)
|
|
91
|
+
const authPolicy = parseAuthPolicy(content);
|
|
88
92
|
|
|
89
93
|
// ネストしたスキーマ参照を検出
|
|
90
94
|
const nestedSchemaRefs = detectNestedSchemaRefs(modelPath, content, schemaName);
|
|
@@ -111,6 +115,7 @@ export async function parseModelFile(modelPath: string): Promise<ModelInfo> {
|
|
|
111
115
|
hasUpdatedAt,
|
|
112
116
|
nestedSchemaRefs,
|
|
113
117
|
...(connectorConfig ? { connectorConfig } : {}),
|
|
118
|
+
...(authPolicy ? { authPolicy } : {}),
|
|
114
119
|
};
|
|
115
120
|
}
|
|
116
121
|
|
|
@@ -727,6 +732,50 @@ export function parseConnectorConfig(content: string): ModelConnectorConfig | un
|
|
|
727
732
|
};
|
|
728
733
|
}
|
|
729
734
|
|
|
735
|
+
/**
|
|
736
|
+
* authPolicy をモデルファイルから抽出
|
|
737
|
+
* パターン: export const authPolicy = { roles: [...], read: [...], write: [...] }
|
|
738
|
+
*/
|
|
739
|
+
export function parseAuthPolicy(content: string): ModelAuthPolicy | undefined {
|
|
740
|
+
const policyMatch = content.match(/export\s+const\s+authPolicy\s*=\s*\{/);
|
|
741
|
+
if (!policyMatch) {
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const startIdx = content.indexOf('{', policyMatch.index!);
|
|
746
|
+
let braceCount = 1;
|
|
747
|
+
let endIdx = startIdx + 1;
|
|
748
|
+
while (braceCount > 0 && endIdx < content.length) {
|
|
749
|
+
if (content[endIdx] === '{') braceCount++;
|
|
750
|
+
if (content[endIdx] === '}') braceCount--;
|
|
751
|
+
endIdx++;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const objectStr = content.substring(startIdx, endIdx);
|
|
755
|
+
|
|
756
|
+
const extractRoles = (key: string): string[] | undefined => {
|
|
757
|
+
const match = objectStr.match(new RegExp(`${key}\\s*:\\s*\\[([^\\]]*)\\]`));
|
|
758
|
+
if (!match) return undefined;
|
|
759
|
+
const entries = match[1].match(/['"]([^'"]+)['"]/g);
|
|
760
|
+
if (!entries) return [];
|
|
761
|
+
return entries.map(e => e.replace(/['"]/g, ''));
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
const roles = extractRoles('roles');
|
|
765
|
+
const read = extractRoles('read');
|
|
766
|
+
const write = extractRoles('write');
|
|
767
|
+
|
|
768
|
+
if (!roles && !read && !write) {
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
...(roles ? { roles } : {}),
|
|
774
|
+
...(read ? { read } : {}),
|
|
775
|
+
...(write ? { write } : {}),
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
730
779
|
/**
|
|
731
780
|
* 文字列を kebab-case に変換
|
|
732
781
|
*/
|
|
@@ -4,15 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { ModelInfo, FieldInfo, toCamelCase, toKebabCase, toPascalCase } from "./model-parser";
|
|
7
|
+
import { ModelAuthPolicy } from "../../types";
|
|
8
|
+
|
|
9
|
+
/** auth が有効かつ authPolicy が存在する場合に渡されるオプション */
|
|
10
|
+
export interface UIAuthOptions {
|
|
11
|
+
/** モデル固有のロールポリシー */
|
|
12
|
+
authPolicy: ModelAuthPolicy;
|
|
13
|
+
}
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
16
|
* 一覧画面を生成
|
|
10
17
|
*/
|
|
11
|
-
export function generateListPage(model: ModelInfo, sharedPackageName: string): string {
|
|
18
|
+
export function generateListPage(model: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions): string {
|
|
12
19
|
const modelName = model.name;
|
|
13
20
|
const modelCamel = toCamelCase(modelName);
|
|
14
21
|
const modelKebab = toKebabCase(modelName);
|
|
15
22
|
|
|
23
|
+
const hasAuth = !!authOptions;
|
|
24
|
+
const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
|
|
25
|
+
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
26
|
+
|
|
16
27
|
// フィールドから表示するカラムを抽出(id以外の最初の3つ)
|
|
17
28
|
const displayFields = model.fields
|
|
18
29
|
.filter(f => f.name !== 'id')
|
|
@@ -64,6 +75,7 @@ import { useEffect, useState } from 'react';
|
|
|
64
75
|
import Link from 'next/link';
|
|
65
76
|
import { z } from 'zod/v4';
|
|
66
77
|
${schemaImportLine}
|
|
78
|
+
${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
|
|
67
79
|
|
|
68
80
|
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
69
81
|
|
|
@@ -72,6 +84,9 @@ export default function ${modelName}ListPage() {
|
|
|
72
84
|
const [loading, setLoading] = useState(true);
|
|
73
85
|
const [error, setError] = useState<string | null>(null);
|
|
74
86
|
${hasForeignKeys ? foreignKeyStates : ''}
|
|
87
|
+
${hasWriteRoles ? ` const { hasAnyRole } = useAuth();
|
|
88
|
+
const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : hasAuth ? ` const { user } = useAuth();
|
|
89
|
+
const canWrite = !!user;` : ''}
|
|
75
90
|
|
|
76
91
|
useEffect(() => {
|
|
77
92
|
fetch('/api/${modelCamel}')
|
|
@@ -135,12 +150,19 @@ ${hasForeignKeys ? foreignKeyFetches : ''}
|
|
|
135
150
|
</div>
|
|
136
151
|
<div className="flex justify-between items-center mb-6">
|
|
137
152
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName}</h1>
|
|
138
|
-
|
|
153
|
+
${hasAuth ? ` {canWrite && (
|
|
154
|
+
<Link
|
|
155
|
+
href="/${modelKebab}/new"
|
|
156
|
+
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
|
|
157
|
+
>
|
|
158
|
+
Create New
|
|
159
|
+
</Link>
|
|
160
|
+
)}` : ` <Link
|
|
139
161
|
href="/${modelKebab}/new"
|
|
140
162
|
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
|
|
141
163
|
>
|
|
142
164
|
Create New
|
|
143
|
-
</Link
|
|
165
|
+
</Link>`}
|
|
144
166
|
</div>
|
|
145
167
|
|
|
146
168
|
{${modelCamel}s.length === 0 ? (
|
|
@@ -199,7 +221,22 @@ ${displayFields.map(f => {
|
|
|
199
221
|
>
|
|
200
222
|
View
|
|
201
223
|
</Link>
|
|
202
|
-
|
|
224
|
+
${hasAuth ? ` {canWrite && (
|
|
225
|
+
<>
|
|
226
|
+
<Link
|
|
227
|
+
href={\`/${modelKebab}/\${item.id}/edit\`}
|
|
228
|
+
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
|
|
229
|
+
>
|
|
230
|
+
Edit
|
|
231
|
+
</Link>
|
|
232
|
+
<button
|
|
233
|
+
onClick={() => handleDelete(item.id)}
|
|
234
|
+
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
|
235
|
+
>
|
|
236
|
+
Delete
|
|
237
|
+
</button>
|
|
238
|
+
</>
|
|
239
|
+
)}` : ` <Link
|
|
203
240
|
href={\`/${modelKebab}/\${item.id}/edit\`}
|
|
204
241
|
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
|
|
205
242
|
>
|
|
@@ -210,7 +247,7 @@ ${displayFields.map(f => {
|
|
|
210
247
|
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
|
211
248
|
>
|
|
212
249
|
Delete
|
|
213
|
-
</button
|
|
250
|
+
</button>`}
|
|
214
251
|
</td>
|
|
215
252
|
</tr>
|
|
216
253
|
))}
|
|
@@ -227,11 +264,15 @@ ${displayFields.map(f => {
|
|
|
227
264
|
/**
|
|
228
265
|
* 詳細画面を生成
|
|
229
266
|
*/
|
|
230
|
-
export function generateDetailPage(model: ModelInfo, sharedPackageName: string): string {
|
|
267
|
+
export function generateDetailPage(model: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions): string {
|
|
231
268
|
const modelName = model.name;
|
|
232
269
|
const modelCamel = toCamelCase(modelName);
|
|
233
270
|
const modelKebab = toKebabCase(modelName);
|
|
234
271
|
|
|
272
|
+
const hasAuth = !!authOptions;
|
|
273
|
+
const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
|
|
274
|
+
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
275
|
+
|
|
235
276
|
// 外部キーフィールドを検出
|
|
236
277
|
const foreignKeyFields = model.fields.filter(f => f.isForeignKey);
|
|
237
278
|
const hasForeignKeys = foreignKeyFields.length > 0;
|
|
@@ -274,6 +315,7 @@ import { useParams, useRouter } from 'next/navigation';
|
|
|
274
315
|
import Link from 'next/link';
|
|
275
316
|
import { z } from 'zod/v4';
|
|
276
317
|
${schemaImportLine}
|
|
318
|
+
${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
|
|
277
319
|
|
|
278
320
|
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
279
321
|
|
|
@@ -284,6 +326,9 @@ export default function ${modelName}DetailPage() {
|
|
|
284
326
|
const [loading, setLoading] = useState(true);
|
|
285
327
|
const [error, setError] = useState<string | null>(null);
|
|
286
328
|
${hasForeignKeys ? foreignKeyStates : ''}
|
|
329
|
+
${hasWriteRoles ? ` const { hasAnyRole } = useAuth();
|
|
330
|
+
const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : hasAuth ? ` const { user } = useAuth();
|
|
331
|
+
const canWrite = !!user;` : ''}
|
|
287
332
|
|
|
288
333
|
useEffect(() => {
|
|
289
334
|
const id = params?.id as string;
|
|
@@ -343,7 +388,22 @@ ${hasForeignKeys ? foreignKeyFetches : ''}
|
|
|
343
388
|
<div className="max-w-2xl mx-auto">
|
|
344
389
|
<div className="flex justify-between items-center mb-6">
|
|
345
390
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName} Details</h1>
|
|
346
|
-
|
|
391
|
+
${hasAuth ? ` {canWrite && (
|
|
392
|
+
<div className="space-x-2">
|
|
393
|
+
<Link
|
|
394
|
+
href={\`/${modelKebab}/\${${modelCamel}.id}/edit\`}
|
|
395
|
+
className="inline-flex items-center bg-green-600 hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 text-white px-4 py-2 rounded"
|
|
396
|
+
>
|
|
397
|
+
Edit
|
|
398
|
+
</Link>
|
|
399
|
+
<button
|
|
400
|
+
onClick={handleDelete}
|
|
401
|
+
className="inline-flex items-center bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 text-white px-4 py-2 rounded"
|
|
402
|
+
>
|
|
403
|
+
Delete
|
|
404
|
+
</button>
|
|
405
|
+
</div>
|
|
406
|
+
)}` : ` <div className="space-x-2">
|
|
347
407
|
<Link
|
|
348
408
|
href={\`/${modelKebab}/\${${modelCamel}.id}/edit\`}
|
|
349
409
|
className="inline-flex items-center bg-green-600 hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 text-white px-4 py-2 rounded"
|
|
@@ -356,7 +416,7 @@ ${hasForeignKeys ? foreignKeyFetches : ''}
|
|
|
356
416
|
>
|
|
357
417
|
Delete
|
|
358
418
|
</button>
|
|
359
|
-
</div
|
|
419
|
+
</div>`}
|
|
360
420
|
</div>
|
|
361
421
|
|
|
362
422
|
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
@@ -849,13 +909,57 @@ ${f.enumValues.map(v => ` <option value="${v}">${v}</option>`).join('\n
|
|
|
849
909
|
/**
|
|
850
910
|
* 新規作成画面を生成
|
|
851
911
|
*/
|
|
852
|
-
export function generateNewPage(model: ModelInfo): string {
|
|
912
|
+
export function generateNewPage(model: ModelInfo, authOptions?: UIAuthOptions): string {
|
|
853
913
|
const modelName = model.name;
|
|
854
914
|
const modelKebab = toKebabCase(modelName);
|
|
855
915
|
|
|
856
|
-
|
|
916
|
+
const hasAuth = !!authOptions;
|
|
917
|
+
const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
|
|
918
|
+
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
919
|
+
|
|
920
|
+
if (!hasAuth) {
|
|
921
|
+
return `import ${modelName}Form from '../_components/${modelName}Form';
|
|
922
|
+
|
|
923
|
+
export default function New${modelName}Page() {
|
|
924
|
+
return (
|
|
925
|
+
<div className="container mx-auto px-4 py-8">
|
|
926
|
+
<div className="max-w-2xl mx-auto">
|
|
927
|
+
<h1 className="text-3xl font-bold mb-6">Create New ${modelName}</h1>
|
|
928
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
929
|
+
<${modelName}Form />
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
`;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return `'use client';
|
|
939
|
+
|
|
940
|
+
import { useEffect } from 'react';
|
|
941
|
+
import { useRouter } from 'next/navigation';
|
|
942
|
+
import ${modelName}Form from '../_components/${modelName}Form';
|
|
943
|
+
import { useAuth } from '@/lib/auth/auth-context';
|
|
857
944
|
|
|
858
945
|
export default function New${modelName}Page() {
|
|
946
|
+
const router = useRouter();
|
|
947
|
+
${hasWriteRoles ? ` const { hasAnyRole, loading } = useAuth();
|
|
948
|
+
const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : ` const { user, loading } = useAuth();
|
|
949
|
+
const canWrite = !!user;`}
|
|
950
|
+
|
|
951
|
+
useEffect(() => {
|
|
952
|
+
if (!loading && !canWrite) {
|
|
953
|
+
router.push('/${modelKebab}');
|
|
954
|
+
}
|
|
955
|
+
}, [loading, canWrite, router]);
|
|
956
|
+
|
|
957
|
+
if (loading) {
|
|
958
|
+
return <div className="flex items-center justify-center min-h-screen"><div className="text-lg">Loading...</div></div>;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (!canWrite) return null;
|
|
962
|
+
|
|
859
963
|
return (
|
|
860
964
|
<div className="container mx-auto px-4 py-8">
|
|
861
965
|
<div className="max-w-2xl mx-auto">
|
|
@@ -873,11 +977,15 @@ export default function New${modelName}Page() {
|
|
|
873
977
|
/**
|
|
874
978
|
* 編集画面を生成
|
|
875
979
|
*/
|
|
876
|
-
export function generateEditPage(model: ModelInfo, sharedPackageName: string): string {
|
|
980
|
+
export function generateEditPage(model: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions): string {
|
|
877
981
|
const modelName = model.name;
|
|
878
982
|
const modelCamel = toCamelCase(modelName);
|
|
879
983
|
const modelKebab = toKebabCase(modelName);
|
|
880
984
|
|
|
985
|
+
const hasAuth = !!authOptions;
|
|
986
|
+
const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
|
|
987
|
+
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
988
|
+
|
|
881
989
|
const schemaName = model.schemaName;
|
|
882
990
|
// Zod公式パターン対応: schemaNameとmodelNameが同じ場合はimportエイリアスで名前衝突を回避
|
|
883
991
|
const needsAlias = schemaName === modelName;
|
|
@@ -889,17 +997,29 @@ export function generateEditPage(model: ModelInfo, sharedPackageName: string): s
|
|
|
889
997
|
return `'use client';
|
|
890
998
|
|
|
891
999
|
import { useEffect, useState } from 'react';
|
|
892
|
-
import { useParams } from 'next/navigation';
|
|
1000
|
+
import { useParams, useRouter } from 'next/navigation';
|
|
893
1001
|
import ${modelName}Form from '../../_components/${modelName}Form';
|
|
894
1002
|
import { z } from 'zod/v4';
|
|
895
1003
|
${schemaImportLine}
|
|
1004
|
+
${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
|
|
896
1005
|
|
|
897
1006
|
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
898
1007
|
|
|
899
1008
|
export default function Edit${modelName}Page() {
|
|
900
1009
|
const params = useParams();
|
|
1010
|
+
${hasAuth ? ' const router = useRouter();' : ''}
|
|
901
1011
|
const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
|
|
902
1012
|
const [loading, setLoading] = useState(true);
|
|
1013
|
+
${hasWriteRoles ? ` const { hasAnyRole, loading: authLoading } = useAuth();
|
|
1014
|
+
const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : hasAuth ? ` const { user, loading: authLoading } = useAuth();
|
|
1015
|
+
const canWrite = !!user;` : ''}
|
|
1016
|
+
${hasAuth ? `
|
|
1017
|
+
useEffect(() => {
|
|
1018
|
+
if (!authLoading && !canWrite) {
|
|
1019
|
+
router.push('/${modelKebab}');
|
|
1020
|
+
}
|
|
1021
|
+
}, [authLoading, canWrite, router]);
|
|
1022
|
+
` : ''}
|
|
903
1023
|
|
|
904
1024
|
useEffect(() => {
|
|
905
1025
|
const id = params?.id as string;
|