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.
Files changed (59) hide show
  1. package/README.ja.md +2 -0
  2. package/README.md +2 -0
  3. package/dist/cli/commands/add-auth.d.ts +10 -0
  4. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  5. package/dist/cli/commands/add-auth.js +365 -0
  6. package/dist/cli/commands/add-auth.js.map +1 -0
  7. package/dist/cli/commands/dev.js +33 -3
  8. package/dist/cli/commands/dev.js.map +1 -1
  9. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  10. package/dist/cli/commands/scaffold.js +145 -10
  11. package/dist/cli/commands/scaffold.js.map +1 -1
  12. package/dist/cli/index.js +10 -0
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/core/config.d.ts +5 -1
  15. package/dist/core/config.d.ts.map +1 -1
  16. package/dist/core/config.js +12 -1
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/core/mock/connector-mock-server.d.ts +28 -0
  19. package/dist/core/mock/connector-mock-server.d.ts.map +1 -1
  20. package/dist/core/mock/connector-mock-server.js +195 -0
  21. package/dist/core/mock/connector-mock-server.js.map +1 -1
  22. package/dist/core/scaffold/auth-generator.d.ts +38 -0
  23. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  24. package/dist/core/scaffold/auth-generator.js +1246 -0
  25. package/dist/core/scaffold/auth-generator.js.map +1 -0
  26. package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
  27. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  28. package/dist/core/scaffold/connector-functions-generator.js +37 -18
  29. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  30. package/dist/core/scaffold/functions-generator.d.ts +2 -1
  31. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  32. package/dist/core/scaffold/functions-generator.js +21 -12
  33. package/dist/core/scaffold/functions-generator.js.map +1 -1
  34. package/dist/core/scaffold/model-parser.d.ts +7 -1
  35. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  36. package/dist/core/scaffold/model-parser.js +45 -0
  37. package/dist/core/scaffold/model-parser.js.map +1 -1
  38. package/dist/core/scaffold/ui-generator.d.ts +10 -4
  39. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  40. package/dist/core/scaffold/ui-generator.js +120 -12
  41. package/dist/core/scaffold/ui-generator.js.map +1 -1
  42. package/dist/types/index.d.ts +30 -0
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/package.json +3 -1
  45. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +9 -1
  46. package/src/__tests__/auth.test.ts +654 -0
  47. package/src/__tests__/connector-mock-server.test.ts +151 -2
  48. package/src/cli/commands/add-auth.ts +413 -0
  49. package/src/cli/commands/dev.ts +34 -4
  50. package/src/cli/commands/scaffold.ts +165 -11
  51. package/src/cli/index.ts +11 -0
  52. package/src/core/config.ts +13 -2
  53. package/src/core/mock/connector-mock-server.ts +248 -0
  54. package/src/core/scaffold/auth-generator.ts +1286 -0
  55. package/src/core/scaffold/connector-functions-generator.ts +42 -18
  56. package/src/core/scaffold/functions-generator.ts +23 -12
  57. package/src/core/scaffold/model-parser.ts +50 -1
  58. package/src/core/scaffold/ui-generator.ts +132 -12
  59. 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
- <Link
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
- <Link
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
- <div className="space-x-2">
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
- return `import ${modelName}Form from '../_components/${modelName}Form';
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;