json-rest-api 1.0.6 → 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.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +99 -33
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /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
|
-
};
|