@avtechno/sfr 1.0.11 → 1.0.13
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/package.json +1 -1
- package/src/mq.mts +16 -16
- package/src/sfr-pipeline.mts +32 -23
- package/dist/example.mjs +0 -33
- package/dist/index.mjs +0 -84
- package/dist/logger.mjs +0 -37
- package/dist/mq.mjs +0 -239
- package/dist/sfr-pipeline.mjs +0 -446
- package/dist/templates.mjs +0 -47
- package/dist/types/example.d.mts +0 -11
- package/dist/types/index.d.mts +0 -7
- package/dist/types/logger.d.mts +0 -3
- package/dist/types/mq.d.mts +0 -114
- package/dist/types/sfr-pipeline.d.mts +0 -33
- package/dist/types/templates.d.mts +0 -8
- package/dist/types/util.d.mts +0 -7
- package/dist/util.mjs +0 -25
package/package.json
CHANGED
package/src/mq.mts
CHANGED
|
@@ -45,22 +45,7 @@ export class MQLib {
|
|
|
45
45
|
|
|
46
46
|
export class BaseMQ{
|
|
47
47
|
constructor(public channel: Channel, protected type: CommunicationPattern) {}
|
|
48
|
-
/**
|
|
49
|
-
* Sends a reply to the message sender in a Request-Reply pattern.
|
|
50
|
-
*
|
|
51
|
-
* @param msg - The original message to reply to.
|
|
52
|
-
* @param payload - The reply message to send back to the requester.
|
|
53
|
-
* @example
|
|
54
|
-
* // Send a response back to the client in a Request-Reply pattern
|
|
55
|
-
* const mq = new TargetedMQ(channel, "Request-Reply");
|
|
56
|
-
* mq.reply(msg, { result: "Processed successfully" });
|
|
57
|
-
*/
|
|
58
|
-
async reply(msg: ConsumeMessage, payload: any) {
|
|
59
|
-
if (this.type !== "Request-Reply") return;
|
|
60
48
|
|
|
61
|
-
this.channel.sendToQueue(msg.properties.replyTo, encode_payload(payload), {correlationId : msg.properties.correlationId});
|
|
62
|
-
this.channel.ack(msg);
|
|
63
|
-
}
|
|
64
49
|
}
|
|
65
50
|
|
|
66
51
|
/**
|
|
@@ -221,7 +206,22 @@ export class TargetedMQ extends BaseMQ{
|
|
|
221
206
|
}
|
|
222
207
|
}
|
|
223
208
|
|
|
224
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Sends a reply to the message sender in a Request-Reply pattern.
|
|
211
|
+
*
|
|
212
|
+
* @param msg - The original message to reply to.
|
|
213
|
+
* @param payload - The reply message to send back to the requester.
|
|
214
|
+
* @example
|
|
215
|
+
* // Send a response back to the client in a Request-Reply pattern
|
|
216
|
+
* const mq = new TargetedMQ(channel, "Request-Reply");
|
|
217
|
+
* mq.reply(msg, { result: "Processed successfully" });
|
|
218
|
+
*/
|
|
219
|
+
async reply(msg: ConsumeMessage, payload: any) {
|
|
220
|
+
if (this.type !== "Request-Reply") return;
|
|
221
|
+
|
|
222
|
+
this.channel.sendToQueue(msg.properties.replyTo, encode_payload(payload), {correlationId : msg.properties.correlationId});
|
|
223
|
+
this.channel.ack(msg);
|
|
224
|
+
}
|
|
225
225
|
set_options(options : Options.AssertQueue){
|
|
226
226
|
this.queue_options = options;
|
|
227
227
|
}
|
package/src/sfr-pipeline.mts
CHANGED
|
@@ -32,7 +32,7 @@ export class SFRPipeline {
|
|
|
32
32
|
},
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
constructor(private cfg: ParserCFG, private oas_cfg: OASConfig, private comms: SFRProtocols) { }
|
|
35
|
+
constructor(private cfg: ParserCFG, private oas_cfg: OASConfig, private comms: SFRProtocols, private debug? : boolean) { }
|
|
36
36
|
|
|
37
37
|
async init(base_url?: string): Promise<ServiceDocuments> {
|
|
38
38
|
this.base_url = base_url;
|
|
@@ -66,7 +66,7 @@ export class SFRPipeline {
|
|
|
66
66
|
protocols = Object.fromEntries(Object.entries(protocols).filter(([k])=> this.comms[k]).map(([k, v])=> [k, v]));
|
|
67
67
|
await this.fn_binding(protocols);
|
|
68
68
|
|
|
69
|
-
this.print_to_console();
|
|
69
|
+
if(this.debug)this.print_to_console();
|
|
70
70
|
|
|
71
71
|
return this.spec_generation(protocols);
|
|
72
72
|
}
|
|
@@ -93,11 +93,14 @@ export class SFRPipeline {
|
|
|
93
93
|
//Import SFRs
|
|
94
94
|
const protocols = await Promise.all(sfr_paths.map((protocol) => import_sfrs(protocol)));
|
|
95
95
|
const REST: RESTNamespaceDeclaration = protocols[0];
|
|
96
|
-
logger.info(`${Object.keys(protocols[0]).length} REST SFR`)
|
|
97
96
|
const WS: WSNamespaceDeclaration = protocols[1];
|
|
98
|
-
logger.info(`${Object.keys(protocols[1]).length} WS SFR`)
|
|
99
97
|
const MQ: MQNamespaceDeclaration = protocols[2];
|
|
100
|
-
|
|
98
|
+
|
|
99
|
+
if(this.debug){
|
|
100
|
+
logger.info(`${Object.keys(protocols[0]).length} REST SFR`)
|
|
101
|
+
logger.info(`${Object.keys(protocols[1]).length} WS SFR`)
|
|
102
|
+
logger.info(`${Object.keys(protocols[2]).length} MQ SFR`)
|
|
103
|
+
}
|
|
101
104
|
|
|
102
105
|
return { REST, WS, MQ } as NamespaceDeclaration;
|
|
103
106
|
}
|
|
@@ -137,7 +140,7 @@ export class SFRPipeline {
|
|
|
137
140
|
Object.entries(handlers).forEach(([method, handler_map]) => {
|
|
138
141
|
for (const [name, handler] of Object.entries(handler_map).filter(([k]) => validators[k])) {
|
|
139
142
|
const dir = `${base_dir}/${namespace}/${name}`;
|
|
140
|
-
|
|
143
|
+
|
|
141
144
|
const validator = validators[name];
|
|
142
145
|
const validator_type = typeof validator !== "function" ? "joi" : "multer";
|
|
143
146
|
|
|
@@ -146,13 +149,16 @@ export class SFRPipeline {
|
|
|
146
149
|
case "joi": {
|
|
147
150
|
comms[method.toLowerCase() as RESTRequestType](dir, (req, res, next) => {
|
|
148
151
|
let error: string | boolean = true;
|
|
149
|
-
|
|
152
|
+
|
|
150
153
|
if (is_public) {
|
|
151
154
|
//if(!req.session.user)return res.status(401).json({error : "Session not found."});
|
|
152
155
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if
|
|
156
|
+
|
|
157
|
+
const validator_keys = Object.keys(validator);
|
|
158
|
+
if(validator_keys.length){
|
|
159
|
+
error = template(validator, method === "GET" ? req.query : req.body);
|
|
160
|
+
if (error) return res.status(400).json({ error });
|
|
161
|
+
}
|
|
156
162
|
next();
|
|
157
163
|
});
|
|
158
164
|
} break;
|
|
@@ -207,25 +213,28 @@ export class SFRPipeline {
|
|
|
207
213
|
//const validator_type = typeof validator !== "function" ? "joi" : "multer"; // TODO: Dynamically update parameter content based on validator type.
|
|
208
214
|
|
|
209
215
|
let validator_fn = function (msg: ConsumeMessage) {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
if
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
const validator_keys = Object.keys(validator);
|
|
217
|
+
|
|
218
|
+
if(validator_keys.length){
|
|
219
|
+
const error = template(validator, msg.content);
|
|
220
|
+
|
|
221
|
+
if (error) {
|
|
222
|
+
if (mq instanceof TargetedMQ) {
|
|
223
|
+
if (error) {
|
|
224
|
+
switch (pattern) {
|
|
225
|
+
case "Request-Reply": mq.reply(msg, { error }); break;
|
|
226
|
+
default: mq.channel.reject(msg, false);
|
|
227
|
+
}
|
|
218
228
|
}
|
|
219
229
|
}
|
|
220
|
-
}
|
|
221
230
|
|
|
222
|
-
|
|
223
|
-
mq.channel.reject(msg, false);
|
|
224
|
-
}
|
|
231
|
+
if (mq instanceof BroadcastMQ)mq.channel.reject(msg, false);
|
|
225
232
|
|
|
226
|
-
|
|
233
|
+
return; //Return immediately to avoid executing handler fn (which may contain reply or ack calls.)
|
|
234
|
+
}
|
|
227
235
|
}
|
|
228
236
|
|
|
237
|
+
|
|
229
238
|
handler.fn(msg);
|
|
230
239
|
}
|
|
231
240
|
|
package/dist/example.mjs
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
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, BroadcastMQ, TargetedMQ } 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, BroadcastMQ, TargetedMQ, inject };
|
package/dist/logger.mjs
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
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
|
-
export class BaseMQ {
|
|
42
|
-
channel;
|
|
43
|
-
type;
|
|
44
|
-
constructor(channel, type) {
|
|
45
|
-
this.channel = channel;
|
|
46
|
-
this.type = type;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Sends a reply to the message sender in a Request-Reply pattern.
|
|
50
|
-
*
|
|
51
|
-
* @param msg - The original message to reply to.
|
|
52
|
-
* @param payload - The reply message to send back to the requester.
|
|
53
|
-
* @example
|
|
54
|
-
* // Send a response back to the client in a Request-Reply pattern
|
|
55
|
-
* const mq = new TargetedMQ(channel, "Request-Reply");
|
|
56
|
-
* mq.reply(msg, { result: "Processed successfully" });
|
|
57
|
-
*/
|
|
58
|
-
async reply(msg, payload) {
|
|
59
|
-
if (this.type !== "Request-Reply")
|
|
60
|
-
return;
|
|
61
|
-
this.channel.sendToQueue(msg.properties.replyTo, encode_payload(payload), { correlationId: msg.properties.correlationId });
|
|
62
|
-
this.channel.ack(msg);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* BroadcastMQ class to handle different types of broadcast communication patterns: Fanout, Direct, and Topic.
|
|
67
|
-
*/
|
|
68
|
-
export class BroadcastMQ extends BaseMQ {
|
|
69
|
-
channel;
|
|
70
|
-
type;
|
|
71
|
-
exchange_options;
|
|
72
|
-
constructor(channel, type, exchange_options) {
|
|
73
|
-
super(channel, type);
|
|
74
|
-
this.channel = channel;
|
|
75
|
-
this.type = type;
|
|
76
|
-
this.exchange_options = exchange_options;
|
|
77
|
-
this.exchange_options = exchange_options || { durable: false };
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Publishes a message to the specified exchange based on the communication pattern.
|
|
81
|
-
*
|
|
82
|
-
* @param exchange - The name of the exchange to send the message to.
|
|
83
|
-
* @param key - The routing key or pattern to use, depending on the communication pattern.
|
|
84
|
-
* @param payload - The message to be sent.
|
|
85
|
-
* @param options - Additional publishing options.
|
|
86
|
-
* @example
|
|
87
|
-
* // Publish a message to a 'logsExchange' with a routing key 'error' for a routing pattern.
|
|
88
|
-
* const mq = new BroadcastMQ(channel, "Direct");
|
|
89
|
-
* mq.publish("logsExchange", "error", { level: "error", message: "Something went wrong" });
|
|
90
|
-
*/
|
|
91
|
-
async publish(exchange, key = "", payload, options) {
|
|
92
|
-
switch (this.type) {
|
|
93
|
-
case "Fanout": {
|
|
94
|
-
await this.channel.assertExchange(exchange, "fanout", this.exchange_options);
|
|
95
|
-
this.channel.publish(exchange, key, encode_payload(payload), options);
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
case "Direct": {
|
|
99
|
-
await this.channel.assertExchange(exchange, "direct", this.exchange_options);
|
|
100
|
-
this.channel.publish(exchange, key, encode_payload(payload), options);
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
case "Topic": {
|
|
104
|
-
await this.channel.assertExchange(exchange, "topic", this.exchange_options);
|
|
105
|
-
this.channel.publish(exchange, key, encode_payload(payload), options);
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Subscribes to an exchange to receive messages based on the communication pattern.
|
|
112
|
-
*
|
|
113
|
-
* @param exchange - The name of the exchange to subscribe to.
|
|
114
|
-
* @param key - The routing key or pattern to listen for.
|
|
115
|
-
* @param cfg - Configuration options for the consumer, including the callback function.
|
|
116
|
-
* @param options - Additional consumption options.
|
|
117
|
-
* @example
|
|
118
|
-
* // Subscribe to the 'logsExchange' for all 'error' messages in the Direct pattern.
|
|
119
|
-
* const mq = new BroadcastMQ(channel, "Direct");
|
|
120
|
-
* mq.subscribe("logsExchange", "error", { fn: handleErrorLogs, options: { noAck: true } });
|
|
121
|
-
*/
|
|
122
|
-
async subscribe(exchange, key, cfg) {
|
|
123
|
-
let fn = cfg.fn.bind({ channel: this.channel, type: this.type });
|
|
124
|
-
switch (this.type) {
|
|
125
|
-
case "Fanout": {
|
|
126
|
-
await this.channel.assertExchange(exchange, "fanout", this.exchange_options);
|
|
127
|
-
const { queue } = await this.channel.assertQueue("", { exclusive: true });
|
|
128
|
-
this.channel.bindQueue(queue, exchange, "");
|
|
129
|
-
this.channel.consume(queue, (v) => fn(parse_payload(v)), cfg.options);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
case "Direct": {
|
|
133
|
-
await this.channel.assertExchange(exchange, "direct", this.exchange_options);
|
|
134
|
-
const { queue } = await this.channel.assertQueue("", { exclusive: true });
|
|
135
|
-
await this.channel.bindQueue(queue, exchange, key);
|
|
136
|
-
this.channel.consume(queue, (v) => fn(parse_payload(v)), cfg.options);
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
case "Topic": {
|
|
140
|
-
await this.channel.assertExchange(exchange, "topic", this.exchange_options);
|
|
141
|
-
const { queue } = await this.channel.assertQueue("", { exclusive: true });
|
|
142
|
-
this.channel.bindQueue(queue, exchange, key);
|
|
143
|
-
this.channel.consume(queue, (v) => fn(parse_payload(v)), cfg.options);
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
set_options(options) {
|
|
149
|
-
this.exchange_options = options;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* TargetedMQ class to handle Point-to-Point and Request-Reply communication patterns.
|
|
154
|
-
*/
|
|
155
|
-
export class TargetedMQ extends BaseMQ {
|
|
156
|
-
channel;
|
|
157
|
-
type;
|
|
158
|
-
queue_options;
|
|
159
|
-
constructor(channel, type, queue_options) {
|
|
160
|
-
super(channel, type);
|
|
161
|
-
this.channel = channel;
|
|
162
|
-
this.type = type;
|
|
163
|
-
this.queue_options = queue_options;
|
|
164
|
-
this.queue_options = queue_options || { durable: true };
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Sends a message to the specified queue depending on the communication pattern.
|
|
168
|
-
*
|
|
169
|
-
* @param binding - The name of the queue or binding to send the message to.
|
|
170
|
-
* @param payload - The message to be sent.
|
|
171
|
-
* @param options - Additional publishing options.
|
|
172
|
-
* @example
|
|
173
|
-
* // Send a message to a queue for point-to-point communication
|
|
174
|
-
* const mq = new TargetedMQ(channel, "Point-to-Point");
|
|
175
|
-
* mq.produce("taskQueue", { taskId: 1, action: "process" });
|
|
176
|
-
*/
|
|
177
|
-
async produce(binding, payload, options) {
|
|
178
|
-
/* Alter produce strategy depending on the instance's configured comm pattern */
|
|
179
|
-
switch (this.type) {
|
|
180
|
-
case "Point-to-Point": {
|
|
181
|
-
await this.channel.assertQueue(binding, this.queue_options);
|
|
182
|
-
return this.channel.sendToQueue(binding, encode_payload(payload), options);
|
|
183
|
-
}
|
|
184
|
-
case "Request-Reply": {
|
|
185
|
-
await this.channel.assertQueue(binding, this.queue_options);
|
|
186
|
-
return this.channel.sendToQueue(binding, encode_payload(payload), options);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Consumes messages from the specified queue based on the communication pattern.
|
|
192
|
-
*
|
|
193
|
-
* @param queue - The name of the queue to consume messages from.
|
|
194
|
-
* @param cfg - Configuration options for the consumer, including the callback function.
|
|
195
|
-
* @example
|
|
196
|
-
* // Consume a message from the 'taskQueue' in a Point-to-Point communication pattern
|
|
197
|
-
* const mq = new TargetedMQ(channel, "Point-to-Point");
|
|
198
|
-
* mq.consume("taskQueue", { fn: processTask, options: { noAck: true } });
|
|
199
|
-
*/
|
|
200
|
-
async consume(queue, cfg) {
|
|
201
|
-
let fn = cfg.fn.bind({ channel: this.channel, type: this.type });
|
|
202
|
-
switch (this.type) {
|
|
203
|
-
case "Point-to-Point": {
|
|
204
|
-
await this.channel.assertQueue(queue, this.queue_options);
|
|
205
|
-
this.channel.consume(queue, (v) => fn(parse_payload(v)), cfg.options);
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
case "Request-Reply": {
|
|
209
|
-
await this.channel.assertQueue(queue, this.queue_options);
|
|
210
|
-
this.channel.consume(queue, (v) => fn(parse_payload(v)), cfg.options);
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
set_options(options) {
|
|
216
|
-
this.queue_options = options;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
/* Helper Functions */
|
|
220
|
-
/**
|
|
221
|
-
* Parses the payload of a message.
|
|
222
|
-
*
|
|
223
|
-
* @param msg - The consumed message.
|
|
224
|
-
* @returns The parsed message content.
|
|
225
|
-
*/
|
|
226
|
-
function parse_payload(msg) {
|
|
227
|
-
if (!msg)
|
|
228
|
-
return {};
|
|
229
|
-
return { ...msg, content: JSON.parse(msg.content.toString()) };
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Encodes a message as a Buffer to be sent over the message queue.
|
|
233
|
-
*
|
|
234
|
-
* @param msg - The message to be encoded.
|
|
235
|
-
* @returns The encoded message as a Buffer.
|
|
236
|
-
*/
|
|
237
|
-
function encode_payload(msg) {
|
|
238
|
-
return Buffer.from(JSON.stringify(msg));
|
|
239
|
-
}
|
package/dist/sfr-pipeline.mjs
DELETED
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
3
|
-
import { template } from "./util.mjs";
|
|
4
|
-
import { BroadcastMQ, TargetedMQ } from "./mq.mjs";
|
|
5
|
-
import j2s from "joi-to-swagger";
|
|
6
|
-
import { logger } from "./logger.mjs";
|
|
7
|
-
const CWD = process.cwd();
|
|
8
|
-
const PATTERNS = ["Point-to-Point", "Request-Reply", "Fanout", "Direct", "Topic"];
|
|
9
|
-
const TARGETED_PATTERN = ["Point-to-Point", "Request-Reply"];
|
|
10
|
-
export class SFRPipeline {
|
|
11
|
-
cfg;
|
|
12
|
-
oas_cfg;
|
|
13
|
-
comms;
|
|
14
|
-
base_url;
|
|
15
|
-
pattern_channels;
|
|
16
|
-
mount_data = {
|
|
17
|
-
rest: 0,
|
|
18
|
-
ws: 0,
|
|
19
|
-
mq: 0
|
|
20
|
-
};
|
|
21
|
-
/* Contains values that are injected into each protocol's handlers. */
|
|
22
|
-
static injections = {
|
|
23
|
-
rest: {
|
|
24
|
-
handlers: {},
|
|
25
|
-
controllers: {}
|
|
26
|
-
},
|
|
27
|
-
mq: {
|
|
28
|
-
handlers: {},
|
|
29
|
-
controllers: {}
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
constructor(cfg, oas_cfg, comms) {
|
|
33
|
-
this.cfg = cfg;
|
|
34
|
-
this.oas_cfg = oas_cfg;
|
|
35
|
-
this.comms = comms;
|
|
36
|
-
}
|
|
37
|
-
async init(base_url) {
|
|
38
|
-
this.base_url = base_url;
|
|
39
|
-
if (this.comms["MQ"]) {
|
|
40
|
-
//Create channels for each type of Communication Pattern
|
|
41
|
-
const channels = await Promise.all(PATTERNS.map(async (v) => {
|
|
42
|
-
const mq = TARGETED_PATTERN.includes(v) ? new TargetedMQ(this.comms["MQ"], v) : new BroadcastMQ(this.comms["MQ"], v);
|
|
43
|
-
return [v, mq];
|
|
44
|
-
}));
|
|
45
|
-
this.pattern_channels = Object.fromEntries(channels);
|
|
46
|
-
SFRPipeline.injections.rest.handlers = {
|
|
47
|
-
...SFRPipeline.injections.rest.handlers,
|
|
48
|
-
mq: this.pattern_channels
|
|
49
|
-
};
|
|
50
|
-
SFRPipeline.injections.mq.handlers = {
|
|
51
|
-
...SFRPipeline.injections.mq.handlers,
|
|
52
|
-
mq: this.pattern_channels
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
//Declarations for each protocol in key/value pairs.
|
|
56
|
-
let protocols = await this.file_parsing();
|
|
57
|
-
//Filter protocols that have been parsed if its' associated SFRProtocol has been defined
|
|
58
|
-
/* @ts-ignore */
|
|
59
|
-
protocols = Object.fromEntries(Object.entries(protocols).filter(([k]) => this.comms[k]).map(([k, v]) => [k, v]));
|
|
60
|
-
await this.fn_binding(protocols);
|
|
61
|
-
this.print_to_console();
|
|
62
|
-
return this.spec_generation(protocols);
|
|
63
|
-
}
|
|
64
|
-
// Handles the parsing of SFR files from the directory specified under cfg.out config.
|
|
65
|
-
// It returns a standard NamespaceDeclaration containing each declaration of each supported protocol.
|
|
66
|
-
async file_parsing() {
|
|
67
|
-
//These are the officially supported protocols of the SFR library
|
|
68
|
-
const SUPPORTED_PROTOCOLS = ["rest", "ws", "mq"];
|
|
69
|
-
/*
|
|
70
|
-
Look for folders that match the supported protocols listed above in the designated directory
|
|
71
|
-
|
|
72
|
-
cfg.root = "Specifies the working directory"
|
|
73
|
-
cfg.path = "Specifies the API directory i.e: where SFR files reside"
|
|
74
|
-
cfg.out = "Specifies the directory where the resulting OAS documents are outputted"
|
|
75
|
-
*/
|
|
76
|
-
const directory = path.join(CWD, this.cfg.root);
|
|
77
|
-
let paths = SUPPORTED_PROTOCOLS.map((v) => path.join(directory, `${this.cfg.path}/${v}`));
|
|
78
|
-
//Create the directories if it doesn't exist.
|
|
79
|
-
await Promise.all(paths.map((v) => fs.mkdir(v, { recursive: true })));
|
|
80
|
-
//Recursively retrieve all SFRs of each protocol
|
|
81
|
-
let sfr_paths = await Promise.all(paths.map((path) => resolve_sfrs(path)));
|
|
82
|
-
//Import SFRs
|
|
83
|
-
const protocols = await Promise.all(sfr_paths.map((protocol) => import_sfrs(protocol)));
|
|
84
|
-
const REST = protocols[0];
|
|
85
|
-
logger.info(`${Object.keys(protocols[0]).length} REST SFR`);
|
|
86
|
-
const WS = protocols[1];
|
|
87
|
-
logger.info(`${Object.keys(protocols[1]).length} WS SFR`);
|
|
88
|
-
const MQ = protocols[2];
|
|
89
|
-
logger.info(`${Object.keys(protocols[2]).length} MQ SFR`);
|
|
90
|
-
return { REST, WS, MQ };
|
|
91
|
-
}
|
|
92
|
-
// Handles the binding of the components of each SFR (e.g: executing validators of matching endpoints, injecting controllers to each handler, etc.)
|
|
93
|
-
//Owing to their differing nature, spec generation and fn_binding is handled this way so as to split up the individual logics involved in parsing them.
|
|
94
|
-
async fn_binding(protocols) {
|
|
95
|
-
Object.entries(protocols).forEach(([protocol, declaration]) => {
|
|
96
|
-
switch (protocol) {
|
|
97
|
-
case "REST":
|
|
98
|
-
this.bind_rest_fns(declaration);
|
|
99
|
-
break;
|
|
100
|
-
case "WS":
|
|
101
|
-
this.bind_ws_fns(declaration);
|
|
102
|
-
break;
|
|
103
|
-
case "MQ":
|
|
104
|
-
this.bind_mq_fns(declaration);
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
/* Fn Binding */
|
|
110
|
-
async bind_rest_fns(declaration) {
|
|
111
|
-
const comms = this.comms["REST"];
|
|
112
|
-
if (!comms)
|
|
113
|
-
throw new Error(`FN Binding failed: \nREST protocol lacks it's associated SFRProtocols`);
|
|
114
|
-
Object.entries(declaration).forEach(([namespace, module]) => {
|
|
115
|
-
let { cfg, validators, handlers } = module.content;
|
|
116
|
-
//Set base_directory
|
|
117
|
-
/*
|
|
118
|
-
Directory Order:
|
|
119
|
-
this.base_url -> Folder Structure -> cfg.base_dir
|
|
120
|
-
*/
|
|
121
|
-
let base_dir = "";
|
|
122
|
-
if (this.base_url)
|
|
123
|
-
base_dir = `/${this.base_url}`;
|
|
124
|
-
if (module.dir)
|
|
125
|
-
base_dir += `/${module.dir.toString().replaceAll(",", "/")}`;
|
|
126
|
-
if (cfg.base_dir)
|
|
127
|
-
base_dir += `/${cfg.base_dir}`;
|
|
128
|
-
logger.info(`[SFR ${module.name}] resolved path: ${base_dir}/${namespace}`);
|
|
129
|
-
const is_public = Boolean(cfg.public);
|
|
130
|
-
//Loop over all methods and their respective handlers
|
|
131
|
-
Object.entries(handlers).forEach(([method, handler_map]) => {
|
|
132
|
-
for (const [name, handler] of Object.entries(handler_map).filter(([k]) => validators[k])) {
|
|
133
|
-
const dir = `${base_dir}/${namespace}/${name}`;
|
|
134
|
-
console.log(dir);
|
|
135
|
-
const validator = validators[name];
|
|
136
|
-
const validator_type = typeof validator !== "function" ? "joi" : "multer";
|
|
137
|
-
/* bind validators */
|
|
138
|
-
switch (validator_type) {
|
|
139
|
-
case "joi":
|
|
140
|
-
{
|
|
141
|
-
comms[method.toLowerCase()](dir, (req, res, next) => {
|
|
142
|
-
let error = true;
|
|
143
|
-
if (is_public) {
|
|
144
|
-
//if(!req.session.user)return res.status(401).json({error : "Session not found."});
|
|
145
|
-
}
|
|
146
|
-
error = template(validator, method === "GET" ? req.query : req.body);
|
|
147
|
-
if (error)
|
|
148
|
-
return res.status(400).json({ error });
|
|
149
|
-
next();
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
break;
|
|
153
|
-
case "multer":
|
|
154
|
-
{
|
|
155
|
-
comms[method.toLowerCase()](dir, validator);
|
|
156
|
-
/* @ts-ignore */
|
|
157
|
-
comms.use((err, req, res, next) => {
|
|
158
|
-
let temp = { error: err.message, details: err.cause };
|
|
159
|
-
if (err)
|
|
160
|
-
return res.status(400).json(temp);
|
|
161
|
-
next();
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
/* Bind to express app */
|
|
167
|
-
comms[method.toLowerCase()](dir, handler.fn);
|
|
168
|
-
this.mount_data.rest++;
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
async bind_ws_fns(declaration) {
|
|
174
|
-
}
|
|
175
|
-
async bind_mq_fns(declaration) {
|
|
176
|
-
const comms = this.comms["MQ"];
|
|
177
|
-
if (!comms)
|
|
178
|
-
throw new Error(`FN Binding failed: \nMQ protocol lacks it's associated SFRProtocols`);
|
|
179
|
-
Object.entries(declaration).forEach(([namespace, module]) => {
|
|
180
|
-
let { cfg, validators, handlers } = module.content;
|
|
181
|
-
//Set base_directory
|
|
182
|
-
let base_dir = cfg.base_dir ? `/${cfg.base_dir}` : "";
|
|
183
|
-
if (this.base_url)
|
|
184
|
-
base_dir = `/${this.base_url}/${base_dir}`;
|
|
185
|
-
if (module.dir)
|
|
186
|
-
base_dir = `${module.dir.toString().replaceAll(",", "/")}/${base_dir}`;
|
|
187
|
-
const is_public = Boolean(cfg.public);
|
|
188
|
-
//Loop over all methods and their respective handlers
|
|
189
|
-
Object.entries(handlers).forEach(([pattern, handler_map]) => {
|
|
190
|
-
//Get associated MQ for pattern
|
|
191
|
-
let mq = this.pattern_channels[pattern];
|
|
192
|
-
//Get all valid handlers (i.e: those with matching validators)
|
|
193
|
-
const valid_handlers = Object.entries(handler_map).filter(([k]) => validators[k]);
|
|
194
|
-
//Loop over each handler
|
|
195
|
-
for (let [name, handler] of valid_handlers) {
|
|
196
|
-
const dir = `${base_dir}${namespace}/${name}`;
|
|
197
|
-
const validator = validators[name];
|
|
198
|
-
//const validator_type = typeof validator !== "function" ? "joi" : "multer"; // TODO: Dynamically update parameter content based on validator type.
|
|
199
|
-
let validator_fn = function (msg) {
|
|
200
|
-
const error = template(validator, msg.content);
|
|
201
|
-
if (error) {
|
|
202
|
-
if (mq instanceof TargetedMQ) {
|
|
203
|
-
if (error) {
|
|
204
|
-
switch (pattern) {
|
|
205
|
-
case "Request-Reply":
|
|
206
|
-
mq.reply(msg, { error });
|
|
207
|
-
break;
|
|
208
|
-
default: mq.channel.reject(msg, false);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (mq instanceof BroadcastMQ) {
|
|
213
|
-
mq.channel.reject(msg, false);
|
|
214
|
-
}
|
|
215
|
-
return; //Return immediately to avoid executing handler fn (which may contain reply or ack calls.)
|
|
216
|
-
}
|
|
217
|
-
handler.fn(msg);
|
|
218
|
-
};
|
|
219
|
-
/* bind validators */
|
|
220
|
-
if (mq instanceof TargetedMQ) {
|
|
221
|
-
mq.consume(dir, {
|
|
222
|
-
options: handler.options,
|
|
223
|
-
fn: validator_fn
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
if (mq instanceof BroadcastMQ) {
|
|
227
|
-
mq.subscribe(dir, handler.key, {
|
|
228
|
-
options: handler.options,
|
|
229
|
-
fn: validator_fn
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
this.mount_data.mq++;
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
// Generate OpenAPI and AsyncAPI specification.
|
|
238
|
-
spec_generation(protocols) {
|
|
239
|
-
// Holds the key/value pair of protocols and their respective API Documentation
|
|
240
|
-
const documents = Object.entries(protocols).map(([protocol, declaration]) => {
|
|
241
|
-
switch (protocol) {
|
|
242
|
-
case "REST": return [protocol, this.generate_open_api_document(declaration)];
|
|
243
|
-
case "WS": return [protocol, this.generate_async_api_document(declaration)];
|
|
244
|
-
case "MQ": return [protocol, this.generate_async_api_document(declaration)];
|
|
245
|
-
default: throw new Error(`Failed to generate SFR Spec: ${protocol} protocol is unknown.`);
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
return Object.fromEntries(documents);
|
|
249
|
-
}
|
|
250
|
-
generate_open_api_document(declaration) {
|
|
251
|
-
// This will hold the final OpenAPI Document.
|
|
252
|
-
const spec = {
|
|
253
|
-
openapi: "3.0.0",
|
|
254
|
-
info: Object.assign({}, this.oas_cfg),
|
|
255
|
-
paths: {}
|
|
256
|
-
};
|
|
257
|
-
// Iterate through each protocol (e.g., REST, WS, MQ)
|
|
258
|
-
Object.entries(declaration).forEach(([namespace, module]) => {
|
|
259
|
-
const { cfg, handlers, validators } = module.content;
|
|
260
|
-
//Set base_directory
|
|
261
|
-
let base_dir = cfg.base_dir ? `/${cfg.base_dir}/` : "/";
|
|
262
|
-
if (this.base_url)
|
|
263
|
-
base_dir = `/${this.base_url}${base_dir}`;
|
|
264
|
-
if (module.dir)
|
|
265
|
-
base_dir = `${module.dir.toString().replaceAll(",", "/")}/${base_dir}`;
|
|
266
|
-
// Collect the OpenAPI path object for each handler (method)
|
|
267
|
-
for (let [method, handler_map] of Object.entries(handlers)) {
|
|
268
|
-
//Transform method to lowercase
|
|
269
|
-
method = method.toLowerCase();
|
|
270
|
-
// Define the operation (method) for this endpoint
|
|
271
|
-
for (const [endpoint, body] of Object.entries(handler_map)) {
|
|
272
|
-
const validator = validators[endpoint];
|
|
273
|
-
// Skip if no matching validator
|
|
274
|
-
if (!validator)
|
|
275
|
-
continue;
|
|
276
|
-
const validator_type = typeof validator !== "function" ? "joi" : "multer";
|
|
277
|
-
//Default tags are used for service and router level classification (developer platform)
|
|
278
|
-
let default_tags = [
|
|
279
|
-
`sfr-router:${namespace}`,
|
|
280
|
-
`sfr-service:${this.oas_cfg.title || "unspecified"}`
|
|
281
|
-
];
|
|
282
|
-
if (body.tags && Array.isArray(body.tags))
|
|
283
|
-
default_tags.push(...body.tags);
|
|
284
|
-
const operation_id = `${base_dir}${namespace}/${endpoint}`;
|
|
285
|
-
const document = {
|
|
286
|
-
operationId: `${method.toUpperCase()}:${operation_id}`,
|
|
287
|
-
summary: body.summary || "",
|
|
288
|
-
description: body.description || "",
|
|
289
|
-
tags: default_tags,
|
|
290
|
-
public: Boolean(cfg.public),
|
|
291
|
-
responses: {
|
|
292
|
-
"200": {
|
|
293
|
-
description: "Successful operation",
|
|
294
|
-
content: { "application/json": { schema: { type: "object" } } }
|
|
295
|
-
},
|
|
296
|
-
"400": {
|
|
297
|
-
description: "An error has occured",
|
|
298
|
-
content: { "application/json": { schema: { type: "object" } } }
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
//Insert either a parameter or a requestBody according to the method type
|
|
303
|
-
if (validator_type === "joi") {
|
|
304
|
-
document[method === "GET" ? "parameters" : "requestBody"] = j2s(validator).swagger;
|
|
305
|
-
}
|
|
306
|
-
//Haven't found a library for converting multer validators to swagger doc
|
|
307
|
-
if (validator_type === "multer") {
|
|
308
|
-
}
|
|
309
|
-
//Create path if it does not exist.
|
|
310
|
-
if (!spec.paths[operation_id])
|
|
311
|
-
spec.paths[operation_id] = {};
|
|
312
|
-
if (!spec.paths[operation_id][method.toLowerCase()])
|
|
313
|
-
spec.paths[operation_id][method.toLowerCase()] = document;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
//Conform to OAPI standards by appending "x-property" on each field under "meta"
|
|
318
|
-
/* @ts-ignore */
|
|
319
|
-
spec.info.meta = Object.fromEntries(Object.entries(spec.info.meta).map(([k, v]) => [`x-${k}`, v]));
|
|
320
|
-
return spec;
|
|
321
|
-
}
|
|
322
|
-
// Method to generate an AsyncAPI document from an MQNamespaceDeclaration
|
|
323
|
-
generate_async_api_document(declaration) {
|
|
324
|
-
// This will hold the final AsyncAPI Document.
|
|
325
|
-
const spec = {
|
|
326
|
-
asyncapi: '3.0.0',
|
|
327
|
-
info: Object.assign({}, this.oas_cfg),
|
|
328
|
-
channels: {},
|
|
329
|
-
operations: {},
|
|
330
|
-
components: {
|
|
331
|
-
messages: {},
|
|
332
|
-
},
|
|
333
|
-
};
|
|
334
|
-
//Conform to OAPI standards by appending "x-property" on each field under "meta"
|
|
335
|
-
/* @ts-ignore */
|
|
336
|
-
spec.info.meta = Object.fromEntries(Object.entries(spec.info.meta).map(([k, v]) => [`x-${k}`, v]));
|
|
337
|
-
Object.entries(declaration).forEach(([namespace, module]) => {
|
|
338
|
-
let { cfg, validators, handlers } = module.content;
|
|
339
|
-
//Set base_directory
|
|
340
|
-
let base_dir = cfg.base_dir ? `/${cfg.base_dir}/` : "";
|
|
341
|
-
if (this.base_url)
|
|
342
|
-
base_dir = `/${this.base_url}/${base_dir}`;
|
|
343
|
-
if (module.dir)
|
|
344
|
-
base_dir = `${module.dir.toString().replaceAll(",", "/")}/${base_dir}`;
|
|
345
|
-
const is_public = Boolean(cfg.public);
|
|
346
|
-
//Loop over all methods and their respective handlers
|
|
347
|
-
Object.entries(handlers).forEach(([pattern, handler_map]) => {
|
|
348
|
-
//Get associated MQ for pattern
|
|
349
|
-
let mq = this.pattern_channels[pattern];
|
|
350
|
-
//Get all valid handlers (i.e: those with matching validators)
|
|
351
|
-
const valid_handlers = Object.entries(handler_map).filter(([k]) => validators[k]);
|
|
352
|
-
//Loop over each handler
|
|
353
|
-
for (let [name, handler] of valid_handlers) {
|
|
354
|
-
const dir = `${base_dir}${namespace}/${name}`;
|
|
355
|
-
const validator = validators[name];
|
|
356
|
-
const validator_type = typeof validator !== "function" ? "joi" : "multer"; // TODO: Dynamically update parameter content based on validator type.
|
|
357
|
-
const channel_document = {
|
|
358
|
-
address: dir,
|
|
359
|
-
messages: {}
|
|
360
|
-
};
|
|
361
|
-
const operation_document = {
|
|
362
|
-
action: "receive",
|
|
363
|
-
summary: handler.summary || "",
|
|
364
|
-
description: handler.description || "",
|
|
365
|
-
channel: `#/channels/${name}`
|
|
366
|
-
};
|
|
367
|
-
//Insert either a parameter or a requestBody according to the method type
|
|
368
|
-
if (validator_type === "joi") {
|
|
369
|
-
channel_document.messages[`${name}-message`] = {
|
|
370
|
-
name: `${name}-message`,
|
|
371
|
-
payload: j2s(validator).swagger
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
//Haven't found a library for converting multer validators to swagger doc
|
|
375
|
-
if (validator_type === "multer") {
|
|
376
|
-
}
|
|
377
|
-
spec.channels[name] = channel_document;
|
|
378
|
-
spec.operations[name] = operation_document;
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
return spec;
|
|
383
|
-
}
|
|
384
|
-
// Method to print SFR info to console
|
|
385
|
-
print_to_console() {
|
|
386
|
-
console.log(`____________________________
|
|
387
|
-
| | Mounted
|
|
388
|
-
| ░██████╗███████╗██████╗░ | REST : ${this.mount_data.rest}
|
|
389
|
-
| ██╔════╝██╔════╝██╔══██╗ | WS : ${this.mount_data.ws}
|
|
390
|
-
| ╚█████╗░█████╗░░██████╔╝ | MQ : ${this.mount_data.mq}
|
|
391
|
-
| ░╚═══██╗██╔══╝░░██╔══██╗ |-----------
|
|
392
|
-
| ██████╔╝██║░░░░░██║░░██║ | Protocols
|
|
393
|
-
| ╚═════╝░╚═╝░░░░░╚═╝░░╚═╝ | REST : ${this.comms["REST"] ? "✅" : "❌"}
|
|
394
|
-
| Single File Router | MQ : ${this.comms["MQ"] ? "✅" : "❌"}
|
|
395
|
-
| | WS : ${this.comms["WS"] ? "✅" : "❌"}
|
|
396
|
-
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯`);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
/* Utility fns */
|
|
400
|
-
/* File Parsing */
|
|
401
|
-
async function resolve_sfrs(dir, prefix) {
|
|
402
|
-
//Get Directory Contents
|
|
403
|
-
const contents = await fs.readdir(dir);
|
|
404
|
-
const results = [];
|
|
405
|
-
//Loop over contents
|
|
406
|
-
await Promise.all(contents.map(async (v) => {
|
|
407
|
-
const content_path = path.join(dir, v);
|
|
408
|
-
const is_dir = (await fs.lstat(content_path)).isDirectory();
|
|
409
|
-
//If Directory, perform a recursion and return output of contents.map fn.
|
|
410
|
-
if (is_dir) {
|
|
411
|
-
const dir_output = await resolve_sfrs(content_path, prefix ? `${prefix}/${v}` : v);
|
|
412
|
-
results.push(dir_output);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
;
|
|
416
|
-
let output = { name: v, path: content_path };
|
|
417
|
-
if (prefix)
|
|
418
|
-
output.dir = prefix.split("/");
|
|
419
|
-
results.push(output);
|
|
420
|
-
}));
|
|
421
|
-
return results.flat();
|
|
422
|
-
}
|
|
423
|
-
async function import_sfrs(protocol_files) {
|
|
424
|
-
const protocols = await Promise.all(protocol_files.map(async (v) => {
|
|
425
|
-
const import_task = await import(`file:///${v.path}`);
|
|
426
|
-
return [v.name.replace(".mjs", ""), {
|
|
427
|
-
...v,
|
|
428
|
-
content: import_task.default
|
|
429
|
-
}];
|
|
430
|
-
}));
|
|
431
|
-
//Removes SFRs without body.
|
|
432
|
-
return Object.fromEntries(protocols.filter((v) => v[1].content));
|
|
433
|
-
}
|
|
434
|
-
// Method to generate JSON schema for the payload based on handler metadata
|
|
435
|
-
function generate_json_schema(metadata) {
|
|
436
|
-
return {
|
|
437
|
-
type: 'object',
|
|
438
|
-
properties: {
|
|
439
|
-
example_field: {
|
|
440
|
-
type: 'string',
|
|
441
|
-
...metadata
|
|
442
|
-
},
|
|
443
|
-
},
|
|
444
|
-
required: ['example_field'],
|
|
445
|
-
};
|
|
446
|
-
}
|
package/dist/templates.mjs
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
//Previous Name: RequestHandlerFactory
|
|
2
|
-
//Dependencies are imported and used by the functions below
|
|
3
|
-
import { SFRPipeline } from "./sfr-pipeline.mjs";
|
|
4
|
-
export function REST(struct) {
|
|
5
|
-
const validators = struct.validators || {};
|
|
6
|
-
const controllers = struct.controllers || {};
|
|
7
|
-
const handlers = struct.handlers || {};
|
|
8
|
-
const cfg = struct.cfg || {};
|
|
9
|
-
//Bind controller injections to each controllers
|
|
10
|
-
Object.entries(controllers).map(([k, v]) => controllers[k] = v.bind({ ...SFRPipeline.injections.rest.controllers }));
|
|
11
|
-
//Bind handler injections and SFR controllers into each handlers.
|
|
12
|
-
Object.entries(handlers).forEach(([method, handlermap]) => {
|
|
13
|
-
Object.entries(handlermap).forEach(([k, v]) => {
|
|
14
|
-
/* @ts-ignore */
|
|
15
|
-
handlers[method][k] = { ...v, fn: v.fn.bind({ ...controllers, ...SFRPipeline.injections.rest.handlers }) };
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
/* @ts-ignore */
|
|
19
|
-
return { validators, controllers, handlers, cfg };
|
|
20
|
-
}
|
|
21
|
-
export function WS(struct) {
|
|
22
|
-
const validators = struct.validators || {};
|
|
23
|
-
const controllers = struct.controllers || {};
|
|
24
|
-
const handlers = struct.handlers || {};
|
|
25
|
-
const cfg = struct.cfg || {};
|
|
26
|
-
//Dependency injection happens here...npm
|
|
27
|
-
return { validators, controllers, handlers, cfg };
|
|
28
|
-
}
|
|
29
|
-
export function MQ(struct) {
|
|
30
|
-
const validators = struct.validators || {};
|
|
31
|
-
const controllers = struct.controllers || {};
|
|
32
|
-
const handlers = struct.handlers || {};
|
|
33
|
-
const cfg = struct.cfg || {};
|
|
34
|
-
//Bind controller injections to each controllers
|
|
35
|
-
Object.entries(controllers).map(([k, v]) => controllers[k] = v.bind({ ...SFRPipeline.injections.mq.controllers }));
|
|
36
|
-
//Bind handler injections and SFR controllers into each handlers.
|
|
37
|
-
Object.entries(handlers).forEach(([pattern, handlermap]) => {
|
|
38
|
-
Object.entries(handlermap).forEach(([k, v]) => {
|
|
39
|
-
/* @ts-ignore */
|
|
40
|
-
const { mq } = (SFRPipeline.injections.mq.handlers || {});
|
|
41
|
-
/* @ts-ignore */
|
|
42
|
-
handlers[pattern][k] = { ...v, fn: v.fn.bind({ ...controllers, mq: mq ? mq[pattern] : null }) };
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
/* @ts-ignore */
|
|
46
|
-
return { validators, controllers, handlers, cfg };
|
|
47
|
-
}
|
package/dist/types/example.d.mts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
declare const _default: {
|
|
2
|
-
GET: {
|
|
3
|
-
"get-something": {
|
|
4
|
-
description: string;
|
|
5
|
-
fn(req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express").Response<any, Record<string, any>>): Promise<void>;
|
|
6
|
-
};
|
|
7
|
-
};
|
|
8
|
-
} & {
|
|
9
|
-
db_call(): Promise<string>;
|
|
10
|
-
};
|
|
11
|
-
export default _default;
|
package/dist/types/index.d.mts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/// <reference path="../../src/types/index.d.ts" />
|
|
2
|
-
import { REST, WS, MQ } from "./templates.mjs";
|
|
3
|
-
import { MQLib, BroadcastMQ, TargetedMQ } from "./mq.mjs";
|
|
4
|
-
declare const _default: (cfg: ParserCFG, oas_cfg: OASConfig, connectors: SFRProtocols, base_url?: string) => Promise<ServiceManifest>;
|
|
5
|
-
export default _default;
|
|
6
|
-
declare function inject(injections: InjectedFacilities): void;
|
|
7
|
-
export { REST, WS, MQ, MQLib, BroadcastMQ, TargetedMQ, inject };
|
package/dist/types/logger.d.mts
DELETED
package/dist/types/mq.d.mts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { ChannelModel, Options, Channel, ConsumeMessage } from "amqplib";
|
|
2
|
-
/**
|
|
3
|
-
* MQLib class for establishing a connection to the message queue and creating channels.
|
|
4
|
-
*/
|
|
5
|
-
export declare class MQLib {
|
|
6
|
-
private connection?;
|
|
7
|
-
private 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
|
-
init(MQ_URL: string): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Returns the connection object.
|
|
19
|
-
*
|
|
20
|
-
* @returns The connection object to the message queue.
|
|
21
|
-
*/
|
|
22
|
-
get_connection(): ChannelModel;
|
|
23
|
-
/**
|
|
24
|
-
* Returns the channel object.
|
|
25
|
-
*
|
|
26
|
-
* @returns The channel object to the message queue.
|
|
27
|
-
*/
|
|
28
|
-
get_channel(): Channel;
|
|
29
|
-
}
|
|
30
|
-
export declare class BaseMQ {
|
|
31
|
-
channel: Channel;
|
|
32
|
-
protected type: CommunicationPattern;
|
|
33
|
-
constructor(channel: Channel, type: CommunicationPattern);
|
|
34
|
-
/**
|
|
35
|
-
* Sends a reply to the message sender in a Request-Reply pattern.
|
|
36
|
-
*
|
|
37
|
-
* @param msg - The original message to reply to.
|
|
38
|
-
* @param payload - The reply message to send back to the requester.
|
|
39
|
-
* @example
|
|
40
|
-
* // Send a response back to the client in a Request-Reply pattern
|
|
41
|
-
* const mq = new TargetedMQ(channel, "Request-Reply");
|
|
42
|
-
* mq.reply(msg, { result: "Processed successfully" });
|
|
43
|
-
*/
|
|
44
|
-
reply(msg: ConsumeMessage, payload: any): Promise<void>;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* BroadcastMQ class to handle different types of broadcast communication patterns: Fanout, Direct, and Topic.
|
|
48
|
-
*/
|
|
49
|
-
export declare class BroadcastMQ extends BaseMQ {
|
|
50
|
-
channel: Channel;
|
|
51
|
-
protected type: CommunicationPattern;
|
|
52
|
-
protected exchange_options?: Options.AssertExchange;
|
|
53
|
-
constructor(channel: Channel, type: CommunicationPattern, exchange_options?: Options.AssertExchange);
|
|
54
|
-
/**
|
|
55
|
-
* Publishes a message to the specified exchange based on the communication pattern.
|
|
56
|
-
*
|
|
57
|
-
* @param exchange - The name of the exchange to send the message to.
|
|
58
|
-
* @param key - The routing key or pattern to use, depending on the communication pattern.
|
|
59
|
-
* @param payload - The message to be sent.
|
|
60
|
-
* @param options - Additional publishing options.
|
|
61
|
-
* @example
|
|
62
|
-
* // Publish a message to a 'logsExchange' with a routing key 'error' for a routing pattern.
|
|
63
|
-
* const mq = new BroadcastMQ(channel, "Direct");
|
|
64
|
-
* mq.publish("logsExchange", "error", { level: "error", message: "Something went wrong" });
|
|
65
|
-
*/
|
|
66
|
-
publish(exchange: string, key: string, payload: any, options?: Options.Publish): Promise<void>;
|
|
67
|
-
/**
|
|
68
|
-
* Subscribes to an exchange to receive messages based on the communication pattern.
|
|
69
|
-
*
|
|
70
|
-
* @param exchange - The name of the exchange to subscribe to.
|
|
71
|
-
* @param key - The routing key or pattern to listen for.
|
|
72
|
-
* @param cfg - Configuration options for the consumer, including the callback function.
|
|
73
|
-
* @param options - Additional consumption options.
|
|
74
|
-
* @example
|
|
75
|
-
* // Subscribe to the 'logsExchange' for all 'error' messages in the Direct pattern.
|
|
76
|
-
* const mq = new BroadcastMQ(channel, "Direct");
|
|
77
|
-
* mq.subscribe("logsExchange", "error", { fn: handleErrorLogs, options: { noAck: true } });
|
|
78
|
-
*/
|
|
79
|
-
subscribe(exchange: string, key: string, cfg: ConsumerConfig): Promise<void>;
|
|
80
|
-
set_options(options: Options.AssertExchange): void;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* TargetedMQ class to handle Point-to-Point and Request-Reply communication patterns.
|
|
84
|
-
*/
|
|
85
|
-
export declare class TargetedMQ extends BaseMQ {
|
|
86
|
-
channel: Channel;
|
|
87
|
-
protected type: CommunicationPattern;
|
|
88
|
-
protected queue_options?: Options.AssertQueue;
|
|
89
|
-
constructor(channel: Channel, type: CommunicationPattern, queue_options?: Options.AssertQueue);
|
|
90
|
-
/**
|
|
91
|
-
* Sends a message to the specified queue depending on the communication pattern.
|
|
92
|
-
*
|
|
93
|
-
* @param binding - The name of the queue or binding to send the message to.
|
|
94
|
-
* @param payload - The message to be sent.
|
|
95
|
-
* @param options - Additional publishing options.
|
|
96
|
-
* @example
|
|
97
|
-
* // Send a message to a queue for point-to-point communication
|
|
98
|
-
* const mq = new TargetedMQ(channel, "Point-to-Point");
|
|
99
|
-
* mq.produce("taskQueue", { taskId: 1, action: "process" });
|
|
100
|
-
*/
|
|
101
|
-
produce(binding: string, payload: any, options?: Options.Publish): Promise<boolean>;
|
|
102
|
-
/**
|
|
103
|
-
* Consumes messages from the specified queue based on the communication pattern.
|
|
104
|
-
*
|
|
105
|
-
* @param queue - The name of the queue to consume messages from.
|
|
106
|
-
* @param cfg - Configuration options for the consumer, including the callback function.
|
|
107
|
-
* @example
|
|
108
|
-
* // Consume a message from the 'taskQueue' in a Point-to-Point communication pattern
|
|
109
|
-
* const mq = new TargetedMQ(channel, "Point-to-Point");
|
|
110
|
-
* mq.consume("taskQueue", { fn: processTask, options: { noAck: true } });
|
|
111
|
-
*/
|
|
112
|
-
consume(queue: string, cfg: ConsumerConfig): Promise<void>;
|
|
113
|
-
set_options(options: Options.AssertQueue): void;
|
|
114
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export declare class SFRPipeline {
|
|
2
|
-
private cfg;
|
|
3
|
-
private oas_cfg;
|
|
4
|
-
private comms;
|
|
5
|
-
base_url: string;
|
|
6
|
-
pattern_channels: PatternMQSet;
|
|
7
|
-
mount_data: {
|
|
8
|
-
rest: number;
|
|
9
|
-
ws: number;
|
|
10
|
-
mq: number;
|
|
11
|
-
};
|
|
12
|
-
static injections: {
|
|
13
|
-
rest: {
|
|
14
|
-
handlers: {};
|
|
15
|
-
controllers: {};
|
|
16
|
-
};
|
|
17
|
-
mq: {
|
|
18
|
-
handlers: {};
|
|
19
|
-
controllers: {};
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
constructor(cfg: ParserCFG, oas_cfg: OASConfig, comms: SFRProtocols);
|
|
23
|
-
init(base_url?: string): Promise<ServiceDocuments>;
|
|
24
|
-
private file_parsing;
|
|
25
|
-
private fn_binding;
|
|
26
|
-
private bind_rest_fns;
|
|
27
|
-
private bind_ws_fns;
|
|
28
|
-
private bind_mq_fns;
|
|
29
|
-
private spec_generation;
|
|
30
|
-
private generate_open_api_document;
|
|
31
|
-
private generate_async_api_document;
|
|
32
|
-
private print_to_console;
|
|
33
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export declare function REST<V, H, C>(struct: RESTHandlerDescriptor<V, H, C>): H & C;
|
|
2
|
-
export declare function WS<V, H, C>(struct: WSHandlerDescriptor<V, H, C>): {
|
|
3
|
-
validators: RequestValidators;
|
|
4
|
-
controllers: RequestControllers;
|
|
5
|
-
handlers: WSRequestHandlers;
|
|
6
|
-
cfg: SFRConfig;
|
|
7
|
-
};
|
|
8
|
-
export declare function MQ<V, H, C>(struct: MQHandlerDescriptor<V, H, C>): H & C;
|
package/dist/types/util.d.mts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { Response } from "express";
|
|
2
|
-
import Joi from "joi";
|
|
3
|
-
export declare function get_stats(dirs: string[], base_dir: string): Promise<string[]>;
|
|
4
|
-
export declare function assess_namespace(stats: string[], dir: string): Promise<any>;
|
|
5
|
-
export declare const template: (rule: object, v: object) => string | false;
|
|
6
|
-
export declare const object_id: Joi.StringSchema<string>;
|
|
7
|
-
export declare const handle_res: (controller: Promise<any>, res: Response) => void;
|
package/dist/util.mjs
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import fs from "fs/promises";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import Joi from "joi";
|
|
4
|
-
export async function get_stats(dirs, base_dir) {
|
|
5
|
-
let temp = await Promise.all(dirs.map((v) => fs.lstat(path.join(base_dir, v)).then((stats) => ({ stats, dir: v }))));
|
|
6
|
-
const result = temp.filter(({ stats }) => stats.isFile()).map((v) => v.dir);
|
|
7
|
-
return result;
|
|
8
|
-
}
|
|
9
|
-
export async function assess_namespace(stats, dir) {
|
|
10
|
-
let dirs = stats.map((v) => `file:///${dir.replaceAll("\\", "/")}/${v}`);
|
|
11
|
-
const result = await Promise.all(dirs.map((d) => import(d).then((v) => [d.replace(".mjs", "").substring(d.lastIndexOf("/") + 1), v.default]))).then(Object.fromEntries);
|
|
12
|
-
return result;
|
|
13
|
-
}
|
|
14
|
-
export const template = (rule, v) => _validate(Joi.object(rule).validate(v));
|
|
15
|
-
function _validate(expression) {
|
|
16
|
-
let result = expression.error;
|
|
17
|
-
return result ? result.details[0].message : false;
|
|
18
|
-
}
|
|
19
|
-
export const object_id = Joi.string().hex().length(24).required();
|
|
20
|
-
/* QoLs */
|
|
21
|
-
export const handle_res = (controller, res) => {
|
|
22
|
-
controller
|
|
23
|
-
.then((data) => res.json({ data }))
|
|
24
|
-
.catch((error) => res.status(400).json({ error }));
|
|
25
|
-
};
|