@scalar/mock-server 0.9.9 → 0.9.11
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/CHANGELOG.md +8 -0
- package/dist/create-mock-server.js +79 -68
- package/dist/index.js +1 -5
- package/dist/libs/store.js +66 -65
- package/dist/routes/mock-any-response.js +70 -63
- package/dist/routes/mock-handler-response.js +140 -100
- package/dist/routes/respond-with-authorize-page.js +89 -80
- package/dist/routes/respond-with-openapi-document.js +32 -35
- package/dist/routes/respond-with-token.js +40 -45
- package/dist/types.js +2 -5
- package/dist/utils/build-handler-context.js +83 -70
- package/dist/utils/build-seed-context.js +59 -52
- package/dist/utils/create-openapi-definition.js +7 -10
- package/dist/utils/execute-handler.js +16 -18
- package/dist/utils/execute-seed.js +22 -21
- package/dist/utils/find-preferred-response-key.js +9 -9
- package/dist/utils/get-open-auth-token-urls.js +45 -35
- package/dist/utils/get-operation.js +12 -12
- package/dist/utils/handle-authentication.js +106 -101
- package/dist/utils/hono-route-from-path.js +6 -6
- package/dist/utils/is-authentication-required.js +17 -15
- package/dist/utils/log-authentication-instructions.js +105 -110
- package/dist/utils/process-openapi-document.js +71 -58
- package/dist/utils/set-up-authentication-routes.js +80 -77
- package/dist/utils/store-wrapper.js +40 -39
- package/package.json +10 -16
- package/dist/create-mock-server.js.map +0 -7
- package/dist/index.js.map +0 -7
- package/dist/libs/store.js.map +0 -7
- package/dist/routes/mock-any-response.js.map +0 -7
- package/dist/routes/mock-handler-response.js.map +0 -7
- package/dist/routes/respond-with-authorize-page.js.map +0 -7
- package/dist/routes/respond-with-openapi-document.js.map +0 -7
- package/dist/routes/respond-with-token.js.map +0 -7
- package/dist/types.js.map +0 -7
- package/dist/utils/build-handler-context.js.map +0 -7
- package/dist/utils/build-seed-context.js.map +0 -7
- package/dist/utils/create-openapi-definition.js.map +0 -7
- package/dist/utils/execute-handler.js.map +0 -7
- package/dist/utils/execute-seed.js.map +0 -7
- package/dist/utils/find-preferred-response-key.js.map +0 -7
- package/dist/utils/get-open-auth-token-urls.js.map +0 -7
- package/dist/utils/get-operation.js.map +0 -7
- package/dist/utils/handle-authentication.js.map +0 -7
- package/dist/utils/hono-route-from-path.js.map +0 -7
- package/dist/utils/is-authentication-required.js.map +0 -7
- package/dist/utils/log-authentication-instructions.js.map +0 -7
- package/dist/utils/process-openapi-document.js.map +0 -7
- package/dist/utils/set-up-authentication-routes.js.map +0 -7
- package/dist/utils/store-wrapper.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,72 +1,83 @@
|
|
|
1
|
-
import { Hono } from
|
|
2
|
-
import { cors } from
|
|
3
|
-
import { buildSeedContext } from
|
|
4
|
-
import { executeSeed } from
|
|
5
|
-
import { getOperations } from
|
|
6
|
-
import { handleAuthentication } from
|
|
7
|
-
import { honoRouteFromPath } from
|
|
8
|
-
import { isAuthenticationRequired } from
|
|
9
|
-
import { logAuthenticationInstructions } from
|
|
10
|
-
import { processOpenApiDocument } from
|
|
11
|
-
import { setUpAuthenticationRoutes } from
|
|
12
|
-
import { store } from
|
|
13
|
-
import { mockAnyResponse } from
|
|
14
|
-
import { mockHandlerResponse } from
|
|
15
|
-
import { respondWithOpenApiDocument } from
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { cors } from 'hono/cors';
|
|
3
|
+
import { buildSeedContext } from './utils/build-seed-context.js';
|
|
4
|
+
import { executeSeed } from './utils/execute-seed.js';
|
|
5
|
+
import { getOperations } from './utils/get-operation.js';
|
|
6
|
+
import { handleAuthentication } from './utils/handle-authentication.js';
|
|
7
|
+
import { honoRouteFromPath } from './utils/hono-route-from-path.js';
|
|
8
|
+
import { isAuthenticationRequired } from './utils/is-authentication-required.js';
|
|
9
|
+
import { logAuthenticationInstructions } from './utils/log-authentication-instructions.js';
|
|
10
|
+
import { processOpenApiDocument } from './utils/process-openapi-document.js';
|
|
11
|
+
import { setUpAuthenticationRoutes } from './utils/set-up-authentication-routes.js';
|
|
12
|
+
import { store } from './libs/store.js';
|
|
13
|
+
import { mockAnyResponse } from './routes/mock-any-response.js';
|
|
14
|
+
import { mockHandlerResponse } from './routes/mock-handler-response.js';
|
|
15
|
+
import { respondWithOpenApiDocument } from './routes/respond-with-openapi-document.js';
|
|
16
|
+
/**
|
|
17
|
+
* Create a mock server instance
|
|
18
|
+
*/
|
|
19
|
+
export async function createMockServer(configuration) {
|
|
20
|
+
const app = new Hono();
|
|
21
|
+
/** Dereferenced OpenAPI document */
|
|
22
|
+
const schema = await processOpenApiDocument(configuration?.document ?? configuration?.specification);
|
|
23
|
+
// Seed data from schemas with x-seed extension
|
|
24
|
+
// This happens before routes are set up so data is available immediately
|
|
25
|
+
const schemas = schema?.components?.schemas;
|
|
26
|
+
if (schemas) {
|
|
27
|
+
for (const [schemaName, schemaObject] of Object.entries(schemas)) {
|
|
28
|
+
const seedCode = schemaObject?.['x-seed'];
|
|
29
|
+
if (seedCode && typeof seedCode === 'string') {
|
|
30
|
+
try {
|
|
31
|
+
// Check if collection is empty (idempotent seeding)
|
|
32
|
+
// Use the schema key directly as the collection name
|
|
33
|
+
const existingItems = store.list(schemaName);
|
|
34
|
+
if (existingItems.length === 0) {
|
|
35
|
+
// Build seed context with schema key (used as collection name)
|
|
36
|
+
const seedContext = buildSeedContext(schemaName);
|
|
37
|
+
// Execute seed code
|
|
38
|
+
await executeSeed(seedCode, seedContext);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// Log error but don't fail server startup
|
|
43
|
+
console.error(`Error seeding schema "${schemaName}":`, error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
32
46
|
}
|
|
33
|
-
}
|
|
34
47
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
schema?.components?.securitySchemes || {}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
// CORS headers
|
|
49
|
+
app.use(cors());
|
|
50
|
+
/** Authentication methods defined in the OpenAPI document */
|
|
51
|
+
setUpAuthenticationRoutes(app, schema);
|
|
52
|
+
logAuthenticationInstructions(schema?.components?.securitySchemes || {});
|
|
53
|
+
/** Paths specified in the OpenAPI document */
|
|
54
|
+
const paths = schema?.paths ?? {};
|
|
55
|
+
Object.keys(paths).forEach((path) => {
|
|
56
|
+
const methods = Object.keys(getOperations(paths[path]));
|
|
57
|
+
/** Keys for all operations of a specified path */
|
|
58
|
+
methods.forEach((method) => {
|
|
59
|
+
const route = honoRouteFromPath(path);
|
|
60
|
+
const operation = schema?.paths?.[path]?.[method];
|
|
61
|
+
// Check if authentication is required for this operation
|
|
62
|
+
if (isAuthenticationRequired(operation.security)) {
|
|
63
|
+
app[method](route, handleAuthentication(schema, operation));
|
|
64
|
+
}
|
|
65
|
+
// Check if operation has x-handler extension
|
|
66
|
+
// Validate that it's a non-empty string (consistent with x-seed validation)
|
|
67
|
+
const handlerCode = operation?.['x-handler'];
|
|
68
|
+
const hasHandler = handlerCode && typeof handlerCode === 'string' && handlerCode.trim().length > 0;
|
|
69
|
+
// Route to appropriate handler
|
|
70
|
+
if (hasHandler) {
|
|
71
|
+
app[method](route, (c) => mockHandlerResponse(c, operation, configuration));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
app[method](route, (c) => mockAnyResponse(c, operation, configuration));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
57
77
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
(c) => respondWithOpenApiDocument(c, configuration?.document ?? configuration?.specification,
|
|
62
|
-
|
|
63
|
-
app.get(
|
|
64
|
-
"/openapi.yaml",
|
|
65
|
-
(c) => respondWithOpenApiDocument(c, configuration?.document ?? configuration?.specification, "yaml")
|
|
66
|
-
);
|
|
67
|
-
return app;
|
|
78
|
+
// OpenAPI JSON file
|
|
79
|
+
app.get('/openapi.json', (c) => respondWithOpenApiDocument(c, configuration?.document ?? configuration?.specification, 'json'));
|
|
80
|
+
// OpenAPI YAML file
|
|
81
|
+
app.get('/openapi.yaml', (c) => respondWithOpenApiDocument(c, configuration?.document ?? configuration?.specification, 'yaml'));
|
|
82
|
+
return app;
|
|
68
83
|
}
|
|
69
|
-
export {
|
|
70
|
-
createMockServer
|
|
71
|
-
};
|
|
72
|
-
//# sourceMappingURL=create-mock-server.js.map
|
package/dist/index.js
CHANGED
package/dist/libs/store.js
CHANGED
|
@@ -1,70 +1,71 @@
|
|
|
1
|
-
class Store {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Get a single item by ID.
|
|
12
|
-
*/
|
|
13
|
-
get(collection, id) {
|
|
14
|
-
return this.data[collection]?.[id];
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Create a new item in a collection.
|
|
18
|
-
* Auto-generates an ID if not provided.
|
|
19
|
-
*/
|
|
20
|
-
create(collection, data) {
|
|
21
|
-
if (!this.data[collection]) {
|
|
22
|
-
this.data[collection] = {};
|
|
1
|
+
export class Store {
|
|
2
|
+
data = {};
|
|
3
|
+
/**
|
|
4
|
+
* Get all items in a collection.
|
|
5
|
+
*/
|
|
6
|
+
list(collection) {
|
|
7
|
+
const items = this.data[collection];
|
|
8
|
+
return items ? Object.values(items) : [];
|
|
23
9
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Update an existing item in a collection.
|
|
32
|
-
* Returns null if the item is not found.
|
|
33
|
-
*/
|
|
34
|
-
update(collection, id, data) {
|
|
35
|
-
if (!this.data[collection]?.[id]) {
|
|
36
|
-
return null;
|
|
10
|
+
/**
|
|
11
|
+
* Get a single item by ID.
|
|
12
|
+
*/
|
|
13
|
+
get(collection, id) {
|
|
14
|
+
return this.data[collection]?.[id];
|
|
37
15
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Create a new item in a collection.
|
|
18
|
+
* Auto-generates an ID if not provided.
|
|
19
|
+
*/
|
|
20
|
+
create(collection, data) {
|
|
21
|
+
if (!this.data[collection]) {
|
|
22
|
+
this.data[collection] = {};
|
|
23
|
+
}
|
|
24
|
+
// Handle null/undefined data by defaulting to empty object
|
|
25
|
+
const safeData = data ?? {};
|
|
26
|
+
const id = safeData.id ?? crypto.randomUUID();
|
|
27
|
+
const item = { ...safeData, id };
|
|
28
|
+
this.data[collection][id] = item;
|
|
29
|
+
return item;
|
|
50
30
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Update an existing item in a collection.
|
|
33
|
+
* Returns null if the item is not found.
|
|
34
|
+
*/
|
|
35
|
+
update(collection, id, data) {
|
|
36
|
+
if (!this.data[collection]?.[id]) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
// Handle null/undefined data by defaulting to empty object
|
|
40
|
+
const safeData = data ?? {};
|
|
41
|
+
const updated = { ...this.data[collection][id], ...safeData, id };
|
|
42
|
+
this.data[collection][id] = updated;
|
|
43
|
+
return updated;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Delete an item from a collection.
|
|
47
|
+
* Returns null if the item is not found.
|
|
48
|
+
*/
|
|
49
|
+
delete(collection, id) {
|
|
50
|
+
if (!this.data[collection]?.[id]) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
delete this.data[collection][id];
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Clear a specific collection or all collections.
|
|
58
|
+
*/
|
|
59
|
+
clear(collection) {
|
|
60
|
+
if (collection) {
|
|
61
|
+
delete this.data[collection];
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.data = {};
|
|
65
|
+
}
|
|
62
66
|
}
|
|
63
|
-
}
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
};
|
|
70
|
-
//# sourceMappingURL=store.js.map
|
|
68
|
+
/**
|
|
69
|
+
* Singleton store instance shared across all requests.
|
|
70
|
+
*/
|
|
71
|
+
export const store = new Store();
|
|
@@ -1,67 +1,74 @@
|
|
|
1
|
-
import { json2xml } from
|
|
2
|
-
import { getExampleFromSchema } from
|
|
3
|
-
import { accepts } from
|
|
4
|
-
import { findPreferredResponseKey } from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { json2xml } from '@scalar/helpers/file/json2xml';
|
|
2
|
+
import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
|
|
3
|
+
import { accepts } from 'hono/accepts';
|
|
4
|
+
import { findPreferredResponseKey } from '../utils/find-preferred-response-key.js';
|
|
5
|
+
/**
|
|
6
|
+
* Mock any response
|
|
7
|
+
*/
|
|
8
|
+
export function mockAnyResponse(c, operation, options) {
|
|
9
|
+
// Call onRequest callback
|
|
10
|
+
if (options?.onRequest) {
|
|
11
|
+
options.onRequest({
|
|
12
|
+
context: c,
|
|
13
|
+
operation,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
// Response
|
|
17
|
+
// default, 200, 201 …
|
|
18
|
+
const preferredResponseKey = findPreferredResponseKey(Object.keys(operation.responses ?? {}));
|
|
19
|
+
const preferredResponse = preferredResponseKey ? operation.responses?.[preferredResponseKey] : null;
|
|
20
|
+
if (!preferredResponse) {
|
|
21
|
+
c.status(500);
|
|
22
|
+
return c.json({ error: 'No response defined for this operation.' });
|
|
23
|
+
}
|
|
24
|
+
// Status code
|
|
25
|
+
const statusCode = Number.parseInt(preferredResponseKey === 'default' ? '200' : (preferredResponseKey ?? '200'), 10);
|
|
26
|
+
// Headers
|
|
27
|
+
const headers = preferredResponse?.headers ?? {};
|
|
28
|
+
Object.keys(headers).forEach((header) => {
|
|
29
|
+
const value = headers[header].schema ? getExampleFromSchema(headers[header].schema) : null;
|
|
30
|
+
if (value !== null) {
|
|
31
|
+
c.header(header, value);
|
|
32
|
+
}
|
|
10
33
|
});
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
c.status(500);
|
|
16
|
-
return c.json({ error: "No response defined for this operation." });
|
|
17
|
-
}
|
|
18
|
-
const statusCode = Number.parseInt(
|
|
19
|
-
preferredResponseKey === "default" ? "200" : preferredResponseKey ?? "200",
|
|
20
|
-
10
|
|
21
|
-
);
|
|
22
|
-
const headers = preferredResponse?.headers ?? {};
|
|
23
|
-
Object.keys(headers).forEach((header) => {
|
|
24
|
-
const value = headers[header].schema ? getExampleFromSchema(headers[header].schema) : null;
|
|
25
|
-
if (value !== null) {
|
|
26
|
-
c.header(header, value);
|
|
34
|
+
// For 204 No Content responses, we should not set Content-Type and should return null body
|
|
35
|
+
if (statusCode === 204) {
|
|
36
|
+
c.status(statusCode);
|
|
37
|
+
return c.body(null);
|
|
27
38
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
const supportedContentTypes = Object.keys(preferredResponse?.content ?? {});
|
|
40
|
+
// If no content types are defined, return the status with no body
|
|
41
|
+
if (supportedContentTypes.length === 0) {
|
|
42
|
+
c.status(statusCode);
|
|
43
|
+
return c.body(null);
|
|
44
|
+
}
|
|
45
|
+
// Content-Type
|
|
46
|
+
const acceptedContentType = accepts(c, {
|
|
47
|
+
header: 'Accept',
|
|
48
|
+
supports: supportedContentTypes,
|
|
49
|
+
default: supportedContentTypes.includes('application/json')
|
|
50
|
+
? 'application/json'
|
|
51
|
+
: (supportedContentTypes[0] ?? 'text/plain;charset=UTF-8'),
|
|
52
|
+
});
|
|
53
|
+
c.header('Content-Type', acceptedContentType);
|
|
54
|
+
const acceptedResponse = preferredResponse?.content?.[acceptedContentType];
|
|
55
|
+
// Body
|
|
56
|
+
const body = acceptedResponse?.example
|
|
57
|
+
? acceptedResponse.example
|
|
58
|
+
: acceptedResponse?.schema
|
|
59
|
+
? getExampleFromSchema(acceptedResponse.schema, {
|
|
60
|
+
emptyString: 'string',
|
|
61
|
+
variables: c.req.param(),
|
|
62
|
+
mode: 'read',
|
|
63
|
+
})
|
|
64
|
+
: null;
|
|
35
65
|
c.status(statusCode);
|
|
36
|
-
return c.body(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const acceptedResponse = preferredResponse?.content?.[acceptedContentType];
|
|
45
|
-
const body = acceptedResponse?.example ? acceptedResponse.example : acceptedResponse?.schema ? getExampleFromSchema(acceptedResponse.schema, {
|
|
46
|
-
emptyString: "string",
|
|
47
|
-
variables: c.req.param(),
|
|
48
|
-
mode: "read"
|
|
49
|
-
}) : null;
|
|
50
|
-
c.status(statusCode);
|
|
51
|
-
return c.body(
|
|
52
|
-
typeof body === "object" ? (
|
|
53
|
-
// XML
|
|
54
|
-
acceptedContentType?.includes("xml") ? json2xml(body) : (
|
|
55
|
-
// JSON
|
|
56
|
-
JSON.stringify(body, null, 2)
|
|
57
|
-
)
|
|
58
|
-
) : (
|
|
59
|
-
// String
|
|
60
|
-
body
|
|
61
|
-
)
|
|
62
|
-
);
|
|
66
|
+
return c.body(typeof body === 'object'
|
|
67
|
+
? // XML
|
|
68
|
+
acceptedContentType?.includes('xml')
|
|
69
|
+
? json2xml(body)
|
|
70
|
+
: // JSON
|
|
71
|
+
JSON.stringify(body, null, 2)
|
|
72
|
+
: // String
|
|
73
|
+
body);
|
|
63
74
|
}
|
|
64
|
-
export {
|
|
65
|
-
mockAnyResponse
|
|
66
|
-
};
|
|
67
|
-
//# sourceMappingURL=mock-any-response.js.map
|