corebasic 1.0.231 → 1.0.232
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/dist/libs/dip.js +1 -0
- package/dist/libs/features.js +110 -60
- package/dist/libs/kafka.js +5 -7
- package/libs/dip.ts +2 -0
- package/libs/features.ts +232 -95
- package/libs/kafka.ts +11 -13
- package/libs/messaging.ts +1 -1
- package/package.json +1 -1
package/dist/libs/dip.js
CHANGED
|
@@ -15,6 +15,7 @@ export const companyMeta = Elabase.companyMeta;
|
|
|
15
15
|
export const outletMeta = Elabase.outletMeta;
|
|
16
16
|
export const customMeta = Elabase.customMeta;
|
|
17
17
|
export const schema = Elabase.schema;
|
|
18
|
+
export const DipMeta = Elabase.DipMeta;
|
|
18
19
|
export const isDipMeta = Elabase.isDipMeta;
|
|
19
20
|
export const excludeCollectionConfig = Elabase.excludeCollectionConfig;
|
|
20
21
|
export const includeCollectionConfig = Elabase.includeCollectionConfig;
|
package/dist/libs/features.js
CHANGED
|
@@ -16,10 +16,13 @@ import axios from 'axios';
|
|
|
16
16
|
// @ts-ignore
|
|
17
17
|
import jwt from 'jsonwebtoken';
|
|
18
18
|
let features = {};
|
|
19
|
-
let apis = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
19
|
+
let apis = {
|
|
20
|
+
// <api>: {<feature>: FeatureEntry}
|
|
21
|
+
};
|
|
22
|
+
const getFeatureMethod = (api) => api.split(' ')[0].toLowerCase();
|
|
23
|
+
const getFeatureUrl = (api) => { const url = api.split(' ')[1]; if (!url)
|
|
24
|
+
throw new Error(`Invalid Feature url in api: ${api}`); return url; };
|
|
25
|
+
const getFeature = (name) => features[name];
|
|
23
26
|
const DEPLOY_TOKEN_SECRET = process.env.DEPLOY_TOKEN_SECRET || "MY_SECRET_DEPLOY_TOKEN";
|
|
24
27
|
const SERVICE_ACCESS_TOKEN = jwt.sign({ app: process.env.APP_DEPLOYMENT_NAME }, DEPLOY_TOKEN_SECRET, { expiresIn: '365d' });
|
|
25
28
|
const NDCURVE_DEVELOPER_LICENSE_ACCESS_TOKEN = await (async () => { if (process.env.NDCURVE_DEVELOPER_SERVICE)
|
|
@@ -50,7 +53,7 @@ async function loadLocalFeatures() {
|
|
|
50
53
|
let SLYP_FEATURES_LIST = await loadLocalFeatures();
|
|
51
54
|
let appId = Utils.uid();
|
|
52
55
|
let SERVICE_ADDRESS = process.env.APP_ENDPOINT || 'http://127.0.0.1:3000';
|
|
53
|
-
export const get = feature => {
|
|
56
|
+
export const get = (feature) => {
|
|
54
57
|
const throwError = () => { throw new Error(`Feature ${feature} not found in internal or external list during Features.subscribe call`); };
|
|
55
58
|
const baseFeature = getFeature(feature);
|
|
56
59
|
let { api, service = SERVICE_ADDRESS, topic } = baseFeature ? baseFeature : (SLYP_FEATURES_LIST[feature] ?? throwError());
|
|
@@ -68,16 +71,27 @@ export const send = async (meta, feature, data, params) => {
|
|
|
68
71
|
if (url.includes(':'))
|
|
69
72
|
throw new Error(`Error: Internal feature call send on feature: ${feature} when api/params/substitution`);
|
|
70
73
|
}
|
|
71
|
-
|
|
74
|
+
const payload = { ...meta, data, feature, txn: Utils.uid() };
|
|
72
75
|
service = service.replace('http://slyp.app', 'https://slyp.app');
|
|
73
76
|
// return (await axios[method](`${service}${url}`, { data: payload, headers: {jwt: headers.jwt}, timeout: 1000 })).data
|
|
74
77
|
// Used to work. But request to 127.0.0.1 fails to send the specified headers along with the request.
|
|
75
78
|
// return (await axios[method](`${service}${url}`, payload, {headers: {jwt: SERVICE_ACCESS_TOKEN, service: true}, timeout: 1000 })).data // Worked Earlier, but issue spotted
|
|
76
|
-
if (
|
|
79
|
+
if (baseFeature) { // Local call
|
|
77
80
|
let response;
|
|
78
|
-
let req = {
|
|
81
|
+
let req = { body: payload, params: params ?? {}, method, path: url, url: '' };
|
|
79
82
|
req = JSON.parse(JSON.stringify(req));
|
|
80
|
-
|
|
83
|
+
const callback = (payload) => { response = payload; };
|
|
84
|
+
const res = {
|
|
85
|
+
json: callback,
|
|
86
|
+
send: callback,
|
|
87
|
+
end: () => { },
|
|
88
|
+
status: (_status) => { return { send: callback, json: callback }; },
|
|
89
|
+
sendStatus: (_status) => { },
|
|
90
|
+
setHeader: (_header, _value) => { },
|
|
91
|
+
flushHeaders: () => { },
|
|
92
|
+
headersSent: false,
|
|
93
|
+
writableEnded: false,
|
|
94
|
+
};
|
|
81
95
|
await apiHandler(req, res);
|
|
82
96
|
return response;
|
|
83
97
|
}
|
|
@@ -109,7 +123,8 @@ async function announce() {
|
|
|
109
123
|
function getFeaturelessFeature(req) {
|
|
110
124
|
if (Session.ALLOWED_URLS.includes(req.path)) {
|
|
111
125
|
for (let key in features) {
|
|
112
|
-
|
|
126
|
+
const feature = features[key];
|
|
127
|
+
if (feature.featureless && feature.api === req.method.toUpperCase() + ' ' + req.path) {
|
|
113
128
|
return getFeature(key);
|
|
114
129
|
}
|
|
115
130
|
}
|
|
@@ -129,44 +144,77 @@ const apiHandler = async (req, res) => {
|
|
|
129
144
|
console.warn(`Feature: ${req.body.feature} not available`);
|
|
130
145
|
throw { status: 404, message: `Resource not found. Feature ${req.body.feature} not available.` };
|
|
131
146
|
}
|
|
147
|
+
feature = feature; // typescript validation with !
|
|
132
148
|
let topic = feature.topic;
|
|
133
|
-
let params = getFeatureUrl(feature.api).split("/").filter(item => item.startsWith(":")).map(item => item.replace(":", ""));
|
|
149
|
+
let params = getFeatureUrl(feature.api).split("/").filter((item) => item.startsWith(":")).map((item) => item.replace(":", ""));
|
|
134
150
|
for (let param of params)
|
|
135
151
|
if (!req.params[param])
|
|
136
152
|
throw { status: 404, message: "Resource not found. One or more url parameter not specified." };
|
|
137
|
-
let meta = { ...req.body, data: undefined };
|
|
138
|
-
req.meta = meta;
|
|
153
|
+
let meta = { ...req.body, data: undefined }; // TODO: Must also include invoiceTxn
|
|
139
154
|
if (process.env.USE_DEFAULT_COMPANY) {
|
|
140
|
-
|
|
141
|
-
|
|
155
|
+
meta.company = 'DEFAULT_COMPANY';
|
|
156
|
+
meta.outlet = 'DEFAULT_OUTLET';
|
|
142
157
|
}
|
|
158
|
+
meta.txn = meta.txn ?? Utils.uid();
|
|
159
|
+
const message = {
|
|
160
|
+
meta,
|
|
161
|
+
data: req.body.data,
|
|
162
|
+
params: req.params,
|
|
163
|
+
// To be removed 1
|
|
164
|
+
date: new Date().getTime(),
|
|
165
|
+
// To be removed 2
|
|
166
|
+
topic,
|
|
167
|
+
feature: req.body.feature,
|
|
168
|
+
user: req.body.user,
|
|
169
|
+
txn: meta.txn,
|
|
170
|
+
};
|
|
171
|
+
const appReq = {
|
|
172
|
+
meta: message.meta,
|
|
173
|
+
body: message,
|
|
174
|
+
params: message.params,
|
|
175
|
+
method: method,
|
|
176
|
+
path: req.path,
|
|
177
|
+
url: req.url,
|
|
178
|
+
headers: req.headers,
|
|
179
|
+
query: req.query,
|
|
180
|
+
on: req.on,
|
|
181
|
+
};
|
|
182
|
+
const appRes = res;
|
|
143
183
|
if (method === "get") {
|
|
144
184
|
try {
|
|
145
|
-
req.body
|
|
146
|
-
|
|
185
|
+
// await feature.handler({...req, headers: req.headers, body: {...req.body, topic} }, res)
|
|
186
|
+
// req.body = {...req.body, topic}
|
|
187
|
+
await feature.handler(appReq, appRes);
|
|
147
188
|
}
|
|
148
189
|
catch (err) {
|
|
149
190
|
if (process.env.DEBUG_MODE)
|
|
150
|
-
console.
|
|
191
|
+
console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err);
|
|
151
192
|
throw { status: 500, message: "Failed to GET feature", ...err };
|
|
152
193
|
}
|
|
153
194
|
}
|
|
154
195
|
else if (method !== "get" && feature.bypass) {
|
|
155
196
|
try {
|
|
156
|
-
await feature.handler(topic,
|
|
197
|
+
await feature.handler(topic, message, appReq, appRes);
|
|
157
198
|
}
|
|
158
199
|
catch (err) {
|
|
159
200
|
if (process.env.DEBUG_MODE)
|
|
160
|
-
console.
|
|
201
|
+
console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err);
|
|
161
202
|
throw { status: 500, message: "Failed to POST feature", ...err };
|
|
162
203
|
}
|
|
163
204
|
}
|
|
164
|
-
else
|
|
165
|
-
|
|
205
|
+
else {
|
|
206
|
+
try {
|
|
207
|
+
await Kafka.send(`Features.${topic}`, message, message.user, { compression: Kafka.CompressionTypes.GZIP });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
throw { status: 500, message: "Failed to queue the transaction" };
|
|
211
|
+
}
|
|
212
|
+
appRes.json({ data: { txn: message.txn, success: true, status: "Queued", featureQueued: true } });
|
|
213
|
+
}
|
|
166
214
|
}
|
|
167
215
|
catch (err) {
|
|
168
216
|
if (process.env.DEBUG_MODE)
|
|
169
|
-
console.
|
|
217
|
+
console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err);
|
|
170
218
|
try { // Sometimes error occurs when disconnecting stream from front end
|
|
171
219
|
res.status(err.status ?? 500).json(err);
|
|
172
220
|
}
|
|
@@ -203,6 +251,8 @@ function registerApi() {
|
|
|
203
251
|
for (let api in apis) {
|
|
204
252
|
let method = getFeatureMethod(api);
|
|
205
253
|
let url = api.split(' ')[1];
|
|
254
|
+
if (!url)
|
|
255
|
+
throw new Error(`No url path found in api ${api} during Features.registerApi()`);
|
|
206
256
|
if (api === 'GET /transactions/:id')
|
|
207
257
|
continue;
|
|
208
258
|
ExpressApp[method](url, apiHandler);
|
|
@@ -219,10 +269,10 @@ export const start = async (app, url, file) => {
|
|
|
219
269
|
app.get('/features', async (req, res) => {
|
|
220
270
|
let exp_features = {};
|
|
221
271
|
for (let [key, { api }] of Object.entries(features))
|
|
222
|
-
exp_features[key] = { api, service: `${SERVICE_ADDRESS}
|
|
272
|
+
exp_features[key] = { api, service: `${SERVICE_ADDRESS}`, subscribe: '', topic: '' };
|
|
223
273
|
if (process.env.LOAD_LOCAL_FEATURES) {
|
|
224
274
|
for (let key in SLYP_FEATURES_LIST)
|
|
225
|
-
SLYP_FEATURES_LIST[key].headers = { JWT: SERVICE_ACCESS_TOKEN, service: true, NDCURVE_DEVELOPER_LICENSE_ACCESS_TOKEN };
|
|
275
|
+
SLYP_FEATURES_LIST[key].headers = { JWT: SERVICE_ACCESS_TOKEN, service: true, NDCURVE_DEVELOPER_LICENSE_ACCESS_TOKEN }; // TODO: CRITICAL: Security issue
|
|
226
276
|
}
|
|
227
277
|
res.json({ data: { ...SLYP_FEATURES_LIST, ...exp_features } });
|
|
228
278
|
});
|
|
@@ -230,20 +280,21 @@ export const start = async (app, url, file) => {
|
|
|
230
280
|
await registerFeatures(features);
|
|
231
281
|
// Get unique kafka topics list from features
|
|
232
282
|
let kafkaTopics = [];
|
|
233
|
-
for (
|
|
283
|
+
for (const name in features) {
|
|
234
284
|
let feature = features[name];
|
|
235
|
-
if (feature.api.split(' ')[0]
|
|
285
|
+
if (feature.api.split(' ')[0]?.toLowerCase() === "get")
|
|
236
286
|
continue;
|
|
237
287
|
kafkaTopics = [...new Set(kafkaTopics.concat([feature.topic]))];
|
|
238
288
|
}
|
|
239
289
|
// Subscribe to each topic
|
|
240
290
|
for (let topic of kafkaTopics) {
|
|
241
|
-
|
|
291
|
+
const groupId = '';
|
|
292
|
+
Kafka.receive(`Features.${topic}`, groupId, async (topic, message) => {
|
|
242
293
|
if (Utils.isEmpty(message.meta?.company))
|
|
243
294
|
message.meta = { ...message.meta, company: "GLOBAL", outlet: "GLOBAL" };
|
|
244
295
|
if (!message?.feature)
|
|
245
296
|
return;
|
|
246
|
-
const timer = ms => new Promise(res => setTimeout(res, ms)); // A promise that resolves after "ms" Milliseconds
|
|
297
|
+
const timer = (ms) => new Promise(res => setTimeout(res, ms)); // A promise that resolves after "ms" Milliseconds
|
|
247
298
|
topic = topic.replace(/^.*Features./, '');
|
|
248
299
|
while (true) {
|
|
249
300
|
try {
|
|
@@ -251,7 +302,12 @@ export const start = async (app, url, file) => {
|
|
|
251
302
|
// // TODO: use $useChunks: [] once support is added in dip insert
|
|
252
303
|
// await Dip.insert(globalMeta, `users.txns`, { _id: `${message.user}_${iso_date}_${message.txn}`, user: message.user, feature: message.feature, date: message.date, created: message.date, updated: message.date, status: "Queued" }, {idempotent: true}) // Can always update the status later
|
|
253
304
|
// TODO: Avoid duplicate processing
|
|
254
|
-
|
|
305
|
+
const feature = features[message.feature];
|
|
306
|
+
if (!feature) {
|
|
307
|
+
console.warn(`Feature ${message.feature} missing, in executing handler during Kafka.receive(topic: ${topic})`);
|
|
308
|
+
throw new Error(`Feature ${message.feature} missing, in executing handler during Kafka.receive(topic: ${topic})`);
|
|
309
|
+
}
|
|
310
|
+
await feature.handler(topic, message);
|
|
255
311
|
await Dip.insert(globalMeta, "Features.txns", { _id: message.txn, status: "Processed" }, { idempotent: true });
|
|
256
312
|
break;
|
|
257
313
|
}
|
|
@@ -296,31 +352,13 @@ export const start = async (app, url, file) => {
|
|
|
296
352
|
console.log('Messaging failed to start. Maybe missing redis');
|
|
297
353
|
}
|
|
298
354
|
};
|
|
299
|
-
function prepareMessage(req) {
|
|
300
|
-
let txn = req.body.txn ?? Utils.uid();
|
|
301
|
-
let _id = req.body.data?._id ?? txn;
|
|
302
|
-
let { data, feature, app, user, outlet, company, client, version } = req.body;
|
|
303
|
-
let meta = req.meta;
|
|
304
|
-
let params = req.params;
|
|
305
|
-
return { data, params, meta, feature, app, user, outlet, company, client, version, txn, id: _id, date: new Date().getTime() };
|
|
306
|
-
}
|
|
307
|
-
const commandAction = async (req, res, topic) => {
|
|
308
|
-
let message = prepareMessage(req);
|
|
309
|
-
try {
|
|
310
|
-
await Kafka.send(`Features.${topic}`, message, message.user, { compression: Kafka.CompressionTypes.GZIP });
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
throw { status: 500, message: "Failed to queue the transaction" };
|
|
314
|
-
}
|
|
315
|
-
res.json({ data: { txn: message.txn, success: true, status: "Queued", featureQueued: true } });
|
|
316
|
-
};
|
|
317
355
|
let subscriptions = {
|
|
318
356
|
// "coins.query.hello": "invoices.command.add", // Consumer : Feature of Topic
|
|
319
357
|
};
|
|
320
358
|
let subscribed_consumers = {};
|
|
321
359
|
async function subscribe() {
|
|
322
360
|
let Features = { get, send };
|
|
323
|
-
for (let [key, { subscribe }] of Object.entries(features))
|
|
361
|
+
for (let [key, { subscribe }] of Object.entries(features)) {
|
|
324
362
|
if (!Utils.isEmpty(subscribe) && subscriptions[key] !== subscribe && subscribed_consumers[key]) {
|
|
325
363
|
try {
|
|
326
364
|
await subscribed_consumers[key].disconnect();
|
|
@@ -328,30 +366,37 @@ async function subscribe() {
|
|
|
328
366
|
catch (_) { }
|
|
329
367
|
delete subscribed_consumers[key];
|
|
330
368
|
}
|
|
331
|
-
|
|
332
|
-
|
|
369
|
+
}
|
|
370
|
+
for (let key in SLYP_FEATURES_LIST) {
|
|
371
|
+
const feature = SLYP_FEATURES_LIST[key];
|
|
372
|
+
if (!Utils.isEmpty(feature?.subscribe) && subscriptions[key] !== feature?.subscribe && subscribed_consumers[key]) {
|
|
333
373
|
try {
|
|
334
374
|
await subscribed_consumers[key].disconnect();
|
|
335
375
|
}
|
|
336
376
|
catch (_) { }
|
|
337
377
|
delete subscribed_consumers[key];
|
|
338
378
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
379
|
+
}
|
|
380
|
+
for (let key in SLYP_FEATURES_LIST) {
|
|
381
|
+
const feature = SLYP_FEATURES_LIST[key];
|
|
382
|
+
if (!Utils.isEmpty(feature?.subscribe)) {
|
|
383
|
+
subscriptions[key] = feature.subscribe;
|
|
342
384
|
}
|
|
343
|
-
|
|
385
|
+
}
|
|
386
|
+
for (let [key, { subscribe }] of Object.entries(features)) {
|
|
344
387
|
if (!Utils.isEmpty(subscribe)) {
|
|
345
388
|
subscriptions[key] = subscribe;
|
|
346
389
|
}
|
|
390
|
+
}
|
|
347
391
|
for (let consumer in subscriptions) {
|
|
348
392
|
let publisher = subscriptions[consumer];
|
|
349
|
-
|
|
350
|
-
if (subscribed_consumers[consumer] || !
|
|
393
|
+
const publisherFeature = getFeature(publisher);
|
|
394
|
+
if (subscribed_consumers[consumer] || !publisherFeature)
|
|
351
395
|
continue;
|
|
352
|
-
let
|
|
396
|
+
let { topic } = publisherFeature;
|
|
397
|
+
let kafka_consumer_promise = Kafka.receive(`Features.${topic}`, consumer, async (topic, message) => {
|
|
353
398
|
topic = topic.replace(/^.*Features./, '');
|
|
354
|
-
const timer = ms => new Promise(res => setTimeout(res, ms)); // A promise that resolves after "ms" Milliseconds
|
|
399
|
+
const timer = (ms) => new Promise(res => setTimeout(res, ms)); // A promise that resolves after "ms" Milliseconds
|
|
355
400
|
while (true) {
|
|
356
401
|
try {
|
|
357
402
|
await Features.send(message.meta, consumer, message.data, message.params);
|
|
@@ -363,6 +408,11 @@ async function subscribe() {
|
|
|
363
408
|
await timer(10000);
|
|
364
409
|
}
|
|
365
410
|
});
|
|
366
|
-
|
|
411
|
+
kafka_consumer_promise
|
|
412
|
+
.then(kafka_consumer => {
|
|
413
|
+
subscribed_consumers[consumer] = kafka_consumer;
|
|
414
|
+
}).catch(err => {
|
|
415
|
+
throw new Error("Error: Remote service to service kafka Feature subscription.");
|
|
416
|
+
});
|
|
367
417
|
}
|
|
368
418
|
}
|
package/dist/libs/kafka.js
CHANGED
|
@@ -33,23 +33,21 @@ export const createTopic = async (topic, partition, replicas) => {
|
|
|
33
33
|
// ===========
|
|
34
34
|
let consumers = [];
|
|
35
35
|
const start_consumer = async function (topic, groupId, callback) {
|
|
36
|
-
|
|
37
|
-
groupId = typeof groupId === "string" ? `${appName}.${groupId}` : `${appName}.${topic}`;
|
|
36
|
+
groupId = groupId ? `${appName}.${groupId}` : `${appName}.${topic}`;
|
|
38
37
|
const consumer = kafka.consumer({ groupId: groupId });
|
|
39
38
|
consumers.push(consumer);
|
|
40
39
|
await consumer.connect();
|
|
41
40
|
await consumer.subscribe({ topic: topic, fromBeginning: true });
|
|
42
|
-
const fn = callback;
|
|
43
41
|
await consumer.run({
|
|
44
42
|
autoCommit: false,
|
|
45
43
|
eachMessage: async ({ topic, partition, message }) => {
|
|
46
|
-
//
|
|
44
|
+
// callback(topic, message.value.toString(), partition) // toString() returns array so won't parse if json.
|
|
47
45
|
let success;
|
|
48
46
|
try {
|
|
49
|
-
success = await
|
|
47
|
+
success = await callback(topic, JSON.parse(message.value), partition);
|
|
50
48
|
}
|
|
51
49
|
catch (ex) {
|
|
52
|
-
success = await
|
|
50
|
+
success = await callback(topic, message.value, partition); // NOTE: Beware: Ensure Kafka.receive<T> can handle raw message type that is not json parseable
|
|
53
51
|
}
|
|
54
52
|
await consumer.commitOffsets([{ topic, partition, offset: (Number(message.offset) + 1).toString() }]);
|
|
55
53
|
},
|
|
@@ -82,7 +80,7 @@ const start_producer = async function (topic, message, key, options) {
|
|
|
82
80
|
// start_producer('quickstart-events', 'Hello KafkaJS user! Little')
|
|
83
81
|
export const receive = async function (topic, groupId, callback) {
|
|
84
82
|
topic = topicPrefix + topic;
|
|
85
|
-
return
|
|
83
|
+
return await start_consumer(topic, groupId, callback);
|
|
86
84
|
};
|
|
87
85
|
export const send = async function (topic, message, key, options) {
|
|
88
86
|
topic = topicPrefix + topic;
|
package/libs/dip.ts
CHANGED
|
@@ -18,6 +18,8 @@ export const companyMeta = Elabase.companyMeta
|
|
|
18
18
|
export const outletMeta = Elabase.outletMeta
|
|
19
19
|
export const customMeta = Elabase.customMeta
|
|
20
20
|
export const schema = Elabase.schema
|
|
21
|
+
export type DipMeta = Elabase.DipMeta;
|
|
22
|
+
export const DipMeta = Elabase.DipMeta;
|
|
21
23
|
export const isDipMeta = Elabase.isDipMeta
|
|
22
24
|
export const excludeCollectionConfig = Elabase.excludeCollectionConfig
|
|
23
25
|
export const includeCollectionConfig = Elabase.includeCollectionConfig
|
package/libs/features.ts
CHANGED
|
@@ -8,11 +8,90 @@ import axios from 'axios'
|
|
|
8
8
|
// @ts-ignore
|
|
9
9
|
import jwt from 'jsonwebtoken'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
|
|
12
|
+
// ===================================
|
|
13
|
+
|
|
14
|
+
type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "head" | "options" | "connect" | "trace"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
type FeatureMeta = {
|
|
18
|
+
feature: string,
|
|
19
|
+
app: string,
|
|
20
|
+
user: string,
|
|
21
|
+
outlet: string,
|
|
22
|
+
company: string,
|
|
23
|
+
staff: string,
|
|
24
|
+
client: string,
|
|
25
|
+
version: string,
|
|
26
|
+
txn: string,
|
|
27
|
+
// invoiceTxn?: string // TODO: Must also include invoiceTxn
|
|
28
|
+
}
|
|
29
|
+
type FeatureParams = Record<string, string>
|
|
30
|
+
type FeatureMessage = {
|
|
31
|
+
meta: FeatureMeta
|
|
32
|
+
data: unknown
|
|
33
|
+
params: FeatureParams
|
|
34
|
+
// To be removed 1
|
|
35
|
+
date: number // used as message.date
|
|
36
|
+
// To be removed 2
|
|
37
|
+
topic: string // used as req.body.topic
|
|
38
|
+
feature: string // used as req.body.feature or message.feature
|
|
39
|
+
user: string // used as req.body.user or message.user
|
|
40
|
+
txn: string // used as message.txn
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
// client // Only used as req.body.client in auth.ts, session.ts and messaging.ts
|
|
44
|
+
// version // Only used as req.body.version in auth.ts and session.ts
|
|
45
|
+
// outlet, company // Only used as req.body.outlet, req.body.company in session.ts
|
|
46
|
+
// app // Only used as req.body.app in auth.ts and session.ts
|
|
47
|
+
}
|
|
48
|
+
type PayloadMessage = FeatureMeta & {
|
|
49
|
+
data: unknown
|
|
50
|
+
params?: FeatureParams
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =========
|
|
54
|
+
|
|
55
|
+
export type Req = {
|
|
56
|
+
meta: FeatureMeta
|
|
57
|
+
body: FeatureMessage
|
|
58
|
+
params: FeatureParams
|
|
59
|
+
method: string
|
|
60
|
+
path: string
|
|
61
|
+
url?: string
|
|
62
|
+
headers?: Record<string, string | string[] | undefined>
|
|
63
|
+
query?: Record<string, unknown>
|
|
64
|
+
on?: (event: string, callback: () => void) => void
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type ResJson = (response: unknown) => void
|
|
68
|
+
type ResSend = (response: unknown) => void;
|
|
69
|
+
type ResStatus = (status: number) => {send: ResSend, json: ResJson};
|
|
70
|
+
export type Res = {
|
|
71
|
+
send: ResSend
|
|
72
|
+
json: ResJson
|
|
73
|
+
end: (finalData?: string) => void
|
|
74
|
+
status: ResStatus
|
|
75
|
+
sendStatus: (status: number) => void
|
|
76
|
+
setHeader: (header: string, value: string) => void
|
|
77
|
+
flushHeaders: () => void
|
|
78
|
+
headersSent: boolean
|
|
79
|
+
writableEnded: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
// ========= Express Types ==========
|
|
84
|
+
type ExpressRequest = Omit<Req, "meta" | "body"> & { body: PayloadMessage }
|
|
85
|
+
type ExpressResponse = Res
|
|
86
|
+
type ExpressHandler = (req: ExpressRequest, res: ExpressResponse) => Promise<void>
|
|
87
|
+
type ExpressApplication = { [Method in HttpMethod]: (path: string, handler: ExpressHandler) => unknown }
|
|
88
|
+
// ===================================
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
// export type Req = unknown
|
|
92
|
+
// export type Res = unknown
|
|
14
93
|
export type Query = (req: Req, res: Res) => Promise<void>
|
|
15
|
-
export type Command = (topic: string, message:
|
|
94
|
+
export type Command = (topic: string, message: FeatureMessage, req?: Req, res?: Res) => Promise<void>
|
|
16
95
|
type Handler = Query | Command
|
|
17
96
|
|
|
18
97
|
|
|
@@ -28,11 +107,13 @@ type FeatureEntry = { api: string, topic: string, bypass: boolean, featureless:
|
|
|
28
107
|
|
|
29
108
|
|
|
30
109
|
let features: Record<string, FeatureEntry> = {}
|
|
31
|
-
let apis = {
|
|
110
|
+
let apis: Record<string, Record<string, FeatureEntry>> = {
|
|
111
|
+
// <api>: {<feature>: FeatureEntry}
|
|
112
|
+
}
|
|
32
113
|
|
|
33
|
-
const getFeatureMethod = api => api.split(' ')[0]
|
|
34
|
-
const getFeatureUrl = api => api.split(' ')[1]
|
|
35
|
-
const getFeature = name => features[name]
|
|
114
|
+
const getFeatureMethod = (api: string): HttpMethod => api.split(' ')[0]!.toLowerCase() as HttpMethod
|
|
115
|
+
const getFeatureUrl = (api: string): string => { const url = api.split(' ')[1]; if (!url) throw new Error(`Invalid Feature url in api: ${api}`); return url}
|
|
116
|
+
const getFeature = (name: string): FeatureEntry | undefined => features[name]
|
|
36
117
|
|
|
37
118
|
const DEPLOY_TOKEN_SECRET = process.env.DEPLOY_TOKEN_SECRET || "MY_SECRET_DEPLOY_TOKEN"
|
|
38
119
|
const SERVICE_ACCESS_TOKEN = jwt.sign({app: process.env.APP_DEPLOYMENT_NAME}, DEPLOY_TOKEN_SECRET, { expiresIn: '365d' });
|
|
@@ -63,39 +144,50 @@ let appId = Utils.uid()
|
|
|
63
144
|
let SERVICE_ADDRESS = process.env.APP_ENDPOINT || 'http://127.0.0.1:3000'
|
|
64
145
|
|
|
65
146
|
|
|
66
|
-
export const get = feature => {
|
|
147
|
+
export const get = (feature: string) => {
|
|
67
148
|
const throwError = () => {throw new Error(`Feature ${feature} not found in internal or external list during Features.subscribe call`)}
|
|
68
|
-
const baseFeature: FeatureEntry = getFeature(feature)
|
|
149
|
+
const baseFeature: FeatureEntry | undefined = getFeature(feature)
|
|
69
150
|
let {api, service = SERVICE_ADDRESS, topic} = baseFeature ? baseFeature as FeatureEntry & {service: string} : (SLYP_FEATURES_LIST[feature] ?? throwError())
|
|
70
151
|
return {api, service, topic}
|
|
71
152
|
}
|
|
72
153
|
|
|
73
|
-
export const send = async (meta
|
|
154
|
+
export const send = async (meta: Partial<FeatureMeta>, feature: string, data?: unknown, params?: FeatureParams) => {
|
|
74
155
|
const throwError = () => {throw new Error(`Feature ${feature} not found in internal or external list during inter feature call`)}
|
|
75
|
-
const baseFeature: FeatureEntry = getFeature(feature)
|
|
156
|
+
const baseFeature: FeatureEntry | undefined = getFeature(feature)
|
|
76
157
|
let {api, service = SERVICE_ADDRESS} = baseFeature ? baseFeature as FeatureEntry & {service: string} : (SLYP_FEATURES_LIST[feature] ?? throwError())
|
|
77
158
|
let method = getFeatureMethod(api)
|
|
78
159
|
let url = getFeatureUrl(api)
|
|
79
160
|
|
|
80
161
|
if (params || url.includes(':')) {
|
|
81
162
|
for (let key in params)
|
|
82
|
-
url = url.replace(`:${key}`, params[key])
|
|
163
|
+
url = url.replace(`:${key}`, params[key]!)
|
|
83
164
|
if (url.includes(':'))
|
|
84
165
|
throw new Error(`Error: Internal feature call send on feature: ${feature} when api/params/substitution`)
|
|
85
166
|
}
|
|
86
167
|
|
|
87
|
-
|
|
168
|
+
const payload: PayloadMessage = { ...meta, data, feature, txn: Utils.uid() } as PayloadMessage
|
|
88
169
|
service = service.replace('http://slyp.app', 'https://slyp.app')
|
|
89
170
|
|
|
90
171
|
// return (await axios[method](`${service}${url}`, { data: payload, headers: {jwt: headers.jwt}, timeout: 1000 })).data
|
|
91
172
|
|
|
92
173
|
// Used to work. But request to 127.0.0.1 fails to send the specified headers along with the request.
|
|
93
174
|
// return (await axios[method](`${service}${url}`, payload, {headers: {jwt: SERVICE_ACCESS_TOKEN, service: true}, timeout: 1000 })).data // Worked Earlier, but issue spotted
|
|
94
|
-
if (
|
|
175
|
+
if (baseFeature) { // Local call
|
|
95
176
|
let response;
|
|
96
|
-
let req = {
|
|
177
|
+
let req: ExpressRequest = { body: payload, params: params ?? {}, method, path: url, url: ''}
|
|
97
178
|
req = JSON.parse(JSON.stringify(req))
|
|
98
|
-
|
|
179
|
+
const callback = (payload: unknown): void => { response = payload }
|
|
180
|
+
const res: ExpressResponse = {
|
|
181
|
+
json: callback,
|
|
182
|
+
send: callback,
|
|
183
|
+
end: () => {},
|
|
184
|
+
status: (_status: number) => { return {send: callback, json: callback} },
|
|
185
|
+
sendStatus: (_status: number) => {},
|
|
186
|
+
setHeader: (_header: string, _value: string) => {},
|
|
187
|
+
flushHeaders: () => {},
|
|
188
|
+
headersSent: false,
|
|
189
|
+
writableEnded: false,
|
|
190
|
+
}
|
|
99
191
|
await apiHandler(req, res);
|
|
100
192
|
return response
|
|
101
193
|
}
|
|
@@ -109,7 +201,7 @@ export const send = async (meta, feature, data, params) => {
|
|
|
109
201
|
})).data;
|
|
110
202
|
}
|
|
111
203
|
|
|
112
|
-
let appids = {}
|
|
204
|
+
let appids: Record<string, boolean> = {}
|
|
113
205
|
async function announce() {
|
|
114
206
|
let exp_features: Record<string, RemoteFeatureEntry> = {}
|
|
115
207
|
for (let [key, {api,subscribe}] of Object.entries(features))
|
|
@@ -128,17 +220,18 @@ async function announce() {
|
|
|
128
220
|
}
|
|
129
221
|
|
|
130
222
|
|
|
131
|
-
function getFeaturelessFeature(req) {
|
|
223
|
+
function getFeaturelessFeature(req: ExpressRequest) {
|
|
132
224
|
if (Session.ALLOWED_URLS.includes(req.path)) {
|
|
133
|
-
for(let key in features){
|
|
134
|
-
|
|
225
|
+
for(let key in features) {
|
|
226
|
+
const feature = features[key]!
|
|
227
|
+
if (feature.featureless && feature.api === req.method.toUpperCase() + ' ' + req.path) {
|
|
135
228
|
return getFeature(key)
|
|
136
229
|
}
|
|
137
230
|
}
|
|
138
231
|
}
|
|
139
232
|
}
|
|
140
233
|
|
|
141
|
-
const apiHandler = async (req, res) => {
|
|
234
|
+
const apiHandler = async (req: ExpressRequest, res: ExpressResponse) => {
|
|
142
235
|
let method = req.method.toLowerCase()
|
|
143
236
|
let featureless
|
|
144
237
|
try {
|
|
@@ -155,50 +248,89 @@ const apiHandler = async (req, res) => {
|
|
|
155
248
|
throw { status: 404, message: `Resource not found. Feature ${req.body.feature} not available.` }
|
|
156
249
|
}
|
|
157
250
|
|
|
251
|
+
feature = feature! // typescript validation with !
|
|
252
|
+
|
|
158
253
|
let topic = feature.topic
|
|
159
|
-
let params = getFeatureUrl(feature.api).split("/").filter(item => item.startsWith(":")).map(item => item.replace(":", ""))
|
|
254
|
+
let params = getFeatureUrl(feature.api).split("/").filter((item: string) => item.startsWith(":")).map((item: string) => item.replace(":", ""))
|
|
160
255
|
for (let param of params)
|
|
161
256
|
if (!req.params[param])
|
|
162
257
|
throw { status: 404, message: "Resource not found. One or more url parameter not specified." }
|
|
163
258
|
|
|
164
|
-
let meta = {...req.body, data: undefined}
|
|
165
|
-
|
|
259
|
+
let meta = {...req.body, data: undefined} as FeatureMeta // TODO: Must also include invoiceTxn
|
|
260
|
+
|
|
166
261
|
if (process.env.USE_DEFAULT_COMPANY) {
|
|
167
|
-
|
|
168
|
-
|
|
262
|
+
meta.company = 'DEFAULT_COMPANY'
|
|
263
|
+
meta.outlet = 'DEFAULT_OUTLET'
|
|
169
264
|
}
|
|
170
265
|
|
|
266
|
+
meta.txn = meta.txn ?? Utils.uid()
|
|
267
|
+
|
|
268
|
+
const message: FeatureMessage = {
|
|
269
|
+
meta,
|
|
270
|
+
data: req.body.data,
|
|
271
|
+
params: req.params,
|
|
272
|
+
|
|
273
|
+
// To be removed 1
|
|
274
|
+
date: new Date().getTime(),
|
|
275
|
+
|
|
276
|
+
// To be removed 2
|
|
277
|
+
topic,
|
|
278
|
+
feature: req.body.feature,
|
|
279
|
+
user: req.body.user,
|
|
280
|
+
txn: meta.txn,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const appReq: Req = {
|
|
284
|
+
meta: message.meta,
|
|
285
|
+
body: message,
|
|
286
|
+
params: message.params,
|
|
287
|
+
method: method,
|
|
288
|
+
path: req.path,
|
|
289
|
+
url: req.url,
|
|
290
|
+
headers: req.headers,
|
|
291
|
+
query: req.query,
|
|
292
|
+
on: req.on,
|
|
293
|
+
}
|
|
294
|
+
const appRes: Res = res as Res
|
|
295
|
+
|
|
171
296
|
if (method === "get") {
|
|
172
297
|
try {
|
|
173
|
-
req.body
|
|
174
|
-
|
|
175
|
-
|
|
298
|
+
// await feature.handler({...req, headers: req.headers, body: {...req.body, topic} }, res)
|
|
299
|
+
// req.body = {...req.body, topic}
|
|
300
|
+
await (feature.handler as Query)(appReq, appRes)
|
|
301
|
+
} catch (err: any) {
|
|
176
302
|
if (process.env.DEBUG_MODE)
|
|
177
|
-
console.
|
|
303
|
+
console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
|
|
178
304
|
throw {status: 500, message: "Failed to GET feature", ...err}
|
|
179
305
|
}
|
|
180
306
|
} else if (method !== "get" && feature.bypass) {
|
|
181
307
|
try {
|
|
182
|
-
await (feature.handler as Command)(topic,
|
|
183
|
-
} catch (err) {
|
|
308
|
+
await (feature.handler as Command)(topic, message, appReq, appRes)
|
|
309
|
+
} catch (err: any) {
|
|
184
310
|
if (process.env.DEBUG_MODE)
|
|
185
|
-
console.
|
|
311
|
+
console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
|
|
186
312
|
throw {status: 500, message: "Failed to POST feature", ...err}
|
|
187
313
|
}
|
|
188
|
-
} else
|
|
189
|
-
|
|
190
|
-
|
|
314
|
+
} else {
|
|
315
|
+
try {
|
|
316
|
+
await Kafka.send(`Features.${topic}`, message, message.user, { compression: Kafka.CompressionTypes.GZIP })
|
|
317
|
+
} catch {
|
|
318
|
+
throw {status: 500, message: "Failed to queue the transaction"}
|
|
319
|
+
}
|
|
320
|
+
appRes.json({ data: { txn: message.txn, success: true, status: "Queued", featureQueued: true } })
|
|
321
|
+
}
|
|
322
|
+
} catch (err: any) {
|
|
191
323
|
if (process.env.DEBUG_MODE)
|
|
192
|
-
console.
|
|
324
|
+
console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
|
|
193
325
|
try { // Sometimes error occurs when disconnecting stream from front end
|
|
194
326
|
res.status(err.status ?? 500).json(err)
|
|
195
327
|
} catch (_) { }
|
|
196
328
|
}
|
|
197
329
|
}
|
|
198
330
|
|
|
199
|
-
let ExpressApp
|
|
200
|
-
let PROJECT_ROOT_URL
|
|
201
|
-
export async function registerFeatures(newFeatures) {
|
|
331
|
+
let ExpressApp: ExpressApplication
|
|
332
|
+
let PROJECT_ROOT_URL: URL
|
|
333
|
+
export async function registerFeatures(newFeatures: Record<string, FeatureEntry>) {
|
|
202
334
|
// Registering handlers
|
|
203
335
|
await registerHandler(newFeatures)
|
|
204
336
|
|
|
@@ -207,15 +339,15 @@ export async function registerFeatures(newFeatures) {
|
|
|
207
339
|
// Registering apis
|
|
208
340
|
registerApi()
|
|
209
341
|
}
|
|
210
|
-
async function registerHandler(features) {
|
|
342
|
+
async function registerHandler(features: Record<string, FeatureEntry>) {
|
|
211
343
|
for (let name in features) {
|
|
212
|
-
let feature = features[name]
|
|
344
|
+
let feature = features[name]!
|
|
213
345
|
apis[feature.api] = apis[feature.api] ?? {}
|
|
214
|
-
apis[feature.api][name] = feature
|
|
346
|
+
apis[feature.api]![name] = feature
|
|
215
347
|
const featureName = name
|
|
216
348
|
|
|
217
349
|
const nameParts = name.split('.')
|
|
218
|
-
let handler = nameParts.pop()
|
|
350
|
+
let handler = nameParts.pop()!
|
|
219
351
|
const featurePath = `src/${nameParts.join('/')}`
|
|
220
352
|
|
|
221
353
|
if (featurePath === 'src/transactions/query')
|
|
@@ -229,8 +361,10 @@ async function registerHandler(features) {
|
|
|
229
361
|
}
|
|
230
362
|
function registerApi() {
|
|
231
363
|
for (let api in apis) {
|
|
232
|
-
let method = getFeatureMethod(api)
|
|
364
|
+
let method: HttpMethod = getFeatureMethod(api)
|
|
233
365
|
let url = api.split(' ')[1]
|
|
366
|
+
if (!url)
|
|
367
|
+
throw new Error(`No url path found in api ${api} during Features.registerApi()`)
|
|
234
368
|
if (api === 'GET /transactions/:id')
|
|
235
369
|
continue
|
|
236
370
|
|
|
@@ -238,22 +372,22 @@ function registerApi() {
|
|
|
238
372
|
}
|
|
239
373
|
}
|
|
240
374
|
|
|
241
|
-
export const start = async (app, url, file) => {
|
|
375
|
+
export const start = async (app: ExpressApplication, url: URL, file: string) => {
|
|
242
376
|
const globalMeta = Dip.globalMeta()
|
|
243
377
|
Dip.excludeCollectionConfig("users.txns")
|
|
244
378
|
Dip.excludeCollectionConfig("Features.txns")
|
|
245
379
|
ExpressApp = app
|
|
246
380
|
PROJECT_ROOT_URL = url
|
|
247
|
-
features = await Utils.fileToJson(url, file)
|
|
381
|
+
features = await Utils.fileToJson(url, file) as Record<string, FeatureEntry>
|
|
248
382
|
await announce()
|
|
249
383
|
|
|
250
|
-
app.get('/features', async (req, res) => {
|
|
251
|
-
let exp_features = {}
|
|
384
|
+
app.get('/features', async (req: ExpressRequest, res: ExpressResponse) => {
|
|
385
|
+
let exp_features: Record<string, RemoteFeatureEntry> = {}
|
|
252
386
|
for (let [key, {api}] of Object.entries(features))
|
|
253
|
-
exp_features[key] = {api, service: `${SERVICE_ADDRESS}
|
|
387
|
+
exp_features[key] = {api, service: `${SERVICE_ADDRESS}`, subscribe: '', topic: ''}
|
|
254
388
|
if (process.env.LOAD_LOCAL_FEATURES) {
|
|
255
389
|
for (let key in SLYP_FEATURES_LIST)
|
|
256
|
-
SLYP_FEATURES_LIST[key]
|
|
390
|
+
SLYP_FEATURES_LIST[key]!.headers = {JWT: SERVICE_ACCESS_TOKEN, service: true, NDCURVE_DEVELOPER_LICENSE_ACCESS_TOKEN} // TODO: CRITICAL: Security issue
|
|
257
391
|
}
|
|
258
392
|
res.json({ data: { ...SLYP_FEATURES_LIST, ...exp_features } })
|
|
259
393
|
})
|
|
@@ -262,23 +396,24 @@ export const start = async (app, url, file) => {
|
|
|
262
396
|
await registerFeatures(features)
|
|
263
397
|
|
|
264
398
|
// Get unique kafka topics list from features
|
|
265
|
-
let kafkaTopics = []
|
|
266
|
-
for (
|
|
267
|
-
let feature = features[name]
|
|
268
|
-
if (feature.api.split(' ')[0]
|
|
399
|
+
let kafkaTopics: string[] = []
|
|
400
|
+
for (const name in features) {
|
|
401
|
+
let feature = features[name]!
|
|
402
|
+
if (feature.api.split(' ')[0]?.toLowerCase() === "get" )
|
|
269
403
|
continue
|
|
270
404
|
kafkaTopics = [... new Set(kafkaTopics.concat([feature.topic]))]
|
|
271
405
|
}
|
|
272
406
|
|
|
273
407
|
// Subscribe to each topic
|
|
274
408
|
for (let topic of kafkaTopics) {
|
|
275
|
-
|
|
409
|
+
const groupId = ''
|
|
410
|
+
Kafka.receive<FeatureMessage>(`Features.${topic}`, groupId, async (topic: string, message: FeatureMessage) => {
|
|
276
411
|
if (Utils.isEmpty(message.meta?.company))
|
|
277
412
|
message.meta = { ...message.meta, company: "GLOBAL", outlet: "GLOBAL" }
|
|
278
413
|
if (!message?.feature)
|
|
279
414
|
return
|
|
280
415
|
|
|
281
|
-
const timer = ms => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
|
|
416
|
+
const timer = (ms: number) => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
|
|
282
417
|
topic = topic.replace(/^.*Features./,'')
|
|
283
418
|
while (true) {
|
|
284
419
|
try {
|
|
@@ -286,7 +421,12 @@ export const start = async (app, url, file) => {
|
|
|
286
421
|
// // TODO: use $useChunks: [] once support is added in dip insert
|
|
287
422
|
// await Dip.insert(globalMeta, `users.txns`, { _id: `${message.user}_${iso_date}_${message.txn}`, user: message.user, feature: message.feature, date: message.date, created: message.date, updated: message.date, status: "Queued" }, {idempotent: true}) // Can always update the status later
|
|
288
423
|
// TODO: Avoid duplicate processing
|
|
289
|
-
|
|
424
|
+
const feature = features[message.feature]
|
|
425
|
+
if (!feature) {
|
|
426
|
+
console.warn(`Feature ${message.feature} missing, in executing handler during Kafka.receive(topic: ${topic})`)
|
|
427
|
+
throw new Error(`Feature ${message.feature} missing, in executing handler during Kafka.receive(topic: ${topic})`)
|
|
428
|
+
}
|
|
429
|
+
await (feature.handler as Command)(topic, message)
|
|
290
430
|
await Dip.insert(globalMeta, "Features.txns", {_id: message.txn, status: "Processed"}, {idempotent: true})
|
|
291
431
|
break
|
|
292
432
|
} catch(err) {
|
|
@@ -302,9 +442,9 @@ export const start = async (app, url, file) => {
|
|
|
302
442
|
await subscribe()
|
|
303
443
|
|
|
304
444
|
// Transaction status api
|
|
305
|
-
app.get('/transactions/:id', async (req, res) => { // Front end calls this as a regular feature call. so `req.meta` exists
|
|
445
|
+
app.get('/transactions/:id', async (req: ExpressRequest, res: ExpressResponse) => { // Front end calls this as a regular feature call. so `req.meta` exists
|
|
306
446
|
try {
|
|
307
|
-
let items = await Dip.query(globalMeta, "Features.txns", {_id: req.params
|
|
447
|
+
let items = await Dip.query(globalMeta, "Features.txns", {_id: req.params!.id})
|
|
308
448
|
if (items.length)
|
|
309
449
|
res.json({ data: { ...items[0], txn: items._id } })
|
|
310
450
|
else
|
|
@@ -325,70 +465,61 @@ export const start = async (app, url, file) => {
|
|
|
325
465
|
// }
|
|
326
466
|
// })
|
|
327
467
|
|
|
328
|
-
try { await Messaging.start(app) } catch { console.log('Messaging failed to start. Maybe missing redis') }
|
|
468
|
+
try { await Messaging.start((app as unknown) as Messaging.ExpressApp ) } catch { console.log('Messaging failed to start. Maybe missing redis') }
|
|
329
469
|
}
|
|
330
470
|
|
|
331
|
-
function prepareMessage(req) {
|
|
332
|
-
let txn = req.body.txn ?? Utils.uid()
|
|
333
|
-
let _id = req.body.data?._id ?? txn
|
|
334
|
-
let {data, feature, app, user, outlet, company, client, version} = req.body
|
|
335
|
-
let meta = req.meta
|
|
336
|
-
let params = req.params
|
|
337
|
-
return { data, params, meta, feature, app, user, outlet, company, client, version, txn, id: _id, date: new Date().getTime() }
|
|
338
|
-
}
|
|
339
|
-
const commandAction = async (req, res, topic) => {
|
|
340
|
-
let message = prepareMessage(req)
|
|
341
|
-
try {
|
|
342
|
-
await Kafka.send(`Features.${topic}`, message, message.user, { compression: Kafka.CompressionTypes.GZIP })
|
|
343
|
-
} catch {
|
|
344
|
-
throw {status: 500, message: "Failed to queue the transaction"}
|
|
345
|
-
}
|
|
346
|
-
res.json({ data: { txn: message.txn, success: true, status: "Queued", featureQueued: true } })
|
|
347
|
-
}
|
|
348
471
|
|
|
349
472
|
|
|
350
473
|
|
|
351
|
-
let subscriptions = {
|
|
474
|
+
let subscriptions: Record<string, string> = {
|
|
352
475
|
// "coins.query.hello": "invoices.command.add", // Consumer : Feature of Topic
|
|
353
476
|
}
|
|
354
|
-
let subscribed_consumers = {
|
|
355
|
-
}
|
|
477
|
+
let subscribed_consumers: Record<string, Kafka.KafkaConsumer> = {}
|
|
356
478
|
async function subscribe() {
|
|
357
479
|
let Features = {get,send}
|
|
358
480
|
|
|
359
|
-
for (let [key, {subscribe}] of Object.entries(features))
|
|
481
|
+
for (let [key, {subscribe}] of Object.entries(features)) {
|
|
360
482
|
if (!Utils.isEmpty(subscribe) && subscriptions[key] !== subscribe && subscribed_consumers[key]) {
|
|
361
483
|
try { await subscribed_consumers[key].disconnect() } catch (_) {}
|
|
362
484
|
delete subscribed_consumers[key]
|
|
363
485
|
}
|
|
364
|
-
|
|
365
|
-
|
|
486
|
+
}
|
|
487
|
+
for (let key in SLYP_FEATURES_LIST) {
|
|
488
|
+
const feature = SLYP_FEATURES_LIST[key]
|
|
489
|
+
if (!Utils.isEmpty(feature?.subscribe) && subscriptions[key] !== feature?.subscribe && subscribed_consumers[key]) {
|
|
366
490
|
try { await subscribed_consumers[key].disconnect() } catch (_) {}
|
|
367
491
|
delete subscribed_consumers[key]
|
|
368
492
|
}
|
|
493
|
+
}
|
|
369
494
|
|
|
370
495
|
|
|
371
|
-
for (let key in SLYP_FEATURES_LIST)
|
|
372
|
-
|
|
373
|
-
|
|
496
|
+
for (let key in SLYP_FEATURES_LIST) {
|
|
497
|
+
const feature = SLYP_FEATURES_LIST[key]
|
|
498
|
+
if (!Utils.isEmpty(feature?.subscribe)) {
|
|
499
|
+
subscriptions[key] = feature!.subscribe
|
|
374
500
|
}
|
|
375
|
-
|
|
501
|
+
}
|
|
502
|
+
for (let [key, {subscribe}] of Object.entries(features)) {
|
|
376
503
|
if (!Utils.isEmpty(subscribe)) {
|
|
377
504
|
subscriptions[key] = subscribe
|
|
378
505
|
}
|
|
506
|
+
}
|
|
379
507
|
|
|
380
508
|
|
|
381
509
|
for (let consumer in subscriptions) {
|
|
382
|
-
let publisher = subscriptions[consumer]
|
|
383
|
-
|
|
510
|
+
let publisher = subscriptions[consumer]!
|
|
511
|
+
|
|
512
|
+
const publisherFeature = getFeature(publisher)
|
|
384
513
|
|
|
385
|
-
if (subscribed_consumers[consumer] || !
|
|
514
|
+
if (subscribed_consumers[consumer] || !publisherFeature)
|
|
386
515
|
continue
|
|
387
516
|
|
|
388
|
-
let
|
|
517
|
+
let {topic} = publisherFeature
|
|
518
|
+
|
|
519
|
+
let kafka_consumer_promise = Kafka.receive<FeatureMessage>(`Features.${topic}`, consumer, async (topic: string, message: FeatureMessage) => {
|
|
389
520
|
topic = topic.replace(/^.*Features./,'')
|
|
390
521
|
|
|
391
|
-
const timer = ms => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
|
|
522
|
+
const timer = (ms: number): Promise<void> => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
|
|
392
523
|
|
|
393
524
|
while (true) {
|
|
394
525
|
try {
|
|
@@ -400,6 +531,12 @@ async function subscribe() {
|
|
|
400
531
|
await timer(10000);
|
|
401
532
|
}
|
|
402
533
|
})
|
|
403
|
-
|
|
534
|
+
|
|
535
|
+
kafka_consumer_promise
|
|
536
|
+
.then(kafka_consumer => {
|
|
537
|
+
subscribed_consumers[consumer] = kafka_consumer
|
|
538
|
+
}).catch(err => {
|
|
539
|
+
throw new Error("Error: Remote service to service kafka Feature subscription.")
|
|
540
|
+
})
|
|
404
541
|
}
|
|
405
542
|
}
|
package/libs/kafka.ts
CHANGED
|
@@ -5,10 +5,11 @@ export const logLevel = KafkaLogLevel
|
|
|
5
5
|
export const CompressionTypes = KafkaCompressionTypes
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
type KafkaCallback = (topic: string, message:
|
|
8
|
+
export type KafkaCallback<T = unknown> = (topic: string, message: T, partition: number) => unknown | Promise<unknown>
|
|
9
9
|
|
|
10
|
-
type KafkaConsumer = {
|
|
10
|
+
export type KafkaConsumer = {
|
|
11
11
|
connect: () => Promise<void>
|
|
12
|
+
disconnect: () => Promise<void>
|
|
12
13
|
subscribe: (arg: {topic: string; fromBeginning: boolean}) => Promise<void>
|
|
13
14
|
run: (arg: {
|
|
14
15
|
autoCommit: boolean
|
|
@@ -100,27 +101,24 @@ export const createTopic = async (topic: string, partition?: number, replicas?:
|
|
|
100
101
|
|
|
101
102
|
let consumers = []
|
|
102
103
|
|
|
103
|
-
const start_consumer = async function
|
|
104
|
-
|
|
105
|
-
groupId = typeof groupId === "string" ? `${appName}.${groupId}` : `${appName}.${topic}`
|
|
104
|
+
const start_consumer = async function<T>(topic: string, groupId: string, callback: KafkaCallback<T>): Promise<KafkaConsumer> {
|
|
105
|
+
groupId = groupId ? `${appName}.${groupId}` : `${appName}.${topic}`
|
|
106
106
|
|
|
107
|
-
const consumer = kafka.consumer({ groupId: groupId })
|
|
107
|
+
const consumer: KafkaConsumer = kafka.consumer({ groupId: groupId })
|
|
108
108
|
consumers.push(consumer)
|
|
109
109
|
|
|
110
110
|
await consumer.connect()
|
|
111
111
|
await consumer.subscribe({ topic: topic, fromBeginning: true })
|
|
112
112
|
|
|
113
|
-
const fn = callback!
|
|
114
|
-
|
|
115
113
|
await consumer.run({
|
|
116
114
|
autoCommit: false,
|
|
117
115
|
eachMessage: async ({ topic, partition, message }) => {
|
|
118
|
-
//
|
|
116
|
+
// callback(topic, message.value.toString(), partition) // toString() returns array so won't parse if json.
|
|
119
117
|
let success
|
|
120
118
|
try {
|
|
121
|
-
success = await
|
|
119
|
+
success = await callback(topic, JSON.parse(message.value as string), partition)
|
|
122
120
|
} catch (ex) {
|
|
123
|
-
success = await
|
|
121
|
+
success = await callback(topic, message.value as T, partition) // NOTE: Beware: Ensure Kafka.receive<T> can handle raw message type that is not json parseable
|
|
124
122
|
}
|
|
125
123
|
|
|
126
124
|
await consumer.commitOffsets([{ topic, partition, offset: (Number(message.offset) + 1).toString() }]);
|
|
@@ -160,9 +158,9 @@ const start_producer = async function (topic: string, message: object, key: stri
|
|
|
160
158
|
// start_consumer('quickstart-events')
|
|
161
159
|
// start_producer('quickstart-events', 'Hello KafkaJS user! Little')
|
|
162
160
|
|
|
163
|
-
export const receive = async function(topic: string, groupId: string, callback
|
|
161
|
+
export const receive = async function<T>(topic: string, groupId: string, callback: KafkaCallback<T>): Promise<KafkaConsumer> {
|
|
164
162
|
topic = topicPrefix + topic
|
|
165
|
-
return
|
|
163
|
+
return await start_consumer(topic, groupId, callback)
|
|
166
164
|
}
|
|
167
165
|
|
|
168
166
|
|
package/libs/messaging.ts
CHANGED