json-rest-api 1.0.7 → 1.0.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 (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +90 -32
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -1,120 +0,0 @@
1
- /**
2
- * http-plugin-express.js
3
- * This plugin for Hooked API provides an HTTP layer using the Express.js framework.
4
- * It automatically generates JSON:API compliant RESTful routes for each scope
5
- * added to the API instance.
6
- */
7
- import { HookedApiError } from './hooked-api.js';
8
- import { handleApiError } from './error-handler.js';
9
-
10
- export const HttpExpressPlugin = {
11
- name: 'http-express',
12
-
13
- /**
14
- * Installs the HTTP plugin, connecting the API to an Express app.
15
- * @param {object} context - The installation context from Hooked API.
16
- * @param {object} context.pluginOptions - Options passed during `api.use()`.
17
- * @param {Express.Application} context.pluginOptions.http.app - The Express app instance. (Required)
18
- * @param {string} [context.pluginOptions.http.prefix='/api'] - The URL prefix for all API routes.
19
- * @param {Api} api - The API instance itself.
20
- */
21
- install(context, api) {
22
- const { pluginOptions, log } = context;
23
- const httpOptions = pluginOptions.http || {};
24
-
25
- const { app, prefix = '/api' } = httpOptions;
26
-
27
- if (!app) {
28
- throw new HookedApiError(
29
- "The 'http-express' plugin requires an Express 'app' instance to be passed in the options. Example: api.use(HttpExpressPlugin, { http: { app: myExpressApp } })",
30
- 'CONFIGURATION_ERROR'
31
- );
32
- }
33
-
34
- log.info(`Express HTTP plugin installed. Routes will be prefixed with '${prefix}'.`);
35
-
36
- /**
37
- * Creates a generic Express request handler for a specific API method.
38
- * This factory function avoids code duplication for route handlers.
39
- * @param {string} methodName - The name of the scope method to call (e.g., 'get', 'query').
40
- * @returns {Function} An async Express route handler.
41
- */
42
- const _createRequestHandler = (methodName) => {
43
- // This is an async function that Express 5 will handle correctly,
44
- // automatically catching promise rejections and passing them to the error handler.
45
- return async (req, res, next) => {
46
- const { scopeName, id } = req.params;
47
-
48
- try {
49
- // 1. Translate Express req into the API method's specific params object.
50
- const apiParams = {};
51
-
52
- if (['get', 'put', 'patch', 'delete'].includes(methodName)) {
53
- apiParams.id = id;
54
- }
55
- if (['query', 'get', 'post', 'put', 'patch'].includes(methodName)) {
56
- // Express automatically parses simple query strings. For nested objects
57
- // like `fields[articles]`, ensure the 'qs' query parser is enabled in your main app.
58
- apiParams.queryParams = req.query;
59
- }
60
- if (['post', 'put', 'patch'].includes(methodName)) {
61
- // Assumes `express.json()` body-parser middleware is used on the app.
62
- apiParams.inputRecord = req.body;
63
- }
64
-
65
- log.debug(`Express request for ${scopeName}.${methodName}`, { id, query: req.query });
66
-
67
- // 2. Execute the corresponding API method
68
- const result = await api.scopes[scopeName][methodName](apiParams);
69
-
70
- // 3. Send the successful HTTP response
71
- switch (methodName) {
72
- case 'post':
73
- res.status(201).json(result);
74
- break;
75
- case 'delete':
76
- res.status(204).send();
77
- break;
78
- default:
79
- res.status(200).json(result);
80
- }
81
- } catch (error) {
82
- // 4. Pass any errors to Express's next-tick error handler.
83
- next(error);
84
- }
85
- };
86
- };
87
-
88
- /**
89
- * Creates all the standard RESTful routes for a given scope.
90
- * @param {string} scopeName - The name of the scope to create routes for.
91
- */
92
- const _createRoutesForScope = (scopeName) => {
93
- // Use a new router for each scope to keep things clean.
94
- const router = express.Router();
95
- const basePath = `/${scopeName}`;
96
- const singlePath = `${basePath}/:id`;
97
-
98
- log.info(`Creating Express HTTP routes for scope: '${scopeName}'`);
99
-
100
- router.get('/', _createRequestHandler('query'));
101
- router.get('/:id', _createRequestHandler('get'));
102
- router.post('/', _createRequestHandler('post'));
103
- router.put('/:id', _createRequestHandler('put'));
104
- router.patch('/:id', _createRequestHandler('patch'));
105
- router.delete('/:id', _createRequestHandler('delete'));
106
-
107
- // Mount the scope's router on the main app at the correct prefix.
108
- app.use(prefix, router);
109
- };
110
-
111
- // --- Main Logic: Wrap `addScope` to automate route creation ---
112
- const originalAddScope = api.addScope;
113
-
114
- api.addScope = (name, options, extras) => {
115
- const result = originalAddScope.call(api, name, options, extras);
116
- _createRoutesForScope(name);
117
- return result;
118
- };
119
- }
120
- };
@@ -1,138 +0,0 @@
1
- /**
2
- * http-plugin.js
3
- * * This plugin for Hooked API provides a lightweight HTTP layer using Node.js's native `http` module.
4
- * It creates a server and automatically generates JSON:API compliant RESTful routes for each scope.
5
- *
6
- * It uses the 'qs' library to parse complex query strings, just like Express.
7
- * You'll need to install it: npm install qs
8
- */
9
- import http from 'http';
10
- import qs from 'qs'; // The same query string parser Express uses
11
- import { HookedApiError } from './hooked-api.js';
12
- import { handleApiError } from './error-handler.js';
13
-
14
- /**
15
- * A helper function to parse the JSON body from an incoming request.
16
- * @param {http.IncomingMessage} req - The request object.
17
- * @returns {Promise<object|null>} A promise that resolves with the parsed JSON object or null.
18
- */
19
- async function parseJsonBody(req) {
20
- return new Promise((resolve, reject) => {
21
- if (req.method !== 'POST' && req.method !== 'PUT' && req.method !== 'PATCH') {
22
- return resolve(null);
23
- }
24
-
25
- let body = '';
26
- req.on('data', chunk => {
27
- body += chunk.toString();
28
- });
29
- req.on('end', () => {
30
- try {
31
- resolve(body ? JSON.parse(body) : null);
32
- } catch (error) {
33
- reject(new HookedApiError('Invalid JSON in request body.', 'BAD_REQUEST'));
34
- }
35
- });
36
- req.on('error', reject);
37
- });
38
- }
39
-
40
- export const HttpPlugin = {
41
- name: 'http',
42
-
43
- install(context, api) {
44
- const { pluginOptions, log } = context;
45
- const httpOptions = pluginOptions.http || {};
46
- const { port = 3000, prefix = '/api' } = httpOptions;
47
-
48
- // This map will store our route handlers
49
- const routes = new Map();
50
-
51
- // A simple regex to match our API routes
52
- // It captures the scope name and an optional ID
53
- const routeRegex = new RegExp(`^${prefix}/([a-zA-Z0-9_-]+)(?:/([a-zA-Z0-9_-]+))?/?$`);
54
-
55
- /**
56
- * The main request handler for the http.Server.
57
- * This function acts as our router.
58
- */
59
- const requestListener = async (req, res) => {
60
- const { url, method } = req;
61
- const [path, queryString] = url.split('?');
62
-
63
- const match = path.match(routeRegex);
64
-
65
- if (!match) {
66
- res.writeHead(404, { 'Content-Type': 'application/json' });
67
- return res.end(JSON.stringify({ errors: [{ status: '404', title: 'Not Found' }] }));
68
- }
69
-
70
- const [, scopeName, id] = match;
71
-
72
- // Determine the API method based on the HTTP verb and presence of an ID
73
- let apiMethodName;
74
- if (method === 'GET') {
75
- apiMethodName = id ? 'get' : 'query';
76
- } else if (method === 'POST' && !id) {
77
- apiMethodName = 'post';
78
- } else if (method === 'PUT' && id) {
79
- apiMethodName = 'put';
80
- } else if (method === 'PATCH' && id) {
81
- apiMethodName = 'patch';
82
- } else if (method === 'DELETE' && id) {
83
- apiMethodName = 'delete';
84
- }
85
-
86
- if (!apiMethodName || !api.scopes[scopeName]) {
87
- res.writeHead(404, { 'Content-Type': 'application/json' });
88
- return res.end(JSON.stringify({ errors: [{ status: '404', title: 'Not Found' }] }));
89
- }
90
-
91
- try {
92
- // 1. Translate the native request into API method params
93
- const apiParams = {};
94
-
95
- // Use 'qs' to parse the query string into a nested object
96
- apiParams.queryParams = queryString ? qs.parse(queryString) : {};
97
- apiParams.inputRecord = await parseJsonBody(req);
98
- if (id) {
99
- apiParams.id = id;
100
- }
101
-
102
- log.debug(`HTTP request for ${scopeName}.${apiMethodName}`, { id, query: apiParams.queryParams });
103
-
104
- // 2. Execute the corresponding API method
105
- const result = await api.scopes[scopeName][apiMethodName](apiParams);
106
-
107
- // 3. Send the successful HTTP response
108
- let statusCode = 200;
109
- let responseBody = JSON.stringify(result);
110
-
111
- if (apiMethodName === 'post') {
112
- statusCode = 201;
113
- } else if (apiMethodName === 'delete') {
114
- statusCode = 204;
115
- responseBody = null;
116
- }
117
-
118
- res.setHeader('Content-Type', 'application/json');
119
- res.writeHead(statusCode);
120
- res.end(responseBody);
121
-
122
- } catch (error) {
123
- // 4. Handle any errors thrown by the API
124
- // Note: The error handler now needs the `res` object directly
125
- handleApiError(error, res, log);
126
- }
127
- };
128
-
129
- // Create and start the HTTP server
130
- const server = http.createServer(requestListener);
131
- server.listen(port, () => {
132
- log.info(`HTTP server started on http://localhost:${port}`);
133
- });
134
-
135
- // We no longer need to wrap addScope, as the router is dynamic.
136
- // The plugin's job is simply to start the server.
137
- }
138
- };
@@ -1,366 +0,0 @@
1
- import express from 'express';
2
-
3
- /**
4
- * Express Plugin for Hooked API
5
- *
6
- * This plugin creates HTTP endpoints for your REST API by translating
7
- * Express HTTP requests into hooked-api method calls.
8
- *
9
- * Features:
10
- * - Automatic route creation for all scopes (including dynamically added ones)
11
- * - JSON:API compliant request/response handling
12
- * - Query parameter parsing (include, fields, filter, sort, page)
13
- * - Error mapping to HTTP status codes
14
- * - Content type validation
15
- * - Middleware injection points
16
- *
17
- * Usage:
18
- * ```javascript
19
- * import { ExpressPlugin } from './express-plugin.js';
20
- *
21
- * const api = new Api({ name: 'my-api', version: '1.0.0' });
22
- * api.use(RestApiPlugin);
23
- * api.use(ExpressPlugin, {
24
- * basePath: '/api', // Default: '/api'
25
- * strictContentType: true, // Default: true
26
- * requestSizeLimit: '10mb' // Default: '1mb'
27
- * });
28
- *
29
- * // In your Express app:
30
- * app.use(api.express.router);
31
- * // or
32
- * api.express.mount(app, '/v1');
33
- * ```
34
- */
35
-
36
- export const ExpressPlugin = {
37
- name: 'express',
38
- dependencies: ['rest-api'],
39
-
40
- install({ on, vars, helpers, pluginOptions, log, scopes, api }) {
41
- const expressOptions = pluginOptions.express || {};
42
- const basePath = expressOptions.basePath || '/api';
43
- const strictContentType = expressOptions.strictContentType !== false;
44
- const requestSizeLimit = expressOptions.requestSizeLimit || '1mb';
45
-
46
- // Track which scopes have routes created
47
- const routesCreated = new Set();
48
-
49
- // Create Express router
50
- const router = expressOptions.router || express.Router();
51
-
52
- // Add body parsing middleware
53
- router.use(express.json({
54
- limit: requestSizeLimit,
55
- type: ['application/json', 'application/vnd.api+json']
56
- }));
57
-
58
- // Content type validation middleware
59
- if (strictContentType) {
60
- router.use((req, res, next) => {
61
- // Only validate for requests with bodies
62
- if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
63
- const contentType = req.get('Content-Type');
64
-
65
- if (contentType && !contentType.includes('application/vnd.api+json') && !contentType.includes('application/json')) {
66
- return res.status(415).json({
67
- errors: [{
68
- status: '415',
69
- title: 'Unsupported Media Type',
70
- detail: `Content-Type must be application/vnd.api+json or application/json`
71
- }]
72
- });
73
- }
74
- }
75
- next();
76
- });
77
- }
78
-
79
- /**
80
- * Query parameter parser
81
- * Handles both standard notation (filter.status) and bracket notation (filter[status])
82
- */
83
- const parseQueryParams = (req) => {
84
- const query = req.query;
85
- const queryParams = {};
86
-
87
- // Parse include (comma-separated string to array)
88
- if (query.include) {
89
- queryParams.include = query.include.split(',').map(s => s.trim());
90
- }
91
-
92
- // Parse fields (nested object)
93
- queryParams.fields = {};
94
-
95
- // Parse filter (object)
96
- queryParams.filters = {};
97
-
98
- // Parse page (object)
99
- queryParams.page = {};
100
-
101
- // Process all query parameters
102
- for (const key in query) {
103
- const value = query[key];
104
-
105
- // Handle bracket notation: fields[articles]=title,body
106
- let match;
107
- if ((match = key.match(/^fields\[(.+)\]$/))) {
108
- queryParams.fields[match[1]] = value;
109
- } else if ((match = key.match(/^filter\[(.+)\]$/))) {
110
- queryParams.filters[match[1]] = value;
111
- } else if ((match = key.match(/^page\[(.+)\]$/))) {
112
- queryParams.page[match[1]] = value;
113
- }
114
- }
115
-
116
- // Handle object notation if express query parser is configured
117
- if (typeof query.fields === 'object' && !Array.isArray(query.fields)) {
118
- Object.assign(queryParams.fields, query.fields);
119
- }
120
- if (typeof query.filters === 'object' && !Array.isArray(query.filters)) {
121
- Object.assign(queryParams.filters, query.filters);
122
- }
123
- if (typeof query.page === 'object' && !Array.isArray(query.page)) {
124
- Object.assign(queryParams.page, query.page);
125
- }
126
-
127
- // Parse sort (comma-separated string to array)
128
- if (query.sort) {
129
- queryParams.sort = query.sort.split(',').map(s => s.trim());
130
- }
131
-
132
- // Clean up empty objects
133
- if (Object.keys(queryParams.fields).length === 0) delete queryParams.fields;
134
- if (Object.keys(queryParams.filters).length === 0) delete queryParams.filters;
135
- if (Object.keys(queryParams.page).length === 0) delete queryParams.page;
136
-
137
- return queryParams;
138
- };
139
-
140
- /**
141
- * Error handler - maps API errors to HTTP responses
142
- */
143
- const handleError = (error, res) => {
144
- let status = 500;
145
- let errorResponse = {
146
- errors: [{
147
- status: '500',
148
- title: 'Internal Server Error',
149
- detail: error.message
150
- }]
151
- };
152
-
153
- // Check for RestApiError types by code
154
- if (error.code) {
155
- switch (error.code) {
156
- case 'REST_API_VALIDATION':
157
- status = 422;
158
- errorResponse.errors = [{
159
- status: '422',
160
- title: 'Validation Error',
161
- detail: error.message,
162
- source: error.details
163
- }];
164
- if (error.violations) {
165
- errorResponse.errors = error.violations.map(v => ({
166
- status: '422',
167
- title: 'Validation Error',
168
- detail: v.message,
169
- source: { pointer: v.field }
170
- }));
171
- }
172
- break;
173
-
174
- case 'REST_API_RESOURCE':
175
- // Map subtype to status
176
- switch (error.subtype) {
177
- case 'not_found':
178
- status = 404;
179
- errorResponse.errors[0].status = '404';
180
- errorResponse.errors[0].title = 'Not Found';
181
- break;
182
- case 'conflict':
183
- status = 409;
184
- errorResponse.errors[0].status = '409';
185
- errorResponse.errors[0].title = 'Conflict';
186
- break;
187
- case 'forbidden':
188
- status = 403;
189
- errorResponse.errors[0].status = '403';
190
- errorResponse.errors[0].title = 'Forbidden';
191
- break;
192
- default:
193
- status = 400;
194
- errorResponse.errors[0].status = '400';
195
- errorResponse.errors[0].title = 'Bad Request';
196
- }
197
- break;
198
-
199
- case 'REST_API_PAYLOAD':
200
- status = 400;
201
- errorResponse.errors = [{
202
- status: '400',
203
- title: 'Bad Request',
204
- detail: error.message,
205
- source: { pointer: error.path }
206
- }];
207
- break;
208
- }
209
- }
210
-
211
- res.status(status).json(errorResponse);
212
- };
213
-
214
- /**
215
- * Creates request handler for a specific HTTP method
216
- */
217
- const createRequestHandler = (scopeName, methodName) => {
218
- return async (req, res) => {
219
- try {
220
- const params = {};
221
-
222
- // Add ID parameter for single-resource operations
223
- if (['get', 'put', 'patch', 'delete'].includes(methodName)) {
224
- params.id = req.params.id;
225
- }
226
-
227
- // Add query parameters
228
- if (['query', 'get'].includes(methodName)) {
229
- params.queryParams = parseQueryParams(req);
230
- }
231
-
232
- // Add body for mutations
233
- if (['post', 'put', 'patch'].includes(methodName)) {
234
- params.inputRecord = req.body;
235
-
236
- // For PUT and PATCH, also parse query params
237
- if (['put', 'patch'].includes(methodName)) {
238
- params.queryParams = parseQueryParams(req);
239
- }
240
- }
241
-
242
- // Store Express request/response in params for plugins that need it
243
- params._expressReq = req;
244
- params._expressRes = res;
245
-
246
- log.debug(`HTTP ${req.method} ${req.path}`, { scopeName, methodName, params });
247
-
248
- // Call the API method
249
- const scope = scopes[scopeName];
250
- if (!scope) {
251
- return handleError({
252
- code: 'REST_API_RESOURCE',
253
- subtype: 'not_found',
254
- message: `Scope '${scopeName}' not found`
255
- }, res);
256
- }
257
-
258
- const result = await scope[methodName](params);
259
-
260
- // Send response
261
- res.set('Content-Type', 'application/vnd.api+json');
262
-
263
- switch (methodName) {
264
- case 'post':
265
- res.status(201).json(result);
266
- break;
267
- case 'delete':
268
- res.sendStatus(204);
269
- break;
270
- default:
271
- res.json(result);
272
- }
273
- } catch (error) {
274
- handleError(error, res);
275
- }
276
- };
277
- };
278
-
279
- /**
280
- * Creates routes for a scope
281
- */
282
- const createRoutesForScope = (scopeName) => {
283
- if (routesCreated.has(scopeName)) {
284
- log.trace(`Routes already created for scope '${scopeName}'`);
285
- return;
286
- }
287
-
288
- const scopePath = `${basePath}/${scopeName}`;
289
-
290
- log.info(`Creating Express routes for scope '${scopeName}' at ${scopePath}`);
291
-
292
- // Apply any before middleware
293
- const beforeMiddleware = expressOptions.middleware?.beforeScope?.[scopeName] || [];
294
-
295
- // GET /api/{scope} - Query collection
296
- router.get(scopePath, ...beforeMiddleware, createRequestHandler(scopeName, 'query'));
297
-
298
- // GET /api/{scope}/{id} - Get single resource
299
- router.get(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'get'));
300
-
301
- // POST /api/{scope} - Create resource
302
- router.post(scopePath, ...beforeMiddleware, createRequestHandler(scopeName, 'post'));
303
-
304
- // PUT /api/{scope}/{id} - Replace resource
305
- router.put(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'put'));
306
-
307
- // PATCH /api/{scope}/{id} - Update resource
308
- router.patch(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'patch'));
309
-
310
- // DELETE /api/{scope}/{id} - Delete resource
311
- router.delete(`${scopePath}/:id`, ...beforeMiddleware, createRequestHandler(scopeName, 'delete'));
312
-
313
- // Apply any after middleware
314
- const afterMiddleware = expressOptions.middleware?.afterScope?.[scopeName] || [];
315
- if (afterMiddleware.length > 0) {
316
- router.use(scopePath, ...afterMiddleware);
317
- }
318
-
319
- routesCreated.add(scopeName);
320
- };
321
-
322
- // Listen for scope additions
323
- addHook('scope:added', 'createExpressRoutes', {}, ({ context }) => {
324
- createRoutesForScope(context.scopeName);
325
- });
326
-
327
- // Create routes for any existing scopes
328
- for (const scopeName of Object.keys(scopes)) {
329
- createRoutesForScope(scopeName);
330
- }
331
-
332
- // 404 handler for unmatched routes within the API
333
- router.use((req, res) => {
334
- res.status(404).json({
335
- errors: [{
336
- status: '404',
337
- title: 'Not Found',
338
- detail: `The requested endpoint ${req.method} ${req.path} does not exist`
339
- }]
340
- });
341
- });
342
-
343
- // Apply global middleware if configured
344
- let finalRouter = router;
345
- if (expressOptions.middleware?.beforeAll) {
346
- const globalRouter = express.Router();
347
- globalRouter.use(...expressOptions.middleware.beforeAll);
348
- globalRouter.use(router);
349
- finalRouter = globalRouter;
350
- }
351
-
352
- // Initialize express namespace
353
- api.express = {};
354
-
355
- // Store router in express namespace
356
- api.express.router = finalRouter;
357
-
358
- // Add convenient mounting method
359
- api.express.mount = (app, path = '') => {
360
- app.use(path, finalRouter);
361
- log.info(`Express routes mounted at ${path || '/'}`);
362
- };
363
-
364
- log.info('Express plugin initialized successfully');
365
- }
366
- };