mock-fried 1.0.5 → 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.
Files changed (37) hide show
  1. package/README.md +187 -19
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +20 -1
  4. package/dist/runtime/server/handlers/openapi.js +125 -4
  5. package/dist/runtime/server/handlers/rpc.d.ts +1 -1
  6. package/dist/runtime/server/handlers/rpc.js +85 -3
  7. package/dist/runtime/server/utils/client-parser.js +8 -5
  8. package/dist/runtime/server/utils/mock/client-generator.d.ts +19 -0
  9. package/dist/runtime/server/utils/mock/client-generator.js +123 -16
  10. package/dist/runtime/server/utils/mock/index.d.ts +1 -1
  11. package/dist/runtime/server/utils/mock/openapi-generator.d.ts +15 -1
  12. package/dist/runtime/server/utils/mock/openapi-generator.js +82 -28
  13. package/dist/runtime/server/utils/mock/pagination/base-manager.d.ts +43 -0
  14. package/dist/runtime/server/utils/mock/pagination/base-manager.js +48 -0
  15. package/dist/runtime/server/utils/mock/pagination/cursor-manager.d.ts +35 -3
  16. package/dist/runtime/server/utils/mock/pagination/cursor-manager.js +124 -12
  17. package/dist/runtime/server/utils/mock/pagination/index.d.ts +6 -3
  18. package/dist/runtime/server/utils/mock/pagination/index.js +1 -0
  19. package/dist/runtime/server/utils/mock/pagination/interfaces/id-generator.d.ts +26 -0
  20. package/dist/runtime/server/utils/mock/pagination/interfaces/id-generator.js +0 -0
  21. package/dist/runtime/server/utils/mock/pagination/interfaces/index.d.ts +5 -0
  22. package/dist/runtime/server/utils/mock/pagination/interfaces/index.js +0 -0
  23. package/dist/runtime/server/utils/mock/pagination/interfaces/item-provider.d.ts +52 -0
  24. package/dist/runtime/server/utils/mock/pagination/interfaces/item-provider.js +0 -0
  25. package/dist/runtime/server/utils/mock/pagination/page-manager.d.ts +38 -3
  26. package/dist/runtime/server/utils/mock/pagination/page-manager.js +87 -7
  27. package/dist/runtime/server/utils/mock/pagination/snapshot-store.d.ts +18 -2
  28. package/dist/runtime/server/utils/mock/pagination/snapshot-store.js +38 -9
  29. package/dist/runtime/server/utils/mock/providers/index.d.ts +7 -0
  30. package/dist/runtime/server/utils/mock/providers/index.js +9 -0
  31. package/dist/runtime/server/utils/mock/providers/openapi-item-provider.d.ts +88 -0
  32. package/dist/runtime/server/utils/mock/providers/openapi-item-provider.js +116 -0
  33. package/dist/runtime/server/utils/mock/providers/proto-item-provider.d.ts +85 -0
  34. package/dist/runtime/server/utils/mock/providers/proto-item-provider.js +100 -0
  35. package/dist/runtime/server/utils/mock/providers/schema-item-provider.d.ts +40 -0
  36. package/dist/runtime/server/utils/mock/providers/schema-item-provider.js +43 -0
  37. package/package.json +10 -4
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version][npm-version-src]][npm-version-href]
4
4
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![CI][ci-src]][ci-href]
5
6
  [![License][license-src]][license-href]
6
7
  [![Nuxt][nuxt-src]][nuxt-href]
7
8
 
@@ -11,6 +12,8 @@ Nuxt 3 Mock API Module - OpenAPI & Protobuf RPC Mock Server
11
12
 
12
13
  - **OpenAPI Mock Server** - OpenAPI 스펙 기반 자동 Mock 응답 생성
13
14
  - **Protobuf RPC Mock** - Proto 파일 기반 gRPC-style RPC Mock
15
+ - **Deterministic Data** - 동일한 요청에 대해 일관된 Mock 데이터 반환
16
+ - **Pagination Support** - 페이지 기반 / 커서 기반 페이지네이션 지원
14
17
  - **API Explorer** - Swagger UI 스타일의 인터랙티브 API 테스트 UI
15
18
  - **Type-Safe Client** - `$api` 클라이언트로 타입 안전한 API 호출
16
19
  - **Zero Config** - 스펙 파일만 있으면 즉시 사용 가능
@@ -38,7 +41,7 @@ export default defineNuxtConfig({
38
41
  enable: true,
39
42
  prefix: '/mock',
40
43
  openapi: './mocks/openapi.yaml', // OpenAPI 스펙 경로
41
- proto: './mocks/example.proto', // Proto 파일 경로
44
+ proto: './mocks/example.proto', // Proto 파일 경로 (optional)
42
45
  },
43
46
  })
44
47
  ```
@@ -51,6 +54,8 @@ export default defineNuxtConfig({
51
54
  | `prefix` | `string` | `'/mock'` | API 라우트 prefix |
52
55
  | `openapi` | `string \| object` | - | OpenAPI 스펙 파일 경로 또는 클라이언트 패키지 설정 |
53
56
  | `proto` | `string` | - | Proto 파일 경로 |
57
+ | `pagination` | `object` | - | 페이지 기반 페이지네이션 설정 |
58
+ | `cursor` | `object` | - | 커서 기반 페이지네이션 설정 |
54
59
 
55
60
  ### OpenAPI 설정 옵션
56
61
 
@@ -63,6 +68,9 @@ mock: {
63
68
 
64
69
  // 절대 경로
65
70
  openapi: '/path/to/openapi.yaml',
71
+
72
+ // npm 패키지 내 스펙 파일
73
+ openapi: '@your-org/api-spec/openapi.yaml',
66
74
  }
67
75
  ```
68
76
 
@@ -85,26 +93,82 @@ mock: {
85
93
  - 실제 API 응답 구조와 일치하는 Mock 응답
86
94
  - snake_case/camelCase JSON 키 변환 자동 처리
87
95
 
96
+ ### Pagination 설정
97
+
98
+ **페이지 기반 페이지네이션:**
99
+
100
+ ```typescript
101
+ mock: {
102
+ openapi: { package: '@your-org/api-client' },
103
+ pagination: {
104
+ defaultLimit: 20, // 기본 페이지 크기
105
+ maxLimit: 100, // 최대 페이지 크기
106
+ totalItems: 100, // 전체 아이템 수
107
+ },
108
+ }
109
+ ```
110
+
111
+ **커서 기반 페이지네이션:**
112
+
113
+ ```typescript
114
+ mock: {
115
+ openapi: { package: '@your-org/api-client' },
116
+ cursor: {
117
+ defaultLimit: 20,
118
+ maxLimit: 100,
119
+ totalItems: 100,
120
+ },
121
+ }
122
+ ```
123
+
88
124
  ## Usage
89
125
 
90
- ### REST API (OpenAPI)
126
+ ### 직접 HTTP 호출 (권장)
127
+
128
+ Mock 서버는 Nitro 핸들러로 동작하므로, Nuxt의 `useFetch`나 `$fetch`로 직접 호출할 수 있습니다.
129
+ 기존 API 클라이언트가 있다면 baseURL만 `/mock`으로 변경하여 사용하세요.
91
130
 
92
131
  ```typescript
93
- const { $api } = useNuxtApp()
132
+ // useFetch 사용 (SSR 지원)
133
+ const { data: users } = await useFetch('/mock/users')
134
+ const { data: user } = await useFetch('/mock/users/123')
94
135
 
95
- // GET 요청
96
- const users = await $api.rest('/users')
136
+ // $fetch 사용
137
+ const products = await $fetch('/mock/products', {
138
+ query: { page: 1, limit: 10 }
139
+ })
97
140
 
98
- // Path 파라미터
99
- const user = await $api.rest('/users/1')
141
+ // 커서 기반 페이지네이션
142
+ const items = await $fetch('/mock/items', {
143
+ query: { cursor: 'abc123', limit: 20 }
144
+ })
145
+
146
+ // POST 요청
147
+ const newUser = await $fetch('/mock/users', {
148
+ method: 'POST',
149
+ body: { name: 'John', email: 'john@example.com' }
150
+ })
151
+ ```
152
+
153
+ ### useApi Composable
154
+
155
+ auto-import 되는 `useApi` composable로 타입 안전한 호출이 가능합니다.
156
+
157
+ ```typescript
158
+ // useApi는 auto-import됨
159
+ const api = useApi()
160
+
161
+ // REST 호출
162
+ const users = await api.rest('/users')
163
+ const user = await api.rest('/users/123')
100
164
 
101
165
  // Query 파라미터
102
- const products = await $api.rest('/products', {
103
- params: { category: 'electronics', limit: 10 }
166
+ const products = await api.rest('/products', {
167
+ params: { page: 1, limit: 10 }
104
168
  })
105
169
 
106
170
  // POST 요청
107
- const newUser = await $api.rest('/users', {
171
+ const newUser = await api.rest('/users', {
108
172
  method: 'POST',
109
173
  body: { name: 'John', email: 'john@example.com' }
110
174
  })
@@ -113,26 +177,57 @@ const newUser = await $api.rest('/users', {
113
177
  ### RPC (Protobuf)
114
178
 
115
179
  ```typescript
116
- const { $api } = useNuxtApp()
180
+ const api = useApi()
117
181
 
118
182
  // 명시적 RPC 호출
119
- const user = await $api.rpc('UserService', 'GetUser', { id: 1 })
183
+ const user = await api.rpc('UserService', 'GetUser', { id: 1 })
120
184
 
121
185
  // 동적 서비스 접근
122
- const user = await $api.UserService.GetUser({ id: 1 })
123
- const products = await $api.ProductService.ListProducts({ page: 1, limit: 10 })
186
+ const user = await api.UserService.GetUser({ id: 1 })
187
+ const products = await api.ProductService.ListProducts({ page: 1, limit: 10 })
188
+
189
+ // 또는 직접 $fetch 사용
190
+ const result = await $fetch('/mock/rpc/UserService/GetUser', {
191
+ method: 'POST',
192
+ body: { id: 1 }
193
+ })
124
194
  ```
125
195
 
126
196
  ### API Schema 조회
127
197
 
128
198
  ```typescript
129
- const { $api } = useNuxtApp()
199
+ const api = useApi()
130
200
 
131
201
  // 전체 스키마 조회
132
- const schema = await $api.getSchema()
202
+ const schema = await api.getSchema()
133
203
  // { openapi: {...}, rpc: {...} }
204
+
205
+ // 또는 직접 호출
206
+ const schema = await $fetch('/mock/__schema')
134
207
  ```
135
208
 
209
+ ### 캐시 초기화
210
+
211
+ 개발 중 설정 변경 시 캐시를 초기화하려면:
212
+
213
+ ```typescript
214
+ // POST 요청으로 캐시 초기화
215
+ await $fetch('/mock/__reset', { method: 'POST' })
216
+ ```
217
+
218
+ ## Deterministic Mock Data
219
+
220
+ Mock-Fried는 **결정론적(deterministic)** Mock 데이터를 생성합니다:
221
+
222
+ - 동일한 엔드포인트 + path 파라미터 조합은 항상 동일한 데이터 반환
223
+ - 예: `/users/1`과 `/users/2`는 각각 다른 일관된 데이터 반환
224
+ - 서버 재시작 후에도 동일한 데이터 유지
225
+
226
+ 이를 통해:
227
+ - 프론트엔드 개발 시 예측 가능한 테스트
228
+ - E2E 테스트에서 안정적인 데이터 검증
229
+ - UI 스냅샷 테스트 지원
230
+
136
231
  ## API Explorer
137
232
 
138
233
  모듈에 포함된 API Explorer 컴포넌트로 Swagger UI 스타일의 인터페이스 제공:
@@ -164,6 +259,64 @@ const schema = await $api.getSchema()
164
259
  | `/mock/rpc/:service/:method` | POST | RPC Mock 핸들러 |
165
260
  | `/mock/**` | * | OpenAPI Mock 핸들러 |
166
261
 
262
+ ## Implementation Coverage
263
+
264
+ ### OpenAPI Mock (✅ Production Ready)
265
+
266
+ | Feature | Status | Description |
267
+ |---------|--------|-------------|
268
+ | Path Parameter | ✅ | `/users/{id}` → `/users/123` 매칭 |
269
+ | Query Parameter | ✅ | `?page=1&limit=10` 파싱 |
270
+ | Path Specificity | ✅ | 더 구체적인 경로 우선 매칭 |
271
+ | Page Pagination | ✅ | page/limit 기반 페이지네이션 |
272
+ | Cursor Pagination | ✅ | cursor 기반 무한 스크롤 |
273
+ | Deterministic Data | ✅ | 동일 요청 = 동일 응답 |
274
+ | Client Package Mode | ✅ | 생성된 TS 클라이언트 파싱 |
275
+ | Spec File Mode | ✅ | YAML/JSON OpenAPI 스펙 |
276
+ | Generic Types | ✅ | `Array<User>`, `PageResponse<T>` |
277
+ | JSON Key Mapping | ✅ | snake_case ↔ camelCase |
278
+ | Request Body | ✅ | POST/PUT/PATCH body 처리 |
279
+ | All HTTP Methods | ✅ | GET, POST, PUT, DELETE, PATCH |
280
+
281
+ ### Protobuf RPC Mock (✅ Production Ready)
282
+
283
+ | Feature | Status | Description |
284
+ |---------|--------|-------------|
285
+ | Unary RPC | ✅ | 단일 요청-응답 |
286
+ | Service Routing | ✅ | `/rpc/:service/:method` |
287
+ | Proto File Parsing | ✅ | .proto 파일 로드 |
288
+ | Basic Types | ✅ | string, int32/64, float, double, bool |
289
+ | Enum Types | ✅ | 첫 번째 enum 값 반환 |
290
+ | Nested Messages | ✅ | 중첩 메시지 타입 |
291
+ | Repeated Fields | ✅ | 배열 필드 (자동 생성) |
292
+ | Map Fields | ✅ | map 타입 지원 |
293
+ | Page Pagination | ✅ | page/limit 기반 페이지네이션 |
294
+ | Cursor Pagination | ✅ | cursor 기반 무한 스크롤 |
295
+ | Deterministic Data | ✅ | 동일 요청 = 동일 응답 |
296
+ | Server Streaming | ❌ | 미구현 |
297
+ | Client Streaming | ❌ | 미구현 |
298
+ | Bidirectional Streaming | ❌ | 미구현 |
299
+
300
+ ### 구현 예정 (Roadmap)
301
+
302
+ Proto RPC 기능 확장:
303
+
304
+ - [ ] Server streaming 지원
305
+ - [ ] Well-known types (Timestamp, Duration 등)
306
+
307
+ ## Compatibility
308
+
309
+ | Environment | Support | Notes |
310
+ |-------------|---------|-------|
311
+ | Node.js Server | ✅ | 기본 대상 |
312
+ | Nuxt 3/4 | ✅ | 테스트됨 |
313
+ | Nitro SSR | ✅ | 번들 환경 지원 |
314
+ | Edge Runtime | ❌ | fs 모듈 필요 |
315
+
316
+ **요구사항:**
317
+ - Node.js 18+
318
+ - Nuxt 3.0+
319
+
167
320
  ## Development
168
321
 
169
322
  ```bash
@@ -173,9 +326,15 @@ yarn install
173
326
  # Generate type stubs
174
327
  yarn dev:prepare
175
328
 
176
- # Develop with OpenAPI playground
329
+ # Prepare all playgrounds
330
+ yarn dev:prepare:playground
331
+
332
+ # Develop with OpenAPI Spec File Mode
177
333
  yarn dev:openapi
178
334
 
335
+ # Develop with OpenAPI Client Package Mode
336
+ yarn dev:openapi-client
337
+
179
338
  # Develop with Proto playground
180
339
  yarn dev:proto
181
340
 
@@ -208,8 +367,14 @@ mock-fried/
208
367
  │ └── server/
209
368
  │ ├── handlers/ # Nitro 서버 핸들러
210
369
  │ └── utils/ # Mock 데이터 생성 유틸
211
- ├── playground-openapi/ # OpenAPI 테스트 환경
212
- └── playground-proto/ # Proto 테스트 환경
370
+ ├── packages/ # 샘플 패키지 (CI + Playground 공용)
371
+ │ ├── sample-openapi/ # OpenAPI 스펙 파일 (Spec File Mode)
372
+ │ ├── openapi-client/ # openapi-generator 출력 (Client Package Mode)
373
+ │ └── sample-proto/ # Proto 파일 샘플
374
+ ├── playground-openapi/ # Spec File Mode 테스트
375
+ ├── playground-openapi-client/ # Client Package Mode 테스트
376
+ ├── playground-proto/ # Proto RPC 테스트
377
+ └── test/ # E2E 테스트
213
378
  ```
214
379
 
215
380
  ## License
@@ -223,6 +388,9 @@ mock-fried/
223
388
  [npm-downloads-src]: https://img.shields.io/npm/dm/mock-fried.svg?style=flat&colorA=020420&colorB=00DC82
224
389
  [npm-downloads-href]: https://npm.chart.dev/mock-fried
225
390
 
391
+ [ci-src]: https://img.shields.io/github/actions/workflow/status/TaeGyumKim/mock-fried/ci.yml?branch=main&style=flat&colorA=020420&colorB=00DC82
392
+ [ci-href]: https://github.com/TaeGyumKim/mock-fried/actions/workflows/ci.yml
393
+
226
394
  [license-src]: https://img.shields.io/npm/l/mock-fried.svg?style=flat&colorA=020420&colorB=00DC82
227
395
  [license-href]: https://npmjs.com/package/mock-fried
228
396
 
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "1.0.5",
7
+ "version": "1.0.7",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { defineNuxtModule, createResolver, useLogger, addServerHandler, addPlugin, addImports, addComponentsDir } from '@nuxt/kit';
2
2
  import { normalize, resolve } from 'pathe';
3
3
  import { createRequire } from 'node:module';
4
+ import { existsSync, readFileSync } from 'node:fs';
4
5
 
5
6
  function parsePackagePath(specPath) {
6
7
  if (specPath.startsWith("@")) {
@@ -100,7 +101,25 @@ const module$1 = defineNuxtModule({
100
101
  const pkgJsonPath = require.resolve(`${options.openapi.package}/package.json`);
101
102
  clientPackagePath = normalize(resolve(pkgJsonPath, ".."));
102
103
  } catch {
103
- logger.warn(`Failed to resolve client package: ${options.openapi.package}`);
104
+ try {
105
+ const pkgJsonPath = resolve(rootDir, "package.json");
106
+ if (existsSync(pkgJsonPath)) {
107
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
108
+ const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
109
+ const depValue = deps[options.openapi.package];
110
+ if (depValue) {
111
+ if (depValue.startsWith("link:") || depValue.startsWith("file:")) {
112
+ const relativePath = depValue.replace(/^(link:|file:)/, "");
113
+ clientPackagePath = normalize(resolve(rootDir, relativePath));
114
+ logger.info(`Resolved linked package: ${clientPackagePath}`);
115
+ }
116
+ }
117
+ }
118
+ } catch {
119
+ }
120
+ if (!clientPackagePath) {
121
+ logger.warn(`Failed to resolve client package: ${options.openapi.package}`);
122
+ }
104
123
  }
105
124
  }
106
125
  }
@@ -11,9 +11,16 @@ import {
11
11
  CursorPaginationManager,
12
12
  PagePaginationManager
13
13
  } from "../utils/mock/index.js";
14
+ import {
15
+ OpenAPIItemProvider,
16
+ analyzePaginationSchema
17
+ } from "../utils/mock/providers/index.js";
14
18
  import { getClientPackage } from "../utils/client-parser.js";
15
19
  let apiInstance = null;
16
20
  let cachedSpecPath = null;
21
+ let specCursorManager = null;
22
+ let specPageManager = null;
23
+ let cachedOpenAPISpec = null;
17
24
  function loadOpenAPISpec(specPath) {
18
25
  const content = readFileSync(specPath, "utf-8");
19
26
  if (specPath.endsWith(".yaml") || specPath.endsWith(".yml")) {
@@ -27,9 +34,14 @@ async function getOpenAPIBackend(specPath) {
27
34
  }
28
35
  const { OpenAPIBackend } = await import("openapi-backend");
29
36
  const definition = loadOpenAPISpec(specPath);
37
+ cachedOpenAPISpec = definition;
30
38
  apiInstance = new OpenAPIBackend({
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
40
  definition,
32
- quick: true
41
+ quick: false,
42
+ // $ref 역참조를 위해 false로 설정
43
+ validate: false
44
+ // Mock 서버에서는 request validation 비활성화
33
45
  });
34
46
  apiInstance.register({
35
47
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -43,6 +55,13 @@ async function getOpenAPIBackend(specPath) {
43
55
  notImplemented: async (c) => {
44
56
  const operationId = c.operation?.operationId || "unknown";
45
57
  const responses = c.operation?.responses;
58
+ if (responses?.["204"]) {
59
+ return {
60
+ statusCode: 204,
61
+ body: null,
62
+ meta: { operationId }
63
+ };
64
+ }
46
65
  const successResponse = responses?.["200"] || responses?.["201"] || Object.values(responses || {})[0];
47
66
  const content = successResponse?.content;
48
67
  const jsonContent = content?.["application/json"];
@@ -50,8 +69,96 @@ async function getOpenAPIBackend(specPath) {
50
69
  if (jsonContent?.example) {
51
70
  mockData = jsonContent.example;
52
71
  } else if (jsonContent?.schema) {
53
- const seed = hashString(operationId + JSON.stringify(c.request?.params || {}));
54
- mockData = generateMockFromSchema(jsonContent.schema, seed);
72
+ const schema = jsonContent.schema;
73
+ const seed = `${operationId}-${JSON.stringify(c.request?.params || {})}`;
74
+ const paginationInfo = analyzePaginationSchema(schema);
75
+ if (paginationInfo) {
76
+ const query = c.request?.query || {};
77
+ const page = Number(query.page) || 1;
78
+ const limit = Number(query.limit) || Number(query.size) || 20;
79
+ const cursor = query.cursor;
80
+ const total = 100;
81
+ const itemProvider = new OpenAPIItemProvider(paginationInfo.itemSchema, {
82
+ modelName: operationId
83
+ });
84
+ if (!specCursorManager) {
85
+ specCursorManager = new CursorPaginationManager(itemProvider);
86
+ }
87
+ if (!specPageManager) {
88
+ specPageManager = new PagePaginationManager(itemProvider);
89
+ }
90
+ if (cursor || paginationInfo.isCursorBased) {
91
+ const result = specCursorManager.getCursorPageWithProvider(itemProvider, {
92
+ cursor,
93
+ limit,
94
+ total,
95
+ seed
96
+ });
97
+ const responseData = {
98
+ [paginationInfo.itemsFieldName]: result.items
99
+ };
100
+ if (paginationInfo.metaFields.includes("nextCursor")) {
101
+ responseData.nextCursor = result.nextCursor;
102
+ }
103
+ if (paginationInfo.metaFields.includes("prevCursor")) {
104
+ responseData.prevCursor = result.prevCursor;
105
+ }
106
+ if (paginationInfo.metaFields.includes("hasMore")) {
107
+ responseData.hasMore = result.hasMore;
108
+ }
109
+ if (paginationInfo.metaFields.includes("total")) {
110
+ responseData.total = total;
111
+ }
112
+ if (paginationInfo.metaFields.includes("cursor")) {
113
+ responseData.cursor = result.nextCursor;
114
+ }
115
+ mockData = responseData;
116
+ } else {
117
+ const result = specPageManager.getPagedResponseWithProvider(itemProvider, {
118
+ page,
119
+ limit,
120
+ total,
121
+ seed
122
+ });
123
+ const responseData = {
124
+ [paginationInfo.itemsFieldName]: result.items
125
+ };
126
+ if (paginationInfo.metaFields.includes("page")) {
127
+ responseData.page = result.page;
128
+ }
129
+ if (paginationInfo.metaFields.includes("totalPages")) {
130
+ responseData.totalPages = result.totalPages;
131
+ }
132
+ if (paginationInfo.metaFields.includes("total")) {
133
+ responseData.total = result.total;
134
+ }
135
+ if (paginationInfo.metaFields.includes("totalItems")) {
136
+ responseData.totalItems = result.total;
137
+ }
138
+ if (paginationInfo.metaFields.includes("limit")) {
139
+ responseData.limit = result.limit;
140
+ }
141
+ if (paginationInfo.metaFields.includes("size")) {
142
+ responseData.size = result.limit;
143
+ }
144
+ if (paginationInfo.metaFields.includes("offset")) {
145
+ responseData.offset = (result.page - 1) * result.limit;
146
+ }
147
+ mockData = responseData;
148
+ }
149
+ } else {
150
+ const numericSeed = hashString(seed);
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
+ );
161
+ }
55
162
  }
56
163
  return {
57
164
  statusCode: 200,
@@ -82,11 +189,14 @@ let pagePaginationManager = null;
82
189
  export function clearOpenApiCache() {
83
190
  apiInstance = null;
84
191
  cachedSpecPath = null;
192
+ cachedOpenAPISpec = null;
85
193
  cachedClientPackage = null;
86
194
  cachedClientPath = null;
87
195
  mockGenerator = null;
88
196
  cursorPaginationManager = null;
89
197
  pagePaginationManager = null;
198
+ specCursorManager = null;
199
+ specPageManager = null;
90
200
  }
91
201
  function getClientPackageData(packagePath, config, paginationConfig, cursorConfig) {
92
202
  if (cachedClientPackage && cachedClientPath === packagePath && mockGenerator && cursorPaginationManager && pagePaginationManager) {
@@ -146,7 +256,18 @@ function handleClientPackageRequest(pkg, generator, cursorManager, pageManager,
146
256
  };
147
257
  }
148
258
  const { endpoint, pathParams } = match;
149
- 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"];
150
271
  if (primitiveTypes.includes(endpoint.responseType.toLowerCase())) {
151
272
  const pathLower = path.toLowerCase();
152
273
  let primitiveResponse = {};
@@ -6,6 +6,6 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
6
6
  success: boolean;
7
7
  service: string;
8
8
  method: string;
9
- data: Record<string, unknown>;
9
+ data: unknown;
10
10
  }>>;
11
11
  export default _default;
@@ -4,9 +4,20 @@ import * as protoLoader from "@grpc/proto-loader";
4
4
  import * as grpc from "@grpc/grpc-js";
5
5
  import { readdirSync, statSync } from "node:fs";
6
6
  import { join, extname, dirname } from "pathe";
7
- import { generateMockMessage, deriveSeedFromRequest } from "../utils/mock/index.js";
7
+ import {
8
+ generateMockMessage,
9
+ deriveSeedFromRequest,
10
+ CursorPaginationManager,
11
+ PagePaginationManager
12
+ } from "../utils/mock/index.js";
13
+ import {
14
+ ProtoItemProvider,
15
+ analyzeProtoPagination
16
+ } from "../utils/mock/providers/index.js";
8
17
  let protoCache = null;
9
18
  let cachedProtoPath = null;
19
+ let protoCursorManager = null;
20
+ let protoPageManager = null;
10
21
  function findProtoFiles(dirPath) {
11
22
  const files = [];
12
23
  const stat = statSync(dirPath);
@@ -71,6 +82,8 @@ async function loadProto(protoPath) {
71
82
  export function clearProtoCache() {
72
83
  protoCache = null;
73
84
  cachedProtoPath = null;
85
+ protoCursorManager = null;
86
+ protoPageManager = null;
74
87
  }
75
88
  function getResponseTypeInfo(methodDef) {
76
89
  const responseType = methodDef.responseType;
@@ -130,13 +143,82 @@ export default defineEventHandler(async (event) => {
130
143
  }
131
144
  let requestBody;
132
145
  try {
133
- requestBody = await readBody(event);
146
+ requestBody = await readBody(event) ?? {};
134
147
  } catch {
135
148
  requestBody = {};
136
149
  }
137
150
  const responseTypeInfo = getResponseTypeInfo(methodDef);
138
151
  const seed = deriveSeedFromRequest(requestBody);
139
- const mockResponse = generateMockMessage(responseTypeInfo, seed);
152
+ const paginationInfo = analyzeProtoPagination(responseTypeInfo);
153
+ let mockResponse;
154
+ if (paginationInfo) {
155
+ const page = Number(requestBody.page) || 1;
156
+ const limit = Number(requestBody.limit) || Number(requestBody.page_size) || 20;
157
+ const cursor = requestBody.cursor;
158
+ const total = 100;
159
+ const itemProvider = new ProtoItemProvider(paginationInfo.itemMessageType, {
160
+ modelName: `${serviceName}.${methodName}`
161
+ });
162
+ if (!protoCursorManager) {
163
+ protoCursorManager = new CursorPaginationManager(itemProvider);
164
+ }
165
+ if (!protoPageManager) {
166
+ protoPageManager = new PagePaginationManager(itemProvider);
167
+ }
168
+ if (cursor || paginationInfo.isCursorBased) {
169
+ const result = protoCursorManager.getCursorPageWithProvider(itemProvider, {
170
+ cursor,
171
+ limit,
172
+ total,
173
+ seed
174
+ });
175
+ const responseData = {
176
+ [paginationInfo.itemsFieldName]: result.items
177
+ };
178
+ for (const metaField of paginationInfo.metaFields) {
179
+ const lowerField = metaField.toLowerCase();
180
+ if (lowerField.includes("next") && lowerField.includes("cursor")) {
181
+ responseData[metaField] = result.nextCursor;
182
+ } else if (lowerField.includes("prev") && lowerField.includes("cursor")) {
183
+ responseData[metaField] = result.prevCursor;
184
+ } else if (lowerField.includes("has_more") || lowerField.includes("hasmore")) {
185
+ responseData[metaField] = result.hasMore;
186
+ } else if (lowerField === "cursor") {
187
+ responseData[metaField] = result.nextCursor;
188
+ } else if (lowerField === "total" || lowerField === "total_items") {
189
+ responseData[metaField] = total;
190
+ }
191
+ }
192
+ mockResponse = responseData;
193
+ } else {
194
+ const result = protoPageManager.getPagedResponseWithProvider(itemProvider, {
195
+ page,
196
+ limit,
197
+ total,
198
+ seed
199
+ });
200
+ const responseData = {
201
+ [paginationInfo.itemsFieldName]: result.items
202
+ };
203
+ for (const metaField of paginationInfo.metaFields) {
204
+ const lowerField = metaField.toLowerCase();
205
+ if (lowerField === "page" || lowerField === "page_number") {
206
+ responseData[metaField] = result.page;
207
+ } else if (lowerField === "total_pages" || lowerField === "totalpages") {
208
+ responseData[metaField] = result.totalPages;
209
+ } else if (lowerField === "total" || lowerField === "total_items") {
210
+ responseData[metaField] = result.total;
211
+ } else if (lowerField === "limit" || lowerField === "page_size" || lowerField === "size") {
212
+ responseData[metaField] = result.limit;
213
+ } else if (lowerField === "offset") {
214
+ responseData[metaField] = (result.page - 1) * result.limit;
215
+ }
216
+ }
217
+ mockResponse = responseData;
218
+ }
219
+ } else {
220
+ mockResponse = generateMockMessage(responseTypeInfo, seed);
221
+ }
140
222
  return {
141
223
  success: true,
142
224
  service: serviceName,