swallowkit 1.0.0-beta.2 → 1.0.0-beta.20

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 +4 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +162 -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 +148 -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 +191 -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
@@ -0,0 +1,841 @@
1
+ /**
2
+ * Zod モデルファイルを解析して、スキーマ情報を抽出する
3
+ */
4
+
5
+ import * as fs from "fs";
6
+ import { execSync } from "child_process";
7
+ import * as path from "path";
8
+
9
+ import { ModelConnectorConfig, ModelAuthPolicy } from "../../types";
10
+ import { detectFromProject, getCommands } from "../../utils/package-manager";
11
+
12
+ export interface ModelInfo {
13
+ name: string; // モデル名(例: "Todo")
14
+ displayName: string; // 表示名(例: "Todo" または "タスク")
15
+ schemaName: string; // スキーマ変数名(例: "todoSchema")
16
+ filePath: string; // モデルファイルの絶対パス
17
+ fields: FieldInfo[]; // フィールド情報
18
+ hasId: boolean; // id フィールドがあるか
19
+ hasCreatedAt: boolean; // createdAt フィールドがあるか
20
+ hasUpdatedAt: boolean; // updatedAt フィールドがあるか
21
+ nestedSchemaRefs: NestedSchemaRef[]; // ネストしたスキーマ参照
22
+ connectorConfig?: ModelConnectorConfig; // コネクタメタデータ(外部データソース用)
23
+ authPolicy?: ModelAuthPolicy; // 認可ポリシー(ロールベースアクセス制御用)
24
+ partitionKey: string; // Cosmos DB パーティションキーパス(デフォルト: "/id")
25
+ }
26
+
27
+ export interface FieldInfo {
28
+ name: string;
29
+ type: string; // "string" | "number" | "boolean" | "date" | "object" | "array"
30
+ isOptional: boolean;
31
+ isArray: boolean;
32
+ enumValues?: string[]; // enum の場合の選択肢
33
+ isForeignKey?: boolean; // 外部キーかどうか
34
+ referencedModel?: string; // 参照先のモデル名(例: "Category")
35
+ isNestedSchema?: boolean; // ネストしたスキーマ参照かどうか
36
+ nestedSchemaName?: string; // 参照先のスキーマ名(例: "categorySchema")
37
+ nestedModelName?: string; // 参照先のモデル名(例: "Category")
38
+ nestedDisplayField?: string; // 参照先の表示フィールド(例: "name")
39
+ }
40
+
41
+ /**
42
+ * ネストしたスキーマ参照の情報
43
+ */
44
+ export interface NestedSchemaRef {
45
+ fieldName: string; // フィールド名(例: "category")
46
+ schemaName: string; // スキーマ変数名(例: "categorySchema")
47
+ modelName: string; // モデル名(例: "Category")
48
+ importPath: string; // インポートパス(例: "./category")
49
+ isArray: boolean; // 配列参照か
50
+ isOptional: boolean; // オプショナルか
51
+ displayField: string; // 表示用フィールド(例: "name")
52
+ }
53
+
54
+ /**
55
+ * モデルファイルを解析して ModelInfo を返す
56
+ */
57
+ export async function parseModelFile(modelPath: string): Promise<ModelInfo> {
58
+ if (!fs.existsSync(modelPath)) {
59
+ throw new Error(`Model file not found: ${modelPath}`);
60
+ }
61
+
62
+ const content = fs.readFileSync(modelPath, "utf-8");
63
+ const fileName = path.basename(modelPath, ".ts");
64
+
65
+ // モデル名を推定(ファイル名を PascalCase に変換)
66
+ const modelName = toPascalCase(fileName);
67
+
68
+ // スキーマ変数名を抽出
69
+ // コメント内のコードパターンを誤認識しないよう、先にコメントを除去してから正規表現を適用する
70
+ // パターン1: export const todoSchema = z.object({ ... }) (camelCase + Schema接尾辞)
71
+ // パターン2: export const Todo = z.object({ ... }) (Zod公式パターン)
72
+ const contentWithoutComments = content
73
+ .replace(/\/\*[\s\S]*?\*\//g, '') // ブロックコメントを除去
74
+ .replace(/\/\/.*/g, ''); // 行コメントを除去
75
+ let schemaMatch = contentWithoutComments.match(/export\s+const\s+(\w+Schema)\s*=/);
76
+ if (!schemaMatch) {
77
+ schemaMatch = contentWithoutComments.match(/export\s+const\s+(\w+)\s*=\s*z\.object\s*\(/);
78
+ }
79
+ if (!schemaMatch) {
80
+ throw new Error(`Could not find exported schema in ${modelPath}. Expected patterns:\n - export const xxxSchema = z.object({ ... })\n - export const Xxx = z.object({ ... })`);
81
+ }
82
+
83
+ const schemaName = schemaMatch[1];
84
+
85
+ // displayName を抽出(例: export const displayName = 'Task')
86
+ const displayNameMatch = content.match(/export\s+const\s+displayName\s*=\s*['"]([^'"]+)['"]/);
87
+ const displayName = displayNameMatch ? displayNameMatch[1] : modelName;
88
+
89
+ // connectorConfig を抽出(外部データソース用メタデータ)
90
+ const connectorConfig = parseConnectorConfig(content);
91
+
92
+ // authPolicy を抽出(ロールベースアクセス制御用メタデータ)
93
+ const authPolicy = parseAuthPolicy(content);
94
+
95
+ // partitionKey を抽出(Cosmos DB パーティションキー)
96
+ const partitionKey = parsePartitionKey(content);
97
+
98
+ // ネストしたスキーマ参照を検出
99
+ const nestedSchemaRefs = detectNestedSchemaRefs(modelPath, content, schemaName);
100
+
101
+ // フィールド情報を抽出(動的インポートを使用)
102
+ const fields = await extractFieldsFromSchema(modelPath, schemaName);
103
+
104
+ // ネストスキーマ情報をフィールドにマージ
105
+ mergeNestedSchemaInfo(fields, nestedSchemaRefs);
106
+
107
+ // id フィールドの存在確認
108
+ const hasId = fields.some(f => f.name === "id");
109
+ const hasCreatedAt = fields.some(f => f.name === "createdAt");
110
+ const hasUpdatedAt = fields.some(f => f.name === "updatedAt");
111
+
112
+ // パーティションキーのバリデーション
113
+ if (partitionKey !== '/id') {
114
+ if (!partitionKey.startsWith('/')) {
115
+ console.warn(`⚠️ [${modelName}] partitionKey should start with '/': got '${partitionKey}'`);
116
+ }
117
+ const pkField = partitionKey.startsWith('/') ? partitionKey.slice(1) : partitionKey;
118
+ if (fields.length > 0 && !fields.some(f => f.name === pkField)) {
119
+ console.warn(`⚠️ [${modelName}] partitionKey field '${pkField}' not found in schema fields`);
120
+ }
121
+ }
122
+
123
+ return {
124
+ name: modelName,
125
+ displayName,
126
+ schemaName,
127
+ filePath: modelPath,
128
+ fields,
129
+ hasId,
130
+ hasCreatedAt,
131
+ hasUpdatedAt,
132
+ nestedSchemaRefs,
133
+ partitionKey,
134
+ ...(connectorConfig ? { connectorConfig } : {}),
135
+ ...(authPolicy ? { authPolicy } : {}),
136
+ };
137
+ }
138
+
139
+ /**
140
+ * import文とフィールド定義を解析し、ネストしたスキーマ参照を検出
141
+ */
142
+ function detectNestedSchemaRefs(
143
+ modelPath: string,
144
+ content: string,
145
+ schemaName: string
146
+ ): NestedSchemaRef[] {
147
+ const refs: NestedSchemaRef[] = [];
148
+
149
+ // 1. import文を解析して外部スキーマ変数を収集
150
+ // パターン: import { categorySchema } from './category'
151
+ // import { tagSchema, Tag } from './tag'
152
+ const importMap = new Map<string, string>(); // schemaVarName -> importPath
153
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
154
+ let importMatch;
155
+ while ((importMatch = importRegex.exec(content)) !== null) {
156
+ const imports = importMatch[1].split(',').map(s => s.trim());
157
+ const importPath = importMatch[2];
158
+ for (const imp of imports) {
159
+ // 'as' エイリアスに対応
160
+ const name = imp.split(/\s+as\s+/).pop()!.trim();
161
+ if (name.endsWith('Schema')) {
162
+ importMap.set(name, importPath);
163
+ } else {
164
+ // Zod公式パターン: インポート先ファイルで z.object として定義されているか確認
165
+ const dir = path.dirname(modelPath);
166
+ let targetPath = path.resolve(dir, importPath);
167
+ if (!targetPath.endsWith('.ts') && !targetPath.endsWith('.js')) {
168
+ targetPath += '.ts';
169
+ }
170
+ if (fs.existsSync(targetPath)) {
171
+ const targetContent = fs.readFileSync(targetPath, 'utf-8');
172
+ const isZodSchema = new RegExp(`(?:export\\s+)?const\\s+${name}\\s*=\\s*z\\.object\\s*\\(`).test(targetContent);
173
+ if (isZodSchema) {
174
+ importMap.set(name, importPath);
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ if (importMap.size === 0) {
182
+ return refs;
183
+ }
184
+
185
+ // 2. z.object({ ... }) の中身を取得
186
+ const objectContent = extractObjectContent(content, schemaName);
187
+ if (!objectContent) {
188
+ return refs;
189
+ }
190
+
191
+ // 3. 各フィールドでインポートしたスキーマ変数が使われているか検出
192
+ for (const [schemaVarName, importPath] of importMap) {
193
+ // スキーマ名からモデル名を推定: categorySchema -> Category
194
+ const modelName = schemaVarName.replace(/Schema$/, '');
195
+ const pascalModelName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
196
+
197
+ // フィールド定義内でこのスキーマが使われているパターンを検出
198
+ // パターン1: fieldName: schemaVarName
199
+ // パターン2: fieldName: schemaVarName.optional()
200
+ // パターン3: fieldName: z.array(schemaVarName)
201
+ // パターン4: fieldName: z.array(schemaVarName).optional()
202
+ const patterns = [
203
+ // 単一オブジェクト参照
204
+ new RegExp(`(\\w+)\\s*:\\s*${schemaVarName}(?:\\.optional\\(\\))?`, 'g'),
205
+ // 配列参照
206
+ new RegExp(`(\\w+)\\s*:\\s*z\\.array\\(\\s*${schemaVarName}\\s*\\)(?:\\.optional\\(\\))?`, 'g'),
207
+ ];
208
+
209
+ for (let patIdx = 0; patIdx < patterns.length; patIdx++) {
210
+ const pattern = patterns[patIdx];
211
+ let fieldMatch;
212
+ while ((fieldMatch = pattern.exec(objectContent)) !== null) {
213
+ const fieldName = fieldMatch[1];
214
+ const fullMatch = fieldMatch[0];
215
+ const isArray = patIdx === 1;
216
+ const isOptional = fullMatch.includes('.optional()');
217
+
218
+ // 参照先スキーマの表示用フィールドを推定
219
+ const displayField = detectDisplayField(modelPath, importPath);
220
+
221
+ refs.push({
222
+ fieldName,
223
+ schemaName: schemaVarName,
224
+ modelName: pascalModelName,
225
+ importPath,
226
+ isArray,
227
+ isOptional,
228
+ displayField,
229
+ });
230
+ }
231
+ }
232
+ }
233
+
234
+ return refs;
235
+ }
236
+
237
+ /**
238
+ * z.object({ ... }) の内容部分を抽出
239
+ */
240
+ function extractObjectContent(content: string, schemaName: string): string | null {
241
+ const objectRegex = new RegExp(`${schemaName}\\s*=\\s*z\\.object\\(\\{`, "s");
242
+ const objectStart = content.search(objectRegex);
243
+
244
+ if (objectStart === -1) {
245
+ return null;
246
+ }
247
+
248
+ const braceStart = content.indexOf('{', objectStart);
249
+ let braceCount = 1;
250
+ let objectEnd = braceStart + 1;
251
+
252
+ while (braceCount > 0 && objectEnd < content.length) {
253
+ if (content[objectEnd] === '{') braceCount++;
254
+ if (content[objectEnd] === '}') braceCount--;
255
+ objectEnd++;
256
+ }
257
+
258
+ return content.substring(braceStart + 1, objectEnd - 1);
259
+ }
260
+
261
+ /**
262
+ * 参照先スキーマファイルから主要な表示フィールドを推定
263
+ */
264
+ function detectDisplayField(currentModelPath: string, importPath: string): string {
265
+ try {
266
+ const dir = path.dirname(currentModelPath);
267
+ let targetPath = path.resolve(dir, importPath);
268
+
269
+ // .ts 拡張子を補完
270
+ if (!targetPath.endsWith('.ts')) {
271
+ targetPath += '.ts';
272
+ }
273
+
274
+ if (!fs.existsSync(targetPath)) {
275
+ return 'name';
276
+ }
277
+
278
+ const targetContent = fs.readFileSync(targetPath, 'utf-8');
279
+
280
+ // 'name' フィールドがあれば最優先
281
+ if (/\bname\s*:\s*z\./.test(targetContent)) {
282
+ return 'name';
283
+ }
284
+ // 'title' フィールドがあれば次点
285
+ if (/\btitle\s*:\s*z\./.test(targetContent)) {
286
+ return 'title';
287
+ }
288
+ // 'label' フィールドがあれば
289
+ if (/\blabel\s*:\s*z\./.test(targetContent)) {
290
+ return 'label';
291
+ }
292
+
293
+ return 'name';
294
+ } catch {
295
+ return 'name';
296
+ }
297
+ }
298
+
299
+ /**
300
+ * ネストスキーマ情報をフィールド情報にマージ
301
+ */
302
+ function mergeNestedSchemaInfo(fields: FieldInfo[], nestedRefs: NestedSchemaRef[]): void {
303
+ for (const ref of nestedRefs) {
304
+ const field = fields.find(f => f.name === ref.fieldName);
305
+ if (field) {
306
+ field.isNestedSchema = true;
307
+ field.nestedSchemaName = ref.schemaName;
308
+ field.nestedModelName = ref.modelName;
309
+ field.nestedDisplayField = ref.displayField;
310
+ // object/array 型のままにする(動的解析が 'object' を返す)
311
+ } else {
312
+ // 動的解析で検出されなかったフィールドを追加
313
+ fields.push({
314
+ name: ref.fieldName,
315
+ type: ref.isArray ? 'array' : 'object',
316
+ isOptional: ref.isOptional,
317
+ isArray: ref.isArray,
318
+ isNestedSchema: true,
319
+ nestedSchemaName: ref.schemaName,
320
+ nestedModelName: ref.modelName,
321
+ nestedDisplayField: ref.displayField,
322
+ });
323
+ }
324
+ }
325
+ }
326
+
327
+ /**
328
+ * ローカル依存ファイルを再帰的にインライン化する(動的インポート用一時スクリプト生成用)。
329
+ * seen によって循環依存を防ぐ。
330
+ * importedNames が指定された場合はその名前の const だけを残し、他を除去する。
331
+ */
332
+ function inlineLocalDeps(
333
+ filePath: string,
334
+ importedNames: Set<string> | null,
335
+ seen: Set<string>
336
+ ): string {
337
+ const resolvedPath = path.resolve(filePath);
338
+ if (seen.has(resolvedPath)) return '';
339
+ seen.add(resolvedPath);
340
+
341
+ if (!fs.existsSync(resolvedPath)) return '';
342
+
343
+ let content = fs.readFileSync(resolvedPath, 'utf8');
344
+ const fileDir = path.dirname(resolvedPath);
345
+ const transitiveParts: string[] = [];
346
+
347
+ // ローカル(相対)インポートを再帰的にインライン化
348
+ content = content.replace(
349
+ /import\s*\{([^}]+)\}\s*from\s*['"](\.[^'"]+)['"]\s*;?/g,
350
+ (_match: string, _imports: string, importPath: string) => {
351
+ let depPath = path.resolve(fileDir, importPath);
352
+ if (!depPath.endsWith('.ts') && !depPath.endsWith('.js')) depPath += '.ts';
353
+ const inlined = inlineLocalDeps(depPath, null, seen);
354
+ if (inlined) transitiveParts.push(inlined);
355
+ return '';
356
+ }
357
+ );
358
+
359
+ // 外部パッケージのインポートを除去
360
+ content = content.replace(/import\s+.*?\s+from\s+['"](?!\.).*?['"];?\s*/g, '');
361
+ // export キーワードを除去
362
+ content = content.replace(/export\s+(const|type|interface|class|function)\s+/g, '$1 ');
363
+ // 型宣言を除去(ランタイムでは不要)
364
+ content = content.replace(/^type\s+\w+\s*=\s*[^;]+;/gm, '');
365
+ content = content.replace(/^interface\s+\w+\s*\{[\s\S]*?\}/gm, '');
366
+ // コメントを除去
367
+ content = content.replace(/\/\*[\s\S]*?\*\//g, '');
368
+ content = content.replace(/\/\/.*/g, '');
369
+
370
+ // 直接インポートされていない const 宣言を除去(displayName 等の重複を防ぐ)
371
+ if (importedNames) {
372
+ content = content.replace(/^const\s+(\w+)\s*=\s*[^;]+;/gm, (constMatch: string, varName: string) => {
373
+ return importedNames.has(varName) ? constMatch : '';
374
+ });
375
+ }
376
+
377
+ return [...transitiveParts, content.trim()].filter(Boolean).join('\n\n');
378
+ }
379
+
380
+ /**
381
+ * Zodスキーマから動的にフィールド情報を抽出
382
+ */
383
+ async function extractFieldsFromSchema(modelPath: string, schemaName: string): Promise<FieldInfo[]> {
384
+ const fields: FieldInfo[] = [];
385
+
386
+ try {
387
+ // モデルファイルの内容を読み込む
388
+ let modelContent = fs.readFileSync(modelPath, 'utf8');
389
+ const modelDir = path.dirname(path.resolve(modelPath));
390
+
391
+ // ローカル相対インポートを再帰的にインライン化して保持
392
+ // パターン: import { categorySchema } from './category'
393
+ const seenPaths = new Set<string>([path.resolve(modelPath)]);
394
+ const localImports: string[] = [];
395
+ modelContent = modelContent.replace(
396
+ /import\s*\{([^}]+)\}\s*from\s*['"](\.[^'"]+)['"]\s*;?/g,
397
+ (_match: string, imports: string, importPath: string) => {
398
+ // インポートされた変数名を取得
399
+ const importedNames = new Set(
400
+ imports.split(',').map(s => s.trim().split(/\s+as\s+/).pop()!.trim()).filter(s => s.length > 0)
401
+ );
402
+
403
+ // 相対パスを絶対パスに解決
404
+ let resolvedPath = path.resolve(modelDir, importPath);
405
+ if (!resolvedPath.endsWith('.ts') && !resolvedPath.endsWith('.js')) {
406
+ resolvedPath += '.ts';
407
+ }
408
+
409
+ // 推移的なローカル依存を含めて再帰的にインライン化
410
+ const inlined = inlineLocalDeps(resolvedPath, importedNames, seenPaths);
411
+ if (inlined) localImports.push(inlined);
412
+ return ''; // 元のインポート文は除去
413
+ }
414
+ );
415
+
416
+ // zod 以外のパッケージインポートを除去
417
+ modelContent = modelContent.replace(/import\s+.*?\s+from\s+['"].*?['"];?\s*/g, '');
418
+ modelContent = modelContent.replace(/export\s+(const|type|interface|class|function)\s+/g, '$1 ');
419
+ // type宣言を削除(ランタイムでは不要)
420
+ modelContent = modelContent.replace(/^type\s+\w+\s*=\s*[^;]+;/gm, '');
421
+ // interface宣言を削除(ランタイムでは不要、複数行対応)
422
+ modelContent = modelContent.replace(/^interface\s+\w+\s*\{[\s\S]*?\}/gm, '');
423
+ // コメントを削除
424
+ modelContent = modelContent.replace(/\/\*[\s\S]*?\*\//g, '');
425
+ modelContent = modelContent.replace(/\/\/.*/g, '');
426
+ // TypeScript の `as const` アサーションを削除(.mjs では構文エラーになる)
427
+ modelContent = modelContent.replace(/\s+as\s+const\b/g, '');
428
+
429
+ // インライン化したローカルインポートを先頭に追加
430
+ const inlinedDeps = localImports.length > 0 ? localImports.join('\n\n') + '\n\n' : '';
431
+
432
+ // プロジェクトルートを探す(package.jsonがある場所)
433
+ let projectRoot = path.dirname(modelPath);
434
+ while (projectRoot !== path.dirname(projectRoot)) {
435
+ if (fs.existsSync(path.join(projectRoot, 'package.json'))) {
436
+ break;
437
+ }
438
+ projectRoot = path.dirname(projectRoot);
439
+ }
440
+
441
+ // プロジェクトルート内に一時スクリプトファイルを作成
442
+ const tempScript = path.join(projectRoot, `.swallowkit-parser-${Date.now()}.mjs`);
443
+ const scriptCode = `
444
+ import { z } from 'zod/v4';
445
+
446
+ // インライン化した依存スキーマ
447
+ ${inlinedDeps}
448
+ // モデルファイルの内容を評価
449
+ ${modelContent}
450
+
451
+ const schema = ${schemaName};
452
+
453
+ // Zod v3とv4の両方に対応
454
+ const isObject = (schema && schema._def &&
455
+ (schema._def.typeName === 'ZodObject' || schema.constructor?.name === 'ZodObject' || typeof schema._def.shape === 'function'));
456
+
457
+ if (isObject) {
458
+ const shape = typeof schema._def.shape === 'function' ? schema._def.shape() : schema._def.shape;
459
+ const fields = Object.keys(shape).map(key => {
460
+ const field = shape[key];
461
+ let type = 'string';
462
+ let isOptional = false;
463
+ let isArray = false;
464
+ let enumValues = undefined;
465
+
466
+ // ZodOptional, ZodDefault, ZodEffects を unwrap
467
+ let fieldDef = field;
468
+ const getTypeName = (def) => def?._def?.typeName || def?.constructor?.name || '';
469
+
470
+ // 繰り返し unwrap(複数のラッパーがある場合に対応)
471
+ let unwrapped = false;
472
+ do {
473
+ unwrapped = false;
474
+ const typeName = getTypeName(fieldDef);
475
+
476
+ if (typeName === 'ZodOptional') {
477
+ isOptional = true;
478
+ fieldDef = fieldDef._def.innerType;
479
+ unwrapped = true;
480
+ } else if (typeName === 'ZodDefault') {
481
+ // .default() は optional と同様に扱う
482
+ isOptional = true;
483
+ fieldDef = fieldDef._def.innerType;
484
+ unwrapped = true;
485
+ } else if (typeName === 'ZodEffects') {
486
+ // .min(), .max(), .regex() などの Effects を unwrap
487
+ fieldDef = fieldDef._def.schema;
488
+ unwrapped = true;
489
+ }
490
+ } while (unwrapped);
491
+
492
+ // ZodArray をチェック
493
+ if (getTypeName(fieldDef) === 'ZodArray') {
494
+ isArray = true;
495
+ fieldDef = fieldDef._def.type || fieldDef._def.element;
496
+ }
497
+
498
+ // 基本型を判定
499
+ const typeName = getTypeName(fieldDef);
500
+ if (typeName === 'ZodString') type = 'string';
501
+ else if (typeName === 'ZodNumber') type = 'number';
502
+ else if (typeName === 'ZodBoolean') type = 'boolean';
503
+ else if (typeName === 'ZodDate') type = 'date';
504
+ else if (typeName === 'ZodObject') type = 'object';
505
+ else if (typeName === 'ZodEnum' || typeName === 'ZodNativeEnum') {
506
+ type = 'string';
507
+ // enum の選択肢を取得(複数のZodバージョンに対応)
508
+ if (fieldDef.options) {
509
+ // Zod v3.23+ では options プロパティを使用
510
+ enumValues = Array.isArray(fieldDef.options)
511
+ ? fieldDef.options
512
+ : Object.values(fieldDef.options);
513
+ } else if (fieldDef._def.values) {
514
+ // 古いバージョンでは _def.values を使用
515
+ enumValues = Array.isArray(fieldDef._def.values)
516
+ ? fieldDef._def.values
517
+ : Object.values(fieldDef._def.values);
518
+ } else if (fieldDef._def.entries) {
519
+ // さらに古いバージョンでは _def.entries を使用
520
+ enumValues = Array.isArray(fieldDef._def.entries)
521
+ ? fieldDef._def.entries
522
+ : Object.values(fieldDef._def.entries);
523
+ }
524
+ }
525
+
526
+ // 外部キー検出: フィールド名が <ModelName>Id のパターンの場合
527
+ let isForeignKey = false;
528
+ let referencedModel = undefined;
529
+ if (key.endsWith('Id') && key.length > 2 && type === 'string') {
530
+ // categoryId -> Category, userId -> User など
531
+ const modelName = key.slice(0, -2); // "Id" を除去
532
+ referencedModel = modelName.charAt(0).toUpperCase() + modelName.slice(1); // 先頭を大文字に
533
+ isForeignKey = true;
534
+ }
535
+
536
+ return { name: key, type, isOptional, isArray, enumValues, isForeignKey, referencedModel };
537
+ });
538
+
539
+ console.log(JSON.stringify(fields));
540
+ }
541
+ `;
542
+
543
+ fs.writeFileSync(tempScript, scriptCode, 'utf8');
544
+
545
+ try {
546
+ // プロジェクトルートでtsxを実行 (npm/pnpm 両対応)
547
+ // Detect package manager from project lockfile
548
+ const pm = detectFromProject(projectRoot);
549
+ const pmCmd = getCommands(pm);
550
+ const result = execSync(`${pmCmd.exec} tsx "${tempScript}"`, {
551
+ encoding: 'utf8',
552
+ cwd: projectRoot,
553
+ });
554
+
555
+ const stdout = result;
556
+
557
+ if (stdout) {
558
+ const parsedFields = JSON.parse(stdout.trim());
559
+ fields.push(...parsedFields);
560
+ }
561
+ } finally {
562
+ // 一時ファイルを削除
563
+ try {
564
+ fs.unlinkSync(tempScript);
565
+ } catch (e) {
566
+ // ファイル削除失敗は無視
567
+ }
568
+ }
569
+ } catch (error) {
570
+ // tsxが使えない場合は正規表現フォールバック
571
+ console.warn('Failed to use dynamic import, falling back to regex parsing');
572
+ console.warn('Error:', error);
573
+ return extractFieldsWithRegex(modelPath, schemaName);
574
+ }
575
+
576
+ return fields;
577
+ }
578
+
579
+ /**
580
+ * 正規表現でフィールド情報を抽出(フォールバック用)
581
+ */
582
+ function extractFieldsWithRegex(modelPath: string, schemaName: string): FieldInfo[] {
583
+ const fields: FieldInfo[] = [];
584
+ const content = fs.readFileSync(modelPath, "utf-8");
585
+
586
+ // z.object({ ... }) の内容を抽出(ネストした括弧に対応)
587
+ const objectContent = extractObjectContent(content, schemaName);
588
+
589
+ if (!objectContent) {
590
+ return fields;
591
+ }
592
+
593
+ // 各フィールドを解析
594
+ const fieldRegex = /(\w+)\s*:\s*(z\.\w+)/g;
595
+ let match;
596
+
597
+ while ((match = fieldRegex.exec(objectContent)) !== null) {
598
+ const fieldName = match[1];
599
+ const zodDef = match[2];
600
+ const zodType = zodDef.split('.')[1];
601
+
602
+ const fieldStart = match.index;
603
+ const fieldEnd = objectContent.indexOf(',', fieldStart);
604
+ const fieldDef = fieldEnd > -1
605
+ ? objectContent.substring(fieldStart, fieldEnd)
606
+ : objectContent.substring(fieldStart);
607
+
608
+ fields.push({
609
+ name: fieldName,
610
+ type: mapZodTypeToTs(zodType),
611
+ isOptional: fieldDef.includes(".optional()"),
612
+ isArray: fieldDef.includes(".array()"),
613
+ });
614
+ }
615
+
616
+ return fields;
617
+ }
618
+
619
+ /**
620
+ * Zod 型を TypeScript 型にマッピング
621
+ */
622
+ function mapZodTypeToTs(zodType: string): string {
623
+ const typeMap: Record<string, string> = {
624
+ string: "string",
625
+ number: "number",
626
+ boolean: "boolean",
627
+ date: "Date",
628
+ object: "object",
629
+ array: "array",
630
+ };
631
+
632
+ return typeMap[zodType] || "any";
633
+ }
634
+
635
+ /**
636
+ * 文字列を PascalCase に変換
637
+ */
638
+ export function toPascalCase(str: string): string {
639
+ return str
640
+ .split(/[-_]/)
641
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
642
+ .join("");
643
+ }
644
+
645
+ /**
646
+ * 文字列を camelCase に変換
647
+ */
648
+ export function toCamelCase(str: string): string {
649
+ const pascal = toPascalCase(str);
650
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
651
+ }
652
+
653
+ /**
654
+ * モデルファイルから connectorConfig エクスポートを静的解析で抽出する
655
+ */
656
+ export function parseConnectorConfig(content: string): ModelConnectorConfig | undefined {
657
+ // export const connectorConfig = { ... } を検出
658
+ const connectorMatch = content.match(/export\s+const\s+connectorConfig\s*=\s*\{/);
659
+ if (!connectorMatch) {
660
+ return undefined;
661
+ }
662
+
663
+ // connectorConfig オブジェクトの内容を抽出
664
+ const startIdx = content.indexOf('{', connectorMatch.index!);
665
+ let braceCount = 1;
666
+ let endIdx = startIdx + 1;
667
+ while (braceCount > 0 && endIdx < content.length) {
668
+ if (content[endIdx] === '{') braceCount++;
669
+ if (content[endIdx] === '}') braceCount--;
670
+ endIdx++;
671
+ }
672
+
673
+ const objectStr = content.substring(startIdx, endIdx);
674
+
675
+ // connector 名を抽出
676
+ const connectorNameMatch = objectStr.match(/connector\s*:\s*['"]([^'"]+)['"]/);
677
+ if (!connectorNameMatch) {
678
+ return undefined;
679
+ }
680
+ const connector = connectorNameMatch[1];
681
+
682
+ // operations を抽出
683
+ const opsMatch = objectStr.match(/operations\s*:\s*\[([^\]]*)\]/);
684
+ const operations: string[] = [];
685
+ if (opsMatch) {
686
+ const opsStr = opsMatch[1];
687
+ const opEntries = opsStr.match(/['"]([^'"]+)['"]/g);
688
+ if (opEntries) {
689
+ for (const entry of opEntries) {
690
+ operations.push(entry.replace(/['"]/g, ''));
691
+ }
692
+ }
693
+ }
694
+
695
+ // table を抽出(RDB 固有)
696
+ const tableMatch = objectStr.match(/table\s*:\s*['"]([^'"]+)['"]/);
697
+
698
+ // idColumn を抽出(RDB 固有)
699
+ const idColumnMatch = objectStr.match(/idColumn\s*:\s*['"]([^'"]+)['"]/);
700
+
701
+ // endpoints を抽出(API 固有)— endpoint値に {id} 等のプレースホルダが含まれるためbrace-countingで抽出
702
+ let endpoints: Record<string, string> | undefined;
703
+ const endpointsStart = objectStr.match(/endpoints\s*:\s*\{/);
704
+ if (endpointsStart) {
705
+ const epStartIdx = objectStr.indexOf('{', endpointsStart.index!);
706
+ let epBraceCount = 1;
707
+ let epEndIdx = epStartIdx + 1;
708
+ while (epBraceCount > 0 && epEndIdx < objectStr.length) {
709
+ const ch = objectStr[epEndIdx];
710
+ // 文字列内の波括弧をスキップ
711
+ if (ch === "'" || ch === '"') {
712
+ epEndIdx++;
713
+ while (epEndIdx < objectStr.length && objectStr[epEndIdx] !== ch) {
714
+ epEndIdx++;
715
+ }
716
+ } else if (ch === '{') {
717
+ epBraceCount++;
718
+ } else if (ch === '}') {
719
+ epBraceCount--;
720
+ }
721
+ epEndIdx++;
722
+ }
723
+ const epStr = objectStr.substring(epStartIdx + 1, epEndIdx - 1);
724
+ endpoints = {};
725
+ const endpointEntries = epStr.matchAll(/(\w+)\s*:\s*['"]([^'"]+)['"]/g);
726
+ for (const entry of endpointEntries) {
727
+ endpoints[entry[1]] = entry[2];
728
+ }
729
+ }
730
+
731
+ if (tableMatch) {
732
+ // RDB コネクタ
733
+ return {
734
+ connector,
735
+ operations: operations as ModelConnectorConfig['operations'],
736
+ table: tableMatch[1],
737
+ ...(idColumnMatch ? { idColumn: idColumnMatch[1] } : {}),
738
+ };
739
+ }
740
+
741
+ // API コネクタ
742
+ return {
743
+ connector,
744
+ operations: operations as ModelConnectorConfig['operations'],
745
+ ...(endpoints ? { endpoints } : {}),
746
+ };
747
+ }
748
+
749
+ /**
750
+ * authPolicy をモデルファイルから抽出
751
+ * パターン: export const authPolicy = { roles: [...], read: [...], write: [...] }
752
+ */
753
+ export function parseAuthPolicy(content: string): ModelAuthPolicy | undefined {
754
+ const policyMatch = content.match(/export\s+const\s+authPolicy\s*=\s*\{/);
755
+ if (!policyMatch) {
756
+ return undefined;
757
+ }
758
+
759
+ const startIdx = content.indexOf('{', policyMatch.index!);
760
+ let braceCount = 1;
761
+ let endIdx = startIdx + 1;
762
+ while (braceCount > 0 && endIdx < content.length) {
763
+ if (content[endIdx] === '{') braceCount++;
764
+ if (content[endIdx] === '}') braceCount--;
765
+ endIdx++;
766
+ }
767
+
768
+ const objectStr = content.substring(startIdx, endIdx);
769
+
770
+ const extractRoles = (key: string): string[] | undefined => {
771
+ const match = objectStr.match(new RegExp(`${key}\\s*:\\s*\\[([^\\]]*)\\]`));
772
+ if (!match) return undefined;
773
+ const entries = match[1].match(/['"]([^'"]+)['"]/g);
774
+ if (!entries) return [];
775
+ return entries.map(e => e.replace(/['"]/g, ''));
776
+ };
777
+
778
+ const roles = extractRoles('roles');
779
+ const read = extractRoles('read');
780
+ const write = extractRoles('write');
781
+
782
+ if (!roles && !read && !write) {
783
+ return undefined;
784
+ }
785
+
786
+ return {
787
+ ...(roles ? { roles } : {}),
788
+ ...(read ? { read } : {}),
789
+ ...(write ? { write } : {}),
790
+ };
791
+ }
792
+
793
+ /**
794
+ * パーティションキーを抽出する
795
+ * export const partitionKey = '/tenantId' のようなエクスポートを検出
796
+ * 未指定の場合はデフォルト '/id' を返す
797
+ */
798
+ export function parsePartitionKey(content: string): string {
799
+ const match = content.match(/export\s+const\s+partitionKey\s*=\s*['"]([^'"]+)['"]/);
800
+ return match ? match[1] : '/id';
801
+ }
802
+
803
+ /**
804
+ * 文字列を kebab-case に変換
805
+ */
806
+ export function toKebabCase(str: string): string {
807
+ return str
808
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
809
+ .toLowerCase();
810
+ }
811
+
812
+ /**
813
+ * models ディレクトリから全てのモデル情報を取得
814
+ * @param modelsDir モデルディレクトリのパス(デフォルト: "shared/models")
815
+ * @returns モデル情報の配列
816
+ */
817
+ export async function getAllModels(modelsDir: string = "shared/models"): Promise<ModelInfo[]> {
818
+ const cwd = process.cwd();
819
+ const fullModelsDir = path.join(cwd, modelsDir);
820
+
821
+ if (!fs.existsSync(fullModelsDir)) {
822
+ return [];
823
+ }
824
+
825
+ const files = fs.readdirSync(fullModelsDir);
826
+ const modelFiles = files.filter(file => file.endsWith('.ts'));
827
+
828
+ const models: ModelInfo[] = [];
829
+
830
+ for (const file of modelFiles) {
831
+ try {
832
+ const modelPath = path.join(fullModelsDir, file);
833
+ const modelInfo = await parseModelFile(modelPath);
834
+ models.push(modelInfo);
835
+ } catch (error: any) {
836
+ console.warn(`⚠️ Failed to parse model file ${file}: ${error.message}`);
837
+ }
838
+ }
839
+
840
+ return models;
841
+ }