@engjts/nexus 0.1.7 → 0.1.8
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/BENCHMARK_REPORT.md +343 -0
- package/dist/advanced/playground/generatePlaygroundHTML.d.ts.map +1 -1
- package/dist/advanced/playground/generatePlaygroundHTML.js +107 -0
- package/dist/advanced/playground/generatePlaygroundHTML.js.map +1 -1
- package/dist/advanced/playground/playground.d.ts +19 -0
- package/dist/advanced/playground/playground.d.ts.map +1 -1
- package/dist/advanced/playground/playground.js +70 -0
- package/dist/advanced/playground/playground.js.map +1 -1
- package/dist/advanced/playground/types.d.ts +20 -0
- package/dist/advanced/playground/types.d.ts.map +1 -1
- package/dist/core/application.d.ts +14 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/application.js +173 -71
- package/dist/core/application.js.map +1 -1
- package/dist/core/context-pool.d.ts +2 -13
- package/dist/core/context-pool.d.ts.map +1 -1
- package/dist/core/context-pool.js +7 -45
- package/dist/core/context-pool.js.map +1 -1
- package/dist/core/context.d.ts +108 -5
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +449 -53
- package/dist/core/context.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +9 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/middleware.d.ts +6 -0
- package/dist/core/middleware.d.ts.map +1 -1
- package/dist/core/middleware.js +83 -84
- package/dist/core/middleware.js.map +1 -1
- package/dist/core/performance/fast-json.d.ts +149 -0
- package/dist/core/performance/fast-json.d.ts.map +1 -0
- package/dist/core/performance/fast-json.js +473 -0
- package/dist/core/performance/fast-json.js.map +1 -0
- package/dist/core/router/file-router.d.ts +20 -7
- package/dist/core/router/file-router.d.ts.map +1 -1
- package/dist/core/router/file-router.js +41 -13
- package/dist/core/router/file-router.js.map +1 -1
- package/dist/core/router/index.d.ts +6 -0
- package/dist/core/router/index.d.ts.map +1 -1
- package/dist/core/router/index.js +33 -6
- package/dist/core/router/index.js.map +1 -1
- package/dist/core/router/radix-tree.d.ts +4 -1
- package/dist/core/router/radix-tree.d.ts.map +1 -1
- package/dist/core/router/radix-tree.js +7 -3
- package/dist/core/router/radix-tree.js.map +1 -1
- package/dist/core/serializer.d.ts +251 -0
- package/dist/core/serializer.d.ts.map +1 -0
- package/dist/core/serializer.js +290 -0
- package/dist/core/serializer.js.map +1 -0
- package/dist/core/types.d.ts +39 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/advanced/playground/generatePlaygroundHTML.ts +107 -0
- package/src/advanced/playground/playground.ts +225 -145
- package/src/advanced/playground/types.ts +29 -0
- package/src/core/application.ts +202 -84
- package/src/core/context-pool.ts +8 -56
- package/src/core/context.ts +497 -53
- package/src/core/index.ts +14 -0
- package/src/core/middleware.ts +99 -89
- package/src/core/router/file-router.ts +41 -12
- package/src/core/router/index.ts +213 -180
- package/src/core/router/radix-tree.ts +20 -4
- package/src/core/serializer.ts +397 -0
- package/src/core/types.ts +43 -1
- package/src/index.ts +17 -0
|
@@ -9,115 +9,195 @@ import { RouteConfig, HTTPMethod, Context, Plugin, SchemaConfig, RouteMeta } fro
|
|
|
9
9
|
import { PlaygroundConfig, StoredRoute } from './types';
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* API Playground Plugin
|
|
14
|
+
* Provides an interactive API explorer with authentication and development-mode security
|
|
15
|
+
*
|
|
16
|
+
* @param config - Configuration options for the playground
|
|
17
|
+
* @returns Plugin instance
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* app.use(playground({
|
|
22
|
+
* path: '/playground',
|
|
23
|
+
* developmentOnly: true,
|
|
24
|
+
* auth: {
|
|
25
|
+
* username: 'admin',
|
|
26
|
+
* password: 'secret123'
|
|
27
|
+
* }
|
|
28
|
+
* }));
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
12
31
|
export function playground(config: PlaygroundConfig = {}): Plugin {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
install(app: Application) {
|
|
34
|
-
appInstance = app;
|
|
35
|
-
const originalRoute = app.route.bind(app);
|
|
36
|
-
const originalGet = app.get.bind(app);
|
|
37
|
-
const originalPost = app.post.bind(app);
|
|
38
|
-
const originalPut = app.put.bind(app);
|
|
39
|
-
const originalDelete = app.delete.bind(app);
|
|
40
|
-
const originalPatch = app.patch.bind(app);
|
|
41
|
-
|
|
42
|
-
app.route = function (routeConfig: RouteConfig) {
|
|
43
|
-
routes.push({
|
|
44
|
-
method: routeConfig.method,
|
|
45
|
-
path: routeConfig.path,
|
|
46
|
-
schema: routeConfig.schema,
|
|
47
|
-
meta: routeConfig.meta
|
|
48
|
-
});
|
|
49
|
-
return originalRoute(routeConfig);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const wrapMethod = (method: HTTPMethod, original: Function) => {
|
|
53
|
-
return function (pathOrRoute: string | any, handlerOrConfig?: any) {
|
|
54
|
-
// Class-based routing
|
|
55
|
-
if (typeof pathOrRoute === 'object' && 'pathName' in pathOrRoute) {
|
|
56
|
-
const route = pathOrRoute;
|
|
57
|
-
routes.push({
|
|
58
|
-
method,
|
|
59
|
-
path: route.pathName,
|
|
60
|
-
schema: route.schema?.(),
|
|
61
|
-
meta: route.meta?.()
|
|
62
|
-
});
|
|
63
|
-
return original(pathOrRoute);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const path = pathOrRoute;
|
|
67
|
-
if (typeof handlerOrConfig !== 'function' && handlerOrConfig) {
|
|
68
|
-
routes.push({ method, path, schema: handlerOrConfig.schema, meta: handlerOrConfig.meta });
|
|
69
|
-
} else {
|
|
70
|
-
routes.push({ method, path });
|
|
71
|
-
}
|
|
72
|
-
return original(path, handlerOrConfig);
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
app.get = wrapMethod('GET', originalGet);
|
|
77
|
-
app.post = wrapMethod('POST', originalPost);
|
|
78
|
-
app.put = wrapMethod('PUT', originalPut);
|
|
79
|
-
app.delete = wrapMethod('DELETE', originalDelete);
|
|
80
|
-
app.patch = wrapMethod('PATCH', originalPatch);
|
|
81
|
-
|
|
82
|
-
const basePath = resolvedConfig.path!;
|
|
83
|
-
|
|
84
|
-
originalGet(basePath, async (ctx: any) => {
|
|
85
|
-
if (!detectedBaseUrl && ctx.raw?.req) {
|
|
86
|
-
const req = ctx.raw.req;
|
|
87
|
-
const protocol = req.headers['x-forwarded-proto'] || 'http';
|
|
88
|
-
const host = req.headers.host || 'localhost:3000';
|
|
89
|
-
detectedBaseUrl = `${protocol}://${host}`;
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
statusCode: 200,
|
|
93
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
94
|
-
body: generatePlaygroundHTML(resolvedConfig, detectedBaseUrl)
|
|
95
|
-
};
|
|
96
|
-
});
|
|
32
|
+
const resolvedConfig: PlaygroundConfig = {
|
|
33
|
+
path: '/playground',
|
|
34
|
+
title: 'API Playground',
|
|
35
|
+
theme: 'dark',
|
|
36
|
+
enableHistory: true,
|
|
37
|
+
maxHistory: 50,
|
|
38
|
+
enableVariables: true,
|
|
39
|
+
developmentOnly: true,
|
|
40
|
+
defaultHeaders: { 'Content-Type': 'application/json' },
|
|
41
|
+
variables: { baseUrl: 'http://localhost:3000', token: '' },
|
|
42
|
+
...config
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const routes: StoredRoute[] = [];
|
|
46
|
+
let detectedBaseUrl = '';
|
|
47
|
+
let appInstance: Application | null = null;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
name: 'playground',
|
|
51
|
+
version: '1.0.0',
|
|
97
52
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
53
|
+
install(app: Application) {
|
|
54
|
+
// Check if playground should be disabled in production
|
|
55
|
+
if (resolvedConfig.developmentOnly && process.env.NODE_ENV === 'production') {
|
|
56
|
+
console.warn('⚠️ API Playground is disabled in production mode');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
appInstance = app;
|
|
61
|
+
const originalRoute = app.route.bind(app);
|
|
62
|
+
const originalGet = app.get.bind(app);
|
|
63
|
+
const originalPost = app.post.bind(app);
|
|
64
|
+
const originalPut = app.put.bind(app);
|
|
65
|
+
const originalDelete = app.delete.bind(app);
|
|
66
|
+
const originalPatch = app.patch.bind(app);
|
|
67
|
+
|
|
68
|
+
app.route = function (routeConfig: RouteConfig) {
|
|
69
|
+
routes.push({
|
|
70
|
+
method: routeConfig.method,
|
|
71
|
+
path: routeConfig.path,
|
|
72
|
+
schema: routeConfig.schema,
|
|
73
|
+
meta: routeConfig.meta
|
|
74
|
+
});
|
|
75
|
+
return originalRoute(routeConfig);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const wrapMethod = (method: HTTPMethod, original: Function) => {
|
|
79
|
+
return function (pathOrRoute: string | any, handlerOrConfig?: any) {
|
|
80
|
+
// Class-based routing
|
|
81
|
+
if (typeof pathOrRoute === 'object' && 'pathName' in pathOrRoute) {
|
|
82
|
+
const route = pathOrRoute;
|
|
83
|
+
routes.push({
|
|
84
|
+
method,
|
|
85
|
+
path: route.pathName,
|
|
86
|
+
schema: route.schema?.(),
|
|
87
|
+
meta: route.meta?.()
|
|
118
88
|
});
|
|
89
|
+
return original(pathOrRoute);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const path = pathOrRoute;
|
|
93
|
+
if (typeof handlerOrConfig !== 'function' && handlerOrConfig) {
|
|
94
|
+
routes.push({ method, path, schema: handlerOrConfig.schema, meta: handlerOrConfig.meta });
|
|
95
|
+
} else {
|
|
96
|
+
routes.push({ method, path });
|
|
97
|
+
}
|
|
98
|
+
return original(path, handlerOrConfig);
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
app.get = wrapMethod('GET', originalGet);
|
|
103
|
+
app.post = wrapMethod('POST', originalPost);
|
|
104
|
+
app.put = wrapMethod('PUT', originalPut);
|
|
105
|
+
app.delete = wrapMethod('DELETE', originalDelete);
|
|
106
|
+
app.patch = wrapMethod('PATCH', originalPatch);
|
|
107
|
+
|
|
108
|
+
const basePath = resolvedConfig.path!;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Authentication middleware for playground routes
|
|
112
|
+
* Implements HTTP Basic Authentication when auth config is provided
|
|
113
|
+
*/
|
|
114
|
+
const authMiddleware = (ctx: any) => {
|
|
115
|
+
if (!resolvedConfig.auth) {
|
|
116
|
+
return true; // No auth required
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const authHeader = ctx.raw?.req?.headers?.authorization || ctx.headers?.get?.('authorization');
|
|
120
|
+
|
|
121
|
+
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const base64Credentials = authHeader.substring(6);
|
|
127
|
+
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
|
|
128
|
+
const [username, password] = credentials.split(':');
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
username === resolvedConfig.auth.username &&
|
|
132
|
+
password === resolvedConfig.auth.password
|
|
133
|
+
);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return false;
|
|
119
136
|
}
|
|
120
|
-
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns 401 Unauthorized response with WWW-Authenticate header
|
|
141
|
+
* This triggers the browser's native authentication dialog
|
|
142
|
+
*/
|
|
143
|
+
const unauthorizedResponse = () => {
|
|
144
|
+
return {
|
|
145
|
+
statusCode: 401,
|
|
146
|
+
headers: {
|
|
147
|
+
'WWW-Authenticate': 'Basic realm="API Playground"',
|
|
148
|
+
'Content-Type': 'text/plain'
|
|
149
|
+
},
|
|
150
|
+
body: 'Unauthorized'
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
originalGet(basePath, async (ctx: any) => {
|
|
155
|
+
// Check authentication
|
|
156
|
+
if (!authMiddleware(ctx)) {
|
|
157
|
+
return unauthorizedResponse();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!detectedBaseUrl && ctx.raw?.req) {
|
|
161
|
+
const req = ctx.raw.req;
|
|
162
|
+
const protocol = req.headers['x-forwarded-proto'] || 'http';
|
|
163
|
+
const host = req.headers.host || 'localhost:3000';
|
|
164
|
+
detectedBaseUrl = `${protocol}://${host}`;
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
statusCode: 200,
|
|
168
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
169
|
+
body: generatePlaygroundHTML(resolvedConfig, detectedBaseUrl)
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
originalGet(basePath + '/api/routes', async (ctx: Context) => {
|
|
174
|
+
// Check authentication
|
|
175
|
+
if (!authMiddleware(ctx)) {
|
|
176
|
+
return unauthorizedResponse();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Combine intercepted routes with routes from router (for file-based routing)
|
|
180
|
+
const allRoutes = getAllRoutes(routes, appInstance, basePath);
|
|
181
|
+
|
|
182
|
+
const routesData = allRoutes.map(r => ({
|
|
183
|
+
method: r.method,
|
|
184
|
+
path: r.path,
|
|
185
|
+
summary: r.meta?.summary || generateSummary(r.method as HTTPMethod, r.path),
|
|
186
|
+
description: r.meta?.description,
|
|
187
|
+
tags: r.meta?.tags || [getTagFromPath(r.path)],
|
|
188
|
+
deprecated: r.meta?.deprecated,
|
|
189
|
+
responses: r.meta?.responses,
|
|
190
|
+
example: r.meta?.example,
|
|
191
|
+
schema: {
|
|
192
|
+
body: r.schema?.body ? zodToExample(r.schema.body) : null,
|
|
193
|
+
query: r.schema?.query ? zodToParams(r.schema.query) : null,
|
|
194
|
+
params: extractPathParams(r.path)
|
|
195
|
+
}
|
|
196
|
+
}));
|
|
197
|
+
return ctx.json(routesData);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
};
|
|
121
201
|
}
|
|
122
202
|
|
|
123
203
|
/**
|
|
@@ -125,46 +205,46 @@ export function playground(config: PlaygroundConfig = {}): Plugin {
|
|
|
125
205
|
* This ensures file-based routes are also included
|
|
126
206
|
*/
|
|
127
207
|
function getAllRoutes(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
208
|
+
interceptedRoutes: StoredRoute[],
|
|
209
|
+
app: Application | null,
|
|
210
|
+
basePath: string
|
|
131
211
|
): StoredRoute[] {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
212
|
+
// Create a Set of already tracked route keys (method + path)
|
|
213
|
+
const trackedKeys = new Set(
|
|
214
|
+
interceptedRoutes.map(r => `${r.method}:${r.path}`)
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Start with intercepted routes (filtered)
|
|
218
|
+
const allRoutes: StoredRoute[] = interceptedRoutes.filter(
|
|
219
|
+
r => !r.path.startsWith(basePath)
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Get routes from router (includes file-based routes)
|
|
223
|
+
if (app) {
|
|
224
|
+
const routerRoutes = app.getRoutes() as Array<{
|
|
225
|
+
method: string;
|
|
226
|
+
path: string;
|
|
227
|
+
schema?: SchemaConfig;
|
|
228
|
+
meta?: RouteMeta;
|
|
229
|
+
}>;
|
|
230
|
+
|
|
231
|
+
for (const route of routerRoutes) {
|
|
232
|
+
const key = `${route.method}:${route.path}`;
|
|
233
|
+
|
|
234
|
+
// Skip if already tracked or is playground route
|
|
235
|
+
if (trackedKeys.has(key) || route.path.startsWith(basePath)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
allRoutes.push({
|
|
240
|
+
method: route.method as HTTPMethod,
|
|
241
|
+
path: route.path,
|
|
242
|
+
schema: route.schema,
|
|
243
|
+
meta: route.meta
|
|
244
|
+
});
|
|
166
245
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Sort routes by path for consistent ordering
|
|
249
|
+
return allRoutes.sort((a, b) => a.path.localeCompare(b.path));
|
|
170
250
|
}
|
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
import { HTTPMethod, RouteMeta, SchemaConfig } from "../../core/types";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for the API Playground
|
|
5
|
+
*/
|
|
3
6
|
export interface PlaygroundConfig {
|
|
7
|
+
/** URL path where the playground will be accessible (default: '/playground') */
|
|
4
8
|
path?: string;
|
|
9
|
+
|
|
10
|
+
/** Title displayed in the playground UI (default: 'API Playground') */
|
|
5
11
|
title?: string;
|
|
12
|
+
|
|
13
|
+
/** Color theme for the playground interface (default: 'dark') */
|
|
6
14
|
theme?: 'dark' | 'light';
|
|
15
|
+
|
|
16
|
+
/** Default HTTP headers to include in API requests */
|
|
7
17
|
defaultHeaders?: Record<string, string>;
|
|
18
|
+
|
|
19
|
+
/** Enable/disable request history tracking (default: true) */
|
|
8
20
|
enableHistory?: boolean;
|
|
21
|
+
|
|
22
|
+
/** Maximum number of requests to keep in history (default: 50) */
|
|
9
23
|
maxHistory?: number;
|
|
24
|
+
|
|
25
|
+
/** Enable/disable variable substitution in requests (default: true) */
|
|
10
26
|
enableVariables?: boolean;
|
|
27
|
+
|
|
28
|
+
/** Predefined variables for use in requests (e.g., {{baseUrl}}, {{token}}) */
|
|
11
29
|
variables?: Record<string, string>;
|
|
30
|
+
|
|
31
|
+
/** Enable only in development mode (default: true) */
|
|
32
|
+
developmentOnly?: boolean;
|
|
33
|
+
|
|
34
|
+
/** Basic authentication configuration to protect playground access */
|
|
35
|
+
auth?: {
|
|
36
|
+
/** Username for basic authentication */
|
|
37
|
+
username: string;
|
|
38
|
+
/** Password for basic authentication */
|
|
39
|
+
password: string;
|
|
40
|
+
};
|
|
12
41
|
}
|
|
13
42
|
|
|
14
43
|
|