mock-fried 1.0.7 → 1.1.1

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
@@ -346,8 +346,11 @@ yarn lint:fix
346
346
  yarn format
347
347
 
348
348
  # Run tests
349
- yarn test
350
- yarn test:watch
349
+ yarn test # All tests
350
+ yarn test:unit # Unit tests only
351
+ yarn test:e2e # E2E tests only
352
+ yarn test:coverage # With coverage report
353
+ yarn test:watch # Watch mode
351
354
 
352
355
  # Type check
353
356
  yarn test:types
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "1.0.7",
7
+ "version": "1.1.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -88,6 +88,8 @@
88
88
  </template>
89
89
 
90
90
  <script setup>
91
+ import { ref, onMounted } from "vue";
92
+ import { useNuxtApp } from "#imports";
91
93
  import EndpointCard from "./EndpointCard.vue";
92
94
  import RpcMethodCard from "./RpcMethodCard.vue";
93
95
  import ResponseViewer from "./ResponseViewer.vue";
@@ -112,6 +112,7 @@
112
112
  </template>
113
113
 
114
114
  <script setup>
115
+ import { ref, computed } from "vue";
115
116
  const props = defineProps({
116
117
  endpoint: { type: Object, required: true },
117
118
  type: { type: String, required: true }
@@ -46,6 +46,7 @@
46
46
  </template>
47
47
 
48
48
  <script setup>
49
+ import { ref, computed } from "vue";
49
50
  const props = defineProps({
50
51
  response: { type: Object, required: true }
51
52
  });
@@ -64,6 +64,7 @@
64
64
  </template>
65
65
 
66
66
  <script setup>
67
+ import { ref, watch } from "vue";
67
68
  const props = defineProps({
68
69
  service: { type: String, required: true },
69
70
  method: { type: Object, required: true }
@@ -85,12 +85,148 @@ export function clearProtoCache() {
85
85
  protoCursorManager = null;
86
86
  protoPageManager = null;
87
87
  }
88
- function getResponseTypeInfo(methodDef) {
89
- const responseType = methodDef.responseType;
90
- if (responseType?.type) {
91
- return responseType.type;
88
+ const PROTO_TYPE_MAP = {
89
+ // 숫자 (legacy/fallback)
90
+ 1: "double",
91
+ 2: "float",
92
+ 3: "int64",
93
+ 4: "uint64",
94
+ 5: "int32",
95
+ 6: "fixed64",
96
+ 7: "fixed32",
97
+ 8: "bool",
98
+ 9: "string",
99
+ 10: "group",
100
+ 11: "message",
101
+ 12: "bytes",
102
+ 13: "uint32",
103
+ 14: "enum",
104
+ 15: "sfixed32",
105
+ 16: "sfixed64",
106
+ 17: "sint32",
107
+ 18: "sint64",
108
+ // 문자열 키 (proto-loader 실제 반환값)
109
+ TYPE_DOUBLE: "double",
110
+ TYPE_FLOAT: "float",
111
+ TYPE_INT64: "int64",
112
+ TYPE_UINT64: "uint64",
113
+ TYPE_INT32: "int32",
114
+ TYPE_FIXED64: "fixed64",
115
+ TYPE_FIXED32: "fixed32",
116
+ TYPE_BOOL: "bool",
117
+ TYPE_STRING: "string",
118
+ TYPE_GROUP: "group",
119
+ TYPE_MESSAGE: "message",
120
+ TYPE_BYTES: "bytes",
121
+ TYPE_UINT32: "uint32",
122
+ TYPE_ENUM: "enum",
123
+ TYPE_SFIXED32: "sfixed32",
124
+ TYPE_SFIXED64: "sfixed64",
125
+ TYPE_SINT32: "sint32",
126
+ TYPE_SINT64: "sint64"
127
+ };
128
+ function findTypeInPackageDefinition(typeName, packageDefinition) {
129
+ const cleanName = typeName.startsWith(".") ? typeName.slice(1) : typeName;
130
+ const directMatch = packageDefinition[cleanName];
131
+ if (directMatch?.type) {
132
+ return directMatch.type;
92
133
  }
93
- return {};
134
+ if (!cleanName.includes(".")) {
135
+ for (const key of Object.keys(packageDefinition)) {
136
+ if (key.endsWith(`.${cleanName}`)) {
137
+ const matchedType = packageDefinition[key];
138
+ if (matchedType?.type) {
139
+ return matchedType.type;
140
+ }
141
+ }
142
+ }
143
+ }
144
+ return null;
145
+ }
146
+ function convertFieldDescriptor(fieldDesc, protoType, context) {
147
+ const typeName = PROTO_TYPE_MAP[fieldDesc.type] || "string";
148
+ const isRepeated = fieldDesc.label === 3 || fieldDesc.label === "LABEL_REPEATED";
149
+ const result = {
150
+ type: typeName,
151
+ rule: isRepeated ? "repeated" : void 0
152
+ };
153
+ if (fieldDesc.typeName) {
154
+ const shortName = fieldDesc.typeName.split(".").pop() || fieldDesc.typeName;
155
+ result.typeName = shortName;
156
+ if (context.depth >= context.maxDepth) {
157
+ return result;
158
+ }
159
+ const enumType = protoType.enumType?.find(
160
+ (e) => e.name === shortName
161
+ );
162
+ if (enumType) {
163
+ result.resolvedType = {
164
+ values: enumType.value?.reduce((acc, v) => {
165
+ acc[v.name] = v.number;
166
+ return acc;
167
+ }, {})
168
+ };
169
+ return result;
170
+ }
171
+ const nestedType = protoType.nestedType?.find(
172
+ (t) => t.name === shortName
173
+ );
174
+ if (nestedType) {
175
+ const nestedContext = {
176
+ ...context,
177
+ depth: context.depth + 1,
178
+ visitedTypes: /* @__PURE__ */ new Set([...context.visitedTypes, fieldDesc.typeName])
179
+ };
180
+ result.resolvedType = resolveProtoType(nestedType, nestedContext);
181
+ return result;
182
+ }
183
+ const externalType = findTypeInPackageDefinition(fieldDesc.typeName, context.packageDefinition);
184
+ if (externalType) {
185
+ const externalContext = {
186
+ ...context,
187
+ depth: context.depth + 1,
188
+ visitedTypes: /* @__PURE__ */ new Set([...context.visitedTypes, fieldDesc.typeName])
189
+ };
190
+ result.resolvedType = resolveProtoType(externalType, externalContext);
191
+ }
192
+ }
193
+ return result;
194
+ }
195
+ function resolveProtoType(protoType, context) {
196
+ if (!protoType.field) {
197
+ return { fields: {}, name: protoType.name };
198
+ }
199
+ const fields = {};
200
+ const oneofGroups = /* @__PURE__ */ new Map();
201
+ for (const fieldDesc of protoType.field) {
202
+ if (fieldDesc.oneofIndex !== void 0 && !fieldDesc.proto3Optional) {
203
+ const group = oneofGroups.get(fieldDesc.oneofIndex) || [];
204
+ group.push(fieldDesc.name);
205
+ oneofGroups.set(fieldDesc.oneofIndex, group);
206
+ }
207
+ fields[fieldDesc.name] = convertFieldDescriptor(fieldDesc, protoType, context);
208
+ }
209
+ return {
210
+ fields,
211
+ name: protoType.name,
212
+ // oneof 그룹 정보 (첫 번째 필드만 생성하도록)
213
+ oneofGroups: oneofGroups.size > 0 ? Object.fromEntries(oneofGroups) : void 0
214
+ };
215
+ }
216
+ function getResponseTypeInfo(methodDef, packageDefinition) {
217
+ const methodDefWithResponseType = methodDef;
218
+ const responseType = methodDefWithResponseType.responseType;
219
+ if (!responseType?.type?.field) {
220
+ return { name: "unknown" };
221
+ }
222
+ const context = {
223
+ packageDefinition,
224
+ visitedTypes: /* @__PURE__ */ new Set(),
225
+ depth: 0,
226
+ maxDepth: 5
227
+ // 최대 재귀 깊이
228
+ };
229
+ return resolveProtoType(responseType.type, context);
94
230
  }
95
231
  export default defineEventHandler(async (event) => {
96
232
  const config = useRuntimeConfig(event);
@@ -147,8 +283,9 @@ export default defineEventHandler(async (event) => {
147
283
  } catch {
148
284
  requestBody = {};
149
285
  }
150
- const responseTypeInfo = getResponseTypeInfo(methodDef);
151
- const seed = deriveSeedFromRequest(requestBody);
286
+ const responseTypeInfo = getResponseTypeInfo(methodDef, cache.packageDefinition);
287
+ const seedNum = deriveSeedFromRequest(requestBody);
288
+ const seed = String(seedNum);
152
289
  const paginationInfo = analyzeProtoPagination(responseTypeInfo);
153
290
  let mockResponse;
154
291
  if (paginationInfo) {
@@ -217,7 +354,7 @@ export default defineEventHandler(async (event) => {
217
354
  mockResponse = responseData;
218
355
  }
219
356
  } else {
220
- mockResponse = generateMockMessage(responseTypeInfo, seed);
357
+ mockResponse = generateMockMessage(responseTypeInfo, seedNum);
221
358
  }
222
359
  return {
223
360
  success: true,
@@ -2,7 +2,8 @@ import { defineEventHandler, createError } from "h3";
2
2
  import { useRuntimeConfig } from "#imports";
3
3
  import { consola } from "consola";
4
4
  import { createRequire } from "node:module";
5
- import { readFileSync, existsSync } from "node:fs";
5
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
6
+ import { join, extname } from "pathe";
6
7
  import yaml from "js-yaml";
7
8
  import { getClientPackage } from "../utils/client-parser.js";
8
9
  const logger = consola.withTag("mock-fried");
@@ -180,18 +181,46 @@ function parseOpenApiSpec(specPath) {
180
181
  return void 0;
181
182
  }
182
183
  }
184
+ function findProtoFiles(dirPath) {
185
+ const files = [];
186
+ const stat = statSync(dirPath);
187
+ if (stat.isFile() && extname(dirPath) === ".proto") {
188
+ return [dirPath];
189
+ }
190
+ if (stat.isDirectory()) {
191
+ const entries = readdirSync(dirPath);
192
+ for (const entry of entries) {
193
+ const fullPath = join(dirPath, entry);
194
+ const entryStat = statSync(fullPath);
195
+ if (entryStat.isFile() && extname(entry) === ".proto") {
196
+ files.push(fullPath);
197
+ } else if (entryStat.isDirectory()) {
198
+ files.push(...findProtoFiles(fullPath));
199
+ }
200
+ }
201
+ }
202
+ return files;
203
+ }
183
204
  async function parseProtoSpec(protoPath) {
184
205
  if (!existsSync(protoPath)) {
185
206
  return void 0;
186
207
  }
187
208
  try {
188
209
  const protoLoader = await import("@grpc/proto-loader");
189
- const packageDefinition = await protoLoader.load(protoPath, {
210
+ const { dirname } = await import("pathe");
211
+ const stat = statSync(protoPath);
212
+ const protoFiles = stat.isDirectory() ? findProtoFiles(protoPath) : [protoPath];
213
+ const includeDir = stat.isDirectory() ? protoPath : dirname(protoPath);
214
+ if (protoFiles.length === 0) {
215
+ return void 0;
216
+ }
217
+ const packageDefinition = await protoLoader.load(protoFiles, {
190
218
  keepCase: false,
191
219
  longs: String,
192
220
  enums: String,
193
221
  defaults: true,
194
- oneofs: true
222
+ oneofs: true,
223
+ includeDirs: [includeDir]
195
224
  });
196
225
  const services = [];
197
226
  let packageName;
@@ -12,19 +12,22 @@ function parseApiFile(filePath, fileName) {
12
12
  while ((match = methodBodyRegex.exec(content)) !== null) {
13
13
  const methodName = match[1];
14
14
  const body = match[2];
15
- methodBodies.set(methodName, body);
15
+ if (methodName && body) {
16
+ methodBodies.set(methodName, body);
17
+ }
16
18
  }
17
19
  rawMethodRegex.lastIndex = 0;
18
20
  while ((match = rawMethodRegex.exec(content)) !== null) {
19
21
  const jsdocContent = match[1];
20
22
  const operationId = match[2];
21
23
  const responseType = match[3];
24
+ if (!jsdocContent || !operationId || !responseType) continue;
22
25
  const jsdocLines = jsdocContent.split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trim()).filter((line) => line.length > 0 && !line.startsWith("@"));
23
26
  const summary = jsdocLines[jsdocLines.length - 1] || jsdocLines[0] || operationId;
24
27
  const body = methodBodies.get(operationId);
25
28
  if (!body) continue;
26
29
  const pathMatch = body.match(/let\s+urlPath\s*=\s*`([^`]+)`/);
27
- if (!pathMatch) continue;
30
+ if (!pathMatch?.[1]) continue;
28
31
  let path = pathMatch[1];
29
32
  path = path.replace(/\$\{[^}]+\}/g, (match2) => {
30
33
  const paramName = match2.match(/requestParameters(?:\.(\w+)|\["?(\w+)"?\])/)?.[1] || match2.match(/requestParameters(?:\.(\w+)|\["?(\w+)"?\])/)?.[2];
@@ -38,25 +41,31 @@ function parseApiFile(filePath, fileName) {
38
41
  const pathParams = [];
39
42
  const pathParamMatches = path.matchAll(/\{(\w+)\}/g);
40
43
  for (const pm of pathParamMatches) {
41
- pathParams.push({
42
- name: pm[1],
43
- type: "string",
44
- required: true
45
- });
44
+ const paramName = pm[1];
45
+ if (paramName) {
46
+ pathParams.push({
47
+ name: paramName,
48
+ type: "string",
49
+ required: true
50
+ });
51
+ }
46
52
  }
47
53
  const queryParams = [];
48
54
  const queryParamRegex = /if\s*\(requestParameters\[?['"]?(\w+)['"]?\]?\s*!==?\s*(?:undefined|null)\)\s*\{\s*queryParameters\[['"](\w+)['"]\]/g;
49
55
  let qpm;
50
56
  while ((qpm = queryParamRegex.exec(body)) !== null) {
51
- queryParams.push({
52
- name: qpm[2],
53
- type: "string",
54
- required: false
55
- });
57
+ const paramName = qpm[2];
58
+ if (paramName) {
59
+ queryParams.push({
60
+ name: paramName,
61
+ type: "string",
62
+ required: false
63
+ });
64
+ }
56
65
  }
57
66
  let requestBodyType;
58
67
  const bodyMatch = body.match(/body:\s*(\w+)ToJSON\(requestParameters\.(\w+)\)/);
59
- if (bodyMatch) {
68
+ if (bodyMatch?.[1]) {
60
69
  requestBodyType = bodyMatch[1];
61
70
  }
62
71
  endpoints.push({
@@ -79,7 +88,7 @@ function analyzeType(rawType) {
79
88
  let type = rawType;
80
89
  if (isArray) {
81
90
  const arrayTypeMatch = rawType.match(/Array<(.+)>/) || rawType.match(/(.+)\[\]/);
82
- if (arrayTypeMatch) {
91
+ if (arrayTypeMatch?.[1]) {
83
92
  type = arrayTypeMatch[1].trim();
84
93
  }
85
94
  }
@@ -96,11 +105,13 @@ function parseModelFile(filePath, fileName) {
96
105
  if (!hasInterface) {
97
106
  const enumRegex = new RegExp(`export\\s+const\\s+(${modelName})\\s*=\\s*\\{([^}]+)\\}\\s*as\\s*const`);
98
107
  const enumMatch = content.match(enumRegex);
99
- if (enumMatch) {
108
+ if (enumMatch?.[2]) {
100
109
  const enumValues = [];
101
110
  const valueMatches = enumMatch[2].matchAll(/(\w+):\s*['"]([^'"]+)['"]/g);
102
111
  for (const vm of valueMatches) {
103
- enumValues.push(vm[2]);
112
+ if (vm[2]) {
113
+ enumValues.push(vm[2]);
114
+ }
104
115
  }
105
116
  if (enumValues.length > 0) {
106
117
  return {
@@ -114,11 +125,12 @@ function parseModelFile(filePath, fileName) {
114
125
  const fields = [];
115
126
  const jsonKeyMap = /* @__PURE__ */ new Map();
116
127
  const toJsonMatch = content.match(/export\s+function\s+\w+ToJSON[^{]*\{[\s\S]*?return\s*\{([\s\S]*?)\};?\s*\}/);
117
- if (toJsonMatch) {
128
+ if (toJsonMatch?.[1]) {
118
129
  const returnBody = toJsonMatch[1];
119
130
  const fieldMatches = returnBody.matchAll(/['"](\w+)['"]\s*:\s*(?:value\.(\w+)|[^,\n]*?value\.(\w+))/g);
120
131
  for (const fm of fieldMatches) {
121
132
  const jsonKey = fm[1];
133
+ if (!jsonKey) continue;
122
134
  const propertyName = fm[2] || fm[3];
123
135
  if (!propertyName) continue;
124
136
  jsonKeyMap.set(propertyName, jsonKey);
@@ -134,29 +146,31 @@ function parseModelFile(filePath, fileName) {
134
146
  }
135
147
  }
136
148
  const fromJsonMatch = content.match(/export\s+function\s+\w+FromJSONTyped[^{]*\{[\s\S]*?return\s*\{([\s\S]*?)\};?\s*\}/);
137
- if (fromJsonMatch) {
149
+ if (fromJsonMatch?.[1]) {
138
150
  const returnBody = fromJsonMatch[1];
139
151
  const dateFields = /* @__PURE__ */ new Set();
140
152
  const dateMatches = returnBody.matchAll(/['"](\w+)['"]\s*:.*?new\s+Date\(/g);
141
153
  for (const dm of dateMatches) {
142
- dateFields.add(dm[1]);
154
+ if (dm[1]) dateFields.add(dm[1]);
143
155
  }
144
156
  const arrayFields = /* @__PURE__ */ new Set();
145
157
  const arrayMatches = returnBody.matchAll(/['"](\w+)['"]\s*:.*?\(json\[['"](\w+)['"]\]\s*as\s*Array/g);
146
158
  for (const am of arrayMatches) {
147
- arrayFields.add(am[1]);
159
+ if (am[1]) arrayFields.add(am[1]);
148
160
  }
149
161
  const refTypeMap = /* @__PURE__ */ new Map();
150
162
  const refMatches = returnBody.matchAll(/['"](\w+)['"]\s*:.*?(\w+)FromJSON\(json\[/g);
151
163
  for (const rm of refMatches) {
152
- if (!rm[2].includes("Array")) {
164
+ if (rm[1] && rm[2] && !rm[2].includes("Array")) {
153
165
  refTypeMap.set(rm[1], rm[2]);
154
166
  }
155
167
  }
156
168
  const arrayRefMatches = returnBody.matchAll(/['"](\w+)['"]\s*:.*?\.map\((\w+)FromJSON\)/g);
157
169
  for (const arm of arrayRefMatches) {
158
- refTypeMap.set(arm[1], arm[2]);
159
- arrayFields.add(arm[1]);
170
+ if (arm[1] && arm[2]) {
171
+ refTypeMap.set(arm[1], arm[2]);
172
+ arrayFields.add(arm[1]);
173
+ }
160
174
  }
161
175
  for (const field of fields) {
162
176
  if (dateFields.has(field.name)) {
@@ -173,14 +187,16 @@ function parseModelFile(filePath, fileName) {
173
187
  }
174
188
  }
175
189
  const interfaceMatch = content.match(/export\s+interface\s+(\w+)(?:\s+extends\s+[\w,\s]+)?\s*\{([\s\S]*?)\n\}/);
176
- if (interfaceMatch) {
190
+ if (interfaceMatch?.[2]) {
177
191
  const interfaceBody = interfaceMatch[2];
178
192
  const simpleFieldRegex = /^\s+(\w+)(\?)?:\s*([^;]+);/gm;
179
193
  let sfm;
180
194
  while ((sfm = simpleFieldRegex.exec(interfaceBody)) !== null) {
181
195
  const name = sfm[1];
196
+ const rawTypeStr = sfm[3];
197
+ if (!name || !rawTypeStr) continue;
182
198
  const optional = sfm[2] === "?";
183
- const rawType = sfm[3].trim();
199
+ const rawType = rawTypeStr.trim();
184
200
  const { type, isArray, refType } = analyzeType(rawType);
185
201
  const existingField = fields.find((f) => f.name === name);
186
202
  if (existingField) {
@@ -2,10 +2,24 @@
2
2
  * Proto 필드 타입에 따른 mock 값 생성
3
3
  */
4
4
  export declare function generateMockValueForProtoField(fieldName: string, fieldType: string, seed?: number): unknown;
5
+ /**
6
+ * 재귀 생성 옵션
7
+ */
8
+ export interface MockGenerationOptions {
9
+ /** 현재 재귀 깊이 */
10
+ depth?: number;
11
+ /** 최대 재귀 깊이 (기본: 3) */
12
+ maxDepth?: number;
13
+ /** 방문한 타입 이름 Set (간접 재귀 감지) */
14
+ visitedTypes?: Set<string>;
15
+ /** 현재 타입 이름 */
16
+ typeName?: string;
17
+ }
5
18
  /**
6
19
  * Proto 메시지 타입에서 mock 객체 생성
20
+ * 재귀적 타입 지원 (깊이 제한으로 무한 루프 방지)
7
21
  */
8
- export declare function generateMockMessage(messageType: Record<string, unknown>, seed?: number): Record<string, unknown>;
22
+ export declare function generateMockMessage(messageType: Record<string, unknown>, seed?: number, options?: MockGenerationOptions): Record<string, unknown>;
9
23
  /**
10
24
  * 요청 객체에서 seed 추출
11
25
  */
@@ -30,7 +30,23 @@ export function generateMockValueForProtoField(fieldName, fieldType, seed = 1) {
30
30
  return null;
31
31
  }
32
32
  }
33
- export function generateMockMessage(messageType, seed = 1) {
33
+ export function generateMockMessage(messageType, seed = 1, options = {}) {
34
+ const {
35
+ depth = 0,
36
+ maxDepth = 3,
37
+ visitedTypes = /* @__PURE__ */ new Set(),
38
+ typeName
39
+ } = options;
40
+ if (depth >= maxDepth) {
41
+ return {};
42
+ }
43
+ if (typeName && visitedTypes.has(typeName)) {
44
+ return {};
45
+ }
46
+ const newVisitedTypes = new Set(visitedTypes);
47
+ if (typeName) {
48
+ newVisitedTypes.add(typeName);
49
+ }
34
50
  const result = {};
35
51
  const fields = messageType.fields || {};
36
52
  let currentSeed = seed;
@@ -41,15 +57,42 @@ export function generateMockMessage(messageType, seed = 1) {
41
57
  const resolvedType = field.resolvedType;
42
58
  if (resolvedType.values) {
43
59
  const enumValues = Object.keys(resolvedType.values);
44
- value = enumValues[0] || "UNKNOWN";
60
+ const random = seededRandom(currentSeed);
61
+ const index = Math.floor(random() * enumValues.length);
62
+ value = enumValues[index] || "UNKNOWN";
45
63
  } else if (resolvedType.fields) {
46
- value = generateMockMessage(resolvedType, currentSeed);
64
+ const nestedTypeName = resolvedType.name || field.typeName || fieldName;
65
+ value = generateMockMessage(resolvedType, currentSeed, {
66
+ depth: depth + 1,
67
+ maxDepth,
68
+ visitedTypes: newVisitedTypes,
69
+ typeName: nestedTypeName
70
+ });
47
71
  }
48
72
  } else {
49
73
  value = generateMockValueForProtoField(fieldName, field.type || "string", currentSeed);
50
74
  }
51
75
  if (field.rule === "repeated") {
52
- value = [value];
76
+ if (depth < maxDepth - 1 && value && typeof value === "object") {
77
+ const itemCount = Math.max(1, 3 - depth);
78
+ const items = [value];
79
+ for (let i = 1; i < itemCount; i++) {
80
+ if (field.resolvedType) {
81
+ const resolvedType = field.resolvedType;
82
+ if (resolvedType.fields) {
83
+ items.push(generateMockMessage(resolvedType, currentSeed + i * 100, {
84
+ depth: depth + 1,
85
+ maxDepth,
86
+ visitedTypes: newVisitedTypes,
87
+ typeName: resolvedType.name || field.typeName || fieldName
88
+ }));
89
+ }
90
+ }
91
+ }
92
+ value = items;
93
+ } else {
94
+ value = value !== null && value !== void 0 ? [value] : [];
95
+ }
53
96
  }
54
97
  if (field.keyType) {
55
98
  const keyValue = field.keyType === "string" ? `key_${currentSeed}` : currentSeed;
@@ -90,6 +90,9 @@ export function analyzeProtoPagination(messageType) {
90
90
  const foundCursorFields = cursorFieldPatterns.filter((p) => fieldNames.includes(p.toLowerCase()));
91
91
  const isPageBased = foundPageFields.length >= 2;
92
92
  const isCursorBased = foundCursorFields.length >= 1;
93
+ if (!isPageBased && !isCursorBased) {
94
+ return null;
95
+ }
93
96
  return {
94
97
  itemsFieldName,
95
98
  itemMessageType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mock-fried",
3
- "version": "1.0.7",
3
+ "version": "1.1.1",
4
4
  "description": "Nuxt3 Mock API Module - OpenAPI & Protobuf RPC Mock Server",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,12 +52,14 @@
52
52
  "format:check": "prettier --check .",
53
53
  "test": "vitest run",
54
54
  "test:watch": "vitest watch",
55
+ "test:coverage": "vitest run --coverage",
55
56
  "test:unit": "vitest run --exclude 'test/e2e/**'",
56
57
  "test:e2e": "vitest run test/e2e/",
57
58
  "test:e2e:openapi": "vitest run test/e2e/playground-openapi.e2e.test.ts",
58
59
  "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",
60
- "test:types": "vue-tsc --noEmit && cd playground-openapi && vue-tsc --noEmit"
60
+ "test:e2e:proto": "vitest run test/e2e/playground-proto.e2e.test.ts test/e2e/playground-proto-advanced.e2e.test.ts",
61
+ "test:e2e:proto-advanced": "vitest run test/e2e/playground-proto-advanced.e2e.test.ts",
62
+ "test:types": "vue-tsc --noEmit"
61
63
  },
62
64
  "dependencies": {
63
65
  "@grpc/grpc-js": "^1.12.0",
@@ -77,6 +79,7 @@
77
79
  "@nuxt/test-utils": "^3.21.0",
78
80
  "@types/js-yaml": "^4.0.9",
79
81
  "@types/node": "latest",
82
+ "@vitest/coverage-v8": "^4.0.16",
80
83
  "changelogen": "^0.6.2",
81
84
  "eslint": "^9.39.2",
82
85
  "nuxt": "^4.2.2",