@koziatek/http 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.
@@ -0,0 +1,26 @@
1
+ class HttpStatusCodes {
2
+
3
+ static continue = 100;
4
+ static ok = 200;
5
+ static created = 201;
6
+ static accepted = 202;
7
+ static noContent = 204;
8
+ static badRequest = 400;
9
+ static unauthorized = 401;
10
+ static forbidden = 403;
11
+ static notFound = 404;
12
+ static notAllowed = 405;
13
+ static notAccepted = 406;
14
+ static conflict = 409;
15
+ static teapot = 418;
16
+ static unprocessible = 422;
17
+ static tooManyAttempts = 429;
18
+ static internal = 500;
19
+ static notImplemented = 501;
20
+ static unavailable = 503;
21
+ }
22
+
23
+
24
+ module.exports = {
25
+ HttpStatusCodes,
26
+ }
package/index.js ADDED
@@ -0,0 +1,23 @@
1
+ const { HttpStatusCodes } = require("./HttpStatusCodes");
2
+ const { CatchAll } = require("./middleware/CatchAll");
3
+ const { ErrorRenderer } = require("./middleware/ErrorRender");
4
+ const { Next } = require("./middleware/Next");
5
+ const { OpenReq } = require("./middleware/OpenReq");
6
+ const { ResData } = require("./middleware/ResData");
7
+ const { SendRes } = require("./middleware/SendRes");
8
+ const { Status } = require("./middleware/Status");
9
+
10
+
11
+ module.exports = {
12
+ HttpStatusCodes,
13
+ middleware: {
14
+ CatchAll,
15
+ ErrorRenderer,
16
+ Next,
17
+ OpenReq,
18
+ ResData,
19
+ SendRes,
20
+ Status,
21
+ },
22
+
23
+ }
@@ -0,0 +1,38 @@
1
+ const { HttpStatusCodes } = require('../HttpStatusCodes.js');
2
+
3
+ /**
4
+ * Default response content for unimplemented routes.
5
+ * @constant {Object}
6
+ */
7
+ const DEFAULT_CONTENT = {
8
+ error: 'Not Implemented',
9
+ messages: [
10
+ 'The requested endpoint does not exist.'
11
+ ]
12
+ };
13
+
14
+ /**
15
+ * Express middleware generator for catching all unmatched routes.
16
+ *
17
+ * @function CatchAll
18
+ * @param {number} [status=HttpStatusCodes.notImplemented] - The HTTP status code to send (default is 501 Not Implemented).
19
+ * @param {Object} [content=DEFAULT_CONTENT] - The response content to send as JSON.
20
+ * @returns {function} Express middleware function to handle unmatched routes.
21
+ *
22
+ * @example
23
+ * const { CatchAll } = require('./middleware/CatchAll');
24
+ * app.use(CatchAll());
25
+ *
26
+ * @example
27
+ * // Custom status and message
28
+ * app.use(CatchAll(404, { error: "Not Found", messages: ["This page does not exist."] }));
29
+ */
30
+ function CatchAll(status = HttpStatusCodes.notImplemented, content = DEFAULT_CONTENT) {
31
+ return (req, res, next) => {
32
+ res.status(status).send(content);
33
+ };
34
+ }
35
+
36
+ module.exports = {
37
+ CatchAll
38
+ };
@@ -0,0 +1,26 @@
1
+ const { HttpStatusCodes } = require('../HttpStatusCodes.js');
2
+ const { Log, isFunction } = require('@precision-sustainable-ag/utils');
3
+
4
+ function ErrorRenderer(err, req, res, next) {
5
+ let response;
6
+ let statusCode = HttpStatusCodes.internal;
7
+
8
+ if(isFunction(err.getStatusCode)) statusCode = err.getStatusCode();
9
+ else if(err.statusCode) statusCode = err.statusCode;
10
+
11
+ if(isFunction(err?.toJSON)) response = err.toJSON();
12
+ else response = {
13
+ metadata: err?.metadata || false,
14
+ message: err?.message || false,
15
+ stack: err?.stack || false,
16
+ };
17
+
18
+ Log.Debug(err);
19
+ res.status(statusCode).json(response);
20
+ }
21
+
22
+
23
+
24
+ module.exports = {
25
+ ErrorRenderer
26
+ }
@@ -0,0 +1,10 @@
1
+
2
+
3
+
4
+
5
+ module.exports = { Next: (middleware) => {
6
+ return async (req,res,next) => {
7
+ await middleware(req,res);
8
+ next();
9
+ }
10
+ }};
@@ -0,0 +1,36 @@
1
+
2
+ function OpenReq(req){
3
+ return {
4
+ method: req.method, // GET, POST, etc.
5
+ url: req.url, // /api/resource
6
+ originalUrl: req.originalUrl, // Full URL (preserves route changes from middleware)
7
+ path: req.path, // Just the pathname, no query string
8
+ query: req.query, // Parsed query string as an object
9
+ params: req.params, // URL route params (e.g., /:id)
10
+ headers: req.headers, // Raw headers object
11
+ ip: req.ip, // Client IP
12
+ ips: req.ips, // If using `trust proxy`
13
+ hostname: req.hostname, // Hostname of the request
14
+ protocol: req.protocol, // http or https
15
+ secure: req.secure, // True if HTTPS
16
+ xhr: req.xhr, // True if requested via AJAX
17
+ body: req.body, // Parsed body (requires middleware)
18
+ cookies: req.cookies, // Parsed cookies (requires cookie-parser)
19
+ fresh: req.fresh, // For caching
20
+ stale: req.stale, // Opposite of fresh
21
+ subdomains: req.subdomains, // Subdomains in the host
22
+ route: req.route, // Current route info (after matching)
23
+ baseUrl: req.baseUrl, // URL path on which a router was mounted
24
+ user: req.user, // If using authentication middleware (e.g. Passport)
25
+ };
26
+ }
27
+
28
+ function AttachReqMeta(req,res,next){
29
+ req.metadata = OpenReq(req);
30
+ return next();
31
+ }
32
+
33
+ module.exports = {
34
+ OpenReq,
35
+ AttachReqMeta,
36
+ }
@@ -0,0 +1,8 @@
1
+
2
+
3
+ module.exports = { ResData: (middleware) => {
4
+ return async (req,res,next) => {
5
+ res.data = await middleware(req,res);
6
+ next();
7
+ }
8
+ }};
@@ -0,0 +1,14 @@
1
+
2
+
3
+
4
+
5
+ module.exports = { SendRes: (data) => {
6
+ return async (req,res,next) => {
7
+ if(typeof data === 'function') data = await data(req,res,next);
8
+ else if (typeof data === 'undefined') data = res?.data;
9
+
10
+ if(typeof data === 'undefined') throw new Error('Could not resolve response data.');
11
+
12
+ return res.send(data);
13
+ }
14
+ }};
@@ -0,0 +1,10 @@
1
+
2
+
3
+
4
+
5
+ module.exports = { Status: (statusCode) => {
6
+ return async (req,res,next) => {
7
+ res.status(statusCode);
8
+ return next();
9
+ }
10
+ }};
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@koziatek/http",
3
+ "version": "1.0.1",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "scripts": {
10
+ "test": "jest"
11
+ },
12
+ "keywords": [],
13
+ "author": "",
14
+ "license": "ISC",
15
+ "dependencies": {
16
+ "@precision-sustainable-ag/utils": "^1.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "jest": "^29.7.0"
20
+ }
21
+ }
@@ -0,0 +1,44 @@
1
+ const { CatchAll } = require('../middleware/CatchAll');
2
+ const { HttpStatusCodes } = require('../HttpStatusCodes');
3
+
4
+ describe('CatchAll middleware', () => {
5
+ let req, res, next;
6
+
7
+ beforeEach(() => {
8
+ req = {};
9
+ res = {
10
+ status: jest.fn().mockReturnThis(),
11
+ send: jest.fn()
12
+ };
13
+ next = jest.fn();
14
+ });
15
+
16
+ test('should respond with default 501 and default content', () => {
17
+ const middleware = CatchAll();
18
+
19
+ middleware(req, res, next);
20
+
21
+ expect(res.status).toHaveBeenCalledWith(HttpStatusCodes.notImplemented);
22
+ expect(res.send).toHaveBeenCalledWith({
23
+ error: 'Not Implemented',
24
+ messages: ['The requested endpoint does not exist.']
25
+ });
26
+ expect(next).not.toHaveBeenCalled();
27
+ });
28
+
29
+ test('should respond with custom status and content', () => {
30
+ const customStatus = 404;
31
+ const customContent = {
32
+ error: 'Not Found',
33
+ messages: ['This page does not exist.']
34
+ };
35
+
36
+ const middleware = CatchAll(customStatus, customContent);
37
+
38
+ middleware(req, res, next);
39
+
40
+ expect(res.status).toHaveBeenCalledWith(customStatus);
41
+ expect(res.send).toHaveBeenCalledWith(customContent);
42
+ expect(next).not.toHaveBeenCalled();
43
+ });
44
+ });
@@ -0,0 +1,44 @@
1
+ const { Next } = require('../middleware/Next');
2
+
3
+ describe('Next wrapper middleware', () => {
4
+ let req, res, next;
5
+
6
+ beforeEach(() => {
7
+ req = {};
8
+ res = {};
9
+ next = jest.fn();
10
+ });
11
+
12
+ test('should call the wrapped middleware and then next()', async () => {
13
+ const mockMiddleware = jest.fn();
14
+
15
+ const wrapped = Next(mockMiddleware);
16
+
17
+ await wrapped(req, res, next);
18
+
19
+ expect(mockMiddleware).toHaveBeenCalledWith(req, res);
20
+ expect(next).toHaveBeenCalled();
21
+ });
22
+
23
+ test('should await async middleware and then call next()', async () => {
24
+ const asyncMiddleware = jest.fn().mockResolvedValue();
25
+
26
+ const wrapped = Next(asyncMiddleware);
27
+
28
+ await wrapped(req, res, next);
29
+
30
+ expect(asyncMiddleware).toHaveBeenCalledWith(req, res);
31
+ expect(next).toHaveBeenCalled();
32
+ });
33
+
34
+ test('should not call next() if middleware throws', async () => {
35
+ const throwingMiddleware = jest.fn().mockImplementation(() => {
36
+ throw new Error('Failure');
37
+ });
38
+
39
+ const wrapped = Next(throwingMiddleware);
40
+
41
+ await expect(wrapped(req, res, next)).rejects.toThrow('Failure');
42
+ expect(next).not.toHaveBeenCalled();
43
+ });
44
+ });
@@ -0,0 +1,90 @@
1
+ const { OpenReq, AttachReqMeta } = require('../middleware/OpenReq.js'); // Adjust path as needed
2
+
3
+ describe('OpenReq', () => {
4
+ test('should extract all relevant metadata from request object', () => {
5
+ const mockReq = {
6
+ method: 'POST',
7
+ url: '/api/test',
8
+ originalUrl: '/api/test?query=true',
9
+ path: '/api/test',
10
+ query: { query: 'true' },
11
+ params: { id: '123' },
12
+ headers: { 'x-custom-header': 'value' },
13
+ ip: '127.0.0.1',
14
+ ips: ['127.0.0.1'],
15
+ hostname: 'localhost',
16
+ protocol: 'https',
17
+ secure: true,
18
+ xhr: true,
19
+ body: { name: 'test' },
20
+ cookies: { session: 'abc123' },
21
+ fresh: true,
22
+ stale: false,
23
+ subdomains: ['api'],
24
+ route: { path: '/test/:id' },
25
+ baseUrl: '/api',
26
+ user: { id: 1, role: 'admin' }
27
+ };
28
+
29
+ const result = OpenReq(mockReq);
30
+
31
+ expect(result).toEqual({
32
+ method: 'POST',
33
+ url: '/api/test',
34
+ originalUrl: '/api/test?query=true',
35
+ path: '/api/test',
36
+ query: { query: 'true' },
37
+ params: { id: '123' },
38
+ headers: { 'x-custom-header': 'value' },
39
+ ip: '127.0.0.1',
40
+ ips: ['127.0.0.1'],
41
+ hostname: 'localhost',
42
+ protocol: 'https',
43
+ secure: true,
44
+ xhr: true,
45
+ body: { name: 'test' },
46
+ cookies: { session: 'abc123' },
47
+ fresh: true,
48
+ stale: false,
49
+ subdomains: ['api'],
50
+ route: { path: '/test/:id' },
51
+ baseUrl: '/api',
52
+ user: { id: 1, role: 'admin' }
53
+ });
54
+ });
55
+ });
56
+
57
+ describe('AttachReqMeta middleware', () => {
58
+ test('should attach metadata to req and call next()', () => {
59
+ const req = {
60
+ method: 'GET',
61
+ url: '/',
62
+ originalUrl: '/',
63
+ path: '/',
64
+ query: {},
65
+ params: {},
66
+ headers: {},
67
+ ip: '127.0.0.1',
68
+ ips: [],
69
+ hostname: 'localhost',
70
+ protocol: 'http',
71
+ secure: false,
72
+ xhr: false,
73
+ body: {},
74
+ cookies: {},
75
+ fresh: false,
76
+ stale: true,
77
+ subdomains: [],
78
+ route: {},
79
+ baseUrl: '',
80
+ user: null
81
+ };
82
+ const res = {};
83
+ const next = jest.fn();
84
+
85
+ AttachReqMeta(req, res, next);
86
+
87
+ expect(req.metadata).toEqual(OpenReq(req));
88
+ expect(next).toHaveBeenCalled();
89
+ });
90
+ });
@@ -0,0 +1,43 @@
1
+ const { ResData } = require('../middleware/ResData'); // Adjust the path as needed
2
+
3
+ describe('ResData wrapper middleware', () => {
4
+ let req, res, next;
5
+
6
+ beforeEach(() => {
7
+ req = {};
8
+ res = {};
9
+ next = jest.fn();
10
+ });
11
+
12
+ test('should await middleware and assign result to res.data, then call next()', async () => {
13
+ const mockMiddleware = jest.fn().mockResolvedValue({ key: 'value' });
14
+
15
+ const wrapped = ResData(mockMiddleware);
16
+
17
+ await wrapped(req, res, next);
18
+
19
+ expect(mockMiddleware).toHaveBeenCalledWith(req, res);
20
+ expect(res.data).toEqual({ key: 'value' });
21
+ expect(next).toHaveBeenCalled();
22
+ });
23
+
24
+ test('should handle async middleware that returns undefined', async () => {
25
+ const mockMiddleware = jest.fn().mockResolvedValue(undefined);
26
+
27
+ const wrapped = ResData(mockMiddleware);
28
+
29
+ await wrapped(req, res, next);
30
+
31
+ expect(res.data).toBeUndefined();
32
+ expect(next).toHaveBeenCalled();
33
+ });
34
+
35
+ test('should not call next() if middleware throws', async () => {
36
+ const mockMiddleware = jest.fn().mockRejectedValue(new Error('Failure'));
37
+
38
+ const wrapped = ResData(mockMiddleware);
39
+
40
+ await expect(wrapped(req, res, next)).rejects.toThrow('Failure');
41
+ expect(next).not.toHaveBeenCalled();
42
+ });
43
+ });
@@ -0,0 +1,52 @@
1
+ const { SendRes } = require('../middleware/SendRes.js'); // Adjust path as needed
2
+
3
+ describe('SendRes middleware', () => {
4
+ let req, res, next;
5
+
6
+ beforeEach(() => {
7
+ req = {};
8
+ res = {
9
+ data: undefined,
10
+ send: jest.fn()
11
+ };
12
+ next = jest.fn();
13
+ });
14
+
15
+ test('should send data if data is a value', async () => {
16
+ const value = { hello: 'world' };
17
+
18
+ const middleware = SendRes(value);
19
+
20
+ await middleware(req, res, next);
21
+
22
+ expect(res.send).toHaveBeenCalledWith(value);
23
+ });
24
+
25
+ test('should call function and send result if data is a function', async () => {
26
+ const mockFn = jest.fn().mockResolvedValue({ message: 'from fn' });
27
+
28
+ const middleware = SendRes(mockFn);
29
+
30
+ await middleware(req, res, next);
31
+
32
+ expect(mockFn).toHaveBeenCalledWith(req, res, next);
33
+ expect(res.send).toHaveBeenCalledWith({ message: 'from fn' });
34
+ });
35
+
36
+ test('should use res.data if data is undefined', async () => {
37
+ res.data = { default: 'response' };
38
+
39
+ const middleware = SendRes(undefined);
40
+
41
+ await middleware(req, res, next);
42
+
43
+ expect(res.send).toHaveBeenCalledWith(res.data);
44
+ });
45
+
46
+ test('should throw error if no data is resolved', async () => {
47
+ const middleware = SendRes(undefined);
48
+
49
+ await expect(middleware(req, res, next)).rejects.toThrow('Could not resolve response data.');
50
+ expect(res.send).not.toHaveBeenCalled();
51
+ });
52
+ });
@@ -0,0 +1,31 @@
1
+ const { Status } = require('../middleware/Status.js'); // Adjust path as needed
2
+
3
+ describe('Status middleware', () => {
4
+ let req, res, next;
5
+
6
+ beforeEach(() => {
7
+ req = {};
8
+ res = {
9
+ status: jest.fn()
10
+ };
11
+ next = jest.fn();
12
+ });
13
+
14
+ test('should set the given status code and call next()', async () => {
15
+ const middleware = Status(418); // I'm a teapot ☕
16
+
17
+ await middleware(req, res, next);
18
+
19
+ expect(res.status).toHaveBeenCalledWith(418);
20
+ expect(next).toHaveBeenCalled();
21
+ });
22
+
23
+ test('should work with standard HTTP status codes', async () => {
24
+ const middleware = Status(200);
25
+
26
+ await middleware(req, res, next);
27
+
28
+ expect(res.status).toHaveBeenCalledWith(200);
29
+ expect(next).toHaveBeenCalled();
30
+ });
31
+ });