mock-fried 1.0.0

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 (55) hide show
  1. package/README.md +229 -0
  2. package/dist/module.d.mts +125 -0
  3. package/dist/module.json +12 -0
  4. package/dist/module.mjs +160 -0
  5. package/dist/runtime/components/ApiExplorer.d.vue.ts +7 -0
  6. package/dist/runtime/components/ApiExplorer.vue +168 -0
  7. package/dist/runtime/components/ApiExplorer.vue.d.ts +7 -0
  8. package/dist/runtime/components/EndpointCard.d.vue.ts +24 -0
  9. package/dist/runtime/components/EndpointCard.vue +173 -0
  10. package/dist/runtime/components/EndpointCard.vue.d.ts +24 -0
  11. package/dist/runtime/components/ResponseViewer.d.vue.ts +16 -0
  12. package/dist/runtime/components/ResponseViewer.vue +78 -0
  13. package/dist/runtime/components/ResponseViewer.vue.d.ts +16 -0
  14. package/dist/runtime/components/RpcMethodCard.d.vue.ts +20 -0
  15. package/dist/runtime/components/RpcMethodCard.vue +129 -0
  16. package/dist/runtime/components/RpcMethodCard.vue.d.ts +20 -0
  17. package/dist/runtime/composables/index.d.ts +1 -0
  18. package/dist/runtime/composables/index.js +1 -0
  19. package/dist/runtime/composables/useApi.d.ts +19 -0
  20. package/dist/runtime/composables/useApi.js +5 -0
  21. package/dist/runtime/plugin.d.ts +7 -0
  22. package/dist/runtime/plugin.js +75 -0
  23. package/dist/runtime/server/handlers/openapi.d.ts +2 -0
  24. package/dist/runtime/server/handlers/openapi.js +346 -0
  25. package/dist/runtime/server/handlers/rpc.d.ts +7 -0
  26. package/dist/runtime/server/handlers/rpc.js +140 -0
  27. package/dist/runtime/server/handlers/schema.d.ts +7 -0
  28. package/dist/runtime/server/handlers/schema.js +190 -0
  29. package/dist/runtime/server/tsconfig.json +3 -0
  30. package/dist/runtime/server/utils/client-parser.d.ts +13 -0
  31. package/dist/runtime/server/utils/client-parser.js +272 -0
  32. package/dist/runtime/server/utils/mock/client-generator.d.ts +108 -0
  33. package/dist/runtime/server/utils/mock/client-generator.js +346 -0
  34. package/dist/runtime/server/utils/mock/index.d.ts +9 -0
  35. package/dist/runtime/server/utils/mock/index.js +38 -0
  36. package/dist/runtime/server/utils/mock/openapi-generator.d.ts +4 -0
  37. package/dist/runtime/server/utils/mock/openapi-generator.js +118 -0
  38. package/dist/runtime/server/utils/mock/pagination/cursor-manager.d.ts +38 -0
  39. package/dist/runtime/server/utils/mock/pagination/cursor-manager.js +129 -0
  40. package/dist/runtime/server/utils/mock/pagination/index.d.ts +8 -0
  41. package/dist/runtime/server/utils/mock/pagination/index.js +18 -0
  42. package/dist/runtime/server/utils/mock/pagination/page-manager.d.ts +41 -0
  43. package/dist/runtime/server/utils/mock/pagination/page-manager.js +96 -0
  44. package/dist/runtime/server/utils/mock/pagination/snapshot-store.d.ts +64 -0
  45. package/dist/runtime/server/utils/mock/pagination/snapshot-store.js +125 -0
  46. package/dist/runtime/server/utils/mock/pagination/types.d.ts +141 -0
  47. package/dist/runtime/server/utils/mock/pagination/types.js +14 -0
  48. package/dist/runtime/server/utils/mock/proto-generator.d.ts +12 -0
  49. package/dist/runtime/server/utils/mock/proto-generator.js +67 -0
  50. package/dist/runtime/server/utils/mock/shared.d.ts +69 -0
  51. package/dist/runtime/server/utils/mock/shared.js +150 -0
  52. package/dist/runtime/server/utils/mock-generator.d.ts +9 -0
  53. package/dist/runtime/server/utils/mock-generator.js +30 -0
  54. package/dist/types.d.mts +9 -0
  55. package/package.json +73 -0
@@ -0,0 +1,346 @@
1
+ import { hashString, SeededRandom, isIdField, generateIdValue, DEFAULT_ID_CONFIG } from "./shared.js";
2
+ export function inferValueByFieldName(fieldName, rng, index = 0, idConfig = DEFAULT_ID_CONFIG) {
3
+ const name = fieldName.toLowerCase();
4
+ if (isIdField(fieldName, idConfig)) {
5
+ return generateIdValue(fieldName, index, rng.hashId(16), idConfig);
6
+ }
7
+ if (name.includes("email") || name.includes("mail")) {
8
+ const domains = ["example.com", "test.com", "mock.io"];
9
+ const names = ["user", "test", "mock", "admin"];
10
+ return `${rng.pick(names)}${rng.nextInt(1, 999)}@${rng.pick(domains)}`;
11
+ }
12
+ if (name === "name" || name.includes("username") || name.includes("firstname") || name.includes("lastname")) {
13
+ const firstNames = ["\uAE40\uCCA0\uC218", "\uC774\uC601\uD76C", "\uBC15\uBBFC\uC218", "John", "Jane", "Mike", "Sarah", "Alex"];
14
+ return rng.pick(firstNames);
15
+ }
16
+ if (name.includes("phone") || name.includes("mobile") || name.includes("tel")) {
17
+ return `010-${rng.nextInt(1e3, 9999)}-${rng.nextInt(1e3, 9999)}`;
18
+ }
19
+ if (name.includes("url") || name.includes("link") || name.includes("href")) {
20
+ return `https://example.com/${fieldName}/${rng.nextInt(1, 1e3)}`;
21
+ }
22
+ if (name.includes("image") || name.includes("avatar") || name.includes("photo") || name.includes("thumbnail") || name.includes("picture")) {
23
+ return `https://picsum.photos/seed/${rng.nextInt(1, 1e3)}/200/200`;
24
+ }
25
+ if (name.includes("date") || name.endsWith("at") || name.includes("time") || name.includes("created") || name.includes("updated")) {
26
+ const now = Date.now();
27
+ const offset = rng.nextInt(-365, 30) * 24 * 60 * 60 * 1e3;
28
+ return new Date(now + offset).toISOString();
29
+ }
30
+ if (name.includes("description") || name.includes("content") || name.includes("body") || name.includes("text")) {
31
+ return `Mock ${fieldName} \uB370\uC774\uD130 #${rng.nextInt(1, 100)}`;
32
+ }
33
+ if (name.includes("title") || name.includes("subject") || name.includes("headline")) {
34
+ return `\uC0D8\uD50C ${fieldName} #${rng.nextInt(1, 100)}`;
35
+ }
36
+ if (name.includes("status")) {
37
+ return rng.pick(["ACTIVE", "INACTIVE", "PENDING", "COMPLETED"]);
38
+ }
39
+ if (name.includes("count") || name.includes("quantity") || name.includes("amount") || name.includes("num")) {
40
+ return rng.nextInt(0, 100);
41
+ }
42
+ if (name.includes("price") || name.includes("cost") || name.includes("fee") || name.includes("amount")) {
43
+ return rng.nextInt(1e3, 1e5);
44
+ }
45
+ if (name === "page") return 1;
46
+ if (name === "limit" || name === "size" || name === "pagesize") return 20;
47
+ if (name === "total" || name === "totalcount" || name === "totalitems") return rng.nextInt(50, 500);
48
+ if (name === "totalpages") return rng.nextInt(3, 25);
49
+ if (name.startsWith("is") || name.startsWith("has") || name.startsWith("can") || name.startsWith("should") || name.startsWith("will")) {
50
+ return rng.next() > 0.5;
51
+ }
52
+ if (name.includes("address") || name.includes("street")) {
53
+ return `\uC11C\uC6B8\uC2DC \uAC15\uB0A8\uAD6C \uD14C\uD5E4\uB780\uB85C ${rng.nextInt(1, 500)}\uBC88\uAE38`;
54
+ }
55
+ if (name.includes("city")) {
56
+ return rng.pick(["\uC11C\uC6B8", "\uBD80\uC0B0", "\uB300\uAD6C", "\uC778\uCC9C", "\uAD11\uC8FC", "\uB300\uC804"]);
57
+ }
58
+ if (name.includes("country")) {
59
+ return rng.pick(["KR", "US", "JP", "CN"]);
60
+ }
61
+ if (name.includes("zipcode") || name.includes("postal")) {
62
+ return `${rng.nextInt(1e4, 99999)}`;
63
+ }
64
+ if (name.includes("code")) {
65
+ return `CODE-${rng.nextInt(1e3, 9999)}`;
66
+ }
67
+ if (name.includes("type") || name.includes("category")) {
68
+ return rng.pick(["TYPE_A", "TYPE_B", "TYPE_C"]);
69
+ }
70
+ if (name.includes("version")) {
71
+ return `${rng.nextInt(1, 10)}.${rng.nextInt(0, 9)}.${rng.nextInt(0, 99)}`;
72
+ }
73
+ if (name.includes("token") || name.includes("key") || name.includes("secret")) {
74
+ return rng.uuid();
75
+ }
76
+ return null;
77
+ }
78
+ export function generateValueByType(type, fieldName, rng, index = 0, idConfig = DEFAULT_ID_CONFIG) {
79
+ const inferred = inferValueByFieldName(fieldName, rng, index, idConfig);
80
+ if (inferred !== null) return inferred;
81
+ switch (type.toLowerCase()) {
82
+ case "string":
83
+ return `mock-${fieldName}-${rng.nextInt(1, 1e3)}`;
84
+ case "number":
85
+ case "int":
86
+ case "integer":
87
+ return rng.nextInt(1, 1e3);
88
+ case "boolean":
89
+ case "bool":
90
+ return rng.next() > 0.5;
91
+ case "date":
92
+ return (/* @__PURE__ */ new Date()).toISOString();
93
+ case "unknown":
94
+ case "any":
95
+ case "object":
96
+ return inferTypeFromFieldName(fieldName, rng, index, idConfig);
97
+ default:
98
+ return `mock-${fieldName}-${rng.nextInt(1, 1e3)}`;
99
+ }
100
+ }
101
+ export function inferTypeFromFieldName(fieldName, rng, index, idConfig = DEFAULT_ID_CONFIG) {
102
+ const name = fieldName.toLowerCase();
103
+ if (isIdField(fieldName, idConfig)) {
104
+ return generateIdValue(fieldName, index, rng.hashId(16), idConfig);
105
+ }
106
+ if (name.includes("count") || name.includes("num") || name.includes("amount") || name.includes("size") || name.includes("total") || name.includes("views") || name.includes("likes") || name.includes("price") || name.includes("age") || name.includes("quantity") || name.includes("index") || name.includes("order")) {
107
+ return rng.nextInt(0, 1e3);
108
+ }
109
+ if (name.startsWith("is") || name.startsWith("has") || name.startsWith("can") || name.startsWith("should") || name.startsWith("will") || name.includes("enabled") || name.includes("active") || name.includes("visible") || name.includes("valid")) {
110
+ return rng.next() > 0.5;
111
+ }
112
+ if (name.includes("date") || name.endsWith("at") || name.includes("time") || name.includes("created") || name.includes("updated") || name.includes("modified")) {
113
+ const now = Date.now();
114
+ const offset = rng.nextInt(-365, 30) * 24 * 60 * 60 * 1e3;
115
+ return new Date(now + offset).toISOString();
116
+ }
117
+ if (name.includes("url") || name.includes("link") || name.includes("href")) {
118
+ return `https://example.com/${fieldName}/${rng.nextInt(1, 1e3)}`;
119
+ }
120
+ if (name.includes("image") || name.includes("thumbnail") || name.includes("avatar") || name.includes("photo") || name.includes("picture")) {
121
+ return `https://picsum.photos/seed/${rng.nextInt(1, 1e3)}/200/200`;
122
+ }
123
+ return `mock-${fieldName}-${rng.nextInt(1, 1e3)}`;
124
+ }
125
+ export function extractDataModelName(responseType, models) {
126
+ const arrayMatch = responseType.match(/^Array<(.+)>$/) || responseType.match(/^(.+)\[\]$/);
127
+ if (arrayMatch) {
128
+ const innerType = arrayMatch[1].trim();
129
+ return {
130
+ modelName: innerType,
131
+ isList: true,
132
+ // listFieldName이 없으면 직접 배열 반환 (wrapper 없음)
133
+ listFieldName: void 0,
134
+ wrapperType: void 0
135
+ };
136
+ }
137
+ const schema = models.get(responseType);
138
+ if (schema) {
139
+ const priorityFields = ["items", "data", "posts", "comments", "results", "records", "list"];
140
+ for (const fieldName of priorityFields) {
141
+ const field = schema.fields.find((f) => f.name === fieldName && f.isArray);
142
+ if (field && field.refType) {
143
+ return {
144
+ modelName: field.refType,
145
+ isList: true,
146
+ listFieldName: fieldName,
147
+ wrapperType: responseType
148
+ };
149
+ }
150
+ }
151
+ const arrayField = schema.fields.find((f) => f.isArray && f.refType);
152
+ if (arrayField && arrayField.refType) {
153
+ return {
154
+ modelName: arrayField.refType,
155
+ isList: true,
156
+ listFieldName: arrayField.name,
157
+ wrapperType: responseType
158
+ };
159
+ }
160
+ const dataField = schema.fields.find((f) => f.name === "data" && !f.isArray);
161
+ if (dataField?.refType) {
162
+ return {
163
+ modelName: dataField.refType,
164
+ isList: false,
165
+ wrapperType: responseType
166
+ };
167
+ }
168
+ }
169
+ return { modelName: responseType, isList: false };
170
+ }
171
+ export class SchemaMockGenerator {
172
+ models;
173
+ dataStore = /* @__PURE__ */ new Map();
174
+ idConfig;
175
+ constructor(models, idConfig = DEFAULT_ID_CONFIG) {
176
+ this.models = models;
177
+ this.idConfig = idConfig;
178
+ }
179
+ /**
180
+ * 모델 스키마 기반 단일 객체 생성
181
+ */
182
+ generateOne(modelName, seed, index = 0) {
183
+ const schema = this.models.get(modelName);
184
+ if (!schema) {
185
+ return {};
186
+ }
187
+ if (schema.enumValues && schema.enumValues.length > 0) {
188
+ const rng2 = new SeededRandom(seed ?? modelName);
189
+ return { value: rng2.pick(schema.enumValues) };
190
+ }
191
+ const rng = new SeededRandom(seed ?? `${modelName}-${index}`);
192
+ const result = {};
193
+ for (const field of schema.fields) {
194
+ const value = this.generateField(field, rng, index);
195
+ if (value !== void 0) {
196
+ const outputKey = field.jsonKey || field.name;
197
+ result[outputKey] = value;
198
+ }
199
+ }
200
+ return result;
201
+ }
202
+ /**
203
+ * ID 부여된 단일 객체 생성 (Pagination용)
204
+ * 주어진 ID를 모델의 ID 필드에 설정하여 cursor와 응답 ID가 일치하도록 함
205
+ */
206
+ generateOneWithId(modelName, itemId, seed, index = 0) {
207
+ const item = this.generateOne(modelName, seed ?? `${modelName}-${itemId}`, index);
208
+ const idFieldName = this.findIdFieldName(modelName);
209
+ if (idFieldName) {
210
+ const outputKey = this.getOutputKey(modelName, idFieldName);
211
+ item[outputKey] = itemId;
212
+ } else if (!("id" in item)) {
213
+ item.id = itemId;
214
+ } else {
215
+ item.id = itemId;
216
+ }
217
+ return item;
218
+ }
219
+ /**
220
+ * 모델의 ID 필드명 찾기 (MockIdConfig 기반)
221
+ */
222
+ findIdFieldName(modelName) {
223
+ const schema = this.models.get(modelName);
224
+ if (!schema) return null;
225
+ for (const field of schema.fields) {
226
+ if (isIdField(field.name, this.idConfig)) {
227
+ return field.name;
228
+ }
229
+ }
230
+ return null;
231
+ }
232
+ /**
233
+ * 필드의 출력 키 가져오기 (jsonKey 또는 name)
234
+ */
235
+ getOutputKey(modelName, fieldName) {
236
+ const schema = this.models.get(modelName);
237
+ if (!schema) return fieldName;
238
+ const field = schema.fields.find((f) => f.name === fieldName);
239
+ return field?.jsonKey || fieldName;
240
+ }
241
+ /**
242
+ * 필드 값 생성
243
+ */
244
+ generateField(field, rng, index) {
245
+ if (!field.required && rng.next() > 0.7) {
246
+ return void 0;
247
+ }
248
+ if (field.refType) {
249
+ const refSchema = this.models.get(field.refType);
250
+ if (refSchema?.enumValues) {
251
+ const value = rng.pick(refSchema.enumValues);
252
+ return field.isArray ? [value] : value;
253
+ }
254
+ if (field.isArray) {
255
+ const count = rng.nextInt(1, 3);
256
+ return Array.from(
257
+ { length: count },
258
+ (_, i) => this.generateOne(field.refType, `${field.refType}-${index}-${i}`, i)
259
+ );
260
+ }
261
+ return this.generateOne(field.refType, `${field.refType}-${index}`, index);
262
+ }
263
+ if (field.isArray) {
264
+ const count = rng.nextInt(1, 5);
265
+ return Array.from(
266
+ { length: count },
267
+ (_, i) => generateValueByType(field.type, field.name, rng, i, this.idConfig)
268
+ );
269
+ }
270
+ return generateValueByType(field.type, field.name, rng, index, this.idConfig);
271
+ }
272
+ /**
273
+ * 리스트 데이터 생성 (캐시 지원 - Pagination)
274
+ * @deprecated Use pagination/page-manager.ts instead for enhanced pagination
275
+ */
276
+ generateList(modelName, options = {}) {
277
+ const { page = 1, limit = 20, total = 100, seed = modelName } = options;
278
+ const cacheKey = `${modelName}-${seed}`;
279
+ let allItems = this.dataStore.get(cacheKey);
280
+ if (!allItems || allItems.length < total) {
281
+ allItems = Array.from(
282
+ { length: total },
283
+ (_, i) => this.generateOne(modelName, `${seed}-${i}`, i)
284
+ );
285
+ this.dataStore.set(cacheKey, allItems);
286
+ }
287
+ const startIndex = (page - 1) * limit;
288
+ const endIndex = Math.min(startIndex + limit, total);
289
+ const items = allItems.slice(startIndex, endIndex);
290
+ return {
291
+ items,
292
+ pagination: {
293
+ page,
294
+ limit,
295
+ total,
296
+ totalPages: Math.ceil(total / limit)
297
+ }
298
+ };
299
+ }
300
+ /**
301
+ * 커서 기반 리스트 생성 (무한 스크롤용)
302
+ * @deprecated Use pagination/cursor-manager.ts instead for enhanced pagination
303
+ */
304
+ generateCursorList(modelName, options = {}) {
305
+ const { cursor, limit = 20, total = 100, seed = modelName } = options;
306
+ const cacheKey = `${modelName}-${seed}`;
307
+ let allItems = this.dataStore.get(cacheKey);
308
+ if (!allItems || allItems.length < total) {
309
+ allItems = Array.from(
310
+ { length: total },
311
+ (_, i) => this.generateOne(modelName, `${seed}-${i}`, i)
312
+ );
313
+ this.dataStore.set(cacheKey, allItems);
314
+ }
315
+ let startIndex = 0;
316
+ if (cursor) {
317
+ try {
318
+ startIndex = Number.parseInt(Buffer.from(cursor, "base64").toString("utf-8"), 10);
319
+ } catch {
320
+ startIndex = 0;
321
+ }
322
+ }
323
+ const endIndex = Math.min(startIndex + limit, total);
324
+ const items = allItems.slice(startIndex, endIndex);
325
+ const hasMore = endIndex < total;
326
+ return {
327
+ items,
328
+ nextCursor: hasMore ? Buffer.from(String(endIndex)).toString("base64") : void 0,
329
+ prevCursor: startIndex > 0 ? Buffer.from(String(Math.max(0, startIndex - limit))).toString("base64") : void 0,
330
+ hasMore
331
+ };
332
+ }
333
+ /**
334
+ * 모델 가져오기
335
+ */
336
+ getModels() {
337
+ return this.models;
338
+ }
339
+ /**
340
+ * 캐시 초기화
341
+ */
342
+ clearCache() {
343
+ this.dataStore.clear();
344
+ }
345
+ }
346
+ export { SeededRandom, hashString };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Mock 생성기 모듈 진입점
3
+ * 하위 호환성을 위해 모든 기존 export를 유지
4
+ */
5
+ export { hashString, seededRandom, SeededRandom, generateId, generateSnapshotId, DEFAULT_ID_CONFIG, isIdField, generateIdValue, generateByFormat, } from './shared.js';
6
+ export { generateMockValueForProtoField, generateMockMessage, deriveSeedFromRequest, } from './proto-generator.js';
7
+ export { generateMockFromSchema, } from './openapi-generator.js';
8
+ export { inferValueByFieldName, generateValueByType, inferTypeFromFieldName, SchemaMockGenerator, extractDataModelName, type ResponseTypeInfo, } from './client-generator.js';
9
+ export { type CursorPayload, type PaginationSnapshot, type PagePaginationOptions, type PagePaginationResult, type CursorPaginationOptions, type CursorPaginationResult, type PaginationConfig, type CursorConfig, DEFAULT_PAGINATION_CONFIG, DEFAULT_CURSOR_CONFIG, SnapshotStore, getSnapshotStore, resetSnapshotStore, CursorPaginationManager, encodeCursor, decodeCursor, isCursorExpired, PagePaginationManager, } from './pagination/index.js';
@@ -0,0 +1,38 @@
1
+ export {
2
+ hashString,
3
+ seededRandom,
4
+ SeededRandom,
5
+ generateId,
6
+ generateSnapshotId,
7
+ DEFAULT_ID_CONFIG,
8
+ isIdField,
9
+ generateIdValue,
10
+ generateByFormat
11
+ } from "./shared.js";
12
+ export {
13
+ generateMockValueForProtoField,
14
+ generateMockMessage,
15
+ deriveSeedFromRequest
16
+ } from "./proto-generator.js";
17
+ export {
18
+ generateMockFromSchema
19
+ } from "./openapi-generator.js";
20
+ export {
21
+ inferValueByFieldName,
22
+ generateValueByType,
23
+ inferTypeFromFieldName,
24
+ SchemaMockGenerator,
25
+ extractDataModelName
26
+ } from "./client-generator.js";
27
+ export {
28
+ DEFAULT_PAGINATION_CONFIG,
29
+ DEFAULT_CURSOR_CONFIG,
30
+ SnapshotStore,
31
+ getSnapshotStore,
32
+ resetSnapshotStore,
33
+ CursorPaginationManager,
34
+ encodeCursor,
35
+ decodeCursor,
36
+ isCursorExpired,
37
+ PagePaginationManager
38
+ } from "./pagination/index.js";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * OpenAPI 스키마에서 mock 값 생성
3
+ */
4
+ export declare function generateMockFromSchema(schema: Record<string, unknown>, seed?: number): unknown;
@@ -0,0 +1,118 @@
1
+ import { seededRandom } from "./shared.js";
2
+ export function generateMockFromSchema(schema, seed = 1) {
3
+ const random = seededRandom(seed);
4
+ const schemaType = schema.type;
5
+ if (schema.example !== void 0) {
6
+ return schema.example;
7
+ }
8
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
9
+ return schema.enum[0];
10
+ }
11
+ switch (schemaType) {
12
+ case "string": {
13
+ const format = schema.format;
14
+ if (format === "date") {
15
+ return "2024-01-15";
16
+ }
17
+ if (format === "date-time") {
18
+ return "2024-01-15T10:30:00Z";
19
+ }
20
+ if (format === "email") {
21
+ return `user${Math.floor(random() * 1e3)}@example.com`;
22
+ }
23
+ if (format === "uuid") {
24
+ return "550e8400-e29b-41d4-a716-446655440000";
25
+ }
26
+ if (format === "uri" || format === "url") {
27
+ return `https://example.com/resource/${Math.floor(random() * 1e3)}`;
28
+ }
29
+ if (format === "hostname") {
30
+ return "example.com";
31
+ }
32
+ if (format === "ipv4") {
33
+ return `192.168.${Math.floor(random() * 256)}.${Math.floor(random() * 256)}`;
34
+ }
35
+ if (format === "ipv6") {
36
+ return "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
37
+ }
38
+ if (format === "byte") {
39
+ return Buffer.from(`mock_${seed}`).toString("base64");
40
+ }
41
+ if (format === "binary") {
42
+ return `binary_data_${seed}`;
43
+ }
44
+ if (format === "password") {
45
+ return "********";
46
+ }
47
+ return `mock_string_${Math.floor(random() * 1e3)}`;
48
+ }
49
+ case "integer":
50
+ case "number": {
51
+ const min = schema.minimum ?? 0;
52
+ const max = schema.maximum ?? 1e3;
53
+ const value = min + random() * (max - min);
54
+ return schemaType === "integer" ? Math.floor(value) : Math.round(value * 100) / 100;
55
+ }
56
+ case "boolean":
57
+ return random() > 0.5;
58
+ case "array": {
59
+ const items = schema.items;
60
+ if (items) {
61
+ const minItems = schema.minItems ?? 1;
62
+ const maxItems = schema.maxItems ?? 3;
63
+ const count = Math.floor(random() * (maxItems - minItems + 1)) + minItems;
64
+ return Array.from(
65
+ { length: count },
66
+ (_, i) => generateMockFromSchema(items, seed + i + 1)
67
+ );
68
+ }
69
+ return [];
70
+ }
71
+ case "object": {
72
+ const properties = schema.properties;
73
+ if (properties) {
74
+ const result = {};
75
+ const required = schema.required ?? [];
76
+ let propSeed = seed;
77
+ for (const [propName, propSchema] of Object.entries(properties)) {
78
+ if (!required.includes(propName) && random() < 0.5) {
79
+ continue;
80
+ }
81
+ result[propName] = generateMockFromSchema(propSchema, propSeed++);
82
+ }
83
+ return result;
84
+ }
85
+ if (schema.additionalProperties) {
86
+ const additionalProps = schema.additionalProperties;
87
+ if (typeof additionalProps === "object") {
88
+ return {
89
+ [`key_${seed}`]: generateMockFromSchema(additionalProps, seed + 1)
90
+ };
91
+ }
92
+ }
93
+ return {};
94
+ }
95
+ // oneOf, anyOf, allOf 처리
96
+ default: {
97
+ if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
98
+ const index = Math.floor(random() * schema.oneOf.length);
99
+ return generateMockFromSchema(schema.oneOf[index], seed);
100
+ }
101
+ if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
102
+ const index = Math.floor(random() * schema.anyOf.length);
103
+ return generateMockFromSchema(schema.anyOf[index], seed);
104
+ }
105
+ if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
106
+ const merged = {};
107
+ for (const subSchema of schema.allOf) {
108
+ const generated = generateMockFromSchema(subSchema, seed);
109
+ if (typeof generated === "object" && generated !== null) {
110
+ Object.assign(merged, generated);
111
+ }
112
+ }
113
+ return merged;
114
+ }
115
+ return null;
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Cursor 기반 Pagination 관리자
3
+ * ID 기반의 연결성 있는 cursor를 제공
4
+ */
5
+ import type { CursorPayload, CursorPaginationOptions, CursorPaginationResult, CursorConfig } from './types.js';
6
+ import type { SnapshotStore } from './snapshot-store.js';
7
+ import type { SchemaMockGenerator } from '../client-generator.js';
8
+ /**
9
+ * Cursor 인코딩
10
+ */
11
+ export declare function encodeCursor(payload: CursorPayload): string;
12
+ /**
13
+ * Cursor 디코딩
14
+ * - base64url 인코딩된 CursorPayload JSON
15
+ * - Legacy base64 인코딩된 인덱스 번호
16
+ * - Raw UUID/ID 문자열 (직접 ID로 사용)
17
+ */
18
+ export declare function decodeCursor(cursor: string): CursorPayload | null;
19
+ /**
20
+ * Cursor 만료 여부 확인
21
+ */
22
+ export declare function isCursorExpired(payload: CursorPayload, config?: CursorConfig): boolean;
23
+ /**
24
+ * Cursor 기반 Pagination 관리자 클래스
25
+ */
26
+ export declare class CursorPaginationManager {
27
+ private generator;
28
+ private snapshotStore;
29
+ private cursorConfig;
30
+ constructor(generator: SchemaMockGenerator, options?: {
31
+ snapshotStore?: SnapshotStore;
32
+ cursorConfig?: CursorConfig;
33
+ });
34
+ /**
35
+ * Cursor 기반 페이지 조회
36
+ */
37
+ getCursorPage(modelName: string, options?: CursorPaginationOptions): CursorPaginationResult<Record<string, unknown>>;
38
+ }
@@ -0,0 +1,129 @@
1
+ import { DEFAULT_CURSOR_CONFIG, DEFAULT_PAGINATION_CONFIG } from "./types.js";
2
+ import { getSnapshotStore } from "./snapshot-store.js";
3
+ export function encodeCursor(payload) {
4
+ const json = JSON.stringify(payload);
5
+ return Buffer.from(json).toString("base64url");
6
+ }
7
+ export function decodeCursor(cursor) {
8
+ try {
9
+ const json = Buffer.from(cursor, "base64url").toString("utf-8");
10
+ const parsed = JSON.parse(json);
11
+ if (parsed && typeof parsed.lastId === "string" && parsed.direction) {
12
+ return parsed;
13
+ }
14
+ } catch {
15
+ }
16
+ try {
17
+ const decoded = Buffer.from(cursor, "base64").toString("utf-8");
18
+ const index = Number.parseInt(decoded, 10);
19
+ if (!Number.isNaN(index) && decoded === String(index)) {
20
+ return {
21
+ lastId: `legacy-${index}`,
22
+ direction: "forward",
23
+ timestamp: Date.now()
24
+ };
25
+ }
26
+ } catch {
27
+ }
28
+ if (cursor && cursor.length >= 8 && !cursor.includes(" ")) {
29
+ return {
30
+ lastId: cursor,
31
+ direction: "forward",
32
+ timestamp: Date.now()
33
+ };
34
+ }
35
+ return null;
36
+ }
37
+ export function isCursorExpired(payload, config = {}) {
38
+ const { enableExpiry = DEFAULT_CURSOR_CONFIG.enableExpiry, cursorTTL = DEFAULT_CURSOR_CONFIG.cursorTTL } = config;
39
+ if (!enableExpiry) return false;
40
+ return Date.now() - payload.timestamp > cursorTTL;
41
+ }
42
+ export class CursorPaginationManager {
43
+ generator;
44
+ snapshotStore;
45
+ cursorConfig;
46
+ constructor(generator, options) {
47
+ this.generator = generator;
48
+ this.snapshotStore = options?.snapshotStore ?? getSnapshotStore();
49
+ this.cursorConfig = { ...DEFAULT_CURSOR_CONFIG, ...options?.cursorConfig };
50
+ }
51
+ /**
52
+ * Cursor 기반 페이지 조회
53
+ */
54
+ getCursorPage(modelName, options = {}) {
55
+ const {
56
+ cursor,
57
+ limit = DEFAULT_PAGINATION_CONFIG.defaultLimit,
58
+ total = DEFAULT_PAGINATION_CONFIG.defaultTotal,
59
+ seed = modelName,
60
+ snapshotId,
61
+ cache = true,
62
+ ttl
63
+ } = options;
64
+ let snapshot;
65
+ if (snapshotId) {
66
+ const existing = this.snapshotStore.getById(snapshotId);
67
+ if (existing) {
68
+ snapshot = existing;
69
+ } else {
70
+ snapshot = this.snapshotStore.getOrCreate(modelName, seed, total, { cache, ttl });
71
+ }
72
+ } else {
73
+ snapshot = this.snapshotStore.getOrCreate(modelName, seed, total, { cache, ttl });
74
+ }
75
+ let startIndex = 0;
76
+ let cursorPayload = null;
77
+ if (cursor) {
78
+ cursorPayload = decodeCursor(cursor);
79
+ if (cursorPayload) {
80
+ if (isCursorExpired(cursorPayload, this.cursorConfig)) {
81
+ startIndex = 0;
82
+ } else if (cursorPayload.lastId.startsWith("legacy-")) {
83
+ startIndex = Number.parseInt(cursorPayload.lastId.replace("legacy-", ""), 10);
84
+ } else {
85
+ const anchorIndex = snapshot.itemIds.findIndex((id) => id === cursorPayload.lastId);
86
+ if (anchorIndex !== -1) {
87
+ startIndex = cursorPayload.direction === "forward" ? anchorIndex + 1 : Math.max(0, anchorIndex - limit);
88
+ } else {
89
+ const elapsedRatio = Math.min(1, (cursorPayload.timestamp - snapshot.createdAt) / (snapshot.expiresAt || Date.now() - snapshot.createdAt));
90
+ startIndex = Math.floor(elapsedRatio * snapshot.total);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ const endIndex = Math.min(startIndex + limit, snapshot.total);
96
+ const pageItemIds = snapshot.itemIds.slice(startIndex, endIndex);
97
+ const items = pageItemIds.map(
98
+ (itemId, i) => this.generator.generateOneWithId(modelName, itemId, `${seed}-${itemId}`, startIndex + i)
99
+ );
100
+ const hasMore = endIndex < snapshot.total;
101
+ const hasPrev = startIndex > 0;
102
+ const result = {
103
+ items,
104
+ hasMore
105
+ };
106
+ if (hasMore && pageItemIds.length > 0) {
107
+ result.nextCursor = encodeCursor({
108
+ lastId: pageItemIds[pageItemIds.length - 1],
109
+ direction: "forward",
110
+ snapshotId: snapshot.id,
111
+ timestamp: Date.now(),
112
+ sortField: this.cursorConfig.includeSortInfo ? "id" : void 0,
113
+ sortOrder: this.cursorConfig.includeSortInfo ? "asc" : void 0
114
+ });
115
+ }
116
+ if (hasPrev && pageItemIds.length > 0) {
117
+ result.prevCursor = encodeCursor({
118
+ lastId: pageItemIds[0],
119
+ direction: "backward",
120
+ snapshotId: snapshot.id,
121
+ timestamp: Date.now(),
122
+ sortField: this.cursorConfig.includeSortInfo ? "id" : void 0,
123
+ sortOrder: this.cursorConfig.includeSortInfo ? "asc" : void 0
124
+ });
125
+ }
126
+ result._snapshotId = snapshot.id;
127
+ return result;
128
+ }
129
+ }