mock-fried 1.2.1 → 1.3.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/dist/module.d.mts CHANGED
@@ -49,6 +49,11 @@ interface MockCursorConfig {
49
49
  * @default false
50
50
  */
51
51
  includeSortInfo?: boolean;
52
+ /**
53
+ * 역방향 페이지네이션 쿼리 파라미터명
54
+ * @default 'isBackward'
55
+ */
56
+ backwardParam?: string;
52
57
  }
53
58
  /**
54
59
  * Mock Module Options
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "1.2.1",
7
+ "version": "1.3.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -0,0 +1,23 @@
1
+ /**
2
+ * OpenAPI Client Package Mode Handler
3
+ * Handles mock responses based on generated OpenAPI client packages
4
+ */
5
+ import { SchemaMockGenerator, CursorPaginationManager, PagePaginationManager } from '../../utils/mock/index.js';
6
+ import type { ParsedClientPackage, OpenApiClientConfig, MockPaginationConfig, MockCursorConfig } from '../../../../types.js';
7
+ /**
8
+ * 클라이언트 패키지에서 파싱된 정보 가져오기 (캐싱 포함)
9
+ */
10
+ export declare function getClientPackageData(packagePath: string, config?: OpenApiClientConfig, paginationConfig?: MockPaginationConfig, cursorConfig?: MockCursorConfig): {
11
+ package: ParsedClientPackage;
12
+ generator: SchemaMockGenerator;
13
+ cursorManager: CursorPaginationManager;
14
+ pageManager: PagePaginationManager;
15
+ };
16
+ /**
17
+ * 클라이언트 패키지 모드에서 Mock 응답 생성
18
+ */
19
+ export declare function handleClientPackageRequest(pkg: ParsedClientPackage, generator: SchemaMockGenerator, cursorManager: CursorPaginationManager, pageManager: PagePaginationManager, path: string, method: string, query: Record<string, string | number>, backwardParam?: string): {
20
+ statusCode: number;
21
+ body: unknown;
22
+ meta?: Record<string, unknown>;
23
+ };
@@ -0,0 +1,217 @@
1
+ import {
2
+ SchemaMockGenerator,
3
+ extractDataModelName,
4
+ CursorPaginationManager,
5
+ PagePaginationManager
6
+ } from "../../utils/mock/index.js";
7
+ import { getClientPackage } from "../../utils/client-parser.js";
8
+ import { cacheManager } from "../../utils/cache-manager.js";
9
+ function getPathSpecificity(path) {
10
+ const segments = path.split("/").filter(Boolean);
11
+ const paramCount = (path.match(/\{(\w+)\}/g) || []).length;
12
+ return segments.length * 100 - paramCount * 10;
13
+ }
14
+ function findMatchingEndpoint(endpoints, path, method) {
15
+ const normalizedMethod = method.toUpperCase();
16
+ const sortedEndpoints = [...endpoints].filter((e) => e.method === normalizedMethod).sort((a, b) => getPathSpecificity(b.path) - getPathSpecificity(a.path));
17
+ for (const endpoint of sortedEndpoints) {
18
+ const pattern = endpoint.path.replace(/\{(\w+)\}/g, "([^/]+)");
19
+ const regex = new RegExp(`^${pattern}$`);
20
+ const match = path.match(regex);
21
+ if (match) {
22
+ const pathParams = {};
23
+ const paramNames = endpoint.path.match(/\{(\w+)\}/g) || [];
24
+ paramNames.forEach((param, index) => {
25
+ const paramName = param.slice(1, -1);
26
+ pathParams[paramName] = match[index + 1] || "";
27
+ });
28
+ return { endpoint, pathParams };
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ export function getClientPackageData(packagePath, config, paginationConfig, cursorConfig) {
34
+ const cache = cacheManager.clientMode;
35
+ if (cache.package && cache.path === packagePath && cache.generator && cache.cursorManager && cache.pageManager) {
36
+ return {
37
+ package: cache.package,
38
+ generator: cache.generator,
39
+ cursorManager: cache.cursorManager,
40
+ pageManager: cache.pageManager
41
+ };
42
+ }
43
+ cache.package = getClientPackage(packagePath, config);
44
+ cache.path = packagePath;
45
+ cache.generator = new SchemaMockGenerator(cache.package.models);
46
+ cache.cursorManager = new CursorPaginationManager(cache.generator, {
47
+ cursorConfig
48
+ });
49
+ cache.pageManager = new PagePaginationManager(cache.generator, {
50
+ config: paginationConfig
51
+ });
52
+ return {
53
+ package: cache.package,
54
+ generator: cache.generator,
55
+ cursorManager: cache.cursorManager,
56
+ pageManager: cache.pageManager
57
+ };
58
+ }
59
+ export function handleClientPackageRequest(pkg, generator, cursorManager, pageManager, path, method, query, backwardParam = "isBackward") {
60
+ const match = findMatchingEndpoint(pkg.endpoints, path, method);
61
+ if (!match) {
62
+ return {
63
+ statusCode: 404,
64
+ body: { error: "Not found", message: `No matching endpoint for ${method} ${path}` }
65
+ };
66
+ }
67
+ const { endpoint, pathParams } = match;
68
+ if (endpoint.responseType.toLowerCase() === "void") {
69
+ return {
70
+ statusCode: 204,
71
+ body: null,
72
+ meta: {
73
+ operationId: endpoint.operationId,
74
+ apiClass: endpoint.apiClassName,
75
+ responseType: endpoint.responseType
76
+ }
77
+ };
78
+ }
79
+ const primitiveTypes = ["object", "string", "number", "boolean", "any", "unknown"];
80
+ if (primitiveTypes.includes(endpoint.responseType.toLowerCase())) {
81
+ return handlePrimitiveResponse(path, endpoint);
82
+ }
83
+ const typeInfo = extractDataModelName(endpoint.responseType, pkg.models);
84
+ const { modelName, isList, listFieldName, wrapperType } = typeInfo;
85
+ const page = Number(query.page) || 1;
86
+ const limit = Number(query.limit) || Number(query.size) || 20;
87
+ const cursor = query.cursor;
88
+ const isBackward = query[backwardParam] === "true" || query[backwardParam] === "1";
89
+ const wrapperSchema = wrapperType ? pkg.models.get(wrapperType) : null;
90
+ const hasItemsField = listFieldName === "items";
91
+ const hasPaginationFields = wrapperSchema?.fields.some(
92
+ (f) => ["page", "totalPages", "total", "totalItems", "pagination"].includes(f.name)
93
+ );
94
+ let responseData;
95
+ if (isList) {
96
+ const seed = `${endpoint.path}-${JSON.stringify(pathParams)}`;
97
+ if (hasItemsField && hasPaginationFields) {
98
+ responseData = handlePaginatedListResponse(
99
+ cursorManager,
100
+ pageManager,
101
+ modelName,
102
+ seed,
103
+ page,
104
+ limit,
105
+ cursor,
106
+ isBackward
107
+ );
108
+ } else {
109
+ responseData = handleSimpleListResponse(
110
+ cursorManager,
111
+ modelName,
112
+ seed,
113
+ limit,
114
+ cursor,
115
+ isBackward,
116
+ listFieldName,
117
+ wrapperSchema
118
+ );
119
+ }
120
+ } else {
121
+ const seed = `${endpoint.operationId}-${JSON.stringify(pathParams)}`;
122
+ responseData = generator.generateOne(endpoint.responseType, seed);
123
+ }
124
+ return {
125
+ statusCode: 200,
126
+ body: responseData,
127
+ meta: {
128
+ operationId: endpoint.operationId,
129
+ apiClass: endpoint.apiClassName,
130
+ responseType: endpoint.responseType
131
+ }
132
+ };
133
+ }
134
+ function handlePrimitiveResponse(path, endpoint) {
135
+ const pathLower = path.toLowerCase();
136
+ let primitiveResponse = {};
137
+ if (pathLower.includes("health")) {
138
+ primitiveResponse = { status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
139
+ } else if (pathLower.includes("ping")) {
140
+ primitiveResponse = { pong: true };
141
+ } else if (endpoint.responseType === "string") {
142
+ primitiveResponse = "success";
143
+ } else if (endpoint.responseType === "number") {
144
+ primitiveResponse = 0;
145
+ } else if (endpoint.responseType === "boolean") {
146
+ primitiveResponse = true;
147
+ }
148
+ return {
149
+ statusCode: 200,
150
+ body: primitiveResponse,
151
+ meta: {
152
+ operationId: endpoint.operationId,
153
+ apiClass: endpoint.apiClassName,
154
+ responseType: endpoint.responseType
155
+ }
156
+ };
157
+ }
158
+ function handlePaginatedListResponse(cursorManager, pageManager, modelName, seed, page, limit, cursor, isBackward) {
159
+ if (cursor || isBackward) {
160
+ const result = cursorManager.getCursorPage(modelName, {
161
+ cursor,
162
+ limit,
163
+ total: 100,
164
+ seed,
165
+ isBackward
166
+ });
167
+ const { _snapshotId: _, ...responseWithoutSnapshotId } = result;
168
+ return responseWithoutSnapshotId;
169
+ } else {
170
+ const result = pageManager.getPagedResponse(modelName, {
171
+ page,
172
+ limit,
173
+ total: 100,
174
+ seed
175
+ });
176
+ const { _snapshotId: _, ...responseWithoutSnapshotId } = result;
177
+ return responseWithoutSnapshotId;
178
+ }
179
+ }
180
+ function handleSimpleListResponse(cursorManager, modelName, seed, limit, cursor, isBackward, listFieldName, wrapperSchema) {
181
+ const result = cursorManager.getCursorPage(modelName, {
182
+ cursor,
183
+ limit,
184
+ total: 100,
185
+ seed,
186
+ isBackward
187
+ });
188
+ if (listFieldName) {
189
+ const listField = wrapperSchema?.fields.find((f) => f.name === listFieldName);
190
+ const listJsonKey = listField?.jsonKey || listFieldName;
191
+ const otherFields = {};
192
+ if (wrapperSchema) {
193
+ for (const field of wrapperSchema.fields) {
194
+ if (field.name !== listFieldName) {
195
+ const outputKey = field.jsonKey || field.name;
196
+ if (field.name === "nextCursor" || field.name === "cursor") {
197
+ otherFields[outputKey] = result.nextCursor ?? null;
198
+ } else if (field.name === "prevCursor") {
199
+ otherFields[outputKey] = result.prevCursor ?? null;
200
+ } else if (field.name === "hasMore") {
201
+ otherFields[outputKey] = result.hasMore;
202
+ } else if (field.name === "hasPrev") {
203
+ otherFields[outputKey] = result.hasPrev ?? false;
204
+ } else if (field.name === "total" || field.name === "totalItems") {
205
+ otherFields[outputKey] = 100;
206
+ }
207
+ }
208
+ }
209
+ }
210
+ return {
211
+ [listJsonKey]: result.items,
212
+ ...otherFields
213
+ };
214
+ } else {
215
+ return result.items;
216
+ }
217
+ }
@@ -2,5 +2,8 @@
2
2
  * OpenAPI 관련 캐시 초기화
3
3
  */
4
4
  export declare function clearOpenApiCache(): void;
5
+ /**
6
+ * OpenAPI Mock 핸들러
7
+ */
5
8
  declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
6
9
  export default _default;
@@ -0,0 +1,79 @@
1
+ import { defineEventHandler, readBody, createError } from "h3";
2
+ import { parseQuery } from "ufo";
3
+ import { useRuntimeConfig } from "#imports";
4
+ import { cacheManager } from "../../utils/cache-manager.js";
5
+ import { getOpenAPIBackend } from "./spec-mode.js";
6
+ import { getClientPackageData, handleClientPackageRequest } from "./client-mode.js";
7
+ export function clearOpenApiCache() {
8
+ cacheManager.clearOpenApi();
9
+ }
10
+ export default defineEventHandler(async (event) => {
11
+ const config = useRuntimeConfig(event);
12
+ const mockConfig = config.mock;
13
+ const prefix = mockConfig?.prefix || "/mock";
14
+ const fullPath = event.path;
15
+ const queryIndex = fullPath.indexOf("?");
16
+ let path = queryIndex >= 0 ? fullPath.substring(0, queryIndex) : fullPath;
17
+ const queryString = queryIndex >= 0 ? fullPath.substring(queryIndex + 1) : "";
18
+ if (path.startsWith(prefix)) {
19
+ path = path.substring(prefix.length) || "/";
20
+ }
21
+ const query = parseQuery(queryString);
22
+ let body;
23
+ if (event.method !== "GET" && event.method !== "HEAD") {
24
+ try {
25
+ body = await readBody(event);
26
+ } catch {
27
+ body = void 0;
28
+ }
29
+ }
30
+ if (mockConfig?.clientPackagePath) {
31
+ const { package: pkg, generator, cursorManager, pageManager } = getClientPackageData(
32
+ mockConfig.clientPackagePath,
33
+ mockConfig.clientPackageConfig,
34
+ mockConfig.pagination,
35
+ mockConfig.cursor
36
+ );
37
+ const backwardParam = mockConfig?.cursor?.backwardParam || "isBackward";
38
+ const result = handleClientPackageRequest(
39
+ pkg,
40
+ generator,
41
+ cursorManager,
42
+ pageManager,
43
+ path,
44
+ event.method,
45
+ query,
46
+ backwardParam
47
+ );
48
+ if (result.statusCode) {
49
+ event.node.res.statusCode = result.statusCode;
50
+ }
51
+ return result.body;
52
+ }
53
+ if (mockConfig?.openapiPath) {
54
+ cacheManager.specMode.backwardParam = mockConfig?.cursor?.backwardParam || "isBackward";
55
+ const backend = await getOpenAPIBackend(mockConfig.openapiPath);
56
+ const headers = {};
57
+ const rawHeaders = event.headers;
58
+ if (rawHeaders) {
59
+ for (const [key, value] of Object.entries(rawHeaders)) {
60
+ if (value) headers[key] = String(value);
61
+ }
62
+ }
63
+ const result = await backend.handleRequest({
64
+ method: event.method,
65
+ path,
66
+ query,
67
+ body,
68
+ headers
69
+ });
70
+ if (result?.statusCode) {
71
+ event.node.res.statusCode = result.statusCode;
72
+ }
73
+ return result?.body ?? result;
74
+ }
75
+ throw createError({
76
+ statusCode: 500,
77
+ message: "OpenAPI configuration not found. Set openapi path or client package."
78
+ });
79
+ });
@@ -0,0 +1,14 @@
1
+ import { type OpenAPISchema } from '../../utils/mock/providers/index.js';
2
+ import { type OpenAPISpec } from '../../utils/cache-manager.js';
3
+ /**
4
+ * OpenAPI 스펙 파일 로드
5
+ */
6
+ export declare function loadOpenAPISpec(specPath: string): OpenAPISpec;
7
+ /**
8
+ * 스키마 참조 해결
9
+ */
10
+ export declare function resolveSchemaRef(schema: OpenAPISchema, schemas?: Record<string, Record<string, unknown>>): OpenAPISchema;
11
+ /**
12
+ * OpenAPI Backend 인스턴스 가져오기 (캐싱 포함)
13
+ */
14
+ export declare function getOpenAPIBackend(specPath: string): Promise<any>;
@@ -0,0 +1,198 @@
1
+ import { readFileSync } from "node:fs";
2
+ import yaml from "js-yaml";
3
+ import {
4
+ generateMockFromSchema,
5
+ hashString
6
+ } from "../../utils/mock/index.js";
7
+ import {
8
+ OpenAPIItemProvider,
9
+ analyzePaginationSchema
10
+ } from "../../utils/mock/providers/index.js";
11
+ import { cacheManager } from "../../utils/cache-manager.js";
12
+ import { getOrCreateCursorManager, getOrCreatePageManager } from "../../utils/pagination-factory.js";
13
+ export function loadOpenAPISpec(specPath) {
14
+ const content = readFileSync(specPath, "utf-8");
15
+ if (specPath.endsWith(".yaml") || specPath.endsWith(".yml")) {
16
+ return yaml.load(content);
17
+ }
18
+ return JSON.parse(content);
19
+ }
20
+ export function resolveSchemaRef(schema, schemas) {
21
+ if (!schema.$ref || !schemas) return schema;
22
+ const schemaName = schema.$ref.split("/").pop();
23
+ const resolved = schemaName ? schemas[schemaName] : void 0;
24
+ return resolved ?? schema;
25
+ }
26
+ export async function getOpenAPIBackend(specPath) {
27
+ const cache = cacheManager.specMode;
28
+ if (cache.apiInstance && cache.specPath === specPath) {
29
+ return cache.apiInstance;
30
+ }
31
+ const { OpenAPIBackend } = await import("openapi-backend");
32
+ const definition = loadOpenAPISpec(specPath);
33
+ cache.spec = definition;
34
+ const apiInstance = new OpenAPIBackend({
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ definition,
37
+ quick: false,
38
+ // $ref 역참조를 위해 false로 설정
39
+ validate: false
40
+ // Mock 서버에서는 request validation 비활성화
41
+ });
42
+ apiInstance.register({
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ notFound: async (_c) => {
45
+ return {
46
+ statusCode: 404,
47
+ body: { error: "Not found", message: "No matching operation found" }
48
+ };
49
+ },
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ notImplemented: async (c) => {
52
+ return handleNotImplemented(c, cache);
53
+ },
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ validationFail: async (c) => {
56
+ return {
57
+ statusCode: 400,
58
+ body: {
59
+ error: "Validation failed",
60
+ details: c.validation?.errors || []
61
+ }
62
+ };
63
+ }
64
+ });
65
+ await apiInstance.init();
66
+ cache.apiInstance = apiInstance;
67
+ cache.specPath = specPath;
68
+ return apiInstance;
69
+ }
70
+ async function handleNotImplemented(c, cache) {
71
+ const operationId = c.operation?.operationId || "unknown";
72
+ const responses = c.operation?.responses;
73
+ if (responses?.["204"]) {
74
+ return {
75
+ statusCode: 204,
76
+ body: null,
77
+ meta: { operationId }
78
+ };
79
+ }
80
+ const successResponse = responses?.["200"] || responses?.["201"] || Object.values(responses || {})[0];
81
+ const content = successResponse?.content;
82
+ const jsonContent = content?.["application/json"];
83
+ let mockData = null;
84
+ if (jsonContent?.example) {
85
+ mockData = jsonContent.example;
86
+ } else if (jsonContent?.schema) {
87
+ const schema = jsonContent.schema;
88
+ const seed = `${operationId}-${JSON.stringify(c.request?.params || {})}`;
89
+ const apiSchemas = c.api?.document?.components?.schemas;
90
+ const schemaContext = {
91
+ schemas: apiSchemas || cache.spec?.components?.schemas,
92
+ maxDepth: 10
93
+ };
94
+ const resolvedSchema = resolveSchemaRef(schema, schemaContext.schemas);
95
+ const paginationInfo = analyzePaginationSchema(resolvedSchema);
96
+ if (paginationInfo) {
97
+ mockData = handlePaginationResponse(c, cache, paginationInfo, seed, schemaContext, operationId);
98
+ } else {
99
+ const numericSeed = hashString(seed);
100
+ mockData = generateMockFromSchema(
101
+ schema,
102
+ numericSeed,
103
+ schemaContext
104
+ );
105
+ }
106
+ }
107
+ return {
108
+ statusCode: 200,
109
+ body: mockData,
110
+ meta: { operationId }
111
+ };
112
+ }
113
+ function handlePaginationResponse(c, cache, paginationInfo, seed, schemaContext, operationId) {
114
+ if (!paginationInfo) {
115
+ return {};
116
+ }
117
+ const query = c.request?.query || {};
118
+ const page = Number(query.page) || 1;
119
+ const limit = Number(query.limit) || Number(query.size) || 20;
120
+ const cursor = query.cursor;
121
+ const isBackward = query[cache.backwardParam] === "true" || query[cache.backwardParam] === "1";
122
+ const total = 100;
123
+ const itemProvider = new OpenAPIItemProvider(paginationInfo.itemSchema, {
124
+ modelName: operationId,
125
+ schemaContext
126
+ });
127
+ const cursorManager = getOrCreateCursorManager(itemProvider, cache);
128
+ const pageManager = getOrCreatePageManager(itemProvider, cache);
129
+ if (cursor || isBackward || paginationInfo.isCursorBased) {
130
+ const result = cursorManager.getCursorPageWithProvider(itemProvider, {
131
+ cursor,
132
+ limit,
133
+ total,
134
+ seed,
135
+ isBackward
136
+ });
137
+ return buildCursorResponse(result, paginationInfo, total);
138
+ } else {
139
+ const result = pageManager.getPagedResponseWithProvider(itemProvider, {
140
+ page,
141
+ limit,
142
+ total,
143
+ seed
144
+ });
145
+ return buildPageResponse(result, paginationInfo);
146
+ }
147
+ }
148
+ function buildCursorResponse(result, paginationInfo, total) {
149
+ const responseData = {
150
+ [paginationInfo.itemsFieldName]: result.items
151
+ };
152
+ if (paginationInfo.metaFields.includes("nextCursor")) {
153
+ responseData.nextCursor = result.nextCursor ?? null;
154
+ }
155
+ if (paginationInfo.metaFields.includes("prevCursor")) {
156
+ responseData.prevCursor = result.prevCursor ?? null;
157
+ }
158
+ if (paginationInfo.metaFields.includes("hasMore")) {
159
+ responseData.hasMore = result.hasMore;
160
+ }
161
+ if (paginationInfo.metaFields.includes("hasPrev")) {
162
+ responseData.hasPrev = result.hasPrev ?? false;
163
+ }
164
+ if (paginationInfo.metaFields.includes("total")) {
165
+ responseData.total = total;
166
+ }
167
+ if (paginationInfo.metaFields.includes("cursor")) {
168
+ responseData.cursor = result.nextCursor ?? null;
169
+ }
170
+ return responseData;
171
+ }
172
+ function buildPageResponse(result, paginationInfo) {
173
+ const responseData = {
174
+ [paginationInfo.itemsFieldName]: result.items
175
+ };
176
+ if (paginationInfo.metaFields.includes("page")) {
177
+ responseData.page = result.page;
178
+ }
179
+ if (paginationInfo.metaFields.includes("totalPages")) {
180
+ responseData.totalPages = result.totalPages;
181
+ }
182
+ if (paginationInfo.metaFields.includes("total")) {
183
+ responseData.total = result.total;
184
+ }
185
+ if (paginationInfo.metaFields.includes("totalItems")) {
186
+ responseData.totalItems = result.total;
187
+ }
188
+ if (paginationInfo.metaFields.includes("limit")) {
189
+ responseData.limit = result.limit;
190
+ }
191
+ if (paginationInfo.metaFields.includes("size")) {
192
+ responseData.size = result.limit;
193
+ }
194
+ if (paginationInfo.metaFields.includes("offset")) {
195
+ responseData.offset = (result.page - 1) * result.limit;
196
+ }
197
+ return responseData;
198
+ }
@@ -1,5 +1,5 @@
1
1
  import { defineEventHandler } from "h3";
2
- import { clearOpenApiCache } from "./openapi.js";
2
+ import { clearOpenApiCache } from "./openapi/index.js";
3
3
  import { clearProtoCache } from "./rpc.js";
4
4
  import { clearSchemaCache } from "./schema.js";
5
5
  import { clearClientPackageCache } from "../utils/client-parser.js";