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.
- package/README.md +187 -19
- package/dist/module.json +1 -1
- package/dist/module.mjs +20 -1
- package/dist/runtime/server/handlers/openapi.js +125 -4
- package/dist/runtime/server/handlers/rpc.d.ts +1 -1
- package/dist/runtime/server/handlers/rpc.js +85 -3
- package/dist/runtime/server/utils/client-parser.js +8 -5
- package/dist/runtime/server/utils/mock/client-generator.d.ts +19 -0
- package/dist/runtime/server/utils/mock/client-generator.js +123 -16
- package/dist/runtime/server/utils/mock/index.d.ts +1 -1
- package/dist/runtime/server/utils/mock/openapi-generator.d.ts +15 -1
- package/dist/runtime/server/utils/mock/openapi-generator.js +82 -28
- package/dist/runtime/server/utils/mock/pagination/base-manager.d.ts +43 -0
- package/dist/runtime/server/utils/mock/pagination/base-manager.js +48 -0
- package/dist/runtime/server/utils/mock/pagination/cursor-manager.d.ts +35 -3
- package/dist/runtime/server/utils/mock/pagination/cursor-manager.js +124 -12
- package/dist/runtime/server/utils/mock/pagination/index.d.ts +6 -3
- package/dist/runtime/server/utils/mock/pagination/index.js +1 -0
- package/dist/runtime/server/utils/mock/pagination/interfaces/id-generator.d.ts +26 -0
- package/dist/runtime/server/utils/mock/pagination/interfaces/id-generator.js +0 -0
- package/dist/runtime/server/utils/mock/pagination/interfaces/index.d.ts +5 -0
- package/dist/runtime/server/utils/mock/pagination/interfaces/index.js +0 -0
- package/dist/runtime/server/utils/mock/pagination/interfaces/item-provider.d.ts +52 -0
- package/dist/runtime/server/utils/mock/pagination/interfaces/item-provider.js +0 -0
- package/dist/runtime/server/utils/mock/pagination/page-manager.d.ts +38 -3
- package/dist/runtime/server/utils/mock/pagination/page-manager.js +87 -7
- package/dist/runtime/server/utils/mock/pagination/snapshot-store.d.ts +18 -2
- package/dist/runtime/server/utils/mock/pagination/snapshot-store.js +38 -9
- package/dist/runtime/server/utils/mock/providers/index.d.ts +7 -0
- package/dist/runtime/server/utils/mock/providers/index.js +9 -0
- package/dist/runtime/server/utils/mock/providers/openapi-item-provider.d.ts +88 -0
- package/dist/runtime/server/utils/mock/providers/openapi-item-provider.js +116 -0
- package/dist/runtime/server/utils/mock/providers/proto-item-provider.d.ts +85 -0
- package/dist/runtime/server/utils/mock/providers/proto-item-provider.js +100 -0
- package/dist/runtime/server/utils/mock/providers/schema-item-provider.d.ts +40 -0
- package/dist/runtime/server/utils/mock/providers/schema-item-provider.js +43 -0
- 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
|
-
###
|
|
126
|
+
### 직접 HTTP 호출 (권장)
|
|
127
|
+
|
|
128
|
+
Mock 서버는 Nitro 핸들러로 동작하므로, Nuxt의 `useFetch`나 `$fetch`로 직접 호출할 수 있습니다.
|
|
129
|
+
기존 API 클라이언트가 있다면 baseURL만 `/mock`으로 변경하여 사용하세요.
|
|
91
130
|
|
|
92
131
|
```typescript
|
|
93
|
-
|
|
132
|
+
// useFetch 사용 (SSR 지원)
|
|
133
|
+
const { data: users } = await useFetch('/mock/users')
|
|
134
|
+
const { data: user } = await useFetch('/mock/users/123')
|
|
94
135
|
|
|
95
|
-
//
|
|
96
|
-
const
|
|
136
|
+
// $fetch 사용
|
|
137
|
+
const products = await $fetch('/mock/products', {
|
|
138
|
+
query: { page: 1, limit: 10 }
|
|
139
|
+
})
|
|
97
140
|
|
|
98
|
-
//
|
|
99
|
-
const
|
|
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
|
|
103
|
-
params: {
|
|
166
|
+
const products = await api.rest('/products', {
|
|
167
|
+
params: { page: 1, limit: 10 }
|
|
104
168
|
})
|
|
105
169
|
|
|
106
170
|
// POST 요청
|
|
107
|
-
const newUser = await
|
|
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
|
|
180
|
+
const api = useApi()
|
|
117
181
|
|
|
118
182
|
// 명시적 RPC 호출
|
|
119
|
-
const user = await
|
|
183
|
+
const user = await api.rpc('UserService', 'GetUser', { id: 1 })
|
|
120
184
|
|
|
121
185
|
// 동적 서비스 접근
|
|
122
|
-
const user = await
|
|
123
|
-
const products = await
|
|
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
|
|
199
|
+
const api = useApi()
|
|
130
200
|
|
|
131
201
|
// 전체 스키마 조회
|
|
132
|
-
const schema = await
|
|
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
|
-
#
|
|
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
|
-
├──
|
|
212
|
-
|
|
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
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
|
-
|
|
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:
|
|
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
|
|
54
|
-
|
|
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
|
-
|
|
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 = {};
|
|
@@ -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 {
|
|
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
|
|
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,
|