@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.
Files changed (72) hide show
  1. package/BENCHMARK_REPORT.md +343 -0
  2. package/dist/advanced/playground/generatePlaygroundHTML.d.ts.map +1 -1
  3. package/dist/advanced/playground/generatePlaygroundHTML.js +107 -0
  4. package/dist/advanced/playground/generatePlaygroundHTML.js.map +1 -1
  5. package/dist/advanced/playground/playground.d.ts +19 -0
  6. package/dist/advanced/playground/playground.d.ts.map +1 -1
  7. package/dist/advanced/playground/playground.js +70 -0
  8. package/dist/advanced/playground/playground.js.map +1 -1
  9. package/dist/advanced/playground/types.d.ts +20 -0
  10. package/dist/advanced/playground/types.d.ts.map +1 -1
  11. package/dist/core/application.d.ts +14 -0
  12. package/dist/core/application.d.ts.map +1 -1
  13. package/dist/core/application.js +173 -71
  14. package/dist/core/application.js.map +1 -1
  15. package/dist/core/context-pool.d.ts +2 -13
  16. package/dist/core/context-pool.d.ts.map +1 -1
  17. package/dist/core/context-pool.js +7 -45
  18. package/dist/core/context-pool.js.map +1 -1
  19. package/dist/core/context.d.ts +108 -5
  20. package/dist/core/context.d.ts.map +1 -1
  21. package/dist/core/context.js +449 -53
  22. package/dist/core/context.js.map +1 -1
  23. package/dist/core/index.d.ts +1 -0
  24. package/dist/core/index.d.ts.map +1 -1
  25. package/dist/core/index.js +9 -1
  26. package/dist/core/index.js.map +1 -1
  27. package/dist/core/middleware.d.ts +6 -0
  28. package/dist/core/middleware.d.ts.map +1 -1
  29. package/dist/core/middleware.js +83 -84
  30. package/dist/core/middleware.js.map +1 -1
  31. package/dist/core/performance/fast-json.d.ts +149 -0
  32. package/dist/core/performance/fast-json.d.ts.map +1 -0
  33. package/dist/core/performance/fast-json.js +473 -0
  34. package/dist/core/performance/fast-json.js.map +1 -0
  35. package/dist/core/router/file-router.d.ts +20 -7
  36. package/dist/core/router/file-router.d.ts.map +1 -1
  37. package/dist/core/router/file-router.js +41 -13
  38. package/dist/core/router/file-router.js.map +1 -1
  39. package/dist/core/router/index.d.ts +6 -0
  40. package/dist/core/router/index.d.ts.map +1 -1
  41. package/dist/core/router/index.js +33 -6
  42. package/dist/core/router/index.js.map +1 -1
  43. package/dist/core/router/radix-tree.d.ts +4 -1
  44. package/dist/core/router/radix-tree.d.ts.map +1 -1
  45. package/dist/core/router/radix-tree.js +7 -3
  46. package/dist/core/router/radix-tree.js.map +1 -1
  47. package/dist/core/serializer.d.ts +251 -0
  48. package/dist/core/serializer.d.ts.map +1 -0
  49. package/dist/core/serializer.js +290 -0
  50. package/dist/core/serializer.js.map +1 -0
  51. package/dist/core/types.d.ts +39 -1
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +12 -2
  57. package/dist/index.js.map +1 -1
  58. package/package.json +3 -1
  59. package/src/advanced/playground/generatePlaygroundHTML.ts +107 -0
  60. package/src/advanced/playground/playground.ts +225 -145
  61. package/src/advanced/playground/types.ts +29 -0
  62. package/src/core/application.ts +202 -84
  63. package/src/core/context-pool.ts +8 -56
  64. package/src/core/context.ts +497 -53
  65. package/src/core/index.ts +14 -0
  66. package/src/core/middleware.ts +99 -89
  67. package/src/core/router/file-router.ts +41 -12
  68. package/src/core/router/index.ts +213 -180
  69. package/src/core/router/radix-tree.ts +20 -4
  70. package/src/core/serializer.ts +397 -0
  71. package/src/core/types.ts +43 -1
  72. 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
- const resolvedConfig: PlaygroundConfig = {
14
- path: '/playground',
15
- title: 'API Playground',
16
- theme: 'dark',
17
- enableHistory: true,
18
- maxHistory: 50,
19
- enableVariables: true,
20
- defaultHeaders: { 'Content-Type': 'application/json' },
21
- variables: { baseUrl: 'http://localhost:3000', token: '' },
22
- ...config
23
- };
24
-
25
- const routes: StoredRoute[] = [];
26
- let detectedBaseUrl = '';
27
- let appInstance: Application | null = null;
28
-
29
- return {
30
- name: 'playground',
31
- version: '1.0.0',
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
- originalGet(basePath + '/api/routes', async (ctx: Context) => {
99
- // Combine intercepted routes with routes from router (for file-based routing)
100
- const allRoutes = getAllRoutes(routes, appInstance, basePath);
101
-
102
- const routesData = allRoutes.map(r => ({
103
- method: r.method,
104
- path: r.path,
105
- summary: r.meta?.summary || generateSummary(r.method as HTTPMethod, r.path),
106
- description: r.meta?.description,
107
- tags: r.meta?.tags || [getTagFromPath(r.path)],
108
- deprecated: r.meta?.deprecated,
109
- responses: r.meta?.responses,
110
- example: r.meta?.example,
111
- schema: {
112
- body: r.schema?.body ? zodToExample(r.schema.body) : null,
113
- query: r.schema?.query ? zodToParams(r.schema.query) : null,
114
- params: extractPathParams(r.path)
115
- }
116
- }));
117
- return ctx.json(routesData);
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
- interceptedRoutes: StoredRoute[],
129
- app: Application | null,
130
- basePath: string
208
+ interceptedRoutes: StoredRoute[],
209
+ app: Application | null,
210
+ basePath: string
131
211
  ): StoredRoute[] {
132
- // Create a Set of already tracked route keys (method + path)
133
- const trackedKeys = new Set(
134
- interceptedRoutes.map(r => `${r.method}:${r.path}`)
135
- );
136
-
137
- // Start with intercepted routes (filtered)
138
- const allRoutes: StoredRoute[] = interceptedRoutes.filter(
139
- r => !r.path.startsWith(basePath)
140
- );
141
-
142
- // Get routes from router (includes file-based routes)
143
- if (app) {
144
- const routerRoutes = app.getRoutes() as Array<{
145
- method: string;
146
- path: string;
147
- schema?: SchemaConfig;
148
- meta?: RouteMeta;
149
- }>;
150
-
151
- for (const route of routerRoutes) {
152
- const key = `${route.method}:${route.path}`;
153
-
154
- // Skip if already tracked or is playground route
155
- if (trackedKeys.has(key) || route.path.startsWith(basePath)) {
156
- continue;
157
- }
158
-
159
- allRoutes.push({
160
- method: route.method as HTTPMethod,
161
- path: route.path,
162
- schema: route.schema,
163
- meta: route.meta
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
- // Sort routes by path for consistent ordering
169
- return allRoutes.sort((a, b) => a.path.localeCompare(b.path));
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