mock-fried 1.0.0
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 +229 -0
- package/dist/module.d.mts +125 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +160 -0
- package/dist/runtime/components/ApiExplorer.d.vue.ts +7 -0
- package/dist/runtime/components/ApiExplorer.vue +168 -0
- package/dist/runtime/components/ApiExplorer.vue.d.ts +7 -0
- package/dist/runtime/components/EndpointCard.d.vue.ts +24 -0
- package/dist/runtime/components/EndpointCard.vue +173 -0
- package/dist/runtime/components/EndpointCard.vue.d.ts +24 -0
- package/dist/runtime/components/ResponseViewer.d.vue.ts +16 -0
- package/dist/runtime/components/ResponseViewer.vue +78 -0
- package/dist/runtime/components/ResponseViewer.vue.d.ts +16 -0
- package/dist/runtime/components/RpcMethodCard.d.vue.ts +20 -0
- package/dist/runtime/components/RpcMethodCard.vue +129 -0
- package/dist/runtime/components/RpcMethodCard.vue.d.ts +20 -0
- package/dist/runtime/composables/index.d.ts +1 -0
- package/dist/runtime/composables/index.js +1 -0
- package/dist/runtime/composables/useApi.d.ts +19 -0
- package/dist/runtime/composables/useApi.js +5 -0
- package/dist/runtime/plugin.d.ts +7 -0
- package/dist/runtime/plugin.js +75 -0
- package/dist/runtime/server/handlers/openapi.d.ts +2 -0
- package/dist/runtime/server/handlers/openapi.js +346 -0
- package/dist/runtime/server/handlers/rpc.d.ts +7 -0
- package/dist/runtime/server/handlers/rpc.js +140 -0
- package/dist/runtime/server/handlers/schema.d.ts +7 -0
- package/dist/runtime/server/handlers/schema.js +190 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/client-parser.d.ts +13 -0
- package/dist/runtime/server/utils/client-parser.js +272 -0
- package/dist/runtime/server/utils/mock/client-generator.d.ts +108 -0
- package/dist/runtime/server/utils/mock/client-generator.js +346 -0
- package/dist/runtime/server/utils/mock/index.d.ts +9 -0
- package/dist/runtime/server/utils/mock/index.js +38 -0
- package/dist/runtime/server/utils/mock/openapi-generator.d.ts +4 -0
- package/dist/runtime/server/utils/mock/openapi-generator.js +118 -0
- package/dist/runtime/server/utils/mock/pagination/cursor-manager.d.ts +38 -0
- package/dist/runtime/server/utils/mock/pagination/cursor-manager.js +129 -0
- package/dist/runtime/server/utils/mock/pagination/index.d.ts +8 -0
- package/dist/runtime/server/utils/mock/pagination/index.js +18 -0
- package/dist/runtime/server/utils/mock/pagination/page-manager.d.ts +41 -0
- package/dist/runtime/server/utils/mock/pagination/page-manager.js +96 -0
- package/dist/runtime/server/utils/mock/pagination/snapshot-store.d.ts +64 -0
- package/dist/runtime/server/utils/mock/pagination/snapshot-store.js +125 -0
- package/dist/runtime/server/utils/mock/pagination/types.d.ts +141 -0
- package/dist/runtime/server/utils/mock/pagination/types.js +14 -0
- package/dist/runtime/server/utils/mock/proto-generator.d.ts +12 -0
- package/dist/runtime/server/utils/mock/proto-generator.js +67 -0
- package/dist/runtime/server/utils/mock/shared.d.ts +69 -0
- package/dist/runtime/server/utils/mock/shared.js +150 -0
- package/dist/runtime/server/utils/mock-generator.d.ts +9 -0
- package/dist/runtime/server/utils/mock-generator.js +30 -0
- package/dist/types.d.mts +9 -0
- package/package.json +73 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { defineEventHandler, getQuery, readBody, createError, getRequestURL } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import {
|
|
6
|
+
generateMockFromSchema,
|
|
7
|
+
hashString,
|
|
8
|
+
SchemaMockGenerator,
|
|
9
|
+
extractDataModelName,
|
|
10
|
+
CursorPaginationManager,
|
|
11
|
+
PagePaginationManager
|
|
12
|
+
} from "../utils/mock/index.js";
|
|
13
|
+
import { getClientPackage } from "../utils/client-parser.js";
|
|
14
|
+
let apiInstance = null;
|
|
15
|
+
let cachedSpecPath = null;
|
|
16
|
+
function loadOpenAPISpec(specPath) {
|
|
17
|
+
const content = readFileSync(specPath, "utf-8");
|
|
18
|
+
if (specPath.endsWith(".yaml") || specPath.endsWith(".yml")) {
|
|
19
|
+
return yaml.load(content);
|
|
20
|
+
}
|
|
21
|
+
return JSON.parse(content);
|
|
22
|
+
}
|
|
23
|
+
async function getOpenAPIBackend(specPath) {
|
|
24
|
+
if (apiInstance && cachedSpecPath === specPath) {
|
|
25
|
+
return apiInstance;
|
|
26
|
+
}
|
|
27
|
+
const { OpenAPIBackend } = await import("openapi-backend");
|
|
28
|
+
const definition = loadOpenAPISpec(specPath);
|
|
29
|
+
apiInstance = new OpenAPIBackend({
|
|
30
|
+
definition,
|
|
31
|
+
quick: true
|
|
32
|
+
});
|
|
33
|
+
apiInstance.register({
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
notFound: async (_c) => {
|
|
36
|
+
return {
|
|
37
|
+
statusCode: 404,
|
|
38
|
+
body: { error: "Not found", message: "No matching operation found" }
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
notImplemented: async (c) => {
|
|
43
|
+
const operationId = c.operation?.operationId || "unknown";
|
|
44
|
+
const responses = c.operation?.responses;
|
|
45
|
+
const successResponse = responses?.["200"] || responses?.["201"] || Object.values(responses || {})[0];
|
|
46
|
+
const content = successResponse?.content;
|
|
47
|
+
const jsonContent = content?.["application/json"];
|
|
48
|
+
let mockData = null;
|
|
49
|
+
if (jsonContent?.example) {
|
|
50
|
+
mockData = jsonContent.example;
|
|
51
|
+
} else if (jsonContent?.schema) {
|
|
52
|
+
const seed = hashString(operationId + JSON.stringify(c.request?.params || {}));
|
|
53
|
+
mockData = generateMockFromSchema(jsonContent.schema, seed);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
statusCode: 200,
|
|
57
|
+
body: mockData,
|
|
58
|
+
meta: { operationId }
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
validationFail: async (c) => {
|
|
63
|
+
return {
|
|
64
|
+
statusCode: 400,
|
|
65
|
+
body: {
|
|
66
|
+
error: "Validation failed",
|
|
67
|
+
details: c.validation?.errors || []
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
await apiInstance.init();
|
|
73
|
+
cachedSpecPath = specPath;
|
|
74
|
+
return apiInstance;
|
|
75
|
+
}
|
|
76
|
+
let cachedClientPackage = null;
|
|
77
|
+
let cachedClientPath = null;
|
|
78
|
+
let mockGenerator = null;
|
|
79
|
+
let cursorPaginationManager = null;
|
|
80
|
+
let pagePaginationManager = null;
|
|
81
|
+
function getClientPackageData(packagePath, config, paginationConfig, cursorConfig) {
|
|
82
|
+
if (cachedClientPackage && cachedClientPath === packagePath && mockGenerator && cursorPaginationManager && pagePaginationManager) {
|
|
83
|
+
return {
|
|
84
|
+
package: cachedClientPackage,
|
|
85
|
+
generator: mockGenerator,
|
|
86
|
+
cursorManager: cursorPaginationManager,
|
|
87
|
+
pageManager: pagePaginationManager
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
cachedClientPackage = getClientPackage(packagePath, config);
|
|
91
|
+
cachedClientPath = packagePath;
|
|
92
|
+
mockGenerator = new SchemaMockGenerator(cachedClientPackage.models);
|
|
93
|
+
cursorPaginationManager = new CursorPaginationManager(mockGenerator, {
|
|
94
|
+
cursorConfig
|
|
95
|
+
});
|
|
96
|
+
pagePaginationManager = new PagePaginationManager(mockGenerator, {
|
|
97
|
+
config: paginationConfig
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
package: cachedClientPackage,
|
|
101
|
+
generator: mockGenerator,
|
|
102
|
+
cursorManager: cursorPaginationManager,
|
|
103
|
+
pageManager: pagePaginationManager
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function getPathSpecificity(path) {
|
|
107
|
+
const segments = path.split("/").filter(Boolean);
|
|
108
|
+
const paramCount = (path.match(/\{(\w+)\}/g) || []).length;
|
|
109
|
+
return segments.length * 100 - paramCount * 10;
|
|
110
|
+
}
|
|
111
|
+
function findMatchingEndpoint(endpoints, path, method) {
|
|
112
|
+
const normalizedMethod = method.toUpperCase();
|
|
113
|
+
const sortedEndpoints = [...endpoints].filter((e) => e.method === normalizedMethod).sort((a, b) => getPathSpecificity(b.path) - getPathSpecificity(a.path));
|
|
114
|
+
for (const endpoint of sortedEndpoints) {
|
|
115
|
+
const pattern = endpoint.path.replace(/\{(\w+)\}/g, "([^/]+)");
|
|
116
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
117
|
+
const match = path.match(regex);
|
|
118
|
+
if (match) {
|
|
119
|
+
const pathParams = {};
|
|
120
|
+
const paramNames = endpoint.path.match(/\{(\w+)\}/g) || [];
|
|
121
|
+
paramNames.forEach((param, index) => {
|
|
122
|
+
const paramName = param.slice(1, -1);
|
|
123
|
+
pathParams[paramName] = match[index + 1] || "";
|
|
124
|
+
});
|
|
125
|
+
return { endpoint, pathParams };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function handleClientPackageRequest(pkg, generator, cursorManager, pageManager, path, method, query, _responseFormat = "auto") {
|
|
131
|
+
const match = findMatchingEndpoint(pkg.endpoints, path, method);
|
|
132
|
+
if (!match) {
|
|
133
|
+
return {
|
|
134
|
+
statusCode: 404,
|
|
135
|
+
body: { error: "Not found", message: `No matching endpoint for ${method} ${path}` }
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const { endpoint, pathParams } = match;
|
|
139
|
+
const primitiveTypes = ["object", "string", "number", "boolean", "void", "any", "unknown"];
|
|
140
|
+
if (primitiveTypes.includes(endpoint.responseType.toLowerCase())) {
|
|
141
|
+
const pathLower = path.toLowerCase();
|
|
142
|
+
let primitiveResponse = {};
|
|
143
|
+
if (pathLower.includes("health")) {
|
|
144
|
+
primitiveResponse = { status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
145
|
+
} else if (pathLower.includes("ping")) {
|
|
146
|
+
primitiveResponse = { pong: true };
|
|
147
|
+
} else if (endpoint.responseType === "string") {
|
|
148
|
+
primitiveResponse = "success";
|
|
149
|
+
} else if (endpoint.responseType === "number") {
|
|
150
|
+
primitiveResponse = 0;
|
|
151
|
+
} else if (endpoint.responseType === "boolean") {
|
|
152
|
+
primitiveResponse = true;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
statusCode: 200,
|
|
156
|
+
body: primitiveResponse,
|
|
157
|
+
meta: {
|
|
158
|
+
operationId: endpoint.operationId,
|
|
159
|
+
apiClass: endpoint.apiClassName,
|
|
160
|
+
responseType: endpoint.responseType
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const typeInfo = extractDataModelName(endpoint.responseType, pkg.models);
|
|
165
|
+
const { modelName, isList, listFieldName, wrapperType } = typeInfo;
|
|
166
|
+
const page = Number(query.page) || 1;
|
|
167
|
+
const limit = Number(query.limit) || Number(query.size) || 20;
|
|
168
|
+
const cursor = query.cursor;
|
|
169
|
+
const wrapperSchema = wrapperType ? pkg.models.get(wrapperType) : null;
|
|
170
|
+
const hasItemsField = listFieldName === "items";
|
|
171
|
+
const hasPaginationFields = wrapperSchema?.fields.some(
|
|
172
|
+
(f) => ["page", "totalPages", "total", "totalItems", "pagination"].includes(f.name)
|
|
173
|
+
);
|
|
174
|
+
let responseData;
|
|
175
|
+
if (isList) {
|
|
176
|
+
const seed = `${endpoint.path}-${JSON.stringify(pathParams)}`;
|
|
177
|
+
if (hasItemsField && hasPaginationFields) {
|
|
178
|
+
if (cursor) {
|
|
179
|
+
const result = cursorManager.getCursorPage(modelName, {
|
|
180
|
+
cursor,
|
|
181
|
+
limit,
|
|
182
|
+
total: 100,
|
|
183
|
+
seed
|
|
184
|
+
});
|
|
185
|
+
const { _snapshotId: _, ...responseWithoutSnapshotId } = result;
|
|
186
|
+
responseData = responseWithoutSnapshotId;
|
|
187
|
+
} else {
|
|
188
|
+
const result = pageManager.getPagedResponse(modelName, {
|
|
189
|
+
page,
|
|
190
|
+
limit,
|
|
191
|
+
total: 100,
|
|
192
|
+
seed
|
|
193
|
+
});
|
|
194
|
+
const { _snapshotId: _, ...responseWithoutSnapshotId } = result;
|
|
195
|
+
responseData = responseWithoutSnapshotId;
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
if (cursor) {
|
|
199
|
+
const result = cursorManager.getCursorPage(modelName, {
|
|
200
|
+
cursor,
|
|
201
|
+
limit,
|
|
202
|
+
total: 100,
|
|
203
|
+
seed
|
|
204
|
+
});
|
|
205
|
+
if (listFieldName) {
|
|
206
|
+
const listField = wrapperSchema?.fields.find((f) => f.name === listFieldName);
|
|
207
|
+
const listJsonKey = listField?.jsonKey || listFieldName;
|
|
208
|
+
const otherFields = {};
|
|
209
|
+
if (wrapperSchema) {
|
|
210
|
+
for (const field of wrapperSchema.fields) {
|
|
211
|
+
if (field.name !== listFieldName) {
|
|
212
|
+
const outputKey = field.jsonKey || field.name;
|
|
213
|
+
if (field.name === "nextCursor" || field.name === "cursor") {
|
|
214
|
+
otherFields[outputKey] = result.nextCursor;
|
|
215
|
+
} else if (field.name === "prevCursor") {
|
|
216
|
+
otherFields[outputKey] = result.prevCursor;
|
|
217
|
+
} else if (field.name === "hasMore") {
|
|
218
|
+
otherFields[outputKey] = result.hasMore;
|
|
219
|
+
} else if (field.name === "total" || field.name === "totalItems") {
|
|
220
|
+
otherFields[outputKey] = 100;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
responseData = {
|
|
226
|
+
[listJsonKey]: result.items,
|
|
227
|
+
...otherFields
|
|
228
|
+
};
|
|
229
|
+
} else {
|
|
230
|
+
responseData = result.items;
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
const result = cursorManager.getCursorPage(modelName, {
|
|
234
|
+
limit,
|
|
235
|
+
total: 100,
|
|
236
|
+
seed
|
|
237
|
+
});
|
|
238
|
+
if (listFieldName) {
|
|
239
|
+
const listField = wrapperSchema?.fields.find((f) => f.name === listFieldName);
|
|
240
|
+
const listJsonKey = listField?.jsonKey || listFieldName;
|
|
241
|
+
const otherFields = {};
|
|
242
|
+
if (wrapperSchema) {
|
|
243
|
+
for (const field of wrapperSchema.fields) {
|
|
244
|
+
if (field.name !== listFieldName) {
|
|
245
|
+
const outputKey = field.jsonKey || field.name;
|
|
246
|
+
if (field.name === "nextCursor" || field.name === "cursor") {
|
|
247
|
+
otherFields[outputKey] = result.nextCursor;
|
|
248
|
+
} else if (field.name === "prevCursor") {
|
|
249
|
+
otherFields[outputKey] = result.prevCursor;
|
|
250
|
+
} else if (field.name === "hasMore") {
|
|
251
|
+
otherFields[outputKey] = result.hasMore;
|
|
252
|
+
} else if (field.name === "total" || field.name === "totalItems") {
|
|
253
|
+
otherFields[outputKey] = 100;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
responseData = {
|
|
259
|
+
[listJsonKey]: result.items,
|
|
260
|
+
...otherFields
|
|
261
|
+
};
|
|
262
|
+
} else {
|
|
263
|
+
responseData = result.items;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
const seed = `${endpoint.operationId}-${JSON.stringify(pathParams)}`;
|
|
269
|
+
responseData = generator.generateOne(endpoint.responseType, seed);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
statusCode: 200,
|
|
273
|
+
body: responseData,
|
|
274
|
+
meta: {
|
|
275
|
+
operationId: endpoint.operationId,
|
|
276
|
+
apiClass: endpoint.apiClassName,
|
|
277
|
+
responseType: endpoint.responseType
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export default defineEventHandler(async (event) => {
|
|
282
|
+
const config = useRuntimeConfig(event);
|
|
283
|
+
const mockConfig = config.mock;
|
|
284
|
+
const requestUrl = getRequestURL(event);
|
|
285
|
+
const prefix = mockConfig?.prefix || "/mock";
|
|
286
|
+
let path = requestUrl.pathname;
|
|
287
|
+
if (path.startsWith(prefix)) {
|
|
288
|
+
path = path.substring(prefix.length) || "/";
|
|
289
|
+
}
|
|
290
|
+
const query = getQuery(event);
|
|
291
|
+
let body;
|
|
292
|
+
if (event.method !== "GET" && event.method !== "HEAD") {
|
|
293
|
+
try {
|
|
294
|
+
body = await readBody(event);
|
|
295
|
+
} catch {
|
|
296
|
+
body = void 0;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (mockConfig?.clientPackagePath) {
|
|
300
|
+
const { package: pkg, generator, cursorManager, pageManager } = getClientPackageData(
|
|
301
|
+
mockConfig.clientPackagePath,
|
|
302
|
+
mockConfig.clientPackageConfig,
|
|
303
|
+
mockConfig.pagination,
|
|
304
|
+
mockConfig.cursor
|
|
305
|
+
);
|
|
306
|
+
const result = handleClientPackageRequest(
|
|
307
|
+
pkg,
|
|
308
|
+
generator,
|
|
309
|
+
cursorManager,
|
|
310
|
+
pageManager,
|
|
311
|
+
path,
|
|
312
|
+
event.method,
|
|
313
|
+
query,
|
|
314
|
+
mockConfig.responseFormat ?? "auto"
|
|
315
|
+
);
|
|
316
|
+
if (result.statusCode) {
|
|
317
|
+
event.node.res.statusCode = result.statusCode;
|
|
318
|
+
}
|
|
319
|
+
return result.body;
|
|
320
|
+
}
|
|
321
|
+
if (mockConfig?.openapiPath) {
|
|
322
|
+
const backend = await getOpenAPIBackend(mockConfig.openapiPath);
|
|
323
|
+
const headers = {};
|
|
324
|
+
const rawHeaders = event.headers;
|
|
325
|
+
if (rawHeaders) {
|
|
326
|
+
for (const [key, value] of Object.entries(rawHeaders)) {
|
|
327
|
+
if (value) headers[key] = String(value);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const result = await backend.handleRequest({
|
|
331
|
+
method: event.method,
|
|
332
|
+
path,
|
|
333
|
+
query,
|
|
334
|
+
body,
|
|
335
|
+
headers
|
|
336
|
+
});
|
|
337
|
+
if (result?.statusCode) {
|
|
338
|
+
event.node.res.statusCode = result.statusCode;
|
|
339
|
+
}
|
|
340
|
+
return result?.body ?? result;
|
|
341
|
+
}
|
|
342
|
+
throw createError({
|
|
343
|
+
statusCode: 500,
|
|
344
|
+
message: "OpenAPI configuration not found. Set openapi path or client package."
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { defineEventHandler, readBody, getRouterParams, createError } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import * as protoLoader from "@grpc/proto-loader";
|
|
4
|
+
import * as grpc from "@grpc/grpc-js";
|
|
5
|
+
import { readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { join, extname } from "pathe";
|
|
7
|
+
import { generateMockMessage, deriveSeedFromRequest } from "../utils/mock/index.js";
|
|
8
|
+
let protoCache = null;
|
|
9
|
+
let cachedProtoPath = null;
|
|
10
|
+
function findProtoFiles(dirPath) {
|
|
11
|
+
const files = [];
|
|
12
|
+
const stat = statSync(dirPath);
|
|
13
|
+
if (stat.isFile() && extname(dirPath) === ".proto") {
|
|
14
|
+
return [dirPath];
|
|
15
|
+
}
|
|
16
|
+
if (stat.isDirectory()) {
|
|
17
|
+
const entries = readdirSync(dirPath);
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const fullPath = join(dirPath, entry);
|
|
20
|
+
const entryStat = statSync(fullPath);
|
|
21
|
+
if (entryStat.isFile() && extname(entry) === ".proto") {
|
|
22
|
+
files.push(fullPath);
|
|
23
|
+
} else if (entryStat.isDirectory()) {
|
|
24
|
+
files.push(...findProtoFiles(fullPath));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
function extractServices(obj, services, prefix = "") {
|
|
31
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
32
|
+
const fullName = prefix ? `${prefix}.${key}` : key;
|
|
33
|
+
if (typeof value === "function" && "service" in value) {
|
|
34
|
+
const serviceConstructor = value;
|
|
35
|
+
services.set(key, serviceConstructor.service);
|
|
36
|
+
services.set(fullName, serviceConstructor.service);
|
|
37
|
+
} else if (typeof value === "object" && value !== null) {
|
|
38
|
+
extractServices(value, services, fullName);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function loadProto(protoPath) {
|
|
43
|
+
if (protoCache && cachedProtoPath === protoPath) {
|
|
44
|
+
return protoCache;
|
|
45
|
+
}
|
|
46
|
+
const protoFiles = findProtoFiles(protoPath);
|
|
47
|
+
if (protoFiles.length === 0) {
|
|
48
|
+
throw new Error(`No .proto files found in ${protoPath}`);
|
|
49
|
+
}
|
|
50
|
+
const packageDefinition = await protoLoader.load(protoFiles, {
|
|
51
|
+
keepCase: true,
|
|
52
|
+
longs: String,
|
|
53
|
+
enums: String,
|
|
54
|
+
defaults: true,
|
|
55
|
+
oneofs: true,
|
|
56
|
+
includeDirs: [protoPath]
|
|
57
|
+
});
|
|
58
|
+
const grpcObject = grpc.loadPackageDefinition(packageDefinition);
|
|
59
|
+
const services = /* @__PURE__ */ new Map();
|
|
60
|
+
extractServices(grpcObject, services);
|
|
61
|
+
protoCache = {
|
|
62
|
+
packageDefinition,
|
|
63
|
+
grpcObject,
|
|
64
|
+
services
|
|
65
|
+
};
|
|
66
|
+
cachedProtoPath = protoPath;
|
|
67
|
+
return protoCache;
|
|
68
|
+
}
|
|
69
|
+
function getResponseTypeInfo(methodDef) {
|
|
70
|
+
const responseType = methodDef.responseType;
|
|
71
|
+
if (responseType?.type) {
|
|
72
|
+
return responseType.type;
|
|
73
|
+
}
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
export default defineEventHandler(async (event) => {
|
|
77
|
+
const config = useRuntimeConfig(event);
|
|
78
|
+
const mockConfig = config.mock;
|
|
79
|
+
if (!mockConfig?.protoPath) {
|
|
80
|
+
throw createError({
|
|
81
|
+
statusCode: 500,
|
|
82
|
+
message: "Proto path not configured"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const params = getRouterParams(event);
|
|
86
|
+
const serviceName = params.service;
|
|
87
|
+
const methodName = params.method;
|
|
88
|
+
if (!serviceName || !methodName) {
|
|
89
|
+
throw createError({
|
|
90
|
+
statusCode: 400,
|
|
91
|
+
message: "Service and method are required"
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
let cache;
|
|
95
|
+
try {
|
|
96
|
+
cache = await loadProto(mockConfig.protoPath);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw createError({
|
|
99
|
+
statusCode: 500,
|
|
100
|
+
message: `Failed to load proto files: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const serviceDefinition = cache.services.get(serviceName);
|
|
104
|
+
if (!serviceDefinition) {
|
|
105
|
+
const availableServices = Array.from(cache.services.keys()).filter((k) => !k.includes("."));
|
|
106
|
+
throw createError({
|
|
107
|
+
statusCode: 404,
|
|
108
|
+
message: `Service '${serviceName}' not found. Available services: ${availableServices.join(", ")}`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const methodDef = serviceDefinition[methodName];
|
|
112
|
+
if (!methodDef) {
|
|
113
|
+
const availableMethods = Object.keys(serviceDefinition);
|
|
114
|
+
throw createError({
|
|
115
|
+
statusCode: 404,
|
|
116
|
+
message: `Method '${methodName}' not found in service '${serviceName}'. Available methods: ${availableMethods.join(", ")}`
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (methodDef.requestStream || methodDef.responseStream) {
|
|
120
|
+
throw createError({
|
|
121
|
+
statusCode: 501,
|
|
122
|
+
message: "Streaming methods are not supported. Only unary RPC is available."
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
let requestBody;
|
|
126
|
+
try {
|
|
127
|
+
requestBody = await readBody(event);
|
|
128
|
+
} catch {
|
|
129
|
+
requestBody = {};
|
|
130
|
+
}
|
|
131
|
+
const responseTypeInfo = getResponseTypeInfo(methodDef);
|
|
132
|
+
const seed = deriveSeedFromRequest(requestBody);
|
|
133
|
+
const mockResponse = generateMockMessage(responseTypeInfo, seed);
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
service: serviceName,
|
|
137
|
+
method: methodName,
|
|
138
|
+
data: mockResponse
|
|
139
|
+
};
|
|
140
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { defineEventHandler, createError } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
let cachedSchema = null;
|
|
5
|
+
function parseOpenApiSpec(specPath) {
|
|
6
|
+
if (!existsSync(specPath)) {
|
|
7
|
+
return void 0;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const content = readFileSync(specPath, "utf-8");
|
|
11
|
+
let spec;
|
|
12
|
+
if (specPath.endsWith(".yaml") || specPath.endsWith(".yml")) {
|
|
13
|
+
const yaml = require("js-yaml");
|
|
14
|
+
spec = yaml.load(content);
|
|
15
|
+
} else {
|
|
16
|
+
spec = JSON.parse(content);
|
|
17
|
+
}
|
|
18
|
+
const info = spec.info || {};
|
|
19
|
+
const paths = spec.paths || {};
|
|
20
|
+
const pathItems = [];
|
|
21
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
22
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
23
|
+
if (["get", "post", "put", "delete", "patch"].includes(method.toLowerCase())) {
|
|
24
|
+
const op = operation;
|
|
25
|
+
const parameters = [];
|
|
26
|
+
if (Array.isArray(op.parameters)) {
|
|
27
|
+
for (const param of op.parameters) {
|
|
28
|
+
const p = param;
|
|
29
|
+
parameters.push({
|
|
30
|
+
name: p.name,
|
|
31
|
+
in: p.in,
|
|
32
|
+
required: p.required,
|
|
33
|
+
description: p.description,
|
|
34
|
+
schema: p.schema
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
pathItems.push({
|
|
39
|
+
path,
|
|
40
|
+
method: method.toUpperCase(),
|
|
41
|
+
operationId: op.operationId,
|
|
42
|
+
summary: op.summary,
|
|
43
|
+
description: op.description,
|
|
44
|
+
tags: op.tags,
|
|
45
|
+
parameters: parameters.length > 0 ? parameters : void 0,
|
|
46
|
+
requestBody: op.requestBody,
|
|
47
|
+
responses: op.responses
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
info: {
|
|
54
|
+
title: info.title || "Unknown API",
|
|
55
|
+
version: info.version || "1.0.0",
|
|
56
|
+
description: info.description
|
|
57
|
+
},
|
|
58
|
+
paths: pathItems
|
|
59
|
+
};
|
|
60
|
+
} catch {
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function parseProtoSpec(protoPath) {
|
|
65
|
+
if (!existsSync(protoPath)) {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const protoLoader = await import("@grpc/proto-loader");
|
|
70
|
+
const packageDefinition = await protoLoader.load(protoPath, {
|
|
71
|
+
keepCase: false,
|
|
72
|
+
longs: String,
|
|
73
|
+
enums: String,
|
|
74
|
+
defaults: true,
|
|
75
|
+
oneofs: true
|
|
76
|
+
});
|
|
77
|
+
const services = [];
|
|
78
|
+
let packageName;
|
|
79
|
+
for (const [fullName, def] of Object.entries(packageDefinition)) {
|
|
80
|
+
const definition = def;
|
|
81
|
+
if (!packageName && fullName.includes(".")) {
|
|
82
|
+
packageName = fullName.split(".").slice(0, -1).join(".");
|
|
83
|
+
}
|
|
84
|
+
if (!definition.format && typeof definition === "object") {
|
|
85
|
+
const methods = [];
|
|
86
|
+
for (const [methodName, methodDef] of Object.entries(definition)) {
|
|
87
|
+
const method = methodDef;
|
|
88
|
+
if (method.requestType && method.responseType) {
|
|
89
|
+
const reqType = method.requestType;
|
|
90
|
+
const resType = method.responseType;
|
|
91
|
+
const requestFields = extractFields(reqType);
|
|
92
|
+
const responseFields = extractFields(resType);
|
|
93
|
+
methods.push({
|
|
94
|
+
name: methodName,
|
|
95
|
+
requestType: reqType.name || "Unknown",
|
|
96
|
+
responseType: resType.name || "Unknown",
|
|
97
|
+
requestFields: requestFields.length > 0 ? requestFields : void 0,
|
|
98
|
+
responseFields: responseFields.length > 0 ? responseFields : void 0
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (methods.length > 0) {
|
|
103
|
+
const serviceName = fullName.split(".").pop() || fullName;
|
|
104
|
+
services.push({
|
|
105
|
+
name: serviceName,
|
|
106
|
+
methods
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
package: packageName,
|
|
113
|
+
services
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function extractFields(messageType) {
|
|
120
|
+
const fields = [];
|
|
121
|
+
const typeObj = messageType.type;
|
|
122
|
+
if (typeObj && typeObj.field && Array.isArray(typeObj.field)) {
|
|
123
|
+
for (const field of typeObj.field) {
|
|
124
|
+
const f = field;
|
|
125
|
+
fields.push({
|
|
126
|
+
name: f.name,
|
|
127
|
+
type: getProtoTypeName(f.type, f.typeName),
|
|
128
|
+
repeated: f.label === 3,
|
|
129
|
+
// LABEL_REPEATED = 3
|
|
130
|
+
optional: f.label === 1
|
|
131
|
+
// LABEL_OPTIONAL = 1
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return fields;
|
|
136
|
+
}
|
|
137
|
+
function getProtoTypeName(typeNum, typeName) {
|
|
138
|
+
const typeMap = {
|
|
139
|
+
1: "double",
|
|
140
|
+
2: "float",
|
|
141
|
+
3: "int64",
|
|
142
|
+
4: "uint64",
|
|
143
|
+
5: "int32",
|
|
144
|
+
6: "fixed64",
|
|
145
|
+
7: "fixed32",
|
|
146
|
+
8: "bool",
|
|
147
|
+
9: "string",
|
|
148
|
+
10: "group",
|
|
149
|
+
11: "message",
|
|
150
|
+
12: "bytes",
|
|
151
|
+
13: "uint32",
|
|
152
|
+
14: "enum",
|
|
153
|
+
15: "sfixed32",
|
|
154
|
+
16: "sfixed64",
|
|
155
|
+
17: "sint32",
|
|
156
|
+
18: "sint64"
|
|
157
|
+
};
|
|
158
|
+
if (typeName) {
|
|
159
|
+
return typeName.split(".").pop() || typeName;
|
|
160
|
+
}
|
|
161
|
+
return typeMap[typeNum] || "unknown";
|
|
162
|
+
}
|
|
163
|
+
export default defineEventHandler(async () => {
|
|
164
|
+
const config = useRuntimeConfig();
|
|
165
|
+
const mockConfig = config.mock;
|
|
166
|
+
if (cachedSchema) {
|
|
167
|
+
return cachedSchema;
|
|
168
|
+
}
|
|
169
|
+
const schema = {};
|
|
170
|
+
if (mockConfig.openapiPath) {
|
|
171
|
+
const openapi = parseOpenApiSpec(mockConfig.openapiPath);
|
|
172
|
+
if (openapi) {
|
|
173
|
+
schema.openapi = openapi;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (mockConfig.protoPath) {
|
|
177
|
+
const rpc = await parseProtoSpec(mockConfig.protoPath);
|
|
178
|
+
if (rpc) {
|
|
179
|
+
schema.rpc = rpc;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!schema.openapi && !schema.rpc) {
|
|
183
|
+
throw createError({
|
|
184
|
+
statusCode: 404,
|
|
185
|
+
message: "No API schema available"
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
cachedSchema = schema;
|
|
189
|
+
return schema;
|
|
190
|
+
});
|