json-rest-api 0.1.1 → 1.0.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/.claude/settings.local.json +16 -0
- package/LICENSE +674 -20
- package/README.md +728 -120
- package/RELATIONSHIPS.md +1013 -0
- package/TODO_rest-api-plugin-extras.js +142 -0
- package/connectors_ideas/express-plugin.js +120 -0
- package/connectors_ideas/fastify.js +219 -0
- package/connectors_ideas/http-plugin.js +138 -0
- package/connectors_ideas/koa-plugin.js +113 -0
- package/docs/fileUploads.md +722 -0
- package/docs/mini-guides/file-uploads-complete.md +159 -0
- package/examples/.gitkeep +0 -0
- package/examples/express-example.js +262 -0
- package/examples/express-plugin.js +366 -0
- package/examples/file-upload-example.js +205 -0
- package/examples/filename-strategies.js +140 -0
- package/examples/knex-basic.js +66 -0
- package/index.js +28 -202
- package/lib/payload-validators.js +685 -0
- package/lib/rest-api-errors.js +58 -0
- package/package.json +37 -20
- package/payloadValdators.js +686 -0
- package/plugins/core/connectors/express-plugin.js +449 -0
- package/plugins/core/connectors/http-plugin.js +450 -0
- package/plugins/core/file-handling-plugin.js +338 -0
- package/plugins/core/lib/busboy-detector.js +167 -0
- package/plugins/core/lib/formidable-detector.js +130 -0
- package/plugins/core/rest-api-knex-plugin.js +200 -0
- package/plugins/core/rest-api-plugin.js +1419 -0
- package/plugins/storage/local-storage.js +218 -0
- package/plugins/storage/s3-storage.js +125 -0
- package/test-examples/package-lock.json +906 -0
- package/test-examples/package.json +15 -0
- package/test-examples/test-basic-api.js +11 -0
- package/test-examples/test-books-resource.js +116 -0
- package/test-examples/test-complete-file-upload.js +140 -0
- package/test-examples/test-debug.js +28 -0
- package/test-examples/test-express-plugin.js +177 -0
- package/test-examples/test-express-simple.js +28 -0
- package/test-examples/test-express-with-import.js +71 -0
- package/test-examples/test-file-upload.js +132 -0
- package/test-examples/test-http-plugin.js +159 -0
- package/test-examples/test-programmatic-api.js +188 -0
- package/test-examples/test-rest-api-plugin.js +13 -0
- package/.jshintrc +0 -48
- package/.npmignore +0 -15
- package/examples/client.js +0 -18
- package/examples/client2.js +0 -27
- package/examples/server.js +0 -23
- package/test.js +0 -101
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express Plugin for Hooked API
|
|
3
|
+
*
|
|
4
|
+
* This plugin creates HTTP endpoints for your REST API by translating
|
|
5
|
+
* Express HTTP requests into hooked-api method calls.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Automatic route creation for all scopes (including dynamically added ones)
|
|
9
|
+
* - JSON:API compliant request/response handling
|
|
10
|
+
* - Query parameter parsing (include, fields, filter, sort, page)
|
|
11
|
+
* - Error mapping to HTTP status codes
|
|
12
|
+
* - Content type validation
|
|
13
|
+
* - Middleware injection points
|
|
14
|
+
* - File upload support with busboy or formidable
|
|
15
|
+
*
|
|
16
|
+
* Required dependencies:
|
|
17
|
+
* - express: The Express framework
|
|
18
|
+
*
|
|
19
|
+
* Optional dependencies:
|
|
20
|
+
* - busboy or formidable: File upload support
|
|
21
|
+
*
|
|
22
|
+
* Basic usage:
|
|
23
|
+
* ```javascript
|
|
24
|
+
* import { ExpressPlugin } from 'json-rest-api';
|
|
25
|
+
* import express from 'express';
|
|
26
|
+
*
|
|
27
|
+
* const api = new Api({ name: 'my-api', version: '1.0.0' });
|
|
28
|
+
* api.use(RestApiPlugin);
|
|
29
|
+
* api.use(ExpressPlugin, {
|
|
30
|
+
* basePath: '/api', // Default: '/api'
|
|
31
|
+
* strictContentType: true, // Default: true
|
|
32
|
+
* requestSizeLimit: '10mb' // Default: '1mb'
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* const app = express();
|
|
36
|
+
*
|
|
37
|
+
* // Both approaches are equivalent:
|
|
38
|
+
*
|
|
39
|
+
* // Approach 1: Direct router usage (standard Express pattern)
|
|
40
|
+
* app.use(api.express.router); // Mount at root
|
|
41
|
+
* app.use('/v1', api.express.router); // Mount at /v1
|
|
42
|
+
* app.use('/api/v2', api.express.router); // Mount at /api/v2
|
|
43
|
+
*
|
|
44
|
+
* // Approach 2: Convenience method (adds logging)
|
|
45
|
+
* api.express.mount(app); // Mount at root
|
|
46
|
+
* api.express.mount(app, '/v1'); // Mount at /v1
|
|
47
|
+
* api.express.mount(app, '/api/v2'); // Mount at /api/v2
|
|
48
|
+
*
|
|
49
|
+
* // The mount method is just syntactic sugar that calls app.use() and logs the mount path
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Advanced usage with middleware:
|
|
53
|
+
* ```javascript
|
|
54
|
+
* api.use(ExpressPlugin, {
|
|
55
|
+
* basePath: '/api',
|
|
56
|
+
* middleware: {
|
|
57
|
+
* // Apply middleware before all routes
|
|
58
|
+
* beforeAll: [authMiddleware, loggingMiddleware],
|
|
59
|
+
*
|
|
60
|
+
* // Apply middleware to specific scopes
|
|
61
|
+
* beforeScope: {
|
|
62
|
+
* users: [requireAuth],
|
|
63
|
+
* posts: [requireAuth, checkPermissions]
|
|
64
|
+
* },
|
|
65
|
+
*
|
|
66
|
+
* // Apply middleware after scope routes
|
|
67
|
+
* afterScope: {
|
|
68
|
+
* users: [auditLogger]
|
|
69
|
+
* }
|
|
70
|
+
* },
|
|
71
|
+
*
|
|
72
|
+
* // Provide your own router instance
|
|
73
|
+
* router: express.Router({ mergeParams: true })
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* File upload configuration:
|
|
78
|
+
* ```javascript
|
|
79
|
+
* api.use(ExpressPlugin, {
|
|
80
|
+
* enableFileUploads: true, // Default: true
|
|
81
|
+
* fileParser: 'busboy', // 'busboy' or 'formidable'
|
|
82
|
+
* fileParserOptions: {
|
|
83
|
+
* limits: { fileSize: 50 * 1024 * 1024 } // 50MB limit
|
|
84
|
+
* }
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
export const ExpressPlugin = {
|
|
90
|
+
name: 'express',
|
|
91
|
+
dependencies: ['rest-api', 'file-handling'],
|
|
92
|
+
|
|
93
|
+
async install({ on, vars, helpers, pluginOptions, log, scopes, addApiMethod, api }) {
|
|
94
|
+
// Initialize express namespace under api.http
|
|
95
|
+
if (!api.http) {
|
|
96
|
+
api.http = {};
|
|
97
|
+
}
|
|
98
|
+
api.http.express = {};
|
|
99
|
+
|
|
100
|
+
// Import Express - will fail with clear error if not installed
|
|
101
|
+
let express;
|
|
102
|
+
try {
|
|
103
|
+
express = await import('express');
|
|
104
|
+
express = express.default || express;
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.error('Express import error:', e);
|
|
107
|
+
throw new Error('Express is required for the Express plugin. Install with: npm install express');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const expressOptions = pluginOptions.express || {};
|
|
111
|
+
const basePath = expressOptions.basePath || '/api';
|
|
112
|
+
const strictContentType = expressOptions.strictContentType !== false;
|
|
113
|
+
const requestSizeLimit = expressOptions.requestSizeLimit || '1mb';
|
|
114
|
+
|
|
115
|
+
// Track which scopes have routes created
|
|
116
|
+
const routesCreated = new Set();
|
|
117
|
+
|
|
118
|
+
// Register file detector if file handling is enabled
|
|
119
|
+
if (expressOptions.enableFileUploads !== false && vars.rest?.registerFileDetector) {
|
|
120
|
+
// Determine which parser to use
|
|
121
|
+
const parserLib = expressOptions.fileParser || 'busboy';
|
|
122
|
+
const parserOptions = expressOptions.fileParserOptions || {};
|
|
123
|
+
|
|
124
|
+
let detector;
|
|
125
|
+
|
|
126
|
+
if (parserLib === 'multer') {
|
|
127
|
+
// Multer is Express-specific and works differently
|
|
128
|
+
log.info('Multer file parsing should be configured via Express middleware');
|
|
129
|
+
} else if (parserLib === 'busboy') {
|
|
130
|
+
try {
|
|
131
|
+
const { createBusboyDetector } = await import('../lib/busboy-detector.js');
|
|
132
|
+
detector = createBusboyDetector(parserOptions);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
log.warn('Busboy not installed. Install with: npm install busboy');
|
|
135
|
+
}
|
|
136
|
+
} else if (parserLib === 'formidable') {
|
|
137
|
+
try {
|
|
138
|
+
const { createFormidableDetector } = await import('../lib/formidable-detector.js');
|
|
139
|
+
detector = createFormidableDetector(parserOptions);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
log.warn('Formidable not installed. Install with: npm install formidable');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Register the detector
|
|
146
|
+
if (detector) {
|
|
147
|
+
vars.rest.registerFileDetector({
|
|
148
|
+
name: `express-${detector.name}`,
|
|
149
|
+
detect: (params) => {
|
|
150
|
+
// Only detect for Express requests
|
|
151
|
+
if (!params._expressReq) return false;
|
|
152
|
+
return detector.detect(params);
|
|
153
|
+
},
|
|
154
|
+
parse: detector.parse
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
log.info(`Express plugin registered file detector: ${detector.name}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Create Express router
|
|
162
|
+
const router = expressOptions.router || express.Router();
|
|
163
|
+
|
|
164
|
+
// Add body parsing middleware
|
|
165
|
+
router.use(express.json({
|
|
166
|
+
limit: requestSizeLimit,
|
|
167
|
+
type: ['application/json', 'application/vnd.api+json']
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
// Content type validation middleware
|
|
171
|
+
if (strictContentType) {
|
|
172
|
+
router.use((req, res, next) => {
|
|
173
|
+
// Only validate for requests with bodies
|
|
174
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
175
|
+
const contentType = req.get('Content-Type');
|
|
176
|
+
|
|
177
|
+
if (contentType && !contentType.includes('application/vnd.api+json') && !contentType.includes('application/json')) {
|
|
178
|
+
return res.status(415).json({
|
|
179
|
+
errors: [{
|
|
180
|
+
status: '415',
|
|
181
|
+
title: 'Unsupported Media Type',
|
|
182
|
+
detail: `Content-Type must be application/vnd.api+json or application/json`
|
|
183
|
+
}]
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
next();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Query parameter parser
|
|
193
|
+
* Express with qs already handles bracket notation (filter[status] -> filter.status)
|
|
194
|
+
* This just transforms the data to match our expected format
|
|
195
|
+
*/
|
|
196
|
+
const parseQueryParams = (req) => {
|
|
197
|
+
const query = req.query;
|
|
198
|
+
const queryParams = {};
|
|
199
|
+
|
|
200
|
+
// Parse include (comma-separated string to array)
|
|
201
|
+
if (query.include) {
|
|
202
|
+
queryParams.include = query.include.split(',').map(s => s.trim());
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Copy fields, filter, and page if they exist as objects
|
|
206
|
+
if (query.fields && typeof query.fields === 'object') {
|
|
207
|
+
queryParams.fields = query.fields;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (query.filter && typeof query.filter === 'object') {
|
|
211
|
+
queryParams.filter = query.filter;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (query.page && typeof query.page === 'object') {
|
|
215
|
+
queryParams.page = query.page;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Parse sort (comma-separated string to array)
|
|
219
|
+
if (query.sort) {
|
|
220
|
+
queryParams.sort = query.sort.split(',').map(s => s.trim());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return queryParams;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Error handler - maps API errors to HTTP responses
|
|
228
|
+
*/
|
|
229
|
+
const handleError = (error, res) => {
|
|
230
|
+
let status = 500;
|
|
231
|
+
let errorResponse = {
|
|
232
|
+
errors: [{
|
|
233
|
+
status: '500',
|
|
234
|
+
title: 'Internal Server Error',
|
|
235
|
+
detail: error.message
|
|
236
|
+
}]
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Check for RestApiError types by code
|
|
240
|
+
if (error.code) {
|
|
241
|
+
switch (error.code) {
|
|
242
|
+
case 'REST_API_VALIDATION':
|
|
243
|
+
status = 422;
|
|
244
|
+
errorResponse.errors = [{
|
|
245
|
+
status: '422',
|
|
246
|
+
title: 'Validation Error',
|
|
247
|
+
detail: error.message,
|
|
248
|
+
source: error.details
|
|
249
|
+
}];
|
|
250
|
+
if (error.violations) {
|
|
251
|
+
errorResponse.errors = error.violations.map(v => ({
|
|
252
|
+
status: '422',
|
|
253
|
+
title: 'Validation Error',
|
|
254
|
+
detail: v.message,
|
|
255
|
+
source: { pointer: v.field }
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case 'REST_API_RESOURCE':
|
|
261
|
+
// Map subtype to status
|
|
262
|
+
switch (error.subtype) {
|
|
263
|
+
case 'not_found':
|
|
264
|
+
status = 404;
|
|
265
|
+
errorResponse.errors[0].status = '404';
|
|
266
|
+
errorResponse.errors[0].title = 'Not Found';
|
|
267
|
+
break;
|
|
268
|
+
case 'conflict':
|
|
269
|
+
status = 409;
|
|
270
|
+
errorResponse.errors[0].status = '409';
|
|
271
|
+
errorResponse.errors[0].title = 'Conflict';
|
|
272
|
+
break;
|
|
273
|
+
case 'forbidden':
|
|
274
|
+
status = 403;
|
|
275
|
+
errorResponse.errors[0].status = '403';
|
|
276
|
+
errorResponse.errors[0].title = 'Forbidden';
|
|
277
|
+
break;
|
|
278
|
+
default:
|
|
279
|
+
status = 400;
|
|
280
|
+
errorResponse.errors[0].status = '400';
|
|
281
|
+
errorResponse.errors[0].title = 'Bad Request';
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
|
|
285
|
+
case 'REST_API_PAYLOAD':
|
|
286
|
+
status = 400;
|
|
287
|
+
errorResponse.errors = [{
|
|
288
|
+
status: '400',
|
|
289
|
+
title: 'Bad Request',
|
|
290
|
+
detail: error.message,
|
|
291
|
+
source: { pointer: error.path }
|
|
292
|
+
}];
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
res.status(status).json(errorResponse);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Creates request handler for a specific HTTP method
|
|
302
|
+
*/
|
|
303
|
+
const createRequestHandler = (scopeName, methodName) => {
|
|
304
|
+
return async (req, res) => {
|
|
305
|
+
try {
|
|
306
|
+
const params = {};
|
|
307
|
+
|
|
308
|
+
// Add ID parameter for single-resource operations
|
|
309
|
+
if (['get', 'put', 'patch', 'delete'].includes(methodName)) {
|
|
310
|
+
params.id = req.params.id;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Add query parameters
|
|
314
|
+
if (['query', 'get'].includes(methodName)) {
|
|
315
|
+
params.queryParams = parseQueryParams(req);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Add body for mutations
|
|
319
|
+
if (['post', 'put', 'patch'].includes(methodName)) {
|
|
320
|
+
params.inputRecord = req.body;
|
|
321
|
+
|
|
322
|
+
// For PUT and PATCH, also parse query params
|
|
323
|
+
if (['put', 'patch'].includes(methodName)) {
|
|
324
|
+
params.queryParams = parseQueryParams(req);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Store Express request/response in params for plugins that need it
|
|
329
|
+
params._expressReq = req;
|
|
330
|
+
params._expressRes = res;
|
|
331
|
+
|
|
332
|
+
log.debug(`HTTP ${req.method} ${req.path}`, { scopeName, methodName, params });
|
|
333
|
+
|
|
334
|
+
// Call the API method
|
|
335
|
+
const scope = scopes[scopeName];
|
|
336
|
+
if (!scope) {
|
|
337
|
+
return handleError({
|
|
338
|
+
code: 'REST_API_RESOURCE',
|
|
339
|
+
subtype: 'not_found',
|
|
340
|
+
message: `Scope '${scopeName}' not found`
|
|
341
|
+
}, res);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const result = await scope[methodName](params);
|
|
345
|
+
|
|
346
|
+
// Send response
|
|
347
|
+
res.set('Content-Type', 'application/vnd.api+json');
|
|
348
|
+
|
|
349
|
+
switch (methodName) {
|
|
350
|
+
case 'post':
|
|
351
|
+
res.status(201).json(result);
|
|
352
|
+
break;
|
|
353
|
+
case 'delete':
|
|
354
|
+
res.sendStatus(204);
|
|
355
|
+
break;
|
|
356
|
+
default:
|
|
357
|
+
res.json(result);
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
handleError(error, res);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Creates routes for a scope
|
|
367
|
+
*/
|
|
368
|
+
const createRoutesForScope = (scopeName) => {
|
|
369
|
+
if (routesCreated.has(scopeName)) {
|
|
370
|
+
log.trace(`Routes already created for scope '${scopeName}'`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const scopePath = `${basePath}/${scopeName}`;
|
|
375
|
+
|
|
376
|
+
log.info(`Creating Express routes for scope '${scopeName}' at ${scopePath}`);
|
|
377
|
+
|
|
378
|
+
// Apply any before middleware
|
|
379
|
+
const beforeMiddleware = expressOptions.middleware?.beforeScope?.[scopeName] || [];
|
|
380
|
+
|
|
381
|
+
// GET /api/{scope} - Query collection
|
|
382
|
+
router.get(scopePath, ...beforeMiddleware, createRequestHandler(scopeName, 'query'));
|
|
383
|
+
|
|
384
|
+
// GET /api/{scope}/{id} - Get single resource
|
|
385
|
+
router.get(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'get'));
|
|
386
|
+
|
|
387
|
+
// POST /api/{scope} - Create resource
|
|
388
|
+
router.post(scopePath, ...beforeMiddleware, createRequestHandler(scopeName, 'post'));
|
|
389
|
+
|
|
390
|
+
// PUT /api/{scope}/{id} - Replace resource
|
|
391
|
+
router.put(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'put'));
|
|
392
|
+
|
|
393
|
+
// PATCH /api/{scope}/{id} - Update resource
|
|
394
|
+
router.patch(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'patch'));
|
|
395
|
+
|
|
396
|
+
// DELETE /api/{scope}/{id} - Delete resource
|
|
397
|
+
router.delete(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'delete'));
|
|
398
|
+
|
|
399
|
+
// Apply any after middleware
|
|
400
|
+
const afterMiddleware = expressOptions.middleware?.afterScope?.[scopeName] || [];
|
|
401
|
+
if (afterMiddleware.length > 0) {
|
|
402
|
+
router.use(scopePath, ...afterMiddleware);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
routesCreated.add(scopeName);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Listen for scope additions
|
|
409
|
+
on('scope:added', 'createExpressRoutes', ({ eventData }) => {
|
|
410
|
+
createRoutesForScope(eventData.scopeName);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Create routes for any existing scopes
|
|
414
|
+
for (const scopeName of Object.keys(scopes)) {
|
|
415
|
+
createRoutesForScope(scopeName);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 404 handler for unmatched routes within the API
|
|
419
|
+
router.use((req, res) => {
|
|
420
|
+
res.status(404).json({
|
|
421
|
+
errors: [{
|
|
422
|
+
status: '404',
|
|
423
|
+
title: 'Not Found',
|
|
424
|
+
detail: `The requested endpoint ${req.method} ${req.path} does not exist`
|
|
425
|
+
}]
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Apply global middleware if configured
|
|
430
|
+
let finalRouter = router;
|
|
431
|
+
if (expressOptions.middleware?.beforeAll) {
|
|
432
|
+
const globalRouter = express.Router();
|
|
433
|
+
globalRouter.use(...expressOptions.middleware.beforeAll);
|
|
434
|
+
globalRouter.use(router);
|
|
435
|
+
finalRouter = globalRouter;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Store router in api.http.express namespace
|
|
439
|
+
api.http.express.router = finalRouter;
|
|
440
|
+
|
|
441
|
+
// Add convenient mounting method
|
|
442
|
+
api.http.express.mount = (app, path = '') => {
|
|
443
|
+
app.use(path, finalRouter);
|
|
444
|
+
log.info(`Express routes mounted at ${path || '/'}`);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
log.info('Express plugin initialized successfully');
|
|
448
|
+
}
|
|
449
|
+
};
|