minimonolith 0.1.0

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/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { serviceHandler } from './src/serviceHanlder/serviceHandler.js';
2
+
3
+ export { serviceHandler };
package/jest.config.js ADDED
@@ -0,0 +1,4 @@
1
+ export default {
2
+ testEnvironment: 'jest-environment-node',
3
+ transform: {}
4
+ };
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "minimonolith",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "main": "index.js",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "start:development": "nodemon --inspect=0.0.0.0 ./server.js",
9
+ "test": "NODE_OPTIONS=--experimental-vm-modules npx jest --coverage"
10
+ },
11
+ "dependencies": {
12
+ "lambda-api": "^1.0.1",
13
+ "mysql2": "^3.2.0",
14
+ "sequelize": "^6.30.0",
15
+ "zod": "^3.21.4"
16
+ },
17
+ "devDependencies": {
18
+ "jest": "^29.5.0",
19
+ "@aws-sdk/client-s3": "^3.304.0",
20
+ "@aws-sdk/s3-request-presigner": "^3.304.0",
21
+ "sqlite3": "^5.1.6"
22
+ }
23
+ }
@@ -0,0 +1,87 @@
1
+ import Sequelize from 'sequelize';
2
+ //import SequelizeDynamo from 'dynamo-sequelize';
3
+ import { loadModels } from './modelsHandler.js';
4
+
5
+ const db = process.env.DB_DB
6
+ const user = process.env.DB_USER;
7
+ const pass = process.env.DB_PASS;
8
+ const host = process.env.DB_ENDPOINT;
9
+ const dialect = 'mysql';
10
+ const logging = false;
11
+ const dialectOptions = null;
12
+ const ROUTE_CODE = 'DB_CONNECTION';
13
+ let sequelize = null;
14
+ let MODELS = null;
15
+
16
+ const tryAuthenticating = async (connectionRetries) => {
17
+ if (!process.env.TEST_ENVIRONMENT)
18
+ console.log(ROUTE_CODE, 'Authenticating sequelize intent:', connectionRetries);
19
+ await sequelize.authenticate();
20
+ };
21
+
22
+ const createSequelizeInstance = () => {
23
+ if (process.env.TEST_ENVIRONMENT) {
24
+ sequelize = new Sequelize('sqlite::memory:');
25
+ sequelize.options.logging = false;
26
+ } else {
27
+ console.log(ROUTE_CODE, 'Trying to connect using { ' + user + ', ' + pass + ', ' + host + ', ' + db + ' }');
28
+ sequelize = new Sequelize(db, user, pass, { host, dialect, dialectOptions, logging });
29
+ }
30
+ };
31
+
32
+
33
+ const establishConnection = async () => {
34
+ const MAX_RETRIES = 5;
35
+ const INITIAL_WAIT_TIME_MS = 100;
36
+ let connectionRetries = 0;
37
+ let waitTime = INITIAL_WAIT_TIME_MS;
38
+
39
+ while (connectionRetries < MAX_RETRIES) {
40
+ try {
41
+ if (!sequelize) createSequelizeInstance();
42
+ await tryAuthenticating(connectionRetries);
43
+ break;
44
+ } catch (databaseConnectionError) {
45
+ await new Promise(resolve => setTimeout(resolve, waitTime));
46
+ waitTime *= 2; connectionRetries += 1;
47
+ createSequelizeInstance();
48
+ }
49
+ }
50
+
51
+ return sequelize;
52
+ };
53
+
54
+
55
+ const loadAndSyncModels = async () => {
56
+ if (!process.env.TEST_ENVIRONMENT)
57
+ console.log(ROUTE_CODE, 'Loading sequelize models');
58
+ MODELS = loadModels(sequelize, Sequelize);
59
+
60
+ if (!process.env.TEST_ENVIRONMENT)
61
+ console.log(ROUTE_CODE, 'Syncing sequelize');
62
+ await sequelize.sync({ alter: process.env.LOCAL ? true : false });
63
+ };
64
+
65
+ const databaseHandler = async () => {
66
+ try {
67
+ await establishConnection();
68
+ await loadAndSyncModels();
69
+ return { MODELS };
70
+ } catch (DB_ERROR) {
71
+ console.error({
72
+ ROUTE_CODE,
73
+ DB_ERROR
74
+ });
75
+
76
+ const databaseErrorResponse = {
77
+ statusCode: 500,
78
+ body: {
79
+ ROUTE_CODE,
80
+ error: DB_ERROR.toString(),
81
+ },
82
+ };
83
+ return { databaseErrorResponse };
84
+ }
85
+ };
86
+
87
+ export { databaseHandler };
@@ -0,0 +1,4 @@
1
+ import { databaseHandler } from './databaseHandler.js';
2
+ import { registerModel } from './modelsHandler.js';
3
+
4
+ export { databaseHandler, registerModel };
@@ -0,0 +1,12 @@
1
+ const modelSchemas = {};
2
+
3
+ export const registerModel = (serviceName, modelSchema) => {
4
+ modelSchemas[serviceName] = modelSchema(serviceName);
5
+ }
6
+
7
+ export const loadModels = (orm, types) => {
8
+ return Object.keys(modelSchemas).reduce((loadedModels, serviceName) => {
9
+ loadedModels[serviceName] = modelSchemas[serviceName](orm, types);
10
+ return loadedModels;
11
+ }, {});
12
+ }
@@ -0,0 +1,3 @@
1
+ export default () => {
2
+ return "API running...";
3
+ }
package/src/index.js ADDED
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ import createAPI from 'lambda-api';
4
+ import healthCheck from './healthCheck/index.js';
5
+ import { serviceHandler } from './serviceHandler/index.js';
6
+
7
+ const API = createAPI();
8
+
9
+ // Health Check
10
+ API.get('/', async (req, res) => {
11
+ console.log('HEALTH_CHECK ENTERING');
12
+ const response = healthCheck();
13
+ console.log('HEALTH_CHECK SUCCESS');
14
+ return { response };
15
+ });
16
+
17
+ serviceHandler(API, 'todo', 'src');
18
+
19
+ export const handler = async (e, context) => {
20
+ console.log({
21
+ EVENT_PATH: e.path,
22
+ EVENT_METHOD: e.httpMethod,
23
+ });
24
+ return await API.run(e, context);
25
+ };
@@ -0,0 +1,5 @@
1
+ import { serviceHandler } from './serviceHandler.js';
2
+ import { registerMethods } from './registerMethods.js';
3
+
4
+ export { serviceHandler, registerMethods };
5
+
@@ -0,0 +1,42 @@
1
+ import { databaseHandler } from '../databaseHandler/index.js';
2
+ import { validationHandler } from './validationHandler.js';
3
+
4
+ const methodHandlerErrorThrower = async (e, METHOD, ROUTE_CODE) => {
5
+
6
+ const { MODELS, databaseErrorResponse } = await databaseHandler(e);
7
+ if (databaseErrorResponse) return databaseErrorResponse;
8
+
9
+ const validationErrorResponse =
10
+ await validationHandler(e, METHOD, MODELS, ROUTE_CODE);
11
+ if (validationErrorResponse) return validationErrorResponse;
12
+
13
+ const methodResponse = await METHOD.handler(e, MODELS);
14
+
15
+ if (!process.env.TEST_ENVIRONMENT) console.log(ROUTE_CODE, methodResponse);
16
+ return methodResponse;
17
+ };
18
+
19
+ const methodErrorHandler = (METHOD_ERROR, METHOD, ROUTE_CODE) => {
20
+
21
+ if (!process.env.TEST_ENVIRONMENT) console.error({ ROUTE_CODE, METHOD_ERROR });
22
+ const methodErrorResponse = {
23
+ statusCode: 500, body: {
24
+ ROUTE_CODE,
25
+ METHOD_ERROR: METHOD_ERROR.toString(),
26
+ }
27
+ };
28
+ return methodErrorResponse;
29
+ }
30
+
31
+ const methodHandler = async (METHOD, ROUTE_CODE, e) => {
32
+ try {
33
+ if (!process.env.TEST_ENVIRONMENT) console.log(ROUTE_CODE, 'ENTERING');
34
+ const methodResponse = await methodHandlerErrorThrower(e, METHOD, ROUTE_CODE);
35
+ return methodResponse;
36
+ } catch (METHOD_ERROR) {
37
+ const methodErrorResponse = methodErrorHandler(METHOD_ERROR, METHOD, ROUTE_CODE);
38
+ return methodErrorResponse;
39
+ }
40
+ };
41
+
42
+ export { methodHandler };
@@ -0,0 +1,33 @@
1
+ import path from 'path';
2
+
3
+ const registerMethod = async (methodName, SERVICE_URL) => {
4
+ const handlerModule = await import(new URL(`./${methodName}/handler.js`, SERVICE_URL));
5
+ const validModule = await import(new URL(`./${methodName}/valid.js`, SERVICE_URL));
6
+
7
+ return {
8
+ handler: handlerModule.handler,
9
+ VALIDATOR: validModule.VALIDATOR,
10
+ };
11
+ };
12
+
13
+ const registerMethods = methodNames => {
14
+ return async SERVICE_URL => {
15
+ const methods = {};
16
+
17
+ for (const methodName of methodNames) {
18
+ try {
19
+ methods[methodName] = await registerMethod(methodName, SERVICE_URL);
20
+ } catch (CREATE_METHOD_ERROR) {
21
+ console.error({
22
+ ROUTE_CODE: 'CREATE_METHOD_ERROR',
23
+ METHOD_NAME: methodName,
24
+ CREATE_METHOD_ERROR
25
+ });
26
+ }
27
+ }
28
+
29
+ return methods;
30
+ };
31
+ };
32
+
33
+ export { registerMethods };
@@ -0,0 +1,32 @@
1
+ import url from 'url'
2
+
3
+ import { registerModel } from '../databaseHandler/modelsHandler.js';
4
+ import { methodHandler } from './methodHandler.js';
5
+ import { registerMethods } from './registerMethods.js';
6
+
7
+ const serviceHandler = async (API, SERVICE_NAME, SRC_FOLDER='src', MODULE_FOLDER='node_modules') => {
8
+ try {
9
+ const SERVICE_URL = new URL(`./${SRC_FOLDER}/${SERVICE_NAME}/`, url.pathToFileURL(process.cwd()+'/'));
10
+ const SERVICE_MODULE = await import(`${SERVICE_URL}index.js`);
11
+
12
+ if (SERVICE_MODULE.modelSchema) registerModel(SERVICE_NAME, SERVICE_MODULE.modelSchema);
13
+ const SERVICE_METHODS = await registerMethods(SERVICE_MODULE.methods)(SERVICE_URL);
14
+
15
+ Object.keys(SERVICE_METHODS).forEach(METHOD_NAME => {
16
+ API[METHOD_NAME](`/${SERVICE_NAME}/${METHOD_NAME}`, async (req, res) => {
17
+ const ROUTE_CODE = SERVICE_NAME.toUpperCase()+'_'+METHOD_NAME.toUpperCase();
18
+ const { statusCode, body } =
19
+ await methodHandler(SERVICE_METHODS[METHOD_NAME], ROUTE_CODE, req);
20
+ res.status(statusCode).json(body);
21
+ });
22
+ });
23
+ } catch (SERVICE_HANDLER_ERROR) {
24
+ console.error({
25
+ ROUTE_CODE: 'SERVICE_HANDLER_ERROR',
26
+ SERVICE_NAME,
27
+ SERVICE_HANDLER_ERROR,
28
+ });
29
+ }
30
+ }
31
+
32
+ export { serviceHandler };
@@ -0,0 +1,17 @@
1
+ const validationHandler = async (e, METHOD, MODELS, ROUTE_CODE) => {
2
+
3
+ let validationErrorResponse = null;
4
+ const validation = await METHOD.VALIDATOR(MODELS, ROUTE_CODE).safeParseAsync(e.body);
5
+ if (!validation.success) {
6
+ const validationErrorMessage = {
7
+ ROUTE_CODE,
8
+ VALIDATION_ERROR: validation.error.format()
9
+ };
10
+ console.log(validationErrorMessage);
11
+ validationErrorResponse = { statusCode: 400, body: validationErrorMessage };
12
+ }
13
+
14
+ return validationErrorResponse;
15
+ };
16
+
17
+ export { validationHandler };
@@ -0,0 +1,34 @@
1
+ import jest from 'jest-mock';
2
+ import path from 'path';
3
+ import { registerMethods } from '../../src/serviceHandler/registerMethods';
4
+
5
+ describe('registerMethods', () => {
6
+ beforeAll(() => { process.env.TEST_ENVIRONMENT = true; });
7
+ afterAll(() => { delete process.env.TEST_ENVIRONMENT; });
8
+
9
+ test('should create methods object', async () => {
10
+ const methods = registerMethods([
11
+ 'serviceHandler/testService/get',
12
+ 'serviceHandler/testService/post',
13
+ 'serviceHandler/testService/delete',
14
+ ]);
15
+
16
+ const result = await methods(path.dirname(import.meta.url));
17
+ expect(result).toHaveProperty('serviceHandler/testService/get');
18
+ expect(result).toHaveProperty('serviceHandler/testService/post');
19
+ expect(result).toHaveProperty('serviceHandler/testService/delete');
20
+ });
21
+
22
+ test('should throw error for invalid path', async () => {
23
+ const methods = registerMethods(['serviceHandler/testService/invalid']);
24
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
25
+
26
+ try {
27
+ await methods(path.dirname(import.meta.url));
28
+ } catch (error) {
29
+ expect(error).toMatch(/Error importing handler or validator/);
30
+ expect(consoleSpy).toHaveBeenCalled()
31
+ consoleSpy.mockRestore();
32
+ }
33
+ });
34
+ });
@@ -0,0 +1,95 @@
1
+ import jest from 'jest-mock';
2
+ import { serviceHandler } from '../../src/serviceHandler/serviceHandler.js';
3
+
4
+ // Mock API object to simulate the behavior of an API (e.g., Express)
5
+ const createMockAPI = () => {
6
+ const routes = {};
7
+
8
+ return {
9
+ get: (path, handler) => (routes[path] = { method: 'get', handler }),
10
+ post: (path, handler) => (routes[path] = { method: 'post', handler }),
11
+ put: (path, handler) => (routes[path] = { method: 'put', handler }),
12
+ delete: (path, handler) => (routes[path] = { method: 'delete', handler }),
13
+ routes,
14
+ };
15
+ };
16
+
17
+ describe('serviceHandler', () => {
18
+ beforeAll(() => { process.env.TEST_ENVIRONMENT = true; });
19
+ afterAll(() => { delete process.env.TEST_ENVIRONMENT; });
20
+
21
+ test('should add routes and methods to the API object', async () => {
22
+ const mockAPI = createMockAPI();
23
+ const serviceName = 'testService';
24
+ const SRC_FOLDER = 'test/serviceHandler';
25
+
26
+ await serviceHandler(mockAPI, serviceName, SRC_FOLDER);
27
+
28
+ // Check if routes are added to the API object
29
+ expect(mockAPI.routes).toHaveProperty(`/${serviceName}/get`);
30
+ expect(mockAPI.routes).toHaveProperty(`/${serviceName}/post`);
31
+ expect(mockAPI.routes).toHaveProperty(`/${serviceName}/delete`);
32
+
33
+ // Check if methods are added to the API object
34
+ expect(mockAPI.routes[`/${serviceName}/get`].method).toBe('get');
35
+ expect(mockAPI.routes[`/${serviceName}/post`].method).toBe('post');
36
+ expect(mockAPI.routes[`/${serviceName}/delete`].method).toBe('delete');
37
+
38
+ // Check if handlers are added to the API object
39
+ expect(mockAPI.routes[`/${serviceName}/get`].handler).toBeInstanceOf(Function);
40
+ expect(mockAPI.routes[`/${serviceName}/post`].handler).toBeInstanceOf(Function);
41
+ expect(mockAPI.routes[`/${serviceName}/delete`].handler).toBeInstanceOf(Function);
42
+ });
43
+
44
+ test('should call handlers with the correct ROUTE_CODE, req, and res', async () => {
45
+ const mockAPI = createMockAPI();
46
+ const serviceName = 'testService';
47
+ const SRC_FOLDER = 'test/serviceHandler';
48
+
49
+ await serviceHandler(mockAPI, serviceName, SRC_FOLDER);
50
+
51
+ const mockReq = { foo: 'bar' };
52
+ const mockRes = {
53
+ status: jest.fn().mockReturnThis(),
54
+ json: jest.fn(),
55
+ };
56
+
57
+ // Call the GET handler
58
+ await mockAPI.routes[`/${serviceName}/get`].handler(mockReq, mockRes);
59
+
60
+ // Check if res.status and res.json are called with the correct values
61
+ expect(mockRes.status).toHaveBeenCalledWith(200);
62
+ expect(mockRes.json).toHaveBeenCalledWith('GET method');
63
+
64
+ // Call the POST handler
65
+ await mockAPI.routes[`/${serviceName}/post`].handler(mockReq, mockRes);
66
+
67
+ // Check if res.status and res.json are called with the correct values
68
+ expect(mockRes.status).toHaveBeenCalledWith(200);
69
+ expect(mockRes.json).toHaveBeenCalledWith('POST method');
70
+
71
+ // Call the DELETE handler
72
+ await mockAPI.routes[`/${serviceName}/delete`].handler(mockReq, mockRes);
73
+
74
+ // Check if res.status and res.json are called with the correct values
75
+ expect(mockRes.status).toHaveBeenCalledWith(200);
76
+ expect(mockRes.json).toHaveBeenCalledWith('DELETE method');
77
+ });
78
+
79
+ test('should log an error if the serviceHandler fails', async () => {
80
+ const mockAPI = createMockAPI();
81
+ const serviceName = 'nonExistentService';
82
+ const SRC_FOLDER = 'test/serviceHandler';
83
+
84
+ // Spy on console.error to check if it's called
85
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
86
+
87
+ await serviceHandler(mockAPI, serviceName, SRC_FOLDER);
88
+
89
+ // Check if console.error is called
90
+ expect(consoleSpy).toHaveBeenCalled();
91
+
92
+ // Restore the original console.error implementation
93
+ consoleSpy.mockRestore();
94
+ });
95
+ });
@@ -0,0 +1,3 @@
1
+ export const handler = async () => {
2
+ return { statusCode: 200, body: 'DELETE method' };
3
+ }
@@ -0,0 +1,3 @@
1
+ export const VALIDATOR = () => {
2
+ return { safeParseAsync: async () => { return { success: true } } };
3
+ }
@@ -0,0 +1,3 @@
1
+ export const handler = async () => {
2
+ return { statusCode: 200, body: 'GET method' };
3
+ }
@@ -0,0 +1,3 @@
1
+ export const VALIDATOR = () => {
2
+ return { safeParseAsync: async () => { return { success: true } } };
3
+ }
@@ -0,0 +1,3 @@
1
+ const methods = ['get', 'post', 'delete'];
2
+
3
+ export { methods };
@@ -0,0 +1,3 @@
1
+ export const handler = async () => {
2
+ return { statusCode: 200, body: 'POST method' };
3
+ }
@@ -0,0 +1,3 @@
1
+ export const VALIDATOR = () => {
2
+ return { safeParseAsync: async () => { return { success: true } } };
3
+ }