@avtechno/sfr 1.0.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/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Single File Router (SFR)
2
+
3
+ ### Overview
4
+ SFR allows the declaration of services including its controllers, handlers and validators in one single file.
5
+
6
+ Featuring a configurable Inversion of Control pattern for dependency libraries, It allows developers to easily inject dependencies on either handlers or controllers for convenient and easy access.
7
+
8
+ Due to it's self-contained and structured format, The SFR has allowed for the development of several extensions such as:
9
+
10
+ * OpenAPI Service Translator
11
+
12
+ `A plugin which also uses API-Bundler's ability to extract metadata from individual SFRs
13
+ It's main purpose is to translate and create service-level documentation which follows the OpenAPI Standard, this essentially opens up doors to extensive tooling support, instrumentation, automated endpoint testing, automated documentation generation, just to name a few.`
14
+
15
+ * UAC - ACM Self-Registration Scheme
16
+
17
+ `The in-house API-Bundler was designed to extract useful information from individual SFRs, termed service-artifacts, they are reported to a centralized authority through the well-documented UAC - ACM Self-Registration Scheme, this is prerogative to the grand scheme of Resource Administration.`
18
+
19
+ ### Structure
20
+
21
+ A regular SFR is composed of the following objects
22
+
23
+ |Object Name|Description|
24
+ |-|-|
25
+ |CFG| configuration information relayed to the API-bundler (service-parser).|
26
+ |Validators|POJO representation of what values are allowed for each endpoint.|
27
+ |Handlers| Express middlewares which acts as the main logic block for each endpoint.|
28
+ |Controllers|Functions that execute calls to the database.|
29
+
30
+ ### Usage
31
+
32
+ ``` javascript
33
+
34
+ import sfr from "@hawkstow/sfr";
35
+ import express from "express";
36
+
37
+ const api_path = "api";
38
+ const doc_path = "docs";
39
+
40
+ const app = express();
41
+
42
+ /*
43
+ Note:
44
+ SFR Bundler will look for two folders, named "rest" and "ws" inside the "path".
45
+ The placement of SFRs define the protocol that they'll use.
46
+ `
47
+ i.e:rest SFRs reside within "rest", websocket SFRs reside in "ws".
48
+ */
49
+
50
+ sfr({
51
+ root : "dist", //Specifies the working directory
52
+ path : api_path, //Specifies the API directory i.e: where SFR files reside
53
+ out : doc_path //Specifies the directory for the resulting OAS documents
54
+ }, app);
55
+
56
+ ```
57
+
58
+ ### Error-Handling
59
+ The library automatically handles errors on both handlers and controllers, however, care must be taken on how error-handling is done by the developer, the following table illustrates what error-handling styles are allowed for both cases.
60
+
61
+ |Handlers|Controllers|
62
+ |-|-|
63
+ |Exception Throws|Promise.resolve/reject returns|
64
+ |Passing Exceptions to Next fn||
65
+
66
+
67
+ ### Bug Reporting
68
+ If you've found a bug, please file it in our Github Issues Page so we can have a look at it, perhaps fix it XD
69
+
70
+ TODO:
71
+ * Multer Upload Validation
72
+ * Built-in Logging
@@ -0,0 +1,33 @@
1
+ import Joi from "joi";
2
+ import { REST } from "./templates.mjs";
3
+ export default REST({
4
+ cfg: {
5
+ base_dir: "example",
6
+ public: false,
7
+ },
8
+ validators: {
9
+ "get-something": {
10
+ "name": Joi.string().required(),
11
+ "age": Joi.number().required(),
12
+ }
13
+ },
14
+ handlers: {
15
+ GET: {
16
+ "get-something": {
17
+ description: "Get something I guess?",
18
+ async fn(req, res) {
19
+ const result = await this.db_call();
20
+ res.status(200).json({
21
+ data: result
22
+ });
23
+ }
24
+ }
25
+ }
26
+ },
27
+ controllers: {
28
+ async db_call() {
29
+ /* Perform database calls here through accessing the "this" context (if injections are set) */
30
+ return "Something";
31
+ }
32
+ }
33
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,84 @@
1
+ /// <reference path="types/index.d.ts" />
2
+ import yaml from "js-yaml";
3
+ import path from "path";
4
+ import fs from "fs/promises";
5
+ import { REST, WS, MQ } from "./templates.mjs";
6
+ import { SFRPipeline } from "./sfr-pipeline.mjs";
7
+ import { MQLib } from "./mq.mjs";
8
+ const cwd = process.cwd();
9
+ export default async (cfg, oas_cfg, connectors, base_url) => {
10
+ //TODO: Verify connectors
11
+ const sfr = new SFRPipeline(cfg, oas_cfg, connectors);
12
+ // Returned service artifacts for both OpenAPI and AsyncAPI are written into the server directory
13
+ // An express static endpoint is pointed to this directory for service directory.
14
+ const documents = await sfr.init(base_url);
15
+ write_service_discovery(cfg, documents); //Writes services to output dir (cfg.out)
16
+ return {
17
+ ...oas_cfg,
18
+ documents
19
+ };
20
+ };
21
+ async function write_service_discovery(cfg, documents) {
22
+ //Setup files in case they do not exist.
23
+ //Setup output folder
24
+ await fs.mkdir(path.join(cwd, cfg.out), { recursive: true });
25
+ // Loop over each protocol
26
+ for (let [protocol, documentation] of Object.entries(documents)) {
27
+ //Strictly await for setup to create dirs for each protocol.
28
+ await fs.mkdir(path.join(cwd, cfg.out, protocol.toLowerCase()), { recursive: true });
29
+ //Convert documentation into service manifest.
30
+ //OpenAPI Documents : paths
31
+ //AsyncAPI Documents : channels
32
+ switch (protocol) {
33
+ case "REST":
34
+ {
35
+ documentation = documentation;
36
+ for (const [path, methods] of Object.entries(documentation.paths)) {
37
+ write_to_file(cfg, `rest/${path}`, methods);
38
+ }
39
+ }
40
+ break;
41
+ case "WS":
42
+ {
43
+ }
44
+ break;
45
+ case "MQ": {
46
+ documentation = documentation;
47
+ write_to_file(cfg, "mq/index.yaml", documentation);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ //Tasked with recursively creating directories and spec files.
53
+ async function write_to_file(cfg, dir, data) {
54
+ //Path is a slash(/) delimited string, with the end delimiter indicating the filename to be used.
55
+ const paths = dir.split("/");
56
+ //Indicates a root file
57
+ if (paths.length === 1) {
58
+ await fs.writeFile(path.join(cwd, cfg.out, paths[0]), yaml.dump(data), { flag: "w+" });
59
+ }
60
+ else { //Indicates a nested file
61
+ const file = paths.pop(); //Pops the path array to be used for dir creation
62
+ await fs.mkdir(path.join(cwd, cfg.out, ...paths), { recursive: true });
63
+ fs.writeFile(path.join(cwd, cfg.out, ...paths, `${file}.yml`), yaml.dump(data), { flag: "w+" });
64
+ }
65
+ }
66
+ function inject(injections) {
67
+ SFRPipeline.injections.rest.handlers = {
68
+ ...SFRPipeline.injections.rest.handlers,
69
+ ...injections.handlers
70
+ };
71
+ SFRPipeline.injections.rest.controllers = {
72
+ ...SFRPipeline.injections.rest.controllers,
73
+ ...injections.controllers
74
+ };
75
+ SFRPipeline.injections.mq.handlers = {
76
+ ...SFRPipeline.injections.mq.handlers,
77
+ ...injections.handlers
78
+ };
79
+ SFRPipeline.injections.mq.controllers = {
80
+ ...SFRPipeline.injections.mq.controllers,
81
+ ...injections.controllers
82
+ };
83
+ }
84
+ export { REST, WS, MQ, MQLib, inject };
@@ -0,0 +1,37 @@
1
+ import winston from 'winston';
2
+ // Define log levels
3
+ const levels = {
4
+ error: 0,
5
+ warn: 1,
6
+ info: 2,
7
+ http: 3,
8
+ verbose: 4,
9
+ debug: 5,
10
+ silly: 6,
11
+ };
12
+ // Define colors for each level
13
+ const colors = {
14
+ error: 'red',
15
+ warn: 'yellow',
16
+ info: 'green',
17
+ http: 'magenta',
18
+ verbose: 'cyan',
19
+ debug: 'blue',
20
+ silly: 'gray',
21
+ };
22
+ // Add colors to Winston
23
+ winston.addColors(colors);
24
+ // Define the format for logs
25
+ const format = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.colorize({ all: true }), winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}${info.metadata ? ` ${JSON.stringify(info.metadata)}` : ''}`));
26
+ // Create the Winston logger
27
+ const logger = winston.createLogger({
28
+ level: process.env.NODE_ENV === 'development' && Boolean(process.env.DEBUG_SFR) ? 'debug' : 'info',
29
+ levels,
30
+ format,
31
+ transports: [
32
+ // Console transport
33
+ new winston.transports.Console(),
34
+ ],
35
+ });
36
+ // Export the logger
37
+ export { logger };
package/dist/mq.mjs ADDED
@@ -0,0 +1,217 @@
1
+ import { connect } from "amqplib";
2
+ /**
3
+ * MQLib class for establishing a connection to the message queue and creating channels.
4
+ */
5
+ export class MQLib {
6
+ connection;
7
+ channel;
8
+ /**
9
+ * Initializes the connection and channel to the message queue.
10
+ *
11
+ * @param MQ_URL - The URL of the message queue.
12
+ * @example
13
+ * const mqLib = new MQLib();
14
+ * await mqLib.init("amqp://localhost");
15
+ */
16
+ async init(MQ_URL) {
17
+ await connect(MQ_URL)
18
+ .then(async (v) => {
19
+ //@ts-ignore
20
+ this.connection = v;
21
+ this.channel = await v.createChannel();
22
+ });
23
+ }
24
+ /**
25
+ * Returns the connection object.
26
+ *
27
+ * @returns The connection object to the message queue.
28
+ */
29
+ get_connection() {
30
+ return this.connection;
31
+ }
32
+ /**
33
+ * Returns the channel object.
34
+ *
35
+ * @returns The channel object to the message queue.
36
+ */
37
+ get_channel() {
38
+ return this.channel;
39
+ }
40
+ }
41
+ /**
42
+ * BroadcastMQ class to handle different types of broadcast communication patterns: Publish-Subscribe, Routing, and Topic.
43
+ */
44
+ export class BroadcastMQ {
45
+ channel;
46
+ type;
47
+ constructor(channel, type) {
48
+ this.channel = channel;
49
+ this.type = type;
50
+ }
51
+ /**
52
+ * Publishes a message to the specified exchange based on the communication pattern.
53
+ *
54
+ * @param exchange - The name of the exchange to send the message to.
55
+ * @param key - The routing key or pattern to use, depending on the communication pattern.
56
+ * @param payload - The message to be sent.
57
+ * @param options - Additional publishing options.
58
+ * @example
59
+ * // Publish a message to a 'logsExchange' with a routing key 'error' for a routing pattern.
60
+ * const mq = new BroadcastMQ(channel, "Routing");
61
+ * mq.publish("logsExchange", "error", { level: "error", message: "Something went wrong" });
62
+ */
63
+ async publish(exchange, key, payload, options) {
64
+ switch (this.type) {
65
+ case "Publish-Subscribe": {
66
+ await this.channel.assertExchange(exchange, "fanout", { durable: false });
67
+ this.channel.publish(exchange, "", encode_payload(payload), options);
68
+ break;
69
+ }
70
+ case "Routing": {
71
+ await this.channel.assertExchange(exchange, "direct", { durable: false });
72
+ this.channel.publish(exchange, key, encode_payload(payload), options);
73
+ break;
74
+ }
75
+ case "Topic": {
76
+ await this.channel.assertExchange(exchange, "topic", { durable: false });
77
+ this.channel.publish(exchange, key, encode_payload(payload), options);
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ /**
83
+ * Subscribes to an exchange to receive messages based on the communication pattern.
84
+ *
85
+ * @param exchange - The name of the exchange to subscribe to.
86
+ * @param key - The routing key or pattern to listen for.
87
+ * @param cfg - Configuration options for the consumer, including the callback function.
88
+ * @param options - Additional consumption options.
89
+ * @example
90
+ * // Subscribe to the 'logsExchange' for all 'error' messages in the Routing pattern.
91
+ * const mq = new BroadcastMQ(channel, "Routing");
92
+ * mq.subscribe("logsExchange", "error", { fn: handleErrorLogs, options: { noAck: true } });
93
+ */
94
+ async subscribe(exchange, key, cfg, options) {
95
+ let fn = cfg.fn.bind({ channel: this.channel, type: this.type });
96
+ switch (this.type) {
97
+ case "Publish-Subscribe": {
98
+ await this.channel.assertExchange(exchange, "fanout", { durable: false });
99
+ const { queue } = await this.channel.assertQueue("", { exclusive: true });
100
+ this.channel.bindQueue(queue, exchange, "");
101
+ this.channel.consume(queue, (v) => fn(parse_payload(v)), cfg.options);
102
+ break;
103
+ }
104
+ case "Routing": {
105
+ await this.channel.assertExchange(exchange, "direct", { durable: false });
106
+ const { queue } = await this.channel.assertQueue("", { exclusive: true });
107
+ await this.channel.bindQueue(queue, exchange, key);
108
+ this.channel.consume(queue, (v) => fn(parse_payload(v)), options);
109
+ break;
110
+ }
111
+ case "Topic": {
112
+ await this.channel.assertExchange(exchange, "topic", { durable: false });
113
+ const { queue } = await this.channel.assertQueue("", { exclusive: true });
114
+ this.channel.bindQueue(queue, exchange, key);
115
+ this.channel.consume(queue, (v) => fn(parse_payload(v)), options);
116
+ break;
117
+ }
118
+ }
119
+ }
120
+ }
121
+ /**
122
+ * TargetedMQ class to handle Point-to-Point and Request-Reply communication patterns.
123
+ */
124
+ export class TargetedMQ {
125
+ channel;
126
+ type;
127
+ constructor(channel, type) {
128
+ this.channel = channel;
129
+ this.type = type;
130
+ }
131
+ /**
132
+ * Sends a message to the specified queue depending on the communication pattern.
133
+ *
134
+ * @param binding - The name of the queue or binding to send the message to.
135
+ * @param payload - The message to be sent.
136
+ * @param options - Additional publishing options.
137
+ * @example
138
+ * // Send a message to a queue for point-to-point communication
139
+ * const mq = new TargetedMQ(channel, "Point-to-Point");
140
+ * mq.produce("taskQueue", { taskId: 1, action: "process" });
141
+ */
142
+ async produce(binding, payload, options) {
143
+ /* Alter produce strategy depending on the instance's configured comm pattern */
144
+ switch (this.type) {
145
+ case "Point-to-Point": {
146
+ await this.channel.assertQueue(binding);
147
+ return this.channel.sendToQueue(binding, encode_payload(payload), options);
148
+ }
149
+ case "Request-Reply": {
150
+ await this.channel.assertQueue(binding);
151
+ return this.channel.sendToQueue(binding, encode_payload(payload), options);
152
+ }
153
+ }
154
+ }
155
+ /**
156
+ * Sends a reply to the message sender in a Request-Reply pattern.
157
+ *
158
+ * @param msg - The original message to reply to.
159
+ * @param payload - The reply message to send back to the requester.
160
+ * @example
161
+ * // Send a response back to the client in a Request-Reply pattern
162
+ * const mq = new TargetedMQ(channel, "Request-Reply");
163
+ * mq.reply(msg, { result: "Processed successfully" });
164
+ */
165
+ async reply(msg, payload) {
166
+ if (this.type !== "Request-Reply")
167
+ return;
168
+ this.channel.sendToQueue(msg.properties.replyTo, encode_payload(payload));
169
+ this.channel.ack(msg);
170
+ }
171
+ /**
172
+ * Consumes messages from the specified queue based on the communication pattern.
173
+ *
174
+ * @param queue - The name of the queue to consume messages from.
175
+ * @param cfg - Configuration options for the consumer, including the callback function.
176
+ * @example
177
+ * // Consume a message from the 'taskQueue' in a Point-to-Point communication pattern
178
+ * const mq = new TargetedMQ(channel, "Point-to-Point");
179
+ * mq.consume("taskQueue", { fn: processTask, options: { noAck: true } });
180
+ */
181
+ async consume(queue, cfg) {
182
+ let fn = cfg.fn.bind({ channel: this.channel, type: this.type });
183
+ switch (this.type) {
184
+ case "Point-to-Point": {
185
+ await this.channel.assertQueue(queue);
186
+ this.channel.consume(queue, (v) => fn(parse_payload(v)), cfg.options);
187
+ break;
188
+ }
189
+ case "Request-Reply": {
190
+ await this.channel.assertQueue(queue);
191
+ this.channel.consume(queue, (v) => fn(parse_payload(v)), cfg.options);
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ }
197
+ /* Helper Functions */
198
+ /**
199
+ * Parses the payload of a message.
200
+ *
201
+ * @param msg - The consumed message.
202
+ * @returns The parsed message content.
203
+ */
204
+ function parse_payload(msg) {
205
+ if (!msg)
206
+ return {};
207
+ return { ...msg, content: JSON.parse(msg.content.toString()) };
208
+ }
209
+ /**
210
+ * Encodes a message as a Buffer to be sent over the message queue.
211
+ *
212
+ * @param msg - The message to be encoded.
213
+ * @returns The encoded message as a Buffer.
214
+ */
215
+ function encode_payload(msg) {
216
+ return Buffer.from(JSON.stringify(msg));
217
+ }