swallowkit 1.0.0-beta.5 → 1.0.0-beta.50

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