swallowkit 1.0.0-beta.2 → 1.0.0-beta.21

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 (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +312 -215
  3. package/README.md +369 -216
  4. package/dist/__tests__/fixtures.d.ts +22 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +146 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/add-auth.d.ts +10 -0
  9. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  10. package/dist/cli/commands/add-auth.js +444 -0
  11. package/dist/cli/commands/add-auth.js.map +1 -0
  12. package/dist/cli/commands/add-connector.d.ts +20 -0
  13. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  14. package/dist/cli/commands/add-connector.js +163 -0
  15. package/dist/cli/commands/add-connector.js.map +1 -0
  16. package/dist/cli/commands/create-model.d.ts +1 -4
  17. package/dist/cli/commands/create-model.d.ts.map +1 -1
  18. package/dist/cli/commands/create-model.js +21 -82
  19. package/dist/cli/commands/create-model.js.map +1 -1
  20. package/dist/cli/commands/dev-seeds.d.ts +35 -0
  21. package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
  22. package/dist/cli/commands/dev-seeds.js +292 -0
  23. package/dist/cli/commands/dev-seeds.js.map +1 -0
  24. package/dist/cli/commands/dev.d.ts +19 -0
  25. package/dist/cli/commands/dev.d.ts.map +1 -1
  26. package/dist/cli/commands/dev.js +476 -117
  27. package/dist/cli/commands/dev.js.map +1 -1
  28. package/dist/cli/commands/index.d.ts +1 -0
  29. package/dist/cli/commands/index.d.ts.map +1 -1
  30. package/dist/cli/commands/index.js +3 -1
  31. package/dist/cli/commands/index.js.map +1 -1
  32. package/dist/cli/commands/init.d.ts +13 -0
  33. package/dist/cli/commands/init.d.ts.map +1 -1
  34. package/dist/cli/commands/init.js +2627 -1708
  35. package/dist/cli/commands/init.js.map +1 -1
  36. package/dist/cli/commands/scaffold.d.ts +3 -0
  37. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  38. package/dist/cli/commands/scaffold.js +617 -129
  39. package/dist/cli/commands/scaffold.js.map +1 -1
  40. package/dist/cli/index.d.ts +5 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +164 -42
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/core/config.d.ts +8 -2
  45. package/dist/core/config.d.ts.map +1 -1
  46. package/dist/core/config.js +90 -4
  47. package/dist/core/config.js.map +1 -1
  48. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  49. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  50. package/dist/core/mock/connector-mock-server.js +480 -0
  51. package/dist/core/mock/connector-mock-server.js.map +1 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  53. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  54. package/dist/core/mock/zod-mock-generator.js +163 -0
  55. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  56. package/dist/core/operations/create-model.d.ts +15 -0
  57. package/dist/core/operations/create-model.d.ts.map +1 -0
  58. package/dist/core/operations/create-model.js +171 -0
  59. package/dist/core/operations/create-model.js.map +1 -0
  60. package/dist/core/operations/runtime.d.ts +32 -0
  61. package/dist/core/operations/runtime.d.ts.map +1 -0
  62. package/dist/core/operations/runtime.js +225 -0
  63. package/dist/core/operations/runtime.js.map +1 -0
  64. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  65. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  66. package/dist/core/operations/scaffold-machine.js +63 -0
  67. package/dist/core/operations/scaffold-machine.js.map +1 -0
  68. package/dist/core/project/manifest.d.ts +92 -0
  69. package/dist/core/project/manifest.d.ts.map +1 -0
  70. package/dist/core/project/manifest.js +321 -0
  71. package/dist/core/project/manifest.js.map +1 -0
  72. package/dist/core/project/validation.d.ts +20 -0
  73. package/dist/core/project/validation.d.ts.map +1 -0
  74. package/dist/core/project/validation.js +204 -0
  75. package/dist/core/project/validation.js.map +1 -0
  76. package/dist/core/scaffold/auth-generator.d.ts +38 -0
  77. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  78. package/dist/core/scaffold/auth-generator.js +1244 -0
  79. package/dist/core/scaffold/auth-generator.js.map +1 -0
  80. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  81. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  82. package/dist/core/scaffold/connector-functions-generator.js +1027 -0
  83. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  84. package/dist/core/scaffold/functions-generator.d.ts +7 -1
  85. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  86. package/dist/core/scaffold/functions-generator.js +920 -213
  87. package/dist/core/scaffold/functions-generator.js.map +1 -1
  88. package/dist/core/scaffold/model-parser.d.ts +20 -1
  89. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  90. package/dist/core/scaffold/model-parser.js +329 -135
  91. package/dist/core/scaffold/model-parser.js.map +1 -1
  92. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  93. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  94. package/dist/core/scaffold/nextjs-generator.js +314 -182
  95. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  96. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  97. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  98. package/dist/core/scaffold/openapi-generator.js +190 -0
  99. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  100. package/dist/core/scaffold/ui-generator.d.ts +10 -4
  101. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  102. package/dist/core/scaffold/ui-generator.js +768 -663
  103. package/dist/core/scaffold/ui-generator.js.map +1 -1
  104. package/dist/database/base-model.d.ts +3 -3
  105. package/dist/database/base-model.js +3 -3
  106. package/dist/index.d.ts +2 -2
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +2 -1
  109. package/dist/index.js.map +1 -1
  110. package/dist/machine/contracts.d.ts +16 -0
  111. package/dist/machine/contracts.d.ts.map +1 -0
  112. package/dist/machine/contracts.js +3 -0
  113. package/dist/machine/contracts.js.map +1 -0
  114. package/dist/machine/errors.d.ts +11 -0
  115. package/dist/machine/errors.d.ts.map +1 -0
  116. package/dist/machine/errors.js +34 -0
  117. package/dist/machine/errors.js.map +1 -0
  118. package/dist/machine/index.d.ts +3 -0
  119. package/dist/machine/index.d.ts.map +1 -0
  120. package/dist/machine/index.js +156 -0
  121. package/dist/machine/index.js.map +1 -0
  122. package/dist/mcp/index.d.ts +25 -0
  123. package/dist/mcp/index.d.ts.map +1 -0
  124. package/dist/mcp/index.js +184 -0
  125. package/dist/mcp/index.js.map +1 -0
  126. package/dist/types/index.d.ts +65 -0
  127. package/dist/types/index.d.ts.map +1 -1
  128. package/dist/utils/package-manager.d.ts +109 -0
  129. package/dist/utils/package-manager.d.ts.map +1 -0
  130. package/dist/utils/package-manager.js +215 -0
  131. package/dist/utils/package-manager.js.map +1 -0
  132. package/package.json +85 -73
  133. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
  134. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  135. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
  136. package/src/__tests__/auth.test.ts +654 -0
  137. package/src/__tests__/config.test.ts +263 -0
  138. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  139. package/src/__tests__/connector-mock-server.test.ts +439 -0
  140. package/src/__tests__/connector-model-bff.test.ts +162 -0
  141. package/src/__tests__/dev-seeds.test.ts +112 -0
  142. package/src/__tests__/dev.test.ts +154 -0
  143. package/src/__tests__/fixtures.ts +144 -0
  144. package/src/__tests__/functions-generator.test.ts +237 -0
  145. package/src/__tests__/init.test.ts +80 -0
  146. package/src/__tests__/machine.test.ts +212 -0
  147. package/src/__tests__/mcp.test.ts +56 -0
  148. package/src/__tests__/model-parser.test.ts +72 -0
  149. package/src/__tests__/nextjs-generator.test.ts +97 -0
  150. package/src/__tests__/openapi-generator.test.ts +43 -0
  151. package/src/__tests__/package-manager.test.ts +189 -0
  152. package/src/__tests__/scaffold.test.ts +39 -0
  153. package/src/__tests__/string-utils.test.ts +75 -0
  154. package/src/__tests__/ui-generator.test.ts +144 -0
  155. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  156. package/src/cli/commands/add-auth.ts +500 -0
  157. package/src/cli/commands/add-connector.ts +158 -0
  158. package/src/cli/commands/create-model.ts +62 -0
  159. package/src/cli/commands/dev-seeds.ts +358 -0
  160. package/src/cli/commands/dev.ts +962 -0
  161. package/src/cli/commands/index.ts +9 -0
  162. package/src/cli/commands/init.ts +3371 -0
  163. package/src/cli/commands/provision.ts +193 -0
  164. package/src/cli/commands/scaffold.ts +1211 -0
  165. package/src/cli/index.ts +193 -0
  166. package/src/core/config.ts +308 -0
  167. package/src/core/mock/connector-mock-server.ts +555 -0
  168. package/src/core/mock/zod-mock-generator.ts +205 -0
  169. package/src/core/operations/create-model.ts +174 -0
  170. package/src/core/operations/runtime.ts +235 -0
  171. package/src/core/operations/scaffold-machine.ts +91 -0
  172. package/src/core/project/manifest.ts +402 -0
  173. package/src/core/project/validation.ts +221 -0
  174. package/src/core/scaffold/auth-generator.ts +1284 -0
  175. package/src/core/scaffold/connector-functions-generator.ts +1128 -0
  176. package/src/core/scaffold/functions-generator.ts +970 -0
  177. package/src/core/scaffold/model-parser.ts +841 -0
  178. package/src/core/scaffold/nextjs-generator.ts +370 -0
  179. package/src/core/scaffold/openapi-generator.ts +212 -0
  180. package/src/core/scaffold/ui-generator.ts +1061 -0
  181. package/src/database/base-model.ts +184 -0
  182. package/src/database/client.ts +140 -0
  183. package/src/database/repository.ts +104 -0
  184. package/src/database/runtime-check.ts +25 -0
  185. package/src/index.ts +27 -0
  186. package/src/machine/contracts.ts +17 -0
  187. package/src/machine/errors.ts +34 -0
  188. package/src/machine/index.ts +173 -0
  189. package/src/mcp/index.ts +185 -0
  190. package/src/types/index.ts +134 -0
  191. package/src/utils/package-manager.ts +229 -0
@@ -13,10 +13,13 @@ const model_parser_1 = require("./model-parser");
13
13
  /**
14
14
  * 一覧画面を生成
15
15
  */
16
- function generateListPage(model, sharedPackageName) {
16
+ function generateListPage(model, sharedPackageName, authOptions) {
17
17
  const modelName = model.name;
18
18
  const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
19
19
  const modelKebab = (0, model_parser_1.toKebabCase)(modelName);
20
+ const hasAuth = !!authOptions;
21
+ const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
22
+ const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
20
23
  // フィールドから表示するカラムを抽出(id以外の最初の3つ)
21
24
  const displayFields = model.fields
22
25
  .filter(f => f.name !== 'id')
@@ -24,9 +27,6 @@ function generateListPage(model, sharedPackageName) {
24
27
  // 外部キーフィールドを検出
25
28
  const foreignKeyFields = displayFields.filter(f => f.isForeignKey);
26
29
  const hasForeignKeys = foreignKeyFields.length > 0;
27
- // ネストスキーマフィールドを検出
28
- const nestedFields = displayFields.filter(f => f.isNestedSchema);
29
- const hasNestedSchemas = nestedFields.length > 0;
30
30
  // 外部キー用のstate定義を生成
31
31
  const foreignKeyStates = foreignKeyFields.map(f => {
32
32
  const refModel = f.referencedModel;
@@ -37,16 +37,16 @@ function generateListPage(model, sharedPackageName) {
37
37
  const foreignKeyFetches = foreignKeyFields.map(f => {
38
38
  const refModel = f.referencedModel;
39
39
  const refModelCamel = (0, model_parser_1.toCamelCase)(refModel);
40
- return ` fetch('/api/${refModelCamel}')
41
- .then(res => res.json())
42
- .then((data: any[]) => {
43
- const map: Record<string, string> = {};
44
- data.forEach(item => {
45
- // name または title フィールドを表示用文字列として使用
46
- map[item.id] = item.name || item.title || item.id;
47
- });
48
- set${refModel}Map(map);
49
- })
40
+ return ` fetch('/api/${refModelCamel}')
41
+ .then(res => res.json())
42
+ .then((data: any[]) => {
43
+ const map: Record<string, string> = {};
44
+ data.forEach(item => {
45
+ // name または title フィールドを表示用文字列として使用
46
+ map[item.id] = item.name || item.title || item.id;
47
+ });
48
+ set${refModel}Map(map);
49
+ })
50
50
  .catch(err => console.error('Failed to fetch ${refModel}s:', err));`;
51
51
  }).join('\n');
52
52
  const schemaName = model.schemaName;
@@ -56,181 +56,210 @@ function generateListPage(model, sharedPackageName) {
56
56
  const schemaImportLine = needsAlias
57
57
  ? `import { ${schemaName} as ${localSchemaName} } from '${sharedPackageName}';`
58
58
  : `import { ${schemaName} } from '${sharedPackageName}';`;
59
- return `'use client';
60
-
61
- import { useEffect, useState } from 'react';
62
- import Link from 'next/link';
63
- import { z } from 'zod/v4';
64
- ${schemaImportLine}
65
-
66
- type ${modelName} = z.infer<typeof ${localSchemaName}>;
67
-
68
- export default function ${modelName}ListPage() {
69
- const [${modelCamel}s, set${modelName}s] = useState<${modelName}[]>([]);
70
- const [loading, setLoading] = useState(true);
71
- const [error, setError] = useState<string | null>(null);
72
- ${hasForeignKeys ? foreignKeyStates : ''}
73
-
74
- useEffect(() => {
75
- fetch('/api/${modelCamel}')
76
- .then((res) => {
77
- if (!res.ok) throw new Error('Failed to fetch ${modelCamel}s');
78
- return res.json();
79
- })
80
- .then((data) => {
81
- set${modelName}s(data);
82
- setLoading(false);
83
- })
84
- .catch((err) => {
85
- setError(err.message);
86
- setLoading(false);
87
- });
88
- ${hasForeignKeys ? '\n // Fetch foreign key reference data' : ''}
89
- ${hasForeignKeys ? foreignKeyFetches : ''}
90
- }, []);
91
-
92
- const handleDelete = async (id: string) => {
93
- if (!confirm('Are you sure you want to delete this item?')) return;
94
-
95
- try {
96
- const res = await fetch(\`/api/${modelCamel}/\${id}\`, {
97
- method: 'DELETE',
98
- });
99
-
100
- if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
101
-
102
- set${modelName}s(${modelCamel}s.filter((item) => item.id !== id));
103
- } catch (err: any) {
104
- alert(\`Error: \${err.message}\`);
105
- }
106
- };
107
-
108
- if (loading) {
109
- return (
110
- <div className="flex items-center justify-center min-h-screen">
111
- <div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
112
- </div>
113
- );
114
- }
115
-
116
- if (error) {
117
- return (
118
- <div className="flex items-center justify-center min-h-screen">
119
- <div className="text-red-600 dark:text-red-400">Error: {error}</div>
120
- </div>
121
- );
122
- }
123
-
124
- return (
125
- <div className="container mx-auto px-4 py-8">
126
- <div className="mb-4">
127
- <Link
128
- href="/"
129
- className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 text-sm"
130
- >
131
- ← Home
132
- </Link>
133
- </div>
134
- <div className="flex justify-between items-center mb-6">
135
- <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName}</h1>
136
- <Link
137
- href="/${modelKebab}/new"
138
- className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
139
- >
140
- Create New
141
- </Link>
142
- </div>
143
-
144
- {${modelCamel}s.length === 0 ? (
145
- <div className="text-center py-12 text-gray-500 dark:text-gray-400">
146
- No ${modelCamel}s found. Create your first one!
147
- </div>
148
- ) : (
149
- <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
150
- <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
151
- <thead className="bg-gray-50 dark:bg-gray-900">
152
- <tr>
59
+ return `'use client';
60
+
61
+ import { useEffect, useState } from 'react';
62
+ import Link from 'next/link';
63
+ import { z } from 'zod/v4';
64
+ ${schemaImportLine}
65
+ ${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
66
+
67
+ type ${modelName} = z.infer<typeof ${localSchemaName}>;
68
+
69
+ export default function ${modelName}ListPage() {
70
+ const [${modelCamel}s, set${modelName}s] = useState<${modelName}[]>([]);
71
+ const [loading, setLoading] = useState(true);
72
+ const [error, setError] = useState<string | null>(null);
73
+ ${hasForeignKeys ? foreignKeyStates : ''}
74
+ ${hasWriteRoles ? ` const { hasAnyRole } = useAuth();
75
+ const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : hasAuth ? ` const { user } = useAuth();
76
+ const canWrite = !!user;` : ''}
77
+
78
+ useEffect(() => {
79
+ fetch('/api/${modelCamel}')
80
+ .then((res) => {
81
+ if (!res.ok) throw new Error('Failed to fetch ${modelCamel}s');
82
+ return res.json();
83
+ })
84
+ .then((data) => {
85
+ set${modelName}s(data);
86
+ setLoading(false);
87
+ })
88
+ .catch((err) => {
89
+ setError(err.message);
90
+ setLoading(false);
91
+ });
92
+ ${hasForeignKeys ? '\n // Fetch foreign key reference data' : ''}
93
+ ${hasForeignKeys ? foreignKeyFetches : ''}
94
+ }, []);
95
+
96
+ const handleDelete = async (id: string) => {
97
+ if (!confirm('Are you sure you want to delete this item?')) return;
98
+
99
+ try {
100
+ const res = await fetch(\`/api/${modelCamel}/\${id}\`, {
101
+ method: 'DELETE',
102
+ });
103
+
104
+ if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
105
+
106
+ set${modelName}s(${modelCamel}s.filter((item) => item.id !== id));
107
+ } catch (err: any) {
108
+ alert(\`Error: \${err.message}\`);
109
+ }
110
+ };
111
+
112
+ if (loading) {
113
+ return (
114
+ <div className="flex items-center justify-center min-h-screen">
115
+ <div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
116
+ </div>
117
+ );
118
+ }
119
+
120
+ if (error) {
121
+ return (
122
+ <div className="flex items-center justify-center min-h-screen">
123
+ <div className="text-red-600 dark:text-red-400">Error: {error}</div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ return (
129
+ <div className="container mx-auto px-4 py-8">
130
+ <div className="mb-4">
131
+ <Link
132
+ href="/"
133
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 text-sm"
134
+ >
135
+ Home
136
+ </Link>
137
+ </div>
138
+ <div className="flex justify-between items-center mb-6">
139
+ <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName}</h1>
140
+ ${hasAuth ? ` {canWrite && (
141
+ <Link
142
+ href="/${modelKebab}/new"
143
+ className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
144
+ >
145
+ Create New
146
+ </Link>
147
+ )}` : ` <Link
148
+ href="/${modelKebab}/new"
149
+ className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
150
+ >
151
+ Create New
152
+ </Link>`}
153
+ </div>
154
+
155
+ {${modelCamel}s.length === 0 ? (
156
+ <div className="text-center py-12 text-gray-500 dark:text-gray-400">
157
+ No ${modelCamel}s found. Create your first one!
158
+ </div>
159
+ ) : (
160
+ <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
161
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
162
+ <thead className="bg-gray-50 dark:bg-gray-900">
163
+ <tr>
153
164
  ${displayFields.map(f => {
154
165
  const headerLabel = f.isNestedSchema && f.nestedModelName
155
166
  ? f.nestedModelName
156
167
  : (f.isForeignKey && f.referencedModel ? f.referencedModel : f.name);
157
- return ` <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
158
- ${headerLabel}
168
+ return ` <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
169
+ ${headerLabel}
159
170
  </th>`;
160
- }).join('\n')}
161
- <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
162
- Actions
163
- </th>
164
- </tr>
165
- </thead>
166
- <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
167
- {${modelCamel}s.map((item) => (
168
- <tr key={item.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
171
+ }).join('\n')}
172
+ <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
173
+ Actions
174
+ </th>
175
+ </tr>
176
+ </thead>
177
+ <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
178
+ {${modelCamel}s.map((item) => (
179
+ <tr key={item.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
169
180
  ${displayFields.map(f => {
170
181
  if (f.isNestedSchema && f.nestedModelName) {
171
182
  const displayField = f.nestedDisplayField || 'name';
172
183
  if (f.isArray) {
173
- return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
174
- {Array.isArray(item.${f.name}) ? item.${f.name}.map((ref: any) => ref?.${displayField} || '-').join(', ') : '-'}
184
+ return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
185
+ {Array.isArray(item.${f.name}) ? item.${f.name}.map((ref: any) => ref?.${displayField} || '-').join(', ') : '-'}
175
186
  </td>`;
176
187
  }
177
188
  else {
178
- return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
179
- {item.${f.name}?.${displayField} || '-'}
189
+ return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
190
+ {item.${f.name}?.${displayField} || '-'}
180
191
  </td>`;
181
192
  }
182
193
  }
183
194
  else if (f.isForeignKey && f.referencedModel) {
184
195
  const refModel = f.referencedModel;
185
196
  const refModelCamel = (0, model_parser_1.toCamelCase)(refModel);
186
- return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
187
- {${refModelCamel}Map[item.${f.name}] || item.${f.name}}
197
+ return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
198
+ {${refModelCamel}Map[item.${f.name}] || item.${f.name}}
188
199
  </td>`;
189
200
  }
190
201
  else {
191
- return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
192
- {String(item.${f.name})}
202
+ return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
203
+ {String(item.${f.name})}
193
204
  </td>`;
194
205
  }
195
- }).join('\n')}
196
- <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
197
- <Link
198
- href={\`/${modelKebab}/\${item.id}\`}
199
- className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 mr-4"
200
- >
201
- View
202
- </Link>
203
- <Link
204
- href={\`/${modelKebab}/\${item.id}/edit\`}
205
- className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
206
- >
207
- Edit
208
- </Link>
209
- <button
210
- onClick={() => handleDelete(item.id)}
211
- className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
212
- >
213
- Delete
214
- </button>
215
- </td>
216
- </tr>
217
- ))}
218
- </tbody>
219
- </table>
220
- </div>
221
- )}
222
- </div>
223
- );
224
- }
206
+ }).join('\n')}
207
+ <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
208
+ <Link
209
+ href={\`/${modelKebab}/\${item.id}\`}
210
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 mr-4"
211
+ >
212
+ View
213
+ </Link>
214
+ ${hasAuth ? ` {canWrite && (
215
+ <>
216
+ <Link
217
+ href={\`/${modelKebab}/\${item.id}/edit\`}
218
+ className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
219
+ >
220
+ Edit
221
+ </Link>
222
+ <button
223
+ onClick={() => handleDelete(item.id)}
224
+ className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
225
+ >
226
+ Delete
227
+ </button>
228
+ </>
229
+ )}` : ` <Link
230
+ href={\`/${modelKebab}/\${item.id}/edit\`}
231
+ className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
232
+ >
233
+ Edit
234
+ </Link>
235
+ <button
236
+ onClick={() => handleDelete(item.id)}
237
+ className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
238
+ >
239
+ Delete
240
+ </button>`}
241
+ </td>
242
+ </tr>
243
+ ))}
244
+ </tbody>
245
+ </table>
246
+ </div>
247
+ )}
248
+ </div>
249
+ );
250
+ }
225
251
  `;
226
252
  }
227
253
  /**
228
254
  * 詳細画面を生成
229
255
  */
230
- function generateDetailPage(model, sharedPackageName) {
256
+ function generateDetailPage(model, sharedPackageName, authOptions) {
231
257
  const modelName = model.name;
232
258
  const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
233
259
  const modelKebab = (0, model_parser_1.toKebabCase)(modelName);
260
+ const hasAuth = !!authOptions;
261
+ const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
262
+ const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
234
263
  // 外部キーフィールドを検出
235
264
  const foreignKeyFields = model.fields.filter(f => f.isForeignKey);
236
265
  const hasForeignKeys = foreignKeyFields.length > 0;
@@ -244,15 +273,15 @@ function generateDetailPage(model, sharedPackageName) {
244
273
  const foreignKeyFetches = foreignKeyFields.map(f => {
245
274
  const refModel = f.referencedModel;
246
275
  const refModelCamel = (0, model_parser_1.toCamelCase)(refModel);
247
- return ` fetch('/api/${refModelCamel}')
248
- .then(res => res.json())
249
- .then((data: any[]) => {
250
- const map: Record<string, string> = {};
251
- data.forEach(item => {
252
- map[item.id] = item.name || item.title || item.id;
253
- });
254
- set${refModel}Map(map);
255
- })
276
+ return ` fetch('/api/${refModelCamel}')
277
+ .then(res => res.json())
278
+ .then((data: any[]) => {
279
+ const map: Record<string, string> = {};
280
+ data.forEach(item => {
281
+ map[item.id] = item.name || item.title || item.id;
282
+ });
283
+ set${refModel}Map(map);
284
+ })
256
285
  .catch(err => console.error('Failed to fetch ${refModel}s:', err));`;
257
286
  }).join('\n');
258
287
  const schemaName = model.schemaName;
@@ -262,147 +291,166 @@ function generateDetailPage(model, sharedPackageName) {
262
291
  const schemaImportLine = needsAlias
263
292
  ? `import { ${schemaName} as ${localSchemaName} } from '${sharedPackageName}';`
264
293
  : `import { ${schemaName} } from '${sharedPackageName}';`;
265
- return `'use client';
266
-
267
- import { useEffect, useState } from 'react';
268
- import { useParams, useRouter } from 'next/navigation';
269
- import Link from 'next/link';
270
- import { z } from 'zod/v4';
271
- ${schemaImportLine}
272
-
273
- type ${modelName} = z.infer<typeof ${localSchemaName}>;
274
-
275
- export default function ${modelName}DetailPage() {
276
- const params = useParams();
277
- const router = useRouter();
278
- const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
279
- const [loading, setLoading] = useState(true);
280
- const [error, setError] = useState<string | null>(null);
281
- ${hasForeignKeys ? foreignKeyStates : ''}
282
-
283
- useEffect(() => {
284
- const id = params?.id as string;
285
- if (!id) return;
286
-
287
- fetch(\`/api/${modelCamel}/\${id}\`)
288
- .then((res) => {
289
- if (!res.ok) throw new Error('Failed to fetch ${modelCamel}');
290
- return res.json();
291
- })
292
- .then((data) => {
293
- set${modelName}(data);
294
- setLoading(false);
295
- })
296
- .catch((err) => {
297
- setError(err.message);
298
- setLoading(false);
299
- });
300
- ${hasForeignKeys ? '\n // Fetch foreign key reference data' : ''}
301
- ${hasForeignKeys ? foreignKeyFetches : ''}
302
- }, [params]);
303
-
304
- const handleDelete = async () => {
305
- if (!confirm('Are you sure you want to delete this item?')) return;
306
-
307
- try {
308
- const res = await fetch(\`/api/${modelCamel}/\${params?.id}\`, {
309
- method: 'DELETE',
310
- });
311
-
312
- if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
313
-
314
- router.push('/${modelKebab}');
315
- } catch (err: any) {
316
- alert(\`Error: \${err.message}\`);
317
- }
318
- };
319
-
320
- if (loading) {
321
- return (
322
- <div className="flex items-center justify-center min-h-screen">
323
- <div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
324
- </div>
325
- );
326
- }
327
-
328
- if (error || !${modelCamel}) {
329
- return (
330
- <div className="flex items-center justify-center min-h-screen">
331
- <div className="text-red-600 dark:text-red-400">Error: {error || '${modelName} not found'}</div>
332
- </div>
333
- );
334
- }
335
-
336
- return (
337
- <div className="container mx-auto px-4 py-8">
338
- <div className="max-w-2xl mx-auto">
339
- <div className="flex justify-between items-center mb-6">
340
- <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName} Details</h1>
341
- <div className="space-x-2">
342
- <Link
343
- href={\`/${modelKebab}/\${${modelCamel}.id}/edit\`}
344
- 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"
345
- >
346
- Edit
347
- </Link>
348
- <button
349
- onClick={handleDelete}
350
- 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"
351
- >
352
- Delete
353
- </button>
354
- </div>
355
- </div>
356
-
357
- <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
358
- <dl className="space-y-4">
294
+ return `'use client';
295
+
296
+ import { useEffect, useState } from 'react';
297
+ import { useParams, useRouter } from 'next/navigation';
298
+ import Link from 'next/link';
299
+ import { z } from 'zod/v4';
300
+ ${schemaImportLine}
301
+ ${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
302
+
303
+ type ${modelName} = z.infer<typeof ${localSchemaName}>;
304
+
305
+ export default function ${modelName}DetailPage() {
306
+ const params = useParams();
307
+ const router = useRouter();
308
+ const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
309
+ const [loading, setLoading] = useState(true);
310
+ const [error, setError] = useState<string | null>(null);
311
+ ${hasForeignKeys ? foreignKeyStates : ''}
312
+ ${hasWriteRoles ? ` const { hasAnyRole } = useAuth();
313
+ const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : hasAuth ? ` const { user } = useAuth();
314
+ const canWrite = !!user;` : ''}
315
+
316
+ useEffect(() => {
317
+ const id = params?.id as string;
318
+ if (!id) return;
319
+
320
+ fetch(\`/api/${modelCamel}/\${id}\`)
321
+ .then((res) => {
322
+ if (!res.ok) throw new Error('Failed to fetch ${modelCamel}');
323
+ return res.json();
324
+ })
325
+ .then((data) => {
326
+ set${modelName}(data);
327
+ setLoading(false);
328
+ })
329
+ .catch((err) => {
330
+ setError(err.message);
331
+ setLoading(false);
332
+ });
333
+ ${hasForeignKeys ? '\n // Fetch foreign key reference data' : ''}
334
+ ${hasForeignKeys ? foreignKeyFetches : ''}
335
+ }, [params]);
336
+
337
+ const handleDelete = async () => {
338
+ if (!confirm('Are you sure you want to delete this item?')) return;
339
+
340
+ try {
341
+ const res = await fetch(\`/api/${modelCamel}/\${params?.id}\`, {
342
+ method: 'DELETE',
343
+ });
344
+
345
+ if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
346
+
347
+ router.push('/${modelKebab}');
348
+ } catch (err: any) {
349
+ alert(\`Error: \${err.message}\`);
350
+ }
351
+ };
352
+
353
+ if (loading) {
354
+ return (
355
+ <div className="flex items-center justify-center min-h-screen">
356
+ <div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
357
+ </div>
358
+ );
359
+ }
360
+
361
+ if (error || !${modelCamel}) {
362
+ return (
363
+ <div className="flex items-center justify-center min-h-screen">
364
+ <div className="text-red-600 dark:text-red-400">Error: {error || '${modelName} not found'}</div>
365
+ </div>
366
+ );
367
+ }
368
+
369
+ return (
370
+ <div className="container mx-auto px-4 py-8">
371
+ <div className="max-w-2xl mx-auto">
372
+ <div className="flex justify-between items-center mb-6">
373
+ <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">${modelName} Details</h1>
374
+ ${hasAuth ? ` {canWrite && (
375
+ <div className="space-x-2">
376
+ <Link
377
+ href={\`/${modelKebab}/\${${modelCamel}.id}/edit\`}
378
+ 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"
379
+ >
380
+ Edit
381
+ </Link>
382
+ <button
383
+ onClick={handleDelete}
384
+ 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"
385
+ >
386
+ Delete
387
+ </button>
388
+ </div>
389
+ )}` : ` <div className="space-x-2">
390
+ <Link
391
+ href={\`/${modelKebab}/\${${modelCamel}.id}/edit\`}
392
+ 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"
393
+ >
394
+ Edit
395
+ </Link>
396
+ <button
397
+ onClick={handleDelete}
398
+ 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"
399
+ >
400
+ Delete
401
+ </button>
402
+ </div>`}
403
+ </div>
404
+
405
+ <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
406
+ <dl className="space-y-4">
359
407
  ${model.fields.map(f => {
360
408
  if (f.isNestedSchema && f.nestedModelName) {
361
409
  const displayField = f.nestedDisplayField || 'name';
362
410
  const label = f.nestedModelName;
363
411
  if (f.isArray) {
364
- return ` <div>
365
- <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${label}</dt>
366
- <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{Array.isArray(${modelCamel}.${f.name}) ? ${modelCamel}.${f.name}.map((ref: any) => ref?.${displayField} || '-').join(', ') : '-'}</dd>
412
+ return ` <div>
413
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${label}</dt>
414
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{Array.isArray(${modelCamel}.${f.name}) ? ${modelCamel}.${f.name}.map((ref: any) => ref?.${displayField} || '-').join(', ') : '-'}</dd>
367
415
  </div>`;
368
416
  }
369
417
  else {
370
- return ` <div>
371
- <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${label}</dt>
372
- <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{${modelCamel}.${f.name}?.${displayField} || '-'}</dd>
418
+ return ` <div>
419
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${label}</dt>
420
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{${modelCamel}.${f.name}?.${displayField} || '-'}</dd>
373
421
  </div>`;
374
422
  }
375
423
  }
376
424
  else if (f.isForeignKey && f.referencedModel) {
377
425
  const refModel = f.referencedModel;
378
426
  const refModelCamel = (0, model_parser_1.toCamelCase)(refModel);
379
- return ` <div>
380
- <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${refModel}</dt>
381
- <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{${refModelCamel}Map[${modelCamel}.${f.name}] || ${modelCamel}.${f.name}}</dd>
427
+ return ` <div>
428
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${refModel}</dt>
429
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{${refModelCamel}Map[${modelCamel}.${f.name}] || ${modelCamel}.${f.name}}</dd>
382
430
  </div>`;
383
431
  }
384
432
  else {
385
- return ` <div>
386
- <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${f.name}</dt>
387
- <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(${modelCamel}.${f.name})}</dd>
433
+ return ` <div>
434
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${f.name}</dt>
435
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(${modelCamel}.${f.name})}</dd>
388
436
  </div>`;
389
437
  }
390
- }).join('\n')}
391
- </dl>
392
- </div>
393
-
394
- <div className="mt-6">
395
- <Link
396
- href="/${modelKebab}"
397
- className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300"
398
- >
399
- &larr; Back to list
400
- </Link>
401
- </div>
402
- </div>
403
- </div>
404
- );
405
- }
438
+ }).join('\n')}
439
+ </dl>
440
+ </div>
441
+
442
+ <div className="mt-6">
443
+ <Link
444
+ href="/${modelKebab}"
445
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300"
446
+ >
447
+ &larr; Back to list
448
+ </Link>
449
+ </div>
450
+ </div>
451
+ </div>
452
+ );
453
+ }
406
454
  `;
407
455
  }
408
456
  /**
@@ -441,38 +489,38 @@ function generateFormComponent(model, sharedPackageName) {
441
489
  const refModelCamel = (0, model_parser_1.toCamelCase)(f.nestedModelName);
442
490
  const refModelPascal = f.nestedModelName;
443
491
  const displayField = f.nestedDisplayField || 'name';
444
- return ` // ${refModelPascal} の一覧を取得
445
- fetch('/api/${refModelCamel}')
446
- .then(res => res.json())
447
- .then(data => set${refModelPascal}Options(data.map((item: any) => ({ id: item.id, name: item.${displayField} || item.name || item.title || item.id }))))
492
+ return ` // ${refModelPascal} の一覧を取得
493
+ fetch('/api/${refModelCamel}')
494
+ .then(res => res.json())
495
+ .then(data => set${refModelPascal}Options(data.map((item: any) => ({ id: item.id, name: item.${displayField} || item.name || item.title || item.id }))))
448
496
  .catch(err => console.error('Failed to load ${refModelPascal} options:', err));`;
449
497
  }).join('\n');
450
- return `'use client';
451
-
452
- import { useState, FormEvent${needsUseEffect ? ', useEffect' : ''} } from 'react';
453
- import { useRouter } from 'next/navigation';
454
- import { z } from 'zod/v4';
455
- ${schemaImportLine}
456
-
457
- // Input schema: SwallowKit-managed fields (id, createdAt, updatedAt) are optional
458
- // These fields are ignored by the backend and auto-managed
459
- const ${modelName}InputSchema = ${localSchemaName}.partial({ id: true, createdAt: true, updatedAt: true });
460
-
461
- type ${modelName} = z.infer<typeof ${localSchemaName}>;
462
-
463
- interface ${modelName}FormProps {
464
- initialData?: ${modelName};
465
- isEdit?: boolean;
466
- }
467
-
468
- export default function ${modelName}Form({ initialData, isEdit = false }: ${modelName}FormProps) {
469
- const router = useRouter();
470
- const [loading, setLoading] = useState(false);
471
- const [errors, setErrors] = useState<Record<string, string>>({});
472
- ${hasForignKeys ? foreignKeyFields.map(f => ` const [${(0, model_parser_1.toCamelCase)(f.referencedModel)}Options, set${f.referencedModel}Options] = useState<Array<{ id: string; name: string }>>([]);`).join('\n') : ''}
473
- ${hasNestedSchemas ? nestedSchemaStates : ''}
474
-
475
- const [formData, setFormData] = useState({
498
+ return `'use client';
499
+
500
+ import { useState, FormEvent${needsUseEffect ? ', useEffect' : ''} } from 'react';
501
+ import { useRouter } from 'next/navigation';
502
+ import { z } from 'zod/v4';
503
+ ${schemaImportLine}
504
+
505
+ // Input schema: SwallowKit-managed fields (id, createdAt, updatedAt) are optional
506
+ // These fields are ignored by the backend and auto-managed
507
+ const ${modelName}InputSchema = ${localSchemaName}.partial({ id: true, createdAt: true, updatedAt: true });
508
+
509
+ type ${modelName} = z.infer<typeof ${localSchemaName}>;
510
+
511
+ interface ${modelName}FormProps {
512
+ initialData?: ${modelName};
513
+ isEdit?: boolean;
514
+ }
515
+
516
+ export default function ${modelName}Form({ initialData, isEdit = false }: ${modelName}FormProps) {
517
+ const router = useRouter();
518
+ const [loading, setLoading] = useState(false);
519
+ const [errors, setErrors] = useState<Record<string, string>>({});
520
+ ${hasForignKeys ? foreignKeyFields.map(f => ` const [${(0, model_parser_1.toCamelCase)(f.referencedModel)}Options, set${f.referencedModel}Options] = useState<Array<{ id: string; name: string }>>([]);`).join('\n') : ''}
521
+ ${hasNestedSchemas ? nestedSchemaStates : ''}
522
+
523
+ const [formData, setFormData] = useState({
476
524
  ${formFields.map(f => {
477
525
  // ネストスキーマの場合は参照ID(単一: string, 配列: string[])を管理
478
526
  if (f.isNestedSchema) {
@@ -506,106 +554,106 @@ ${formFields.map(f => {
506
554
  return ` ${f.name}: initialData?.${f.name} !== undefined ? String(initialData.${f.name}) : ${defaultValue},`;
507
555
  }
508
556
  return ` ${f.name}: initialData?.${f.name} ?? ${defaultValue},`;
509
- }).join('\n')}
510
- });
511
- ${needsUseEffect ? `
512
- // 参照データの選択肢を取得
513
- useEffect(() => {
514
- ${hasForignKeys ? foreignKeyFields.map(f => ` // ${f.referencedModel} の一覧を取得
515
- fetch('/api/${(0, model_parser_1.toKebabCase)(f.referencedModel)}')
516
- .then(res => res.json())
517
- .then(data => set${f.referencedModel}Options(data.map((item: any) => ({ id: item.id, name: item.name || item.title || item.id }))))
518
- .catch(err => console.error('Failed to load ${f.referencedModel} options:', err));`).join('\n') : ''}
519
- ${hasNestedSchemas ? nestedSchemaFetches : ''}
520
- }, []);
521
- ` : ''}
522
- const handleSubmit = async (e: FormEvent) => {
523
- e.preventDefault();
524
- setLoading(true);
525
- setErrors({});
526
-
527
- try {
528
- // Array フィールドをカンマ区切りから配列に変換
529
- const submitData: any = { ...formData };
530
- ${formFields.filter(f => f.isArray && !f.isNestedSchema).map(f => ` if (typeof submitData.${f.name} === 'string') {
531
- submitData.${f.name} = submitData.${f.name}.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
532
- }`).join('\n')}
533
- ${formFields.filter(f => f.enumValues && f.enumValues.length > 0).length > 0 ? `
534
- // Enum フィールドの空文字列を undefined に変換(.default() を有効にする)
535
- ${formFields.filter(f => f.enumValues && f.enumValues.length > 0).map(f => ` if (submitData.${f.name} === '') {
536
- submitData.${f.name} = undefined;
537
- }`).join('\n')}` : ''}
538
- ${formFields.filter(f => f.type === 'number').length > 0 ? `
539
- // Number フィールドを変換(空文字列 → undefined、文字列 → 数値)
540
- ${formFields.filter(f => f.type === 'number').map(f => ` if (submitData.${f.name} === '' || submitData.${f.name} === null) {
541
- submitData.${f.name} = undefined;
542
- } else if (typeof submitData.${f.name} === 'string') {
543
- submitData.${f.name} = Number(submitData.${f.name});
544
- }`).join('\n')}` : ''}
545
- ${hasNestedSchemas ? `
546
- // ネストスキーマ参照をIDからオブジェクトに変換
557
+ }).join('\n')}
558
+ });
559
+ ${needsUseEffect ? `
560
+ // 参照データの選択肢を取得
561
+ useEffect(() => {
562
+ ${hasForignKeys ? foreignKeyFields.map(f => ` // ${f.referencedModel} の一覧を取得
563
+ fetch('/api/${(0, model_parser_1.toKebabCase)(f.referencedModel)}')
564
+ .then(res => res.json())
565
+ .then(data => set${f.referencedModel}Options(data.map((item: any) => ({ id: item.id, name: item.name || item.title || item.id }))))
566
+ .catch(err => console.error('Failed to load ${f.referencedModel} options:', err));`).join('\n') : ''}
567
+ ${hasNestedSchemas ? nestedSchemaFetches : ''}
568
+ }, []);
569
+ ` : ''}
570
+ const handleSubmit = async (e: FormEvent) => {
571
+ e.preventDefault();
572
+ setLoading(true);
573
+ setErrors({});
574
+
575
+ try {
576
+ // Array フィールドをカンマ区切りから配列に変換
577
+ const submitData: any = { ...formData };
578
+ ${formFields.filter(f => f.isArray && !f.isNestedSchema).map(f => ` if (typeof submitData.${f.name} === 'string') {
579
+ submitData.${f.name} = submitData.${f.name}.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
580
+ }`).join('\n')}
581
+ ${formFields.filter(f => f.enumValues && f.enumValues.length > 0).length > 0 ? `
582
+ // Enum フィールドの空文字列を undefined に変換(.default() を有効にする)
583
+ ${formFields.filter(f => f.enumValues && f.enumValues.length > 0).map(f => ` if (submitData.${f.name} === '') {
584
+ submitData.${f.name} = undefined;
585
+ }`).join('\n')}` : ''}
586
+ ${formFields.filter(f => f.type === 'number').length > 0 ? `
587
+ // Number フィールドを変換(空文字列 → undefined、文字列 → 数値)
588
+ ${formFields.filter(f => f.type === 'number').map(f => ` if (submitData.${f.name} === '' || submitData.${f.name} === null) {
589
+ submitData.${f.name} = undefined;
590
+ } else if (typeof submitData.${f.name} === 'string') {
591
+ submitData.${f.name} = Number(submitData.${f.name});
592
+ }`).join('\n')}` : ''}
593
+ ${hasNestedSchemas ? `
594
+ // ネストスキーマ参照をIDからオブジェクトに変換
547
595
  ${nestedSchemaFields.map(f => {
548
596
  const refModelCamel = (0, model_parser_1.toCamelCase)(f.nestedModelName);
549
597
  const refModelPascal = f.nestedModelName;
550
598
  if (f.isArray) {
551
- return ` // ${refModelPascal} 配列参照の変換
552
- if (submitData.${f.name}Ids) {
553
- submitData.${f.name} = submitData.${f.name}Ids
554
- .map((refId: string) => ${refModelCamel}Options.find(opt => opt.id === refId))
555
- .filter(Boolean);
556
- delete submitData.${f.name}Ids;
599
+ return ` // ${refModelPascal} 配列参照の変換
600
+ if (submitData.${f.name}Ids) {
601
+ submitData.${f.name} = submitData.${f.name}Ids
602
+ .map((refId: string) => ${refModelCamel}Options.find(opt => opt.id === refId))
603
+ .filter(Boolean);
604
+ delete submitData.${f.name}Ids;
557
605
  }`;
558
606
  }
559
607
  else {
560
- return ` // ${refModelPascal} 単一参照の変換
561
- if (submitData.${f.name}Id) {
562
- const selected = ${refModelCamel}Options.find(opt => opt.id === submitData.${f.name}Id);
563
- submitData.${f.name} = selected || undefined;
564
- delete submitData.${f.name}Id;
565
- } else {
566
- submitData.${f.name} = undefined;
567
- delete submitData.${f.name}Id;
608
+ return ` // ${refModelPascal} 単一参照の変換
609
+ if (submitData.${f.name}Id) {
610
+ const selected = ${refModelCamel}Options.find(opt => opt.id === submitData.${f.name}Id);
611
+ submitData.${f.name} = selected || undefined;
612
+ delete submitData.${f.name}Id;
613
+ } else {
614
+ submitData.${f.name} = undefined;
615
+ delete submitData.${f.name}Id;
568
616
  }`;
569
617
  }
570
- }).join('\n')}
571
- ` : ''}
572
-
573
- // Validate input (excluding SwallowKit-managed fields)
574
- ${modelName}InputSchema.parse(submitData);
575
-
576
- const url = isEdit ? \`/api/${modelCamel}/\${initialData!.id}\` : '/api/${modelCamel}';
577
- const method = isEdit ? 'PUT' : 'POST';
578
-
579
- const res = await fetch(url, {
580
- method,
581
- headers: { 'Content-Type': 'application/json' },
582
- body: JSON.stringify(submitData),
583
- });
584
-
585
- if (!res.ok) {
586
- const errorData = await res.json();
587
- throw new Error(errorData.error || 'Failed to save ${modelCamel}');
588
- }
589
-
590
- router.push('/${(0, model_parser_1.toKebabCase)(modelName)}');
591
- } catch (err: any) {
592
- if (err.issues) {
593
- // Zod validation errors
594
- const fieldErrors: Record<string, string> = {};
595
- err.issues.forEach((error: any) => {
596
- const field = error.path[0];
597
- fieldErrors[field] = error.message;
598
- });
599
- setErrors(fieldErrors);
600
- } else {
601
- alert(\`Error: \${err.message}\`);
602
- }
603
- setLoading(false);
604
- }
605
- };
606
-
607
- return (
608
- <form onSubmit={handleSubmit} className="space-y-6">
618
+ }).join('\n')}
619
+ ` : ''}
620
+
621
+ // Validate input (excluding SwallowKit-managed fields)
622
+ ${modelName}InputSchema.parse(submitData);
623
+
624
+ const url = isEdit ? \`/api/${modelCamel}/\${initialData!.id}\` : '/api/${modelCamel}';
625
+ const method = isEdit ? 'PUT' : 'POST';
626
+
627
+ const res = await fetch(url, {
628
+ method,
629
+ headers: { 'Content-Type': 'application/json' },
630
+ body: JSON.stringify(submitData),
631
+ });
632
+
633
+ if (!res.ok) {
634
+ const errorData = await res.json();
635
+ throw new Error(errorData.error || 'Failed to save ${modelCamel}');
636
+ }
637
+
638
+ router.push('/${(0, model_parser_1.toKebabCase)(modelName)}');
639
+ } catch (err: any) {
640
+ if (err.issues) {
641
+ // Zod validation errors
642
+ const fieldErrors: Record<string, string> = {};
643
+ err.issues.forEach((error: any) => {
644
+ const field = error.path[0];
645
+ fieldErrors[field] = error.message;
646
+ });
647
+ setErrors(fieldErrors);
648
+ } else {
649
+ alert(\`Error: \${err.message}\`);
650
+ }
651
+ setLoading(false);
652
+ }
653
+ };
654
+
655
+ return (
656
+ <form onSubmit={handleSubmit} className="space-y-6">
609
657
  ${formFields.map(f => {
610
658
  // ネストスキーマの場合はセレクトボックス(単一)またはマルチセレクト(配列)
611
659
  if (f.isNestedSchema && f.nestedModelName) {
@@ -613,253 +661,298 @@ ${formFields.map(f => {
613
661
  const label = f.nestedModelName;
614
662
  if (f.isArray) {
615
663
  // 配列参照: マルチセレクト
616
- return ` <div>
617
- <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
618
- ${label}${!f.isOptional ? ' *' : ''} <span className="text-xs text-gray-500">(複数選択可)</span>
619
- </label>
620
- <select
621
- id="${f.name}"
622
- name="${f.name}"
623
- multiple
624
- value={formData.${f.name}Ids}
625
- onChange={(e) => {
626
- const selected = Array.from(e.target.selectedOptions, option => option.value);
627
- setFormData({ ...formData, ${f.name}Ids: selected });
628
- }}
629
- className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400 min-h-[120px]"
630
- ${!f.isOptional ? 'required' : ''}
631
- >
632
- {${optionsVar}.map((option) => (
633
- <option key={option.id} value={option.id}>{option.name}</option>
634
- ))}
635
- </select>
636
- {errors.${f.name} && (
637
- <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
638
- )}
664
+ return ` <div>
665
+ <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
666
+ ${label}${!f.isOptional ? ' *' : ''} <span className="text-xs text-gray-500">(複数選択可)</span>
667
+ </label>
668
+ <select
669
+ id="${f.name}"
670
+ name="${f.name}"
671
+ multiple
672
+ value={formData.${f.name}Ids}
673
+ onChange={(e) => {
674
+ const selected = Array.from(e.target.selectedOptions, option => option.value);
675
+ setFormData({ ...formData, ${f.name}Ids: selected });
676
+ }}
677
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400 min-h-[120px]"
678
+ ${!f.isOptional ? 'required' : ''}
679
+ >
680
+ {${optionsVar}.map((option) => (
681
+ <option key={option.id} value={option.id}>{option.name}</option>
682
+ ))}
683
+ </select>
684
+ {errors.${f.name} && (
685
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
686
+ )}
639
687
  </div>`;
640
688
  }
641
689
  else {
642
690
  // 単一オブジェクト参照: セレクトボックス
643
- return ` <div>
644
- <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
645
- ${label}${!f.isOptional ? ' *' : ''}
646
- </label>
647
- <select
648
- id="${f.name}"
649
- name="${f.name}"
650
- value={formData.${f.name}Id}
651
- onChange={(e) => setFormData({ ...formData, ${f.name}Id: e.target.value })}
652
- className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
653
- ${!f.isOptional ? 'required' : ''}
654
- >
655
- <option value="">選択してください</option>
656
- {${optionsVar}.map((option) => (
657
- <option key={option.id} value={option.id}>{option.name}</option>
658
- ))}
659
- </select>
660
- {errors.${f.name} && (
661
- <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
662
- )}
691
+ return ` <div>
692
+ <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
693
+ ${label}${!f.isOptional ? ' *' : ''}
694
+ </label>
695
+ <select
696
+ id="${f.name}"
697
+ name="${f.name}"
698
+ value={formData.${f.name}Id}
699
+ onChange={(e) => setFormData({ ...formData, ${f.name}Id: e.target.value })}
700
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
701
+ ${!f.isOptional ? 'required' : ''}
702
+ >
703
+ <option value="">選択してください</option>
704
+ {${optionsVar}.map((option) => (
705
+ <option key={option.id} value={option.id}>{option.name}</option>
706
+ ))}
707
+ </select>
708
+ {errors.${f.name} && (
709
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
710
+ )}
663
711
  </div>`;
664
712
  }
665
713
  }
666
714
  // 外部キーの場合は参照先モデルのドロップダウン
667
715
  if (f.isForeignKey && f.referencedModel) {
668
716
  const optionsVar = `${(0, model_parser_1.toCamelCase)(f.referencedModel)}Options`;
669
- return ` <div>
670
- <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
671
- ${f.referencedModel}${!f.isOptional ? ' *' : ''}
672
- </label>
673
- <select
674
- id="${f.name}"
675
- name="${f.name}"
676
- value={formData.${f.name}}
677
- onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
678
- className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
679
- ${!f.isOptional ? 'required' : ''}
680
- >
681
- <option value="">選択してください</option>
682
- {${optionsVar}.map((option) => (
683
- <option key={option.id} value={option.id}>{option.name}</option>
684
- ))}
685
- </select>
686
- {errors.${f.name} && (
687
- <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
688
- )}
717
+ return ` <div>
718
+ <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
719
+ ${f.referencedModel}${!f.isOptional ? ' *' : ''}
720
+ </label>
721
+ <select
722
+ id="${f.name}"
723
+ name="${f.name}"
724
+ value={formData.${f.name}}
725
+ onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
726
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
727
+ ${!f.isOptional ? 'required' : ''}
728
+ >
729
+ <option value="">選択してください</option>
730
+ {${optionsVar}.map((option) => (
731
+ <option key={option.id} value={option.id}>{option.name}</option>
732
+ ))}
733
+ </select>
734
+ {errors.${f.name} && (
735
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
736
+ )}
689
737
  </div>`;
690
738
  }
691
739
  // Enum の場合は select 要素
692
740
  if (f.enumValues && f.enumValues.length > 0) {
693
- return ` <div>
694
- <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
695
- ${f.name}${!f.isOptional ? ' *' : ''}
696
- </label>
697
- <select
698
- id="${f.name}"
699
- name="${f.name}"
700
- value={formData.${f.name}}
701
- onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
702
- className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
703
- ${!f.isOptional ? 'required' : ''}
704
- >
705
- <option value="">選択してください</option>
706
- ${f.enumValues.map(v => ` <option value="${v}">${v}</option>`).join('\n')}
707
- </select>
708
- {errors.${f.name} && (
709
- <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
710
- )}
741
+ return ` <div>
742
+ <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
743
+ ${f.name}${!f.isOptional ? ' *' : ''}
744
+ </label>
745
+ <select
746
+ id="${f.name}"
747
+ name="${f.name}"
748
+ value={formData.${f.name}}
749
+ onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
750
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
751
+ ${!f.isOptional ? 'required' : ''}
752
+ >
753
+ <option value="">選択してください</option>
754
+ ${f.enumValues.map(v => ` <option value="${v}">${v}</option>`).join('\n')}
755
+ </select>
756
+ {errors.${f.name} && (
757
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
758
+ )}
711
759
  </div>`;
712
760
  }
713
761
  // Boolean の場合は checkbox
714
762
  if (f.type === 'boolean') {
715
- return ` <div className="flex items-center">
716
- <input
717
- type="checkbox"
718
- id="${f.name}"
719
- name="${f.name}"
720
- checked={formData.${f.name}}
721
- onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.checked })}
722
- className="h-4 w-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 dark:focus:ring-blue-400 border border-gray-300 dark:border-gray-600 rounded"
723
- />
724
- <label htmlFor="${f.name}" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
725
- ${f.name}
726
- </label>
727
- {errors.${f.name} && (
728
- <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
729
- )}
763
+ return ` <div className="flex items-center">
764
+ <input
765
+ type="checkbox"
766
+ id="${f.name}"
767
+ name="${f.name}"
768
+ checked={formData.${f.name}}
769
+ onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.checked })}
770
+ className="h-4 w-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 dark:focus:ring-blue-400 border border-gray-300 dark:border-gray-600 rounded"
771
+ />
772
+ <label htmlFor="${f.name}" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
773
+ ${f.name}
774
+ </label>
775
+ {errors.${f.name} && (
776
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
777
+ )}
730
778
  </div>`;
731
779
  }
732
780
  // Array の場合はカンマ区切りテキスト
733
781
  if (f.isArray) {
734
- return ` <div>
735
- <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
736
- ${f.name}${!f.isOptional ? ' *' : ''} <span className="text-xs text-gray-500">(カンマ区切りで入力)</span>
737
- </label>
738
- <input
739
- type="text"
740
- id="${f.name}"
741
- name="${f.name}"
742
- value={formData.${f.name}}
743
- onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
744
- placeholder="例: item1, item2, item3"
745
- className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
746
- ${!f.isOptional ? 'required' : ''}
747
- />
748
- {errors.${f.name} && (
749
- <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
750
- )}
782
+ return ` <div>
783
+ <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
784
+ ${f.name}${!f.isOptional ? ' *' : ''} <span className="text-xs text-gray-500">(カンマ区切りで入力)</span>
785
+ </label>
786
+ <input
787
+ type="text"
788
+ id="${f.name}"
789
+ name="${f.name}"
790
+ value={formData.${f.name}}
791
+ onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
792
+ placeholder="例: item1, item2, item3"
793
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
794
+ ${!f.isOptional ? 'required' : ''}
795
+ />
796
+ {errors.${f.name} && (
797
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
798
+ )}
751
799
  </div>`;
752
800
  }
753
801
  // Number の場合
754
802
  if (f.type === 'number') {
755
- return ` <div>
756
- <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
757
- ${f.name}${!f.isOptional ? ' *' : ''}
758
- </label>
759
- <input
760
- type="number"
761
- id="${f.name}"
762
- name="${f.name}"
763
- value={formData.${f.name}}
764
- onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
765
- className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
766
- ${!f.isOptional ? 'required' : ''}
767
- />
768
- {errors.${f.name} && (
769
- <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
770
- )}
803
+ return ` <div>
804
+ <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
805
+ ${f.name}${!f.isOptional ? ' *' : ''}
806
+ </label>
807
+ <input
808
+ type="number"
809
+ id="${f.name}"
810
+ name="${f.name}"
811
+ value={formData.${f.name}}
812
+ onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
813
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
814
+ ${!f.isOptional ? 'required' : ''}
815
+ />
816
+ {errors.${f.name} && (
817
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
818
+ )}
771
819
  </div>`;
772
820
  }
773
821
  // Date の場合
774
822
  if (f.type === 'date') {
775
- return ` <div>
776
- <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
777
- ${f.name}${!f.isOptional ? ' *' : ''}
778
- </label>
779
- <input
780
- type="date"
781
- id="${f.name}"
782
- name="${f.name}"
783
- value={formData.${f.name}}
784
- onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
785
- className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
786
- ${!f.isOptional ? 'required' : ''}
787
- />
788
- {errors.${f.name} && (
789
- <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
790
- )}
823
+ return ` <div>
824
+ <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
825
+ ${f.name}${!f.isOptional ? ' *' : ''}
826
+ </label>
827
+ <input
828
+ type="date"
829
+ id="${f.name}"
830
+ name="${f.name}"
831
+ value={formData.${f.name}}
832
+ onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
833
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
834
+ ${!f.isOptional ? 'required' : ''}
835
+ />
836
+ {errors.${f.name} && (
837
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
838
+ )}
791
839
  </div>`;
792
840
  }
793
841
  // デフォルト: text input
794
- return ` <div>
795
- <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
796
- ${f.name}${!f.isOptional ? ' *' : ''}
797
- </label>
798
- <input
799
- type="text"
800
- id="${f.name}"
801
- name="${f.name}"
802
- value={formData.${f.name}}
803
- onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
804
- className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
805
- ${!f.isOptional ? 'required' : ''}
806
- />
807
- {errors.${f.name} && (
808
- <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
809
- )}
842
+ return ` <div>
843
+ <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
844
+ ${f.name}${!f.isOptional ? ' *' : ''}
845
+ </label>
846
+ <input
847
+ type="text"
848
+ id="${f.name}"
849
+ name="${f.name}"
850
+ value={formData.${f.name}}
851
+ onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
852
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
853
+ ${!f.isOptional ? 'required' : ''}
854
+ />
855
+ {errors.${f.name} && (
856
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
857
+ )}
810
858
  </div>`;
811
- }).join('\n\n')}
812
-
813
- <div className="flex gap-4">
814
- <button
815
- type="submit"
816
- disabled={loading}
817
- className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded disabled:opacity-50"
818
- >
819
- {loading ? 'Saving...' : isEdit ? 'Update' : 'Create'}
820
- </button>
821
- <button
822
- type="button"
823
- onClick={() => router.push('/${(0, model_parser_1.toKebabCase)(modelName)}')}
824
- className="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded"
825
- >
826
- Cancel
827
- </button>
828
- </div>
829
- </form>
830
- );
831
- }
859
+ }).join('\n\n')}
860
+
861
+ <div className="flex gap-4">
862
+ <button
863
+ type="submit"
864
+ disabled={loading}
865
+ className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded disabled:opacity-50"
866
+ >
867
+ {loading ? 'Saving...' : isEdit ? 'Update' : 'Create'}
868
+ </button>
869
+ <button
870
+ type="button"
871
+ onClick={() => router.push('/${(0, model_parser_1.toKebabCase)(modelName)}')}
872
+ className="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded"
873
+ >
874
+ Cancel
875
+ </button>
876
+ </div>
877
+ </form>
878
+ );
879
+ }
832
880
  `;
833
881
  }
834
882
  /**
835
883
  * 新規作成画面を生成
836
884
  */
837
- function generateNewPage(model) {
885
+ function generateNewPage(model, authOptions) {
838
886
  const modelName = model.name;
839
887
  const modelKebab = (0, model_parser_1.toKebabCase)(modelName);
840
- return `import ${modelName}Form from '../_components/${modelName}Form';
841
-
842
- export default function New${modelName}Page() {
843
- return (
844
- <div className="container mx-auto px-4 py-8">
845
- <div className="max-w-2xl mx-auto">
846
- <h1 className="text-3xl font-bold mb-6">Create New ${modelName}</h1>
847
- <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
848
- <${modelName}Form />
849
- </div>
850
- </div>
851
- </div>
852
- );
853
- }
888
+ const hasAuth = !!authOptions;
889
+ const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
890
+ const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
891
+ if (!hasAuth) {
892
+ return `import ${modelName}Form from '../_components/${modelName}Form';
893
+
894
+ export default function New${modelName}Page() {
895
+ return (
896
+ <div className="container mx-auto px-4 py-8">
897
+ <div className="max-w-2xl mx-auto">
898
+ <h1 className="text-3xl font-bold mb-6">Create New ${modelName}</h1>
899
+ <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
900
+ <${modelName}Form />
901
+ </div>
902
+ </div>
903
+ </div>
904
+ );
905
+ }
906
+ `;
907
+ }
908
+ return `'use client';
909
+
910
+ import { useEffect } from 'react';
911
+ import { useRouter } from 'next/navigation';
912
+ import ${modelName}Form from '../_components/${modelName}Form';
913
+ import { useAuth } from '@/lib/auth/auth-context';
914
+
915
+ export default function New${modelName}Page() {
916
+ const router = useRouter();
917
+ ${hasWriteRoles ? ` const { hasAnyRole, loading } = useAuth();
918
+ const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : ` const { user, loading } = useAuth();
919
+ const canWrite = !!user;`}
920
+
921
+ useEffect(() => {
922
+ if (!loading && !canWrite) {
923
+ router.push('/${modelKebab}');
924
+ }
925
+ }, [loading, canWrite, router]);
926
+
927
+ if (loading) {
928
+ return <div className="flex items-center justify-center min-h-screen"><div className="text-lg">Loading...</div></div>;
929
+ }
930
+
931
+ if (!canWrite) return null;
932
+
933
+ return (
934
+ <div className="container mx-auto px-4 py-8">
935
+ <div className="max-w-2xl mx-auto">
936
+ <h1 className="text-3xl font-bold mb-6">Create New ${modelName}</h1>
937
+ <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
938
+ <${modelName}Form />
939
+ </div>
940
+ </div>
941
+ </div>
942
+ );
943
+ }
854
944
  `;
855
945
  }
856
946
  /**
857
947
  * 編集画面を生成
858
948
  */
859
- function generateEditPage(model, sharedPackageName) {
949
+ function generateEditPage(model, sharedPackageName, authOptions) {
860
950
  const modelName = model.name;
861
951
  const modelCamel = (0, model_parser_1.toCamelCase)(modelName);
862
952
  const modelKebab = (0, model_parser_1.toKebabCase)(modelName);
953
+ const hasAuth = !!authOptions;
954
+ const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
955
+ const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
863
956
  const schemaName = model.schemaName;
864
957
  // Zod公式パターン対応: schemaNameとmodelNameが同じ場合はimportエイリアスで名前衝突を回避
865
958
  const needsAlias = schemaName === modelName;
@@ -867,61 +960,73 @@ function generateEditPage(model, sharedPackageName) {
867
960
  const schemaImportLine = needsAlias
868
961
  ? `import { ${schemaName} as ${localSchemaName} } from '${sharedPackageName}';`
869
962
  : `import { ${schemaName} } from '${sharedPackageName}';`;
870
- return `'use client';
871
-
872
- import { useEffect, useState } from 'react';
873
- import { useParams } from 'next/navigation';
874
- import ${modelName}Form from '../../_components/${modelName}Form';
875
- import { z } from 'zod/v4';
876
- ${schemaImportLine}
877
-
878
- type ${modelName} = z.infer<typeof ${localSchemaName}>;
879
-
880
- export default function Edit${modelName}Page() {
881
- const params = useParams();
882
- const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
883
- const [loading, setLoading] = useState(true);
884
-
885
- useEffect(() => {
886
- const id = params?.id as string;
887
- if (!id) return;
888
-
889
- fetch(\`/api/${modelCamel}/\${id}\`)
890
- .then((res) => res.json())
891
- .then((data) => {
892
- set${modelName}(data);
893
- setLoading(false);
894
- })
895
- .catch(() => setLoading(false));
896
- }, [params]);
897
-
898
- if (loading) {
899
- return (
900
- <div className="flex items-center justify-center min-h-screen">
901
- <div className="text-lg">Loading...</div>
902
- </div>
903
- );
904
- }
905
-
906
- if (!${modelCamel}) {
907
- return (
908
- <div className="flex items-center justify-center min-h-screen">
909
- <div className="text-red-600">${modelName} not found</div>
910
- </div>
911
- );
912
- }
913
-
914
- return (
915
- <div className="container mx-auto px-4 py-8">
916
- <div className="max-w-2xl mx-auto">
917
- <h1 className="text-3xl font-bold mb-6">Edit ${modelName}</h1>
918
- <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
919
- <${modelName}Form initialData={${modelCamel}} isEdit={true} />
920
- </div>
921
- </div>
922
- </div>
923
- );
924
- }
963
+ return `'use client';
964
+
965
+ import { useEffect, useState } from 'react';
966
+ import { useParams, useRouter } from 'next/navigation';
967
+ import ${modelName}Form from '../../_components/${modelName}Form';
968
+ import { z } from 'zod/v4';
969
+ ${schemaImportLine}
970
+ ${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
971
+
972
+ type ${modelName} = z.infer<typeof ${localSchemaName}>;
973
+
974
+ export default function Edit${modelName}Page() {
975
+ const params = useParams();
976
+ ${hasAuth ? ' const router = useRouter();' : ''}
977
+ const [${modelCamel}, set${modelName}] = useState<${modelName} | null>(null);
978
+ const [loading, setLoading] = useState(true);
979
+ ${hasWriteRoles ? ` const { hasAnyRole, loading: authLoading } = useAuth();
980
+ const canWrite = hasAnyRole(${JSON.stringify(writeRoles)});` : hasAuth ? ` const { user, loading: authLoading } = useAuth();
981
+ const canWrite = !!user;` : ''}
982
+ ${hasAuth ? `
983
+ useEffect(() => {
984
+ if (!authLoading && !canWrite) {
985
+ router.push('/${modelKebab}');
986
+ }
987
+ }, [authLoading, canWrite, router]);
988
+ ` : ''}
989
+
990
+ useEffect(() => {
991
+ const id = params?.id as string;
992
+ if (!id) return;
993
+
994
+ fetch(\`/api/${modelCamel}/\${id}\`)
995
+ .then((res) => res.json())
996
+ .then((data) => {
997
+ set${modelName}(data);
998
+ setLoading(false);
999
+ })
1000
+ .catch(() => setLoading(false));
1001
+ }, [params]);
1002
+
1003
+ if (loading) {
1004
+ return (
1005
+ <div className="flex items-center justify-center min-h-screen">
1006
+ <div className="text-lg">Loading...</div>
1007
+ </div>
1008
+ );
1009
+ }
1010
+
1011
+ if (!${modelCamel}) {
1012
+ return (
1013
+ <div className="flex items-center justify-center min-h-screen">
1014
+ <div className="text-red-600">${modelName} not found</div>
1015
+ </div>
1016
+ );
1017
+ }
1018
+
1019
+ return (
1020
+ <div className="container mx-auto px-4 py-8">
1021
+ <div className="max-w-2xl mx-auto">
1022
+ <h1 className="text-3xl font-bold mb-6">Edit ${modelName}</h1>
1023
+ <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
1024
+ <${modelName}Form initialData={${modelCamel}} isEdit={true} />
1025
+ </div>
1026
+ </div>
1027
+ </div>
1028
+ );
1029
+ }
925
1030
  `;
926
1031
  }
927
1032
  //# sourceMappingURL=ui-generator.js.map