json-rest-api 1.0.0 → 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.
Files changed (67) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +655 -309
  4. package/RELATIONSHIPS.md +1013 -0
  5. package/TODO_rest-api-plugin-extras.js +142 -0
  6. package/connectors_ideas/express-plugin.js +120 -0
  7. package/connectors_ideas/fastify.js +219 -0
  8. package/connectors_ideas/http-plugin.js +138 -0
  9. package/connectors_ideas/koa-plugin.js +113 -0
  10. package/docs/fileUploads.md +722 -0
  11. package/docs/mini-guides/file-uploads-complete.md +159 -0
  12. package/examples/.gitkeep +0 -0
  13. package/examples/express-example.js +262 -0
  14. package/examples/express-plugin.js +366 -0
  15. package/examples/file-upload-example.js +205 -0
  16. package/examples/filename-strategies.js +140 -0
  17. package/examples/knex-basic.js +66 -0
  18. package/index.js +31 -56
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +41 -14
  22. package/payloadValdators.js +686 -0
  23. package/plugins/core/connectors/express-plugin.js +449 -0
  24. package/plugins/core/connectors/http-plugin.js +450 -0
  25. package/plugins/core/file-handling-plugin.js +338 -0
  26. package/plugins/core/lib/busboy-detector.js +167 -0
  27. package/plugins/core/lib/formidable-detector.js +130 -0
  28. package/plugins/core/rest-api-knex-plugin.js +200 -0
  29. package/plugins/core/rest-api-plugin.js +1419 -0
  30. package/plugins/storage/local-storage.js +218 -0
  31. package/plugins/storage/s3-storage.js +125 -0
  32. package/test-examples/package-lock.json +906 -0
  33. package/test-examples/package.json +15 -0
  34. package/test-examples/test-basic-api.js +11 -0
  35. package/test-examples/test-books-resource.js +116 -0
  36. package/test-examples/test-complete-file-upload.js +140 -0
  37. package/test-examples/test-debug.js +28 -0
  38. package/test-examples/test-express-plugin.js +177 -0
  39. package/test-examples/test-express-simple.js +28 -0
  40. package/test-examples/test-express-with-import.js +71 -0
  41. package/test-examples/test-file-upload.js +132 -0
  42. package/test-examples/test-http-plugin.js +159 -0
  43. package/test-examples/test-programmatic-api.js +188 -0
  44. package/test-examples/test-rest-api-plugin.js +13 -0
  45. package/TODO.todo +0 -113
  46. package/api.js +0 -628
  47. package/docs/API.md +0 -1306
  48. package/docs/FIXES_APPLIED.md +0 -59
  49. package/docs/GUIDES.md +0 -1792
  50. package/docs/RESOURCES_IN_EXPRESS.md +0 -456
  51. package/docs/example-versioning.js +0 -168
  52. package/docs/example.js +0 -195
  53. package/plugins/API guide.md +0 -364
  54. package/plugins/api-registry.js +0 -142
  55. package/plugins/api-versioning.md +0 -135
  56. package/plugins/http.js +0 -273
  57. package/plugins/logging.js +0 -255
  58. package/plugins/memory.js +0 -182
  59. package/plugins/mysql.js +0 -753
  60. package/plugins/openapi.js +0 -401
  61. package/plugins/positioning.js +0 -220
  62. package/plugins/security.js +0 -261
  63. package/plugins/validation.js +0 -173
  64. package/plugins/versioning.js +0 -288
  65. package/resource-helper.js +0 -79
  66. package/schema.js +0 -481
  67. package/test-basic.js +0 -107
@@ -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
+ };