corebasic 1.0.231 → 1.0.233
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/auth.js +3 -3
- package/dist/libs/dip/suffix/index.js +1 -1
- package/dist/libs/dip/suffix/suffix.js +2 -3
- package/dist/libs/dip/suffix/tests/policy.applySuffixPolicy.test.js +26 -99
- package/dist/libs/dip/suffix/tests/policy.test.js +1 -1
- package/dist/libs/dip.js +1 -0
- package/dist/libs/dipper.js +13 -12
- package/dist/libs/elabase.js +36 -45
- package/dist/libs/features.js +113 -62
- package/dist/libs/kafka.js +5 -8
- package/dist/libs/messaging.js +1 -1
- package/dist/libs/utils.js +1 -1
- package/libs/auth.ts +3 -3
- package/libs/dip/suffix/index.ts +1 -1
- package/libs/dip/suffix/policy.ts +1 -1
- package/libs/dip/suffix/suffix.ts +3 -4
- package/libs/dip/suffix/tests/policy.applySuffixPolicy.test.ts +26 -180
- package/libs/dip/suffix/tests/policy.test.ts +1 -1
- package/libs/dip.ts +7 -5
- package/libs/dipper.ts +14 -13
- package/libs/elabase.ts +172 -126
- package/libs/features.ts +234 -96
- package/libs/kafka.ts +12 -14
- package/libs/messaging.ts +2 -2
- package/libs/utils.ts +1 -1
- package/package.json +1 -1
- package/tsconfig.json +16 -1
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: '', headers: {}, query: {}, on: (_event: string, _callback:()=>void) => {} }
|
|
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,13 +201,13 @@ 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))
|
|
116
208
|
exp_features[key] = {api, service: SERVICE_ADDRESS, subscribe, topic: ""}
|
|
117
209
|
|
|
118
|
-
await Messaging.subscribe(`${process.env.REDIS_CHANNEL_PREFIX}_SLYP_FEATURES_LIST`, async (message,
|
|
210
|
+
await Messaging.subscribe(`${process.env.REDIS_CHANNEL_PREFIX}_SLYP_FEATURES_LIST`, async (message, _channel) => {
|
|
119
211
|
let {uid, ...msg} = JSON.parse(message) as Record<string, RemoteFeatureEntry> & {uid: string}
|
|
120
212
|
if (uid !== appId && !appids[uid]) {
|
|
121
213
|
appids[uid] = true
|
|
@@ -128,17 +220,19 @@ async function announce() {
|
|
|
128
220
|
}
|
|
129
221
|
|
|
130
222
|
|
|
131
|
-
function getFeaturelessFeature(req) {
|
|
223
|
+
function getFeaturelessFeature(req: ExpressRequest): FeatureEntry | undefined {
|
|
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
|
}
|
|
232
|
+
return undefined
|
|
139
233
|
}
|
|
140
234
|
|
|
141
|
-
const apiHandler = async (req, res) => {
|
|
235
|
+
const apiHandler = async (req: ExpressRequest, res: ExpressResponse) => {
|
|
142
236
|
let method = req.method.toLowerCase()
|
|
143
237
|
let featureless
|
|
144
238
|
try {
|
|
@@ -155,50 +249,89 @@ const apiHandler = async (req, res) => {
|
|
|
155
249
|
throw { status: 404, message: `Resource not found. Feature ${req.body.feature} not available.` }
|
|
156
250
|
}
|
|
157
251
|
|
|
252
|
+
feature = feature! // typescript validation with !
|
|
253
|
+
|
|
158
254
|
let topic = feature.topic
|
|
159
|
-
let params = getFeatureUrl(feature.api).split("/").filter(item => item.startsWith(":")).map(item => item.replace(":", ""))
|
|
255
|
+
let params = getFeatureUrl(feature.api).split("/").filter((item: string) => item.startsWith(":")).map((item: string) => item.replace(":", ""))
|
|
160
256
|
for (let param of params)
|
|
161
257
|
if (!req.params[param])
|
|
162
258
|
throw { status: 404, message: "Resource not found. One or more url parameter not specified." }
|
|
163
259
|
|
|
164
|
-
let meta = {...req.body, data: undefined}
|
|
165
|
-
|
|
260
|
+
let meta = {...req.body, data: undefined} as FeatureMeta // TODO: Must also include invoiceTxn
|
|
261
|
+
|
|
166
262
|
if (process.env.USE_DEFAULT_COMPANY) {
|
|
167
|
-
|
|
168
|
-
|
|
263
|
+
meta.company = 'DEFAULT_COMPANY'
|
|
264
|
+
meta.outlet = 'DEFAULT_OUTLET'
|
|
169
265
|
}
|
|
170
266
|
|
|
267
|
+
meta.txn = meta.txn ?? Utils.uid()
|
|
268
|
+
|
|
269
|
+
const message: FeatureMessage = {
|
|
270
|
+
meta,
|
|
271
|
+
data: req.body.data,
|
|
272
|
+
params: req.params,
|
|
273
|
+
|
|
274
|
+
// To be removed 1
|
|
275
|
+
date: new Date().getTime(),
|
|
276
|
+
|
|
277
|
+
// To be removed 2
|
|
278
|
+
topic,
|
|
279
|
+
feature: req.body.feature,
|
|
280
|
+
user: req.body.user,
|
|
281
|
+
txn: meta.txn,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const appReq: Req = {
|
|
285
|
+
meta: message.meta,
|
|
286
|
+
body: message,
|
|
287
|
+
params: message.params,
|
|
288
|
+
method: method,
|
|
289
|
+
path: req.path,
|
|
290
|
+
url: req.url,
|
|
291
|
+
headers: req.headers,
|
|
292
|
+
query: req.query,
|
|
293
|
+
on: req.on,
|
|
294
|
+
}
|
|
295
|
+
const appRes: Res = res as Res
|
|
296
|
+
|
|
171
297
|
if (method === "get") {
|
|
172
298
|
try {
|
|
173
|
-
req.body
|
|
174
|
-
|
|
175
|
-
|
|
299
|
+
// await feature.handler({...req, headers: req.headers, body: {...req.body, topic} }, res)
|
|
300
|
+
// req.body = {...req.body, topic}
|
|
301
|
+
await (feature.handler as Query)(appReq, appRes)
|
|
302
|
+
} catch (err: any) {
|
|
176
303
|
if (process.env.DEBUG_MODE)
|
|
177
|
-
console.
|
|
304
|
+
console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
|
|
178
305
|
throw {status: 500, message: "Failed to GET feature", ...err}
|
|
179
306
|
}
|
|
180
307
|
} else if (method !== "get" && feature.bypass) {
|
|
181
308
|
try {
|
|
182
|
-
await (feature.handler as Command)(topic,
|
|
183
|
-
} catch (err) {
|
|
309
|
+
await (feature.handler as Command)(topic, message, appReq, appRes)
|
|
310
|
+
} catch (err: any) {
|
|
184
311
|
if (process.env.DEBUG_MODE)
|
|
185
|
-
console.
|
|
312
|
+
console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
|
|
186
313
|
throw {status: 500, message: "Failed to POST feature", ...err}
|
|
187
314
|
}
|
|
188
|
-
} else
|
|
189
|
-
|
|
190
|
-
|
|
315
|
+
} else {
|
|
316
|
+
try {
|
|
317
|
+
await Kafka.send(`Features.${topic}`, message, message.user, { compression: Kafka.CompressionTypes.GZIP })
|
|
318
|
+
} catch {
|
|
319
|
+
throw {status: 500, message: "Failed to queue the transaction"}
|
|
320
|
+
}
|
|
321
|
+
appRes.json({ data: { txn: message.txn, success: true, status: "Queued", featureQueued: true } })
|
|
322
|
+
}
|
|
323
|
+
} catch (err: any) {
|
|
191
324
|
if (process.env.DEBUG_MODE)
|
|
192
|
-
console.
|
|
325
|
+
console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
|
|
193
326
|
try { // Sometimes error occurs when disconnecting stream from front end
|
|
194
327
|
res.status(err.status ?? 500).json(err)
|
|
195
328
|
} catch (_) { }
|
|
196
329
|
}
|
|
197
330
|
}
|
|
198
331
|
|
|
199
|
-
let ExpressApp
|
|
200
|
-
let PROJECT_ROOT_URL
|
|
201
|
-
export async function registerFeatures(newFeatures) {
|
|
332
|
+
let ExpressApp: ExpressApplication
|
|
333
|
+
let PROJECT_ROOT_URL: URL
|
|
334
|
+
export async function registerFeatures(newFeatures: Record<string, FeatureEntry>) {
|
|
202
335
|
// Registering handlers
|
|
203
336
|
await registerHandler(newFeatures)
|
|
204
337
|
|
|
@@ -207,15 +340,15 @@ export async function registerFeatures(newFeatures) {
|
|
|
207
340
|
// Registering apis
|
|
208
341
|
registerApi()
|
|
209
342
|
}
|
|
210
|
-
async function registerHandler(features) {
|
|
343
|
+
async function registerHandler(features: Record<string, FeatureEntry>) {
|
|
211
344
|
for (let name in features) {
|
|
212
|
-
let feature = features[name]
|
|
345
|
+
let feature = features[name]!
|
|
213
346
|
apis[feature.api] = apis[feature.api] ?? {}
|
|
214
|
-
apis[feature.api][name] = feature
|
|
347
|
+
apis[feature.api]![name] = feature
|
|
215
348
|
const featureName = name
|
|
216
349
|
|
|
217
350
|
const nameParts = name.split('.')
|
|
218
|
-
let handler = nameParts.pop()
|
|
351
|
+
let handler = nameParts.pop()!
|
|
219
352
|
const featurePath = `src/${nameParts.join('/')}`
|
|
220
353
|
|
|
221
354
|
if (featurePath === 'src/transactions/query')
|
|
@@ -229,8 +362,10 @@ async function registerHandler(features) {
|
|
|
229
362
|
}
|
|
230
363
|
function registerApi() {
|
|
231
364
|
for (let api in apis) {
|
|
232
|
-
let method = getFeatureMethod(api)
|
|
365
|
+
let method: HttpMethod = getFeatureMethod(api)
|
|
233
366
|
let url = api.split(' ')[1]
|
|
367
|
+
if (!url)
|
|
368
|
+
throw new Error(`No url path found in api ${api} during Features.registerApi()`)
|
|
234
369
|
if (api === 'GET /transactions/:id')
|
|
235
370
|
continue
|
|
236
371
|
|
|
@@ -238,22 +373,22 @@ function registerApi() {
|
|
|
238
373
|
}
|
|
239
374
|
}
|
|
240
375
|
|
|
241
|
-
export const start = async (app, url, file) => {
|
|
376
|
+
export const start = async (app: ExpressApplication, url: URL, file: string) => {
|
|
242
377
|
const globalMeta = Dip.globalMeta()
|
|
243
378
|
Dip.excludeCollectionConfig("users.txns")
|
|
244
379
|
Dip.excludeCollectionConfig("Features.txns")
|
|
245
380
|
ExpressApp = app
|
|
246
381
|
PROJECT_ROOT_URL = url
|
|
247
|
-
features = await Utils.fileToJson(url, file)
|
|
382
|
+
features = await Utils.fileToJson(url, file) as Record<string, FeatureEntry>
|
|
248
383
|
await announce()
|
|
249
384
|
|
|
250
|
-
app.get('/features', async (
|
|
251
|
-
let exp_features = {}
|
|
385
|
+
app.get('/features', async (_req: ExpressRequest, res: ExpressResponse) => {
|
|
386
|
+
let exp_features: Record<string, RemoteFeatureEntry> = {}
|
|
252
387
|
for (let [key, {api}] of Object.entries(features))
|
|
253
|
-
exp_features[key] = {api, service: `${SERVICE_ADDRESS}
|
|
388
|
+
exp_features[key] = {api, service: `${SERVICE_ADDRESS}`, subscribe: '', topic: ''}
|
|
254
389
|
if (process.env.LOAD_LOCAL_FEATURES) {
|
|
255
390
|
for (let key in SLYP_FEATURES_LIST)
|
|
256
|
-
SLYP_FEATURES_LIST[key]
|
|
391
|
+
SLYP_FEATURES_LIST[key]!.headers = {JWT: SERVICE_ACCESS_TOKEN, service: true, NDCURVE_DEVELOPER_LICENSE_ACCESS_TOKEN} // TODO: CRITICAL: Security issue
|
|
257
392
|
}
|
|
258
393
|
res.json({ data: { ...SLYP_FEATURES_LIST, ...exp_features } })
|
|
259
394
|
})
|
|
@@ -262,23 +397,24 @@ export const start = async (app, url, file) => {
|
|
|
262
397
|
await registerFeatures(features)
|
|
263
398
|
|
|
264
399
|
// Get unique kafka topics list from features
|
|
265
|
-
let kafkaTopics = []
|
|
266
|
-
for (
|
|
267
|
-
let feature = features[name]
|
|
268
|
-
if (feature.api.split(' ')[0]
|
|
400
|
+
let kafkaTopics: string[] = []
|
|
401
|
+
for (const name in features) {
|
|
402
|
+
let feature = features[name]!
|
|
403
|
+
if (feature.api.split(' ')[0]?.toLowerCase() === "get" )
|
|
269
404
|
continue
|
|
270
405
|
kafkaTopics = [... new Set(kafkaTopics.concat([feature.topic]))]
|
|
271
406
|
}
|
|
272
407
|
|
|
273
408
|
// Subscribe to each topic
|
|
274
409
|
for (let topic of kafkaTopics) {
|
|
275
|
-
|
|
410
|
+
const groupId = ''
|
|
411
|
+
Kafka.receive<FeatureMessage>(`Features.${topic}`, groupId, async (topic: string, message: FeatureMessage) => {
|
|
276
412
|
if (Utils.isEmpty(message.meta?.company))
|
|
277
413
|
message.meta = { ...message.meta, company: "GLOBAL", outlet: "GLOBAL" }
|
|
278
414
|
if (!message?.feature)
|
|
279
415
|
return
|
|
280
416
|
|
|
281
|
-
const timer = ms => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
|
|
417
|
+
const timer = (ms: number) => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
|
|
282
418
|
topic = topic.replace(/^.*Features./,'')
|
|
283
419
|
while (true) {
|
|
284
420
|
try {
|
|
@@ -286,7 +422,12 @@ export const start = async (app, url, file) => {
|
|
|
286
422
|
// // TODO: use $useChunks: [] once support is added in dip insert
|
|
287
423
|
// 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
424
|
// TODO: Avoid duplicate processing
|
|
289
|
-
|
|
425
|
+
const feature = features[message.feature]
|
|
426
|
+
if (!feature) {
|
|
427
|
+
console.warn(`Feature ${message.feature} missing, in executing handler during Kafka.receive(topic: ${topic})`)
|
|
428
|
+
throw new Error(`Feature ${message.feature} missing, in executing handler during Kafka.receive(topic: ${topic})`)
|
|
429
|
+
}
|
|
430
|
+
await (feature.handler as Command)(topic, message)
|
|
290
431
|
await Dip.insert(globalMeta, "Features.txns", {_id: message.txn, status: "Processed"}, {idempotent: true})
|
|
291
432
|
break
|
|
292
433
|
} catch(err) {
|
|
@@ -302,9 +443,9 @@ export const start = async (app, url, file) => {
|
|
|
302
443
|
await subscribe()
|
|
303
444
|
|
|
304
445
|
// Transaction status api
|
|
305
|
-
app.get('/transactions/:id', async (req, res) => { // Front end calls this as a regular feature call. so `req.meta` exists
|
|
446
|
+
app.get('/transactions/:id', async (req: ExpressRequest, res: ExpressResponse) => { // Front end calls this as a regular feature call. so `req.meta` exists
|
|
306
447
|
try {
|
|
307
|
-
let items = await Dip.query(globalMeta, "Features.txns", {_id: req.params
|
|
448
|
+
let items = await Dip.query(globalMeta, "Features.txns", {_id: req.params!.id})
|
|
308
449
|
if (items.length)
|
|
309
450
|
res.json({ data: { ...items[0], txn: items._id } })
|
|
310
451
|
else
|
|
@@ -325,70 +466,61 @@ export const start = async (app, url, file) => {
|
|
|
325
466
|
// }
|
|
326
467
|
// })
|
|
327
468
|
|
|
328
|
-
try { await Messaging.start(app) } catch { console.log('Messaging failed to start. Maybe missing redis') }
|
|
469
|
+
try { await Messaging.start((app as unknown) as Messaging.ExpressApp ) } catch { console.log('Messaging failed to start. Maybe missing redis') }
|
|
329
470
|
}
|
|
330
471
|
|
|
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
472
|
|
|
349
473
|
|
|
350
474
|
|
|
351
|
-
let subscriptions = {
|
|
475
|
+
let subscriptions: Record<string, string> = {
|
|
352
476
|
// "coins.query.hello": "invoices.command.add", // Consumer : Feature of Topic
|
|
353
477
|
}
|
|
354
|
-
let subscribed_consumers = {
|
|
355
|
-
}
|
|
478
|
+
let subscribed_consumers: Record<string, Kafka.KafkaConsumer> = {}
|
|
356
479
|
async function subscribe() {
|
|
357
480
|
let Features = {get,send}
|
|
358
481
|
|
|
359
|
-
for (let [key, {subscribe}] of Object.entries(features))
|
|
482
|
+
for (let [key, {subscribe}] of Object.entries(features)) {
|
|
360
483
|
if (!Utils.isEmpty(subscribe) && subscriptions[key] !== subscribe && subscribed_consumers[key]) {
|
|
361
484
|
try { await subscribed_consumers[key].disconnect() } catch (_) {}
|
|
362
485
|
delete subscribed_consumers[key]
|
|
363
486
|
}
|
|
364
|
-
|
|
365
|
-
|
|
487
|
+
}
|
|
488
|
+
for (let key in SLYP_FEATURES_LIST) {
|
|
489
|
+
const feature = SLYP_FEATURES_LIST[key]
|
|
490
|
+
if (!Utils.isEmpty(feature?.subscribe) && subscriptions[key] !== feature?.subscribe && subscribed_consumers[key]) {
|
|
366
491
|
try { await subscribed_consumers[key].disconnect() } catch (_) {}
|
|
367
492
|
delete subscribed_consumers[key]
|
|
368
493
|
}
|
|
494
|
+
}
|
|
369
495
|
|
|
370
496
|
|
|
371
|
-
for (let key in SLYP_FEATURES_LIST)
|
|
372
|
-
|
|
373
|
-
|
|
497
|
+
for (let key in SLYP_FEATURES_LIST) {
|
|
498
|
+
const feature = SLYP_FEATURES_LIST[key]
|
|
499
|
+
if (!Utils.isEmpty(feature?.subscribe)) {
|
|
500
|
+
subscriptions[key] = feature!.subscribe
|
|
374
501
|
}
|
|
375
|
-
|
|
502
|
+
}
|
|
503
|
+
for (let [key, {subscribe}] of Object.entries(features)) {
|
|
376
504
|
if (!Utils.isEmpty(subscribe)) {
|
|
377
505
|
subscriptions[key] = subscribe
|
|
378
506
|
}
|
|
507
|
+
}
|
|
379
508
|
|
|
380
509
|
|
|
381
510
|
for (let consumer in subscriptions) {
|
|
382
|
-
let publisher = subscriptions[consumer]
|
|
383
|
-
|
|
511
|
+
let publisher = subscriptions[consumer]!
|
|
512
|
+
|
|
513
|
+
const publisherFeature = getFeature(publisher)
|
|
384
514
|
|
|
385
|
-
if (subscribed_consumers[consumer] || !
|
|
515
|
+
if (subscribed_consumers[consumer] || !publisherFeature)
|
|
386
516
|
continue
|
|
387
517
|
|
|
388
|
-
let
|
|
518
|
+
let {topic} = publisherFeature
|
|
519
|
+
|
|
520
|
+
let kafka_consumer_promise = Kafka.receive<FeatureMessage>(`Features.${topic}`, consumer, async (topic: string, message: FeatureMessage) => {
|
|
389
521
|
topic = topic.replace(/^.*Features./,'')
|
|
390
522
|
|
|
391
|
-
const timer = ms => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
|
|
523
|
+
const timer = (ms: number): Promise<void> => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
|
|
392
524
|
|
|
393
525
|
while (true) {
|
|
394
526
|
try {
|
|
@@ -400,6 +532,12 @@ async function subscribe() {
|
|
|
400
532
|
await timer(10000);
|
|
401
533
|
}
|
|
402
534
|
})
|
|
403
|
-
|
|
535
|
+
|
|
536
|
+
kafka_consumer_promise
|
|
537
|
+
.then(kafka_consumer => {
|
|
538
|
+
subscribed_consumers[consumer] = kafka_consumer
|
|
539
|
+
}).catch(() => {
|
|
540
|
+
throw new Error("Error: Remote service to service kafka Feature subscription.")
|
|
541
|
+
})
|
|
404
542
|
}
|
|
405
543
|
}
|
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
|
-
//
|
|
119
|
-
|
|
116
|
+
// callback(topic, message.value.toString(), partition) // toString() returns array so won't parse if json.
|
|
117
|
+
|
|
120
118
|
try {
|
|
121
|
-
|
|
119
|
+
await callback(topic, JSON.parse(message.value as string), partition)
|
|
122
120
|
} catch (ex) {
|
|
123
|
-
|
|
121
|
+
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
|
@@ -59,7 +59,7 @@ type UserPool = {
|
|
|
59
59
|
listeners: UserListeners
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
type ExpressApp = {
|
|
62
|
+
export type ExpressApp = {
|
|
63
63
|
get: (
|
|
64
64
|
path: string,
|
|
65
65
|
handler: (req: Request, res: Response) => Promise<void>
|
|
@@ -102,7 +102,7 @@ async function connect({user, req, res, uid}: {user: string, req: Request, res:
|
|
|
102
102
|
const listener = (message: string, channel: string) => {
|
|
103
103
|
if (users_pool[user]) {
|
|
104
104
|
try {message = JSON.parse(message)} catch {}
|
|
105
|
-
for (let {
|
|
105
|
+
for (let {res} of users_pool[user].reqres) {
|
|
106
106
|
res.write(JSON.stringify({message, channel}) + "\n")
|
|
107
107
|
if (res.flush) // If compression enabled
|
|
108
108
|
res.flush()
|
package/libs/utils.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function isEmpty(str: undefined | number | string): boolean { // returns
|
|
|
40
40
|
export let GLOBAL_META = { company: "GLOBAL", outlet: "GLOBAL" }
|
|
41
41
|
|
|
42
42
|
export function isEmptyJson(json: object): boolean {
|
|
43
|
-
for (let
|
|
43
|
+
for (let _i in json)
|
|
44
44
|
return false
|
|
45
45
|
return true
|
|
46
46
|
}
|