mock-fried 1.0.6 → 1.0.7

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.md CHANGED
@@ -278,7 +278,7 @@ Mock-Fried는 **결정론적(deterministic)** Mock 데이터를 생성합니다:
278
278
  | Request Body | ✅ | POST/PUT/PATCH body 처리 |
279
279
  | All HTTP Methods | ✅ | GET, POST, PUT, DELETE, PATCH |
280
280
 
281
- ### Protobuf RPC Mock (⚠️ Basic Support)
281
+ ### Protobuf RPC Mock ( Production Ready)
282
282
 
283
283
  | Feature | Status | Description |
284
284
  |---------|--------|-------------|
@@ -288,20 +288,20 @@ Mock-Fried는 **결정론적(deterministic)** Mock 데이터를 생성합니다:
288
288
  | Basic Types | ✅ | string, int32/64, float, double, bool |
289
289
  | Enum Types | ✅ | 첫 번째 enum 값 반환 |
290
290
  | Nested Messages | ✅ | 중첩 메시지 타입 |
291
- | Repeated Fields | ✅ | 배열 필드 (단일 요소) |
291
+ | Repeated Fields | ✅ | 배열 필드 (자동 생성) |
292
292
  | Map Fields | ✅ | map 타입 지원 |
293
+ | Page Pagination | ✅ | page/limit 기반 페이지네이션 |
294
+ | Cursor Pagination | ✅ | cursor 기반 무한 스크롤 |
295
+ | Deterministic Data | ✅ | 동일 요청 = 동일 응답 |
293
296
  | Server Streaming | ❌ | 미구현 |
294
297
  | Client Streaming | ❌ | 미구현 |
295
298
  | Bidirectional Streaming | ❌ | 미구현 |
296
- | Pagination | ❌ | 미구현 |
297
- | Dynamic List Size | ❌ | repeated 필드 고정 1개 |
298
299
 
299
300
  ### 구현 예정 (Roadmap)
300
301
 
301
302
  Proto RPC 기능 확장:
303
+
302
304
  - [ ] Server streaming 지원
303
- - [ ] Pagination 패턴 지원
304
- - [ ] repeated 필드 다수 아이템 생성
305
305
  - [ ] Well-known types (Timestamp, Duration 등)
306
306
 
307
307
  ## Compatibility
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "1.0.6",
7
+ "version": "1.0.7",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -20,6 +20,7 @@ let apiInstance = null;
20
20
  let cachedSpecPath = null;
21
21
  let specCursorManager = null;
22
22
  let specPageManager = null;
23
+ let cachedOpenAPISpec = null;
23
24
  function loadOpenAPISpec(specPath) {
24
25
  const content = readFileSync(specPath, "utf-8");
25
26
  if (specPath.endsWith(".yaml") || specPath.endsWith(".yml")) {
@@ -33,10 +34,12 @@ async function getOpenAPIBackend(specPath) {
33
34
  }
34
35
  const { OpenAPIBackend } = await import("openapi-backend");
35
36
  const definition = loadOpenAPISpec(specPath);
37
+ cachedOpenAPISpec = definition;
36
38
  apiInstance = new OpenAPIBackend({
37
39
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
40
  definition,
39
- quick: true,
41
+ quick: false,
42
+ // $ref 역참조를 위해 false로 설정
40
43
  validate: false
41
44
  // Mock 서버에서는 request validation 비활성화
42
45
  });
@@ -52,6 +55,13 @@ async function getOpenAPIBackend(specPath) {
52
55
  notImplemented: async (c) => {
53
56
  const operationId = c.operation?.operationId || "unknown";
54
57
  const responses = c.operation?.responses;
58
+ if (responses?.["204"]) {
59
+ return {
60
+ statusCode: 204,
61
+ body: null,
62
+ meta: { operationId }
63
+ };
64
+ }
55
65
  const successResponse = responses?.["200"] || responses?.["201"] || Object.values(responses || {})[0];
56
66
  const content = successResponse?.content;
57
67
  const jsonContent = content?.["application/json"];
@@ -138,7 +148,16 @@ async function getOpenAPIBackend(specPath) {
138
148
  }
139
149
  } else {
140
150
  const numericSeed = hashString(seed);
141
- mockData = generateMockFromSchema(schema, numericSeed);
151
+ const apiSchemas = c.api?.document?.components?.schemas;
152
+ const schemaContext = {
153
+ schemas: apiSchemas || cachedOpenAPISpec?.components?.schemas,
154
+ maxDepth: 10
155
+ };
156
+ mockData = generateMockFromSchema(
157
+ schema,
158
+ numericSeed,
159
+ schemaContext
160
+ );
142
161
  }
143
162
  }
144
163
  return {
@@ -170,6 +189,7 @@ let pagePaginationManager = null;
170
189
  export function clearOpenApiCache() {
171
190
  apiInstance = null;
172
191
  cachedSpecPath = null;
192
+ cachedOpenAPISpec = null;
173
193
  cachedClientPackage = null;
174
194
  cachedClientPath = null;
175
195
  mockGenerator = null;
@@ -236,7 +256,18 @@ function handleClientPackageRequest(pkg, generator, cursorManager, pageManager,
236
256
  };
237
257
  }
238
258
  const { endpoint, pathParams } = match;
239
- const primitiveTypes = ["object", "string", "number", "boolean", "void", "any", "unknown"];
259
+ if (endpoint.responseType.toLowerCase() === "void") {
260
+ return {
261
+ statusCode: 204,
262
+ body: null,
263
+ meta: {
264
+ operationId: endpoint.operationId,
265
+ apiClass: endpoint.apiClassName,
266
+ responseType: endpoint.responseType
267
+ }
268
+ };
269
+ }
270
+ const primitiveTypes = ["object", "string", "number", "boolean", "any", "unknown"];
240
271
  if (primitiveTypes.includes(endpoint.responseType.toLowerCase())) {
241
272
  const pathLower = path.toLowerCase();
242
273
  let primitiveResponse = {};
@@ -24,6 +24,7 @@ export declare function inferNumberByFieldName(fieldName: string, rng: SeededRan
24
24
  export declare function inferBooleanByFieldName(fieldName: string, rng: SeededRandom): boolean;
25
25
  /**
26
26
  * Date 타입 필드의 값 생성
27
+ * 결정적 타임스탬프 생성 (2024-01-01 기준)
27
28
  */
28
29
  export declare function inferDateByFieldName(fieldName: string, rng: SeededRandom): string;
29
30
  /**
@@ -23,9 +23,9 @@ export function inferValueByFieldName(fieldName, rng, index = 0, idConfig = DEFA
23
23
  return `https://picsum.photos/seed/${rng.nextInt(1, 1e3)}/200/200`;
24
24
  }
25
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();
26
+ const baseTimestamp = 17040672e5;
27
+ const offset = rng.nextInt(-365, 365) * 24 * 60 * 60 * 1e3;
28
+ return new Date(baseTimestamp + offset).toISOString();
29
29
  }
30
30
  if (name.includes("description") || name.includes("content") || name.includes("body") || name.includes("text")) {
31
31
  return `Mock ${fieldName} \uB370\uC774\uD130 #${rng.nextInt(1, 100)}`;
@@ -219,29 +219,29 @@ export function inferBooleanByFieldName(fieldName, rng) {
219
219
  }
220
220
  export function inferDateByFieldName(fieldName, rng) {
221
221
  const name = fieldName.toLowerCase();
222
- const now = Date.now();
222
+ const baseTimestamp = 17040672e5;
223
223
  if (name.includes("created") || name.includes("registered") || name.includes("joined")) {
224
224
  const offset2 = rng.nextInt(-365, -1) * 24 * 60 * 60 * 1e3;
225
- return new Date(now + offset2).toISOString();
225
+ return new Date(baseTimestamp + offset2).toISOString();
226
226
  }
227
227
  if (name.includes("updated") || name.includes("modified")) {
228
228
  const offset2 = rng.nextInt(-30, 0) * 24 * 60 * 60 * 1e3;
229
- return new Date(now + offset2).toISOString();
229
+ return new Date(baseTimestamp + offset2).toISOString();
230
230
  }
231
231
  if (name.includes("expir") || name.includes("deadline") || name.includes("due")) {
232
232
  const offset2 = rng.nextInt(1, 365) * 24 * 60 * 60 * 1e3;
233
- return new Date(now + offset2).toISOString();
233
+ return new Date(baseTimestamp + offset2).toISOString();
234
234
  }
235
235
  if (name.includes("start") || name.includes("begin")) {
236
236
  const offset2 = rng.nextInt(-30, 30) * 24 * 60 * 60 * 1e3;
237
- return new Date(now + offset2).toISOString();
237
+ return new Date(baseTimestamp + offset2).toISOString();
238
238
  }
239
239
  if (name.includes("end") || name.includes("finish")) {
240
240
  const offset2 = rng.nextInt(1, 90) * 24 * 60 * 60 * 1e3;
241
- return new Date(now + offset2).toISOString();
241
+ return new Date(baseTimestamp + offset2).toISOString();
242
242
  }
243
- const offset = rng.nextInt(-365, 30) * 24 * 60 * 60 * 1e3;
244
- return new Date(now + offset).toISOString();
243
+ const offset = rng.nextInt(-365, 365) * 24 * 60 * 60 * 1e3;
244
+ return new Date(baseTimestamp + offset).toISOString();
245
245
  }
246
246
  export function inferStringByFieldName(fieldName, rng, index = 0, idConfig = DEFAULT_ID_CONFIG) {
247
247
  const inferred = inferValueByFieldName(fieldName, rng, index, idConfig);
@@ -270,9 +270,9 @@ export function inferTypeFromFieldName(fieldName, rng, index, idConfig = DEFAULT
270
270
  return rng.next() > 0.5;
271
271
  }
272
272
  if (name.includes("date") || name.endsWith("at") || name.includes("time") || name.includes("created") || name.includes("updated") || name.includes("modified") || name.includes("deadline")) {
273
- const now = Date.now();
274
- const offset = rng.nextInt(-365, 30) * 24 * 60 * 60 * 1e3;
275
- return new Date(now + offset).toISOString();
273
+ const baseTimestamp = 17040672e5;
274
+ const offset = rng.nextInt(-365, 365) * 24 * 60 * 60 * 1e3;
275
+ return new Date(baseTimestamp + offset).toISOString();
276
276
  }
277
277
  if (name.includes("days") || name.includes("months") || name.includes("weeks") || name.includes("years")) {
278
278
  return rng.nextInt(1, 30);
@@ -4,6 +4,6 @@
4
4
  */
5
5
  export { hashString, seededRandom, SeededRandom, generateId, generateSnapshotId, DEFAULT_ID_CONFIG, isIdField, generateIdValue, generateByFormat, } from './shared.js';
6
6
  export { generateMockValueForProtoField, generateMockMessage, deriveSeedFromRequest, } from './proto-generator.js';
7
- export { generateMockFromSchema, } from './openapi-generator.js';
7
+ export { generateMockFromSchema, type SchemaContext, } from './openapi-generator.js';
8
8
  export { inferValueByFieldName, generateValueByType, inferTypeFromFieldName, SchemaMockGenerator, extractDataModelName, type ResponseTypeInfo, } from './client-generator.js';
9
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';
@@ -1,4 +1,18 @@
1
+ /**
2
+ * $ref 참조를 해결하는 컨텍스트
3
+ */
4
+ export interface SchemaContext {
5
+ schemas?: Record<string, Record<string, unknown>>;
6
+ maxDepth?: number;
7
+ /** 현재 처리 중인 스키마 경로 (순환 참조 감지용) */
8
+ _visitedRefs?: Set<string>;
9
+ }
1
10
  /**
2
11
  * OpenAPI 스키마에서 mock 값 생성
12
+ * @param schema - OpenAPI 스키마
13
+ * @param seed - 랜덤 시드
14
+ * @param context - $ref 해결을 위한 컨텍스트
15
+ * @param depth - 현재 재귀 깊이 (재귀 스키마 무한 루프 방지)
16
+ * @param debugPath - 디버그용 경로 (개발 시에만 사용)
3
17
  */
4
- export declare function generateMockFromSchema(schema: Record<string, unknown>, seed?: number): unknown;
18
+ export declare function generateMockFromSchema(schema: Record<string, unknown>, seed?: number, context?: SchemaContext, depth?: number, debugPath?: string): unknown;
@@ -1,6 +1,40 @@
1
1
  import { seededRandom } from "./shared.js";
2
- export function generateMockFromSchema(schema, seed = 1) {
2
+ function extractRefName(ref) {
3
+ const parts = ref.split("/");
4
+ return parts[parts.length - 1] || "";
5
+ }
6
+ function resolveRef(schema, context) {
7
+ const ref = schema.$ref;
8
+ if (!ref) return schema;
9
+ const schemaName = extractRefName(ref);
10
+ const resolved = context.schemas?.[schemaName];
11
+ if (!resolved) {
12
+ return schema;
13
+ }
14
+ return resolved;
15
+ }
16
+ export function generateMockFromSchema(schema, seed = 1, context = {}, depth = 0, debugPath = "root") {
17
+ const maxDepth = context.maxDepth ?? 5;
3
18
  const random = seededRandom(seed);
19
+ if (depth > maxDepth) {
20
+ return null;
21
+ }
22
+ if (schema.$ref) {
23
+ const refPath = schema.$ref;
24
+ const visitedRefs = context._visitedRefs ?? /* @__PURE__ */ new Set();
25
+ const refKey = `${refPath}@${depth}`;
26
+ if (visitedRefs.has(refKey)) {
27
+ return null;
28
+ }
29
+ const newVisitedRefs = new Set(visitedRefs);
30
+ newVisitedRefs.add(refKey);
31
+ const newContext = { ...context, _visitedRefs: newVisitedRefs };
32
+ const resolved = resolveRef(schema, context);
33
+ if (resolved !== schema) {
34
+ return generateMockFromSchema(resolved, seed, newContext, depth + 1, `${debugPath}->$ref`);
35
+ }
36
+ return null;
37
+ }
4
38
  const schemaType = schema.type;
5
39
  if (schema.example !== void 0) {
6
40
  return schema.example;
@@ -8,6 +42,38 @@ export function generateMockFromSchema(schema, seed = 1) {
8
42
  if (Array.isArray(schema.enum) && schema.enum.length > 0) {
9
43
  return schema.enum[0];
10
44
  }
45
+ if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
46
+ const index = Math.floor(random() * schema.oneOf.length);
47
+ return generateMockFromSchema(
48
+ schema.oneOf[index],
49
+ seed,
50
+ context,
51
+ depth + 1,
52
+ `${debugPath}->oneOf[${index}]`
53
+ );
54
+ }
55
+ if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
56
+ const index = Math.floor(random() * schema.anyOf.length);
57
+ return generateMockFromSchema(
58
+ schema.anyOf[index],
59
+ seed,
60
+ context,
61
+ depth + 1,
62
+ `${debugPath}->anyOf[${index}]`
63
+ );
64
+ }
65
+ if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
66
+ const merged = {};
67
+ let propSeed = seed;
68
+ let idx = 0;
69
+ for (const subSchema of schema.allOf) {
70
+ const generated = generateMockFromSchema(subSchema, propSeed++, context, depth + 1, `${debugPath}->allOf[${idx++}]`);
71
+ if (typeof generated === "object" && generated !== null) {
72
+ Object.assign(merged, generated);
73
+ }
74
+ }
75
+ return merged;
76
+ }
11
77
  switch (schemaType) {
12
78
  case "string": {
13
79
  const format = schema.format;
@@ -59,26 +125,27 @@ export function generateMockFromSchema(schema, seed = 1) {
59
125
  const items = schema.items;
60
126
  if (items) {
61
127
  const minItems = schema.minItems ?? 1;
62
- const maxItems = schema.maxItems ?? 3;
128
+ const maxItems = schema.maxItems ?? (depth > 3 ? 1 : 3);
63
129
  const count = Math.floor(random() * (maxItems - minItems + 1)) + minItems;
64
130
  return Array.from(
65
131
  { length: count },
66
- (_, i) => generateMockFromSchema(items, seed + i + 1)
132
+ (_, i) => generateMockFromSchema(items, seed + i + 1, context, depth + 1, `${debugPath}[${i}]`)
67
133
  );
68
134
  }
69
135
  return [];
70
136
  }
71
- case "object": {
137
+ case "object":
138
+ default: {
72
139
  const properties = schema.properties;
73
140
  if (properties) {
74
141
  const result = {};
75
142
  const required = schema.required ?? [];
76
143
  let propSeed = seed;
77
144
  for (const [propName, propSchema] of Object.entries(properties)) {
78
- if (!required.includes(propName) && random() < 0.5) {
145
+ if (!required.includes(propName) && random() < 0.2) {
79
146
  continue;
80
147
  }
81
- result[propName] = generateMockFromSchema(propSchema, propSeed++);
148
+ result[propName] = generateMockFromSchema(propSchema, propSeed++, context, depth + 1, `${debugPath}.${propName}`);
82
149
  }
83
150
  return result;
84
151
  }
@@ -86,31 +153,18 @@ export function generateMockFromSchema(schema, seed = 1) {
86
153
  const additionalProps = schema.additionalProperties;
87
154
  if (typeof additionalProps === "object") {
88
155
  return {
89
- [`key_${seed}`]: generateMockFromSchema(additionalProps, seed + 1)
156
+ key1: generateMockFromSchema(additionalProps, seed, context, depth + 1, `${debugPath}.key1`),
157
+ key2: generateMockFromSchema(additionalProps, seed + 1, context, depth + 1, `${debugPath}.key2`),
158
+ key3: generateMockFromSchema(additionalProps, seed + 2, context, depth + 1, `${debugPath}.key3`)
90
159
  };
91
160
  }
161
+ return {
162
+ key1: "value1",
163
+ key2: "value2"
164
+ };
92
165
  }
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;
166
+ if (schemaType === "object") {
167
+ return {};
114
168
  }
115
169
  return null;
116
170
  }
@@ -42,6 +42,9 @@ export class OpenAPIItemProvider {
42
42
  */
43
43
  generateItemWithId(id, index, seed) {
44
44
  const item = this.generateItem(index, seed);
45
+ if (typeof item !== "object" || item === null) {
46
+ return { [this.idFieldName]: id, value: item };
47
+ }
45
48
  item[this.idFieldName] = id;
46
49
  return item;
47
50
  }
@@ -100,6 +103,9 @@ export function analyzePaginationSchema(schema) {
100
103
  const foundCursorFields = cursorFields.filter((f) => f in properties);
101
104
  const isPageBased = foundPageFields.length >= 2;
102
105
  const isCursorBased = foundCursorFields.length >= 1;
106
+ if (!isPageBased && !isCursorBased) {
107
+ return null;
108
+ }
103
109
  return {
104
110
  itemsFieldName,
105
111
  itemSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mock-fried",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Nuxt3 Mock API Module - OpenAPI & Protobuf RPC Mock Server",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,6 +52,11 @@
52
52
  "format:check": "prettier --check .",
53
53
  "test": "vitest run",
54
54
  "test:watch": "vitest watch",
55
+ "test:unit": "vitest run --exclude 'test/e2e/**'",
56
+ "test:e2e": "vitest run test/e2e/",
57
+ "test:e2e:openapi": "vitest run test/e2e/playground-openapi.e2e.test.ts",
58
+ "test:e2e:openapi-client": "vitest run test/e2e/playground-openapi-client.e2e.test.ts",
59
+ "test:e2e:proto": "vitest run test/e2e/playground-proto.e2e.test.ts",
55
60
  "test:types": "vue-tsc --noEmit && cd playground-openapi && vue-tsc --noEmit"
56
61
  },
57
62
  "dependencies": {