swallowkit 1.0.0-beta.12 → 1.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +33 -9
- package/README.md +33 -9
- package/dist/__tests__/fixtures.d.ts +8 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -1
- package/dist/__tests__/fixtures.js +60 -0
- package/dist/__tests__/fixtures.js.map +1 -1
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +161 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -0
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +65 -1
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +49 -3
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +97 -6
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.js +15 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +3 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +37 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +67 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +285 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1009 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/model-parser.d.ts +6 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +94 -0
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +133 -0
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/types/index.d.ts +31 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/config.test.ts +141 -0
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +252 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/fixtures.ts +60 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-connector.ts +157 -0
- package/src/cli/commands/create-model.ts +72 -2
- package/src/cli/commands/dev.ts +55 -4
- package/src/cli/commands/scaffold.ts +176 -10
- package/src/cli/index.ts +16 -0
- package/src/core/config.ts +42 -1
- package/src/core/mock/connector-mock-server.ts +307 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/scaffold/connector-functions-generator.ts +1106 -0
- package/src/core/scaffold/model-parser.ts +103 -0
- package/src/core/scaffold/nextjs-generator.ts +154 -0
- package/src/types/index.ts +47 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* コネクタモデル用モックサーバー
|
|
3
|
+
* - コネクタモデルへのリクエスト → Zodベースのインメモリ CRUD で応答
|
|
4
|
+
* - その他のリクエスト → 実Azure Functions へプロキシ
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as http from "http";
|
|
8
|
+
import { ModelInfo, toCamelCase } from "../scaffold/model-parser";
|
|
9
|
+
import { generateMockDocuments } from "./zod-mock-generator";
|
|
10
|
+
import { loadDevSeedFiles } from "../../cli/commands/dev-seeds";
|
|
11
|
+
|
|
12
|
+
export interface ConnectorMockServerOptions {
|
|
13
|
+
/** モックサーバーのリッスンポート */
|
|
14
|
+
port: number;
|
|
15
|
+
/** Azure Functions の転送先ホスト:ポート(例: "localhost:7071") */
|
|
16
|
+
functionsTarget: string;
|
|
17
|
+
/** コネクタモデル一覧 */
|
|
18
|
+
connectorModels: ModelInfo[];
|
|
19
|
+
/** 全モデル一覧(dev-seeds 読み込み用) */
|
|
20
|
+
allModels?: ModelInfo[];
|
|
21
|
+
/** dev-seeds 環境名(指定時はシードデータを初期データとして読み込む) */
|
|
22
|
+
seedEnv?: string;
|
|
23
|
+
/** dev-seeds ディレクトリ */
|
|
24
|
+
seedsDir?: string;
|
|
25
|
+
/** 各モデルの初期生成レコード数(デフォルト: 5) */
|
|
26
|
+
mockCount?: number;
|
|
27
|
+
/** ホスト名 */
|
|
28
|
+
host?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type MockDocument = Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* コネクタモデル用モックサーバー
|
|
35
|
+
*/
|
|
36
|
+
export class ConnectorMockServer {
|
|
37
|
+
private server: http.Server | null = null;
|
|
38
|
+
private stores = new Map<string, MockDocument[]>();
|
|
39
|
+
private routeMap = new Map<string, ModelInfo>(); // route → model
|
|
40
|
+
private options: ConnectorMockServerOptions;
|
|
41
|
+
|
|
42
|
+
constructor(options: ConnectorMockServerOptions) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
|
|
45
|
+
// ルートマップを構築: モデル名(camelCase) → ModelInfo
|
|
46
|
+
for (const model of options.connectorModels) {
|
|
47
|
+
const route = toCamelCase(model.name);
|
|
48
|
+
this.routeMap.set(route, model);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* モックデータを初期化し、サーバーを起動
|
|
54
|
+
*/
|
|
55
|
+
async start(): Promise<void> {
|
|
56
|
+
await this.initializeStores();
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
this.server = http.createServer((req, res) => {
|
|
60
|
+
this.handleRequest(req, res);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.server.on("error", reject);
|
|
64
|
+
this.server.listen(this.options.port, this.options.host || "localhost", () => {
|
|
65
|
+
resolve();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* サーバーを停止
|
|
72
|
+
*/
|
|
73
|
+
stop(): Promise<void> {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
if (this.server) {
|
|
76
|
+
// Force close keep-alive connections (Node.js 18.2+)
|
|
77
|
+
if (typeof (this.server as any).closeAllConnections === "function") {
|
|
78
|
+
(this.server as any).closeAllConnections();
|
|
79
|
+
}
|
|
80
|
+
this.server.close(() => resolve());
|
|
81
|
+
} else {
|
|
82
|
+
resolve();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 特定モデルの現在のストアデータを取得(テスト用)
|
|
89
|
+
*/
|
|
90
|
+
getStore(modelName: string): MockDocument[] {
|
|
91
|
+
const route = toCamelCase(modelName);
|
|
92
|
+
return this.stores.get(route) || [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Internal ─────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
private async initializeStores() {
|
|
98
|
+
const allModels = this.options.allModels || this.options.connectorModels;
|
|
99
|
+
const mockCount = this.options.mockCount ?? 5;
|
|
100
|
+
|
|
101
|
+
// まず Zod ベースのモックデータで初期化
|
|
102
|
+
for (const model of this.options.connectorModels) {
|
|
103
|
+
const route = toCamelCase(model.name);
|
|
104
|
+
this.stores.set(route, generateMockDocuments(model, mockCount, allModels));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// dev-seeds があれば上書き
|
|
108
|
+
if (this.options.seedEnv) {
|
|
109
|
+
try {
|
|
110
|
+
const seedFiles = await loadDevSeedFiles(
|
|
111
|
+
this.options.seedEnv,
|
|
112
|
+
this.options.connectorModels,
|
|
113
|
+
this.options.seedsDir
|
|
114
|
+
);
|
|
115
|
+
for (const seedFile of seedFiles) {
|
|
116
|
+
const route = toCamelCase(seedFile.model.name);
|
|
117
|
+
if (this.stores.has(route)) {
|
|
118
|
+
this.stores.set(route, seedFile.documents as MockDocument[]);
|
|
119
|
+
console.log(` 📂 Loaded ${seedFile.documents.length} seed doc(s) for ${seedFile.model.name}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.warn(`⚠️ Failed to load dev-seeds for connectors: ${(err as Error).message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
129
|
+
const { route, id } = this.parseRoute(req.url || "");
|
|
130
|
+
|
|
131
|
+
// コネクタモデルのルートか判定
|
|
132
|
+
if (route && this.routeMap.has(route)) {
|
|
133
|
+
this.handleMockCrud(req, res, route, id);
|
|
134
|
+
} else {
|
|
135
|
+
this.proxyToFunctions(req, res);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* URL パス解析: /api/backlogIssue/123 → { route: "backlogIssue", id: "123" }
|
|
141
|
+
*/
|
|
142
|
+
private parseRoute(url: string): { route: string | null; id: string | null } {
|
|
143
|
+
const parsed = new URL(url, "http://localhost");
|
|
144
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
145
|
+
|
|
146
|
+
// /api/<route> or /api/<route>/<id>
|
|
147
|
+
if (segments.length >= 2 && segments[0] === "api") {
|
|
148
|
+
return {
|
|
149
|
+
route: segments[1],
|
|
150
|
+
id: segments.length >= 3 ? segments[2] : null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { route: null, id: null };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private handleMockCrud(
|
|
158
|
+
req: http.IncomingMessage,
|
|
159
|
+
res: http.ServerResponse,
|
|
160
|
+
route: string,
|
|
161
|
+
id: string | null
|
|
162
|
+
) {
|
|
163
|
+
const method = (req.method || "GET").toUpperCase();
|
|
164
|
+
const store = this.stores.get(route) || [];
|
|
165
|
+
const model = this.routeMap.get(route)!;
|
|
166
|
+
const ops = model.connectorConfig?.operations || [];
|
|
167
|
+
|
|
168
|
+
switch (method) {
|
|
169
|
+
case "GET":
|
|
170
|
+
req.resume();
|
|
171
|
+
if (id) {
|
|
172
|
+
if (!ops.includes("getById")) {
|
|
173
|
+
return this.sendJson(res, 405, { error: "getById not supported" });
|
|
174
|
+
}
|
|
175
|
+
const item = store.find((doc) => doc.id === id);
|
|
176
|
+
return item
|
|
177
|
+
? this.sendJson(res, 200, item)
|
|
178
|
+
: this.sendJson(res, 404, { error: "Item not found" });
|
|
179
|
+
}
|
|
180
|
+
if (!ops.includes("getAll")) {
|
|
181
|
+
return this.sendJson(res, 405, { error: "getAll not supported" });
|
|
182
|
+
}
|
|
183
|
+
return this.sendJson(res, 200, store);
|
|
184
|
+
|
|
185
|
+
case "POST":
|
|
186
|
+
if (!ops.includes("create")) {
|
|
187
|
+
return this.drainAndRespond(req, res, 405, { error: "create not supported" });
|
|
188
|
+
}
|
|
189
|
+
this.readBody(req, (body) => {
|
|
190
|
+
if (!body.id) {
|
|
191
|
+
body.id = `${route}-${Date.now()}`;
|
|
192
|
+
}
|
|
193
|
+
body.createdAt = body.createdAt || new Date().toISOString();
|
|
194
|
+
body.updatedAt = body.updatedAt || new Date().toISOString();
|
|
195
|
+
store.push(body);
|
|
196
|
+
this.sendJson(res, 201, body);
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
|
|
200
|
+
case "PUT":
|
|
201
|
+
if (!ops.includes("update")) {
|
|
202
|
+
return this.drainAndRespond(req, res, 405, { error: "update not supported" });
|
|
203
|
+
}
|
|
204
|
+
if (!id) return this.drainAndRespond(req, res, 400, { error: "id required" });
|
|
205
|
+
this.readBody(req, (body) => {
|
|
206
|
+
const idx = store.findIndex((doc) => doc.id === id);
|
|
207
|
+
if (idx === -1) return this.sendJson(res, 404, { error: "Item not found" });
|
|
208
|
+
body.updatedAt = new Date().toISOString();
|
|
209
|
+
store[idx] = { ...store[idx], ...body, id };
|
|
210
|
+
this.sendJson(res, 200, store[idx]);
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
|
|
214
|
+
case "DELETE":
|
|
215
|
+
req.resume();
|
|
216
|
+
if (!ops.includes("delete")) {
|
|
217
|
+
return this.sendJson(res, 405, { error: "delete not supported" });
|
|
218
|
+
}
|
|
219
|
+
if (!id) return this.sendJson(res, 400, { error: "id required" });
|
|
220
|
+
const deleteIdx = store.findIndex((doc) => doc.id === id);
|
|
221
|
+
if (deleteIdx === -1) return this.sendJson(res, 404, { error: "Item not found" });
|
|
222
|
+
store.splice(deleteIdx, 1);
|
|
223
|
+
return this.sendJson(res, 204, null);
|
|
224
|
+
|
|
225
|
+
default:
|
|
226
|
+
req.resume();
|
|
227
|
+
return this.sendJson(res, 405, { error: `Method ${method} not allowed` });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* コネクタモデル以外のリクエストをAzure Functionsへプロキシ
|
|
233
|
+
*/
|
|
234
|
+
private proxyToFunctions(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
235
|
+
const [targetHost, targetPort] = this.options.functionsTarget.split(":");
|
|
236
|
+
|
|
237
|
+
const proxyOpts: http.RequestOptions = {
|
|
238
|
+
hostname: targetHost,
|
|
239
|
+
port: parseInt(targetPort, 10),
|
|
240
|
+
path: req.url,
|
|
241
|
+
method: req.method,
|
|
242
|
+
headers: { ...req.headers, host: this.options.functionsTarget },
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const proxyReq = http.request(proxyOpts, (proxyRes) => {
|
|
246
|
+
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
|
247
|
+
proxyRes.pipe(res, { end: true });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
proxyReq.on("error", (err) => {
|
|
251
|
+
// Functions がまだ起動していない場合
|
|
252
|
+
this.sendJson(res, 502, {
|
|
253
|
+
error: "Azure Functions not available",
|
|
254
|
+
detail: err.message,
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
req.pipe(proxyReq, { end: true });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── Utilities ────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
private readBody(req: http.IncomingMessage, callback: (body: MockDocument) => void) {
|
|
264
|
+
let data = "";
|
|
265
|
+
req.on("data", (chunk) => (data += chunk));
|
|
266
|
+
req.on("end", () => {
|
|
267
|
+
try {
|
|
268
|
+
callback(JSON.parse(data));
|
|
269
|
+
} catch {
|
|
270
|
+
callback({});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private sendJson(res: http.ServerResponse, status: number, body: unknown) {
|
|
276
|
+
res.writeHead(status, {
|
|
277
|
+
"Content-Type": "application/json",
|
|
278
|
+
"Access-Control-Allow-Origin": "*",
|
|
279
|
+
"Connection": "close",
|
|
280
|
+
});
|
|
281
|
+
if (body !== null && body !== undefined) {
|
|
282
|
+
res.end(JSON.stringify(body));
|
|
283
|
+
} else {
|
|
284
|
+
res.end();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* リクエストボディを完全に読み捨ててからレスポンスを送信
|
|
290
|
+
* POST/PUT の 405 等で未読ボディがある場合に Linux で RST を防ぐ
|
|
291
|
+
*/
|
|
292
|
+
private drainAndRespond(
|
|
293
|
+
req: http.IncomingMessage,
|
|
294
|
+
res: http.ServerResponse,
|
|
295
|
+
status: number,
|
|
296
|
+
body: unknown
|
|
297
|
+
) {
|
|
298
|
+
if (req.readableEnded) {
|
|
299
|
+
this.sendJson(res, status, body);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
req.resume();
|
|
303
|
+
req.on("end", () => {
|
|
304
|
+
this.sendJson(res, status, body);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod スキーマ(ModelInfo)からモックデータを自動生成する
|
|
3
|
+
* フィールド名・型のヒューリスティクスで現実的な値を生成
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ModelInfo, FieldInfo, toCamelCase, toKebabCase } from "../scaffold/model-parser";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 単一のモックドキュメントを生成
|
|
10
|
+
*/
|
|
11
|
+
export function generateMockDocument(
|
|
12
|
+
model: ModelInfo,
|
|
13
|
+
index: number,
|
|
14
|
+
allModels: ModelInfo[] = [model],
|
|
15
|
+
seenModels: Set<string> = new Set()
|
|
16
|
+
): Record<string, unknown> {
|
|
17
|
+
const nextSeen = new Set(seenModels);
|
|
18
|
+
nextSeen.add(model.name);
|
|
19
|
+
|
|
20
|
+
const doc: Record<string, unknown> = {};
|
|
21
|
+
for (const field of model.fields) {
|
|
22
|
+
doc[field.name] = generateFieldValue(model, field, index, allModels, nextSeen);
|
|
23
|
+
}
|
|
24
|
+
return doc;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 複数のモックドキュメントを生成
|
|
29
|
+
*/
|
|
30
|
+
export function generateMockDocuments(
|
|
31
|
+
model: ModelInfo,
|
|
32
|
+
count: number = 5,
|
|
33
|
+
allModels: ModelInfo[] = [model]
|
|
34
|
+
): Record<string, unknown>[] {
|
|
35
|
+
const docs: Record<string, unknown>[] = [];
|
|
36
|
+
for (let i = 0; i < count; i++) {
|
|
37
|
+
docs.push(generateMockDocument(model, i + 1, allModels));
|
|
38
|
+
}
|
|
39
|
+
return docs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Field Value Generation ─────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function generateFieldValue(
|
|
45
|
+
model: ModelInfo,
|
|
46
|
+
field: FieldInfo,
|
|
47
|
+
index: number,
|
|
48
|
+
allModels: ModelInfo[],
|
|
49
|
+
seenModels: Set<string>
|
|
50
|
+
): unknown {
|
|
51
|
+
// ネストされたスキーマ
|
|
52
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
53
|
+
const nestedModel = allModels.find((m) => m.name === field.nestedModelName);
|
|
54
|
+
if (nestedModel && !seenModels.has(nestedModel.name)) {
|
|
55
|
+
const nested = generateMockDocument(nestedModel, index, allModels, seenModels);
|
|
56
|
+
return field.isArray ? [nested] : nested;
|
|
57
|
+
}
|
|
58
|
+
const fallback = { id: `${toKebabCase(field.nestedModelName!)}-${padIndex(index)}` };
|
|
59
|
+
return field.isArray ? [fallback] : fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (field.isArray) {
|
|
63
|
+
return [generateScalarValue(model, field, index)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return generateScalarValue(model, field, index);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function generateScalarValue(
|
|
70
|
+
model: ModelInfo,
|
|
71
|
+
field: FieldInfo,
|
|
72
|
+
index: number
|
|
73
|
+
): unknown {
|
|
74
|
+
const name = field.name.toLowerCase();
|
|
75
|
+
const modelKebab = toKebabCase(model.name);
|
|
76
|
+
|
|
77
|
+
// id フィールド
|
|
78
|
+
if (field.name === "id") {
|
|
79
|
+
return `${modelKebab}-${padIndex(index)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// enum フィールド — ラウンドロビン
|
|
83
|
+
if (field.enumValues && field.enumValues.length > 0) {
|
|
84
|
+
return field.enumValues[(index - 1) % field.enumValues.length];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// boolean フィールド
|
|
88
|
+
if (field.type === "boolean") {
|
|
89
|
+
return index % 2 === 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// number フィールド
|
|
93
|
+
if (field.type === "number") {
|
|
94
|
+
return generateNumberValue(name, index);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// date フィールド / *At パターン
|
|
98
|
+
if (field.type === "date" || name.endsWith("at")) {
|
|
99
|
+
return generateDateValue(index);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// object フィールド
|
|
103
|
+
if (field.type === "object") {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// string フィールド — 名前パターンで分岐
|
|
108
|
+
return generateStringValue(model, field, index);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function generateStringValue(model: ModelInfo, field: FieldInfo, index: number): string {
|
|
112
|
+
const name = field.name.toLowerCase();
|
|
113
|
+
const modelKebab = toKebabCase(model.name);
|
|
114
|
+
|
|
115
|
+
// email
|
|
116
|
+
if (name.includes("email") || name.includes("mail")) {
|
|
117
|
+
return `${modelKebab}-${padIndex(index)}@example.com`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// URL / ウェブサイト
|
|
121
|
+
if (name.includes("url") || name.includes("website") || name.includes("link") || name === "href") {
|
|
122
|
+
return `https://example.com/${modelKebab}/${padIndex(index)}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 電話番号
|
|
126
|
+
if (name.includes("phone") || name.includes("tel")) {
|
|
127
|
+
return `090-0000-${String(index).padStart(4, "0")}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 名前系
|
|
131
|
+
if (name === "name" || name === "displayname" || name === "fullname" || name === "username") {
|
|
132
|
+
return `${model.name} ${index}`;
|
|
133
|
+
}
|
|
134
|
+
if (name === "firstname" || name === "givenname") {
|
|
135
|
+
return `FirstName${index}`;
|
|
136
|
+
}
|
|
137
|
+
if (name === "lastname" || name === "familyname" || name === "surname") {
|
|
138
|
+
return `LastName${index}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// タイトル / 件名
|
|
142
|
+
if (name === "title" || name === "subject" || name === "summary") {
|
|
143
|
+
return `Sample ${model.displayName || model.name} ${index}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 説明 / 本文
|
|
147
|
+
if (name === "description" || name === "body" || name === "content" || name === "text") {
|
|
148
|
+
return `This is a sample ${toCamelCase(model.name)} description for item ${index}.`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// コード / キー
|
|
152
|
+
if (name.includes("code") || name.includes("key") && !name.includes("api")) {
|
|
153
|
+
return `${modelKebab.toUpperCase().replace(/-/g, "_")}-${String(index).padStart(3, "0")}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ステータス
|
|
157
|
+
if (name === "status") {
|
|
158
|
+
const statuses = ["active", "inactive", "pending"];
|
|
159
|
+
return statuses[(index - 1) % statuses.length];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// タイプ / カテゴリ
|
|
163
|
+
if (name === "type" || name === "category") {
|
|
164
|
+
return `type-${((index - 1) % 3) + 1}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 住所
|
|
168
|
+
if (name.includes("address") || name.includes("city") || name.includes("country")) {
|
|
169
|
+
return `Address ${index}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 部署
|
|
173
|
+
if (name === "department" || name === "team" || name === "group") {
|
|
174
|
+
const depts = ["Engineering", "Sales", "Marketing", "Support", "HR"];
|
|
175
|
+
return depts[(index - 1) % depts.length];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// *Id パターン(外部キー)
|
|
179
|
+
if (name.endsWith("id") && name !== "id") {
|
|
180
|
+
const refName = field.name.replace(/Id$/, "");
|
|
181
|
+
return `${toKebabCase(refName)}-${padIndex(((index - 1) % 3) + 1)}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// デフォルト
|
|
185
|
+
return `${modelKebab}-${toKebabCase(field.name)}-${index}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function generateNumberValue(name: string, index: number): number {
|
|
189
|
+
if (name.includes("age")) return 20 + index;
|
|
190
|
+
if (name.includes("price") || name.includes("cost") || name.includes("amount")) return index * 1000;
|
|
191
|
+
if (name.includes("count") || name.includes("quantity") || name.includes("qty")) return index * 5;
|
|
192
|
+
if (name.includes("priority") || name.includes("order") || name.includes("sort")) return index;
|
|
193
|
+
if (name.includes("rating") || name.includes("score")) return Math.min(index, 5);
|
|
194
|
+
return index;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function generateDateValue(index: number): string {
|
|
198
|
+
const baseDate = new Date("2026-01-01T00:00:00.000Z");
|
|
199
|
+
baseDate.setDate(baseDate.getDate() + index);
|
|
200
|
+
return baseDate.toISOString();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function padIndex(index: number): string {
|
|
204
|
+
return String(index).padStart(3, "0");
|
|
205
|
+
}
|