@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 +72 -0
- package/dist/example.mjs +33 -0
- package/dist/index.mjs +84 -0
- package/dist/logger.mjs +37 -0
- package/dist/mq.mjs +217 -0
- package/dist/sfr-pipeline.mjs +465 -0
- package/dist/templates.mjs +47 -0
- package/dist/types/example.d.mts +11 -0
- package/dist/types/index.d.mts +7 -0
- package/dist/types/logger.d.mts +3 -0
- package/dist/types/mq.d.mts +105 -0
- package/dist/types/sfr-pipeline.d.mts +33 -0
- package/dist/types/templates.d.mts +8 -0
- package/dist/types/util.d.mts +7 -0
- package/dist/util.mjs +25 -0
- package/package.json +51 -0
- package/src/example.mts +35 -0
- package/src/index.mts +99 -0
- package/src/logger.mts +52 -0
- package/src/mq.mts +236 -0
- package/src/sfr-pipeline.mts +508 -0
- package/src/templates.mts +55 -0
- package/src/types/index.d.ts +395 -0
- package/src/util.mts +32 -0
|
@@ -0,0 +1,508 @@
|
|
|
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
|
+
|
|
7
|
+
import {logger} from "./logger.mjs";
|
|
8
|
+
|
|
9
|
+
const CWD = process.cwd();
|
|
10
|
+
|
|
11
|
+
const PATTERNS: CommunicationPattern[] = ["Point-to-Point", "Request-Reply", "Publish-Subscribe", "Routing", "Topic"];
|
|
12
|
+
const TARGETED_PATTERN = ["Point-to-Point", "Request-Reply"];
|
|
13
|
+
|
|
14
|
+
export class SFRPipeline {
|
|
15
|
+
base_url: string;
|
|
16
|
+
pattern_channels: PatternMQSet;
|
|
17
|
+
mount_data = {
|
|
18
|
+
rest : 0,
|
|
19
|
+
ws : 0,
|
|
20
|
+
mq : 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Contains values that are injected into each protocol's handlers. */
|
|
24
|
+
static injections = {
|
|
25
|
+
rest: {
|
|
26
|
+
handlers: {},
|
|
27
|
+
controllers: {}
|
|
28
|
+
},
|
|
29
|
+
mq: {
|
|
30
|
+
handlers: {},
|
|
31
|
+
controllers: {}
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
constructor(private cfg: ParserCFG, private oas_cfg: OASConfig, private comms: SFRProtocols) { }
|
|
36
|
+
|
|
37
|
+
async init(base_url?: string): Promise<ServiceDocuments> {
|
|
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 channel = await this.comms["MQ"].createChannel();
|
|
43
|
+
const mq = TARGETED_PATTERN.includes(v) ? new TargetedMQ(channel, v) : new BroadcastMQ(channel, v);
|
|
44
|
+
|
|
45
|
+
return [v, mq];
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
this.pattern_channels = Object.fromEntries(channels);
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
SFRPipeline.injections.rest.handlers = {
|
|
52
|
+
...SFRPipeline.injections.rest.handlers,
|
|
53
|
+
mq: this.pattern_channels
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
SFRPipeline.injections.mq.handlers = {
|
|
57
|
+
...SFRPipeline.injections.mq.handlers,
|
|
58
|
+
mq: this.pattern_channels
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//Declarations for each protocol in key/value pairs.
|
|
63
|
+
let protocols = await this.file_parsing();
|
|
64
|
+
|
|
65
|
+
//Filter protocols that have been parsed if its' associated SFRProtocol has been defined
|
|
66
|
+
/* @ts-ignore */
|
|
67
|
+
protocols = Object.fromEntries(Object.entries(protocols).filter(([k])=> this.comms[k]).map(([k, v])=> [k, v]));
|
|
68
|
+
await this.fn_binding(protocols);
|
|
69
|
+
|
|
70
|
+
this.print_to_console();
|
|
71
|
+
|
|
72
|
+
return this.spec_generation(protocols);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Handles the parsing of SFR files from the directory specified under cfg.out config.
|
|
76
|
+
// It returns a standard NamespaceDeclaration containing each declaration of each supported protocol.
|
|
77
|
+
private async file_parsing() {
|
|
78
|
+
//These are the officially supported protocols of the SFR library
|
|
79
|
+
const SUPPORTED_PROTOCOLS = ["rest", "ws", "mq"];
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
Look for folders that match the supported protocols listed above in the designated directory
|
|
83
|
+
|
|
84
|
+
cfg.root = "Specifies the working directory"
|
|
85
|
+
cfg.path = "Specifies the API directory i.e: where SFR files reside"
|
|
86
|
+
cfg.out = "Specifies the directory where the resulting OAS documents are outputted"
|
|
87
|
+
*/
|
|
88
|
+
const directory = path.join(CWD, this.cfg.root);
|
|
89
|
+
let paths = SUPPORTED_PROTOCOLS.map((v) => path.join(directory, `${this.cfg.path}/${v}`));
|
|
90
|
+
//Create the directories if it doesn't exist.
|
|
91
|
+
await Promise.all(paths.map((v) => fs.mkdir(v, { recursive: true })));
|
|
92
|
+
//Recursively retrieve all SFRs of each protocol
|
|
93
|
+
let sfr_paths = await Promise.all(paths.map((path) => resolve_sfrs(path)));
|
|
94
|
+
//Import SFRs
|
|
95
|
+
const protocols = await Promise.all(sfr_paths.map((protocol) => import_sfrs(protocol)));
|
|
96
|
+
const REST: RESTNamespaceDeclaration = protocols[0];
|
|
97
|
+
logger.info(`${Object.keys(protocols[0]).length} REST SFR`)
|
|
98
|
+
const WS: WSNamespaceDeclaration = protocols[1];
|
|
99
|
+
logger.info(`${Object.keys(protocols[1]).length} WS SFR`)
|
|
100
|
+
const MQ: MQNamespaceDeclaration = protocols[2];
|
|
101
|
+
logger.info(`${Object.keys(protocols[2]).length} MQ SFR`)
|
|
102
|
+
|
|
103
|
+
return { REST, WS, MQ } as NamespaceDeclaration;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handles the binding of the components of each SFR (e.g: executing validators of matching endpoints, injecting controllers to each handler, etc.)
|
|
107
|
+
//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.
|
|
108
|
+
private async fn_binding(protocols: NamespaceDeclaration) {
|
|
109
|
+
Object.entries(protocols).forEach(([protocol, declaration]) => {
|
|
110
|
+
switch (protocol) {
|
|
111
|
+
case "REST": this.bind_rest_fns(declaration as RESTNamespaceDeclaration); break;
|
|
112
|
+
case "WS": this.bind_ws_fns(declaration as WSNamespaceDeclaration); break;
|
|
113
|
+
case "MQ": this.bind_mq_fns(declaration as MQNamespaceDeclaration); break;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/* Fn Binding */
|
|
118
|
+
private async bind_rest_fns(declaration: RESTNamespaceDeclaration) {
|
|
119
|
+
const comms = this.comms["REST"];
|
|
120
|
+
if (!comms) throw new Error(`FN Binding failed: \nREST protocol lacks it's associated SFRProtocols`);
|
|
121
|
+
|
|
122
|
+
Object.entries(declaration).forEach(([namespace, module]) => {
|
|
123
|
+
let { cfg, validators, handlers } = module.content;
|
|
124
|
+
//Set base_directory
|
|
125
|
+
/*
|
|
126
|
+
Directory Order:
|
|
127
|
+
this.base_url -> Folder Structure -> cfg.base_dir
|
|
128
|
+
*/
|
|
129
|
+
let base_dir = "";
|
|
130
|
+
|
|
131
|
+
if(this.base_url)base_dir = `/${this.base_url}`;
|
|
132
|
+
if(module.dir)base_dir += `/${module.dir.toString().replaceAll(",", "/")}`;
|
|
133
|
+
if(cfg.base_dir)base_dir += `/${cfg.base_dir}`;
|
|
134
|
+
logger.info(`[SFR ${module.name}] resolved path: ${base_dir}/${namespace}`);
|
|
135
|
+
|
|
136
|
+
const is_public = Boolean(cfg.public);
|
|
137
|
+
//Loop over all methods and their respective handlers
|
|
138
|
+
Object.entries(handlers).forEach(([method, handler_map]) => {
|
|
139
|
+
for (const [name, handler] of Object.entries(handler_map).filter(([k]) => validators[k])) {
|
|
140
|
+
const dir = `${base_dir}/${namespace}/${name}`;
|
|
141
|
+
console.log(dir)
|
|
142
|
+
const validator = validators[name];
|
|
143
|
+
const validator_type = typeof validator !== "function" ? "joi" : "multer";
|
|
144
|
+
|
|
145
|
+
/* bind validators */
|
|
146
|
+
switch (validator_type) {
|
|
147
|
+
case "joi": {
|
|
148
|
+
comms[method.toLowerCase() as RESTRequestType](dir, (req, res, next) => {
|
|
149
|
+
let error: string | boolean = true;
|
|
150
|
+
|
|
151
|
+
if (is_public) {
|
|
152
|
+
//if(!req.session.user)return res.status(401).json({error : "Session not found."});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
error = template(validator, method === "GET" ? req.query : req.body);
|
|
156
|
+
if (error) return res.status(400).json({ error });
|
|
157
|
+
next();
|
|
158
|
+
});
|
|
159
|
+
} break;
|
|
160
|
+
|
|
161
|
+
case "multer": {
|
|
162
|
+
comms[method.toLowerCase() as RESTRequestType](dir, validator as RequestHandler);
|
|
163
|
+
|
|
164
|
+
/* @ts-ignore */
|
|
165
|
+
comms.use((err, req, res, next) => {
|
|
166
|
+
let temp = { error: err.message, details: err.cause };
|
|
167
|
+
if (err) return res.status(400).json(temp);
|
|
168
|
+
next();
|
|
169
|
+
});
|
|
170
|
+
} break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Bind to express app */
|
|
174
|
+
comms[method.toLowerCase()](dir, handler.fn);
|
|
175
|
+
this.mount_data.rest++;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
private async bind_ws_fns(declaration: WSNamespaceDeclaration) {
|
|
182
|
+
|
|
183
|
+
}
|
|
184
|
+
private async bind_mq_fns(declaration: MQNamespaceDeclaration) {
|
|
185
|
+
const comms = this.comms["MQ"];
|
|
186
|
+
if (!comms) throw new Error(`FN Binding failed: \nMQ protocol lacks it's associated SFRProtocols`);
|
|
187
|
+
|
|
188
|
+
Object.entries(declaration).forEach(([namespace, module]) => {
|
|
189
|
+
let { cfg, validators, handlers } = module.content;
|
|
190
|
+
//Set base_directory
|
|
191
|
+
let base_dir = cfg.base_dir ? `/${cfg.base_dir}` : "";
|
|
192
|
+
if (this.base_url) base_dir = `/${this.base_url}/${base_dir}`;
|
|
193
|
+
if (module.dir) base_dir = `${module.dir.toString().replaceAll(",", "/")}/${base_dir}`
|
|
194
|
+
const is_public = Boolean(cfg.public);
|
|
195
|
+
|
|
196
|
+
//Loop over all methods and their respective handlers
|
|
197
|
+
Object.entries(handlers).forEach(([pattern, handler_map]: [CommunicationPattern, MQRequestHandlerMap]) => {
|
|
198
|
+
//Get associated MQ for pattern
|
|
199
|
+
let mq = this.pattern_channels[pattern];
|
|
200
|
+
|
|
201
|
+
//Get all valid handlers (i.e: those with matching validators)
|
|
202
|
+
const valid_handlers = Object.entries(handler_map).filter(([k]) => validators[k]);
|
|
203
|
+
|
|
204
|
+
//Loop over each handler
|
|
205
|
+
for (let [name, handler] of valid_handlers) {
|
|
206
|
+
const dir = `${base_dir}${namespace}/${name}`;
|
|
207
|
+
const validator = validators[name];
|
|
208
|
+
const validator_type = typeof validator !== "function" ? "joi" : "multer"; // TODO: Dynamically update parameter content based on validator type.
|
|
209
|
+
|
|
210
|
+
let validator_fn = function (msg: ConsumeMessage) {
|
|
211
|
+
const error = template(validator, msg.content);
|
|
212
|
+
|
|
213
|
+
if (error) {
|
|
214
|
+
if (mq instanceof TargetedMQ) {
|
|
215
|
+
if (error) {
|
|
216
|
+
switch (pattern) {
|
|
217
|
+
case "Request-Reply": mq.reply(msg, { error }); break;
|
|
218
|
+
default: mq.channel.reject(msg, false);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (mq instanceof BroadcastMQ) {
|
|
224
|
+
mq.channel.reject(msg, false);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
handler.fn(msg);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* bind validators */
|
|
232
|
+
if (mq instanceof TargetedMQ) {
|
|
233
|
+
mq.consume(dir, {
|
|
234
|
+
options: handler.options,
|
|
235
|
+
fn: validator_fn
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (mq instanceof BroadcastMQ) {
|
|
240
|
+
switch (pattern) {
|
|
241
|
+
case "Publish-Subscribe": {
|
|
242
|
+
mq.subscribe(dir, "", {
|
|
243
|
+
options: handler.options,
|
|
244
|
+
fn: validator_fn
|
|
245
|
+
})
|
|
246
|
+
} break;
|
|
247
|
+
|
|
248
|
+
case "Routing": {
|
|
249
|
+
/* @ts-ignore */
|
|
250
|
+
mq.subscribe(dir, handler.routing_key, {
|
|
251
|
+
options: handler.options,
|
|
252
|
+
fn: validator_fn
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
case "Topic": {
|
|
257
|
+
/* @ts-ignore */
|
|
258
|
+
mq.subscribe(dir, handler.binding_key, {
|
|
259
|
+
options: handler.options
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.mount_data.mq++;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Generate OpenAPI and AsyncAPI specification.
|
|
272
|
+
private spec_generation(protocols: NamespaceDeclaration): ServiceDocuments {
|
|
273
|
+
// Holds the key/value pair of protocols and their respective API Documentation
|
|
274
|
+
const documents = Object.entries(protocols).map(([protocol, declaration]) => {
|
|
275
|
+
switch (protocol) {
|
|
276
|
+
case "REST": return [protocol, this.generate_open_api_document(declaration as RESTNamespaceDeclaration)];
|
|
277
|
+
case "WS": return [protocol, this.generate_async_api_document(declaration as WSNamespaceDeclaration)];
|
|
278
|
+
case "MQ": return [protocol, this.generate_async_api_document(declaration as MQNamespaceDeclaration)];
|
|
279
|
+
default: throw new Error(`Failed to generate SFR Spec: ${protocol} protocol is unknown.`);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return Object.fromEntries(documents);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private generate_open_api_document(declaration: RESTNamespaceDeclaration) {
|
|
287
|
+
// This will hold the final OpenAPI Document.
|
|
288
|
+
const spec: OAPI_Document = {
|
|
289
|
+
openapi: "3.0.0",
|
|
290
|
+
info : Object.assign({}, this.oas_cfg),
|
|
291
|
+
paths: {}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Iterate through each protocol (e.g., REST, WS, MQ)
|
|
295
|
+
Object.entries(declaration).forEach(([namespace, module]) => {
|
|
296
|
+
const { cfg, handlers, validators } = module.content;
|
|
297
|
+
|
|
298
|
+
//Set base_directory
|
|
299
|
+
let base_dir = cfg.base_dir ? `/${cfg.base_dir}/` : "/";
|
|
300
|
+
if (this.base_url) base_dir = `/${this.base_url}${base_dir}`;
|
|
301
|
+
if (module.dir) base_dir = `${module.dir.toString().replaceAll(",", "/")}/${base_dir}`
|
|
302
|
+
// Collect the OpenAPI path object for each handler (method)
|
|
303
|
+
for (let [method, handler_map] of Object.entries(handlers)) {
|
|
304
|
+
//Transform method to lowercase
|
|
305
|
+
method = method.toLowerCase();
|
|
306
|
+
|
|
307
|
+
// Define the operation (method) for this endpoint
|
|
308
|
+
for (const [endpoint, body] of Object.entries(handler_map)) {
|
|
309
|
+
const validator: any = validators[endpoint];
|
|
310
|
+
// Skip if no matching validator
|
|
311
|
+
if (!validator) continue;
|
|
312
|
+
const validator_type = typeof validator !== "function" ? "joi" : "multer";
|
|
313
|
+
|
|
314
|
+
//Default tags are used for service and router level classification (developer platform)
|
|
315
|
+
let default_tags = [
|
|
316
|
+
`sfr-router:${namespace}`,
|
|
317
|
+
`sfr-service:${this.oas_cfg.title || "unspecified"}`
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
if (body.tags && Array.isArray(body.tags)) default_tags.push(...body.tags);
|
|
321
|
+
|
|
322
|
+
const operation_id = `${base_dir}${namespace}/${endpoint}`;
|
|
323
|
+
const document: any = {
|
|
324
|
+
operationId : `${method.toUpperCase()}:${operation_id}`,
|
|
325
|
+
summary : body.summary || "",
|
|
326
|
+
description : body.description || "",
|
|
327
|
+
tags : default_tags,
|
|
328
|
+
public : Boolean(cfg.public),
|
|
329
|
+
|
|
330
|
+
responses: {
|
|
331
|
+
"200": {
|
|
332
|
+
description: "Successful operation",
|
|
333
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
334
|
+
},
|
|
335
|
+
"400": {
|
|
336
|
+
description: "An error has occured",
|
|
337
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
//Insert either a parameter or a requestBody according to the method type
|
|
343
|
+
if(validator_type === "joi"){
|
|
344
|
+
document[method === "GET" ? "parameters" : "requestBody"] = j2s(validator).swagger;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
//Haven't found a library for converting multer validators to swagger doc
|
|
348
|
+
if(validator_type === "multer"){
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
//Create path if it does not exist.
|
|
352
|
+
if (!spec.paths[operation_id]) spec.paths[operation_id] = {};
|
|
353
|
+
if (!spec.paths[operation_id][method.toLowerCase()]) spec.paths[operation_id][method.toLowerCase()] = document;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
//Conform to OAPI standards by appending "x-property" on each field under "meta"
|
|
359
|
+
/* @ts-ignore */
|
|
360
|
+
spec.info.meta = Object.fromEntries(Object.entries(spec.info.meta).map(([k, v])=> [`x-${k}`, v]));
|
|
361
|
+
|
|
362
|
+
return spec;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Method to generate an AsyncAPI document from an MQNamespaceDeclaration
|
|
366
|
+
private generate_async_api_document(declaration: MQNamespaceDeclaration): AsyncAPIDocument {
|
|
367
|
+
// This will hold the final AsyncAPI Document.
|
|
368
|
+
const spec = {
|
|
369
|
+
asyncapi: '3.0.0',
|
|
370
|
+
info : Object.assign({}, this.oas_cfg),
|
|
371
|
+
|
|
372
|
+
channels: {},
|
|
373
|
+
operations: {},
|
|
374
|
+
components: {
|
|
375
|
+
messages: {},
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
//Conform to OAPI standards by appending "x-property" on each field under "meta"
|
|
380
|
+
/* @ts-ignore */
|
|
381
|
+
spec.info.meta = Object.fromEntries(Object.entries(spec.info.meta).map(([k, v])=> [`x-${k}`, v]));
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
Object.entries(declaration).forEach(([namespace, module]) => {
|
|
385
|
+
let { cfg, validators, handlers } = module.content;
|
|
386
|
+
//Set base_directory
|
|
387
|
+
let base_dir = cfg.base_dir ? `/${cfg.base_dir}/` : "";
|
|
388
|
+
if (this.base_url) base_dir = `/${this.base_url}/${base_dir}`;
|
|
389
|
+
if (module.dir) base_dir = `${module.dir.toString().replaceAll(",", "/")}/${base_dir}`
|
|
390
|
+
const is_public = Boolean(cfg.public);
|
|
391
|
+
|
|
392
|
+
//Loop over all methods and their respective handlers
|
|
393
|
+
Object.entries(handlers).forEach(([pattern, handler_map]: [CommunicationPattern, MQRequestHandlerMap]) => {
|
|
394
|
+
//Get associated MQ for pattern
|
|
395
|
+
let mq = this.pattern_channels[pattern];
|
|
396
|
+
|
|
397
|
+
//Get all valid handlers (i.e: those with matching validators)
|
|
398
|
+
const valid_handlers = Object.entries(handler_map).filter(([k]) => validators[k]);
|
|
399
|
+
|
|
400
|
+
//Loop over each handler
|
|
401
|
+
for (let [name, handler] of valid_handlers) {
|
|
402
|
+
const dir = `${base_dir}${namespace}/${name}`;
|
|
403
|
+
const validator:any = validators[name];
|
|
404
|
+
const validator_type = typeof validator !== "function" ? "joi" : "multer"; // TODO: Dynamically update parameter content based on validator type.
|
|
405
|
+
|
|
406
|
+
const channel_document: any = {
|
|
407
|
+
address : dir,
|
|
408
|
+
messages : {}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const operation_document: any = {
|
|
412
|
+
action : "receive",
|
|
413
|
+
summary : handler.summary || "",
|
|
414
|
+
description : handler.description || "",
|
|
415
|
+
channel : `#/channels/${name}`
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
//Insert either a parameter or a requestBody according to the method type
|
|
419
|
+
if(validator_type === "joi"){
|
|
420
|
+
channel_document.messages[`${name}-message`] = {
|
|
421
|
+
name : `${name}-message`,
|
|
422
|
+
payload : j2s(validator).swagger
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
//Haven't found a library for converting multer validators to swagger doc
|
|
427
|
+
if(validator_type === "multer"){
|
|
428
|
+
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
spec.channels[name] = channel_document;
|
|
432
|
+
spec.operations[name] = operation_document;
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
return spec;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
// Method to print SFR info to console
|
|
442
|
+
private print_to_console(){
|
|
443
|
+
console.log(`____________________________
|
|
444
|
+
| | Mounted
|
|
445
|
+
| ░██████╗███████╗██████╗░ | REST : ${this.mount_data.rest}
|
|
446
|
+
| ██╔════╝██╔════╝██╔══██╗ | WS : ${this.mount_data.ws}
|
|
447
|
+
| ╚█████╗░█████╗░░██████╔╝ | MQ : ${this.mount_data.mq}
|
|
448
|
+
| ░╚═══██╗██╔══╝░░██╔══██╗ |-----------
|
|
449
|
+
| ██████╔╝██║░░░░░██║░░██║ | Protocols
|
|
450
|
+
| ╚═════╝░╚═╝░░░░░╚═╝░░╚═╝ | REST : ${this.comms["REST"] ? "✅" : "❌"}
|
|
451
|
+
| Single File Router | MQ : ${this.comms["MQ"] ? "✅" : "❌"}
|
|
452
|
+
| | WS : ${this.comms["WS"] ? "✅" : "❌"}
|
|
453
|
+
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯`)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* Utility fns */
|
|
458
|
+
/* File Parsing */
|
|
459
|
+
async function resolve_sfrs(dir: string, prefix?: string) {
|
|
460
|
+
//Get Directory Contents
|
|
461
|
+
const contents = await fs.readdir(dir);
|
|
462
|
+
|
|
463
|
+
const results = [];
|
|
464
|
+
//Loop over contents
|
|
465
|
+
await Promise.all(contents.map(async (v) => {
|
|
466
|
+
const content_path = path.join(dir, v);
|
|
467
|
+
const is_dir = (await fs.lstat(content_path)).isDirectory();
|
|
468
|
+
|
|
469
|
+
//If Directory, perform a recursion and return output of contents.map fn.
|
|
470
|
+
if (is_dir) {
|
|
471
|
+
const dir_output = await resolve_sfrs(content_path, prefix ? `${prefix}/${v}` : v);
|
|
472
|
+
results.push(dir_output);
|
|
473
|
+
return;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
let output: any = { name: v, path: content_path };
|
|
477
|
+
if (prefix) output.dir = prefix.split("/");
|
|
478
|
+
results.push(output);
|
|
479
|
+
}));
|
|
480
|
+
|
|
481
|
+
return results.flat();
|
|
482
|
+
}
|
|
483
|
+
async function import_sfrs(protocol_files: any[]) {
|
|
484
|
+
const protocols = await Promise.all(protocol_files.map(async (v) => {
|
|
485
|
+
const import_task = await import(`file:///${v.path}`);
|
|
486
|
+
return [v.name.replace(".mjs", ""), {
|
|
487
|
+
...v,
|
|
488
|
+
content: import_task.default
|
|
489
|
+
}];
|
|
490
|
+
}));
|
|
491
|
+
|
|
492
|
+
//Removes SFRs without body.
|
|
493
|
+
return Object.fromEntries(protocols.filter((v) => v[1].content));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Method to generate JSON schema for the payload based on handler metadata
|
|
497
|
+
function generate_json_schema(metadata: SFRMetadata): any {
|
|
498
|
+
return {
|
|
499
|
+
type: 'object',
|
|
500
|
+
properties: {
|
|
501
|
+
example_field: {
|
|
502
|
+
type: 'string',
|
|
503
|
+
...metadata
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
required: ['example_field'],
|
|
507
|
+
};
|
|
508
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//Previous Name: RequestHandlerFactory
|
|
2
|
+
//Dependencies are imported and used by the functions below
|
|
3
|
+
import { SFRPipeline } from "./sfr-pipeline.mjs";
|
|
4
|
+
|
|
5
|
+
export function REST<V, H, C>(struct : RESTHandlerDescriptor<V, H, C>) : H & C {
|
|
6
|
+
const validators : RequestValidators = struct.validators || {};
|
|
7
|
+
const controllers : RequestControllers = struct.controllers || {};
|
|
8
|
+
const handlers : RESTRequestHandlers = struct.handlers || {};
|
|
9
|
+
const cfg : SFRConfig = struct.cfg || {};
|
|
10
|
+
|
|
11
|
+
//Bind controller injections to each controllers
|
|
12
|
+
Object.entries(controllers).map(([k, v])=>controllers[k] = v.bind({...SFRPipeline.injections.rest.controllers}) as RequestController);
|
|
13
|
+
|
|
14
|
+
//Bind handler injections and SFR controllers into each handlers.
|
|
15
|
+
Object.entries(handlers).forEach(([method, handlermap])=>{
|
|
16
|
+
Object.entries(handlermap).forEach(([k, v])=> {
|
|
17
|
+
/* @ts-ignore */
|
|
18
|
+
handlers[method][k] = {...v, fn : v.fn.bind({ ...controllers, ...SFRPipeline.injections.rest.handlers })};
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
/* @ts-ignore */
|
|
22
|
+
return { validators, controllers, handlers, cfg } as H & C
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function WS<V, H, C>(struct : WSHandlerDescriptor<V, H, C>){
|
|
26
|
+
const validators : RequestValidators = struct.validators || {};
|
|
27
|
+
const controllers : RequestControllers = struct.controllers || {};
|
|
28
|
+
const handlers : WSRequestHandlers = struct.handlers || {};
|
|
29
|
+
const cfg : SFRConfig = struct.cfg || {};
|
|
30
|
+
|
|
31
|
+
//Dependency injection happens here...npm
|
|
32
|
+
return { validators, controllers, handlers, cfg }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function MQ<V, H, C>(struct : MQHandlerDescriptor<V, H, C>): H & C{
|
|
36
|
+
const validators : RequestValidators = struct.validators || {};
|
|
37
|
+
const controllers : RequestControllers = struct.controllers || {};
|
|
38
|
+
const handlers : MQRequestHandlers<C> = struct.handlers || {};
|
|
39
|
+
const cfg : SFRConfig = struct.cfg || {};
|
|
40
|
+
|
|
41
|
+
//Bind controller injections to each controllers
|
|
42
|
+
Object.entries(controllers).map(([k, v])=>controllers[k] = v.bind({...SFRPipeline.injections.mq.controllers}) as RequestController);
|
|
43
|
+
|
|
44
|
+
//Bind handler injections and SFR controllers into each handlers.
|
|
45
|
+
Object.entries(handlers).forEach(([pattern, handlermap])=>{
|
|
46
|
+
Object.entries(handlermap).forEach(([k, v])=> {
|
|
47
|
+
/* @ts-ignore */
|
|
48
|
+
const {mq} = (SFRPipeline.injections.mq.handlers || {});
|
|
49
|
+
/* @ts-ignore */
|
|
50
|
+
handlers[pattern][k] = {...v, fn : v.fn.bind({ ...controllers, mq : mq ? mq[pattern] : null })};
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
/* @ts-ignore */
|
|
54
|
+
return { validators, controllers, handlers, cfg } as H & C;
|
|
55
|
+
}
|