corebasic 1.0.230 → 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/auth.js +5 -4
- package/dist/libs/compression.js +1 -7
- package/dist/libs/dip/suffix/index.js +8 -6
- package/dist/libs/dip/suffix/policy.js +6 -4
- package/dist/libs/dip/suffix/query.js +290 -279
- package/dist/libs/dip/suffix/suffix.js +17 -13
- package/dist/libs/dip/suffix/tests/date.test.js +21 -34
- package/dist/libs/dip/suffix/tests/policy.test.js +41 -80
- package/dist/libs/dip.js +29 -26
- package/dist/libs/dipper.js +0 -1
- package/dist/libs/elabase.js +1 -1
- package/dist/libs/features.js +110 -60
- package/dist/libs/kafka.js +6 -5
- package/dist/libs/messaging.js +7 -4
- package/dist/libs/session.js +2 -2
- package/libs/auth.ts +48 -9
- package/libs/compression.ts +6 -1
- package/libs/cpp.ts +15 -13
- package/libs/dip/suffix/date.ts +6 -6
- package/libs/dip/suffix/index.ts +41 -26
- package/libs/dip/suffix/policy.ts +16 -13
- package/libs/dip/suffix/query.ts +289 -289
- package/libs/dip/suffix/suffix.ts +26 -20
- package/libs/dip/suffix/tests/date.test.ts +32 -64
- package/libs/dip/suffix/tests/policy.applySuffixPolicy.test.ts +20 -20
- package/libs/dip/suffix/tests/policy.test.ts +41 -109
- package/libs/dip/suffix/tests/suffix.test.ts +2 -2
- package/libs/dip.ts +61 -33
- package/libs/dipper.ts +25 -10
- package/libs/elabase.ts +1 -1
- package/libs/features.ts +232 -95
- package/libs/kafka.ts +75 -14
- package/libs/messaging.ts +80 -19
- package/libs/session.ts +21 -10
- package/package.json +1 -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: ''}
|
|
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
|
@@ -4,12 +4,74 @@ import { Kafka, logLevel as KafkaLogLevel, CompressionTypes as KafkaCompressionT
|
|
|
4
4
|
export const logLevel = KafkaLogLevel
|
|
5
5
|
export const CompressionTypes = KafkaCompressionTypes
|
|
6
6
|
|
|
7
|
-
let kafka, producer, admin
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
export type KafkaCallback<T = unknown> = (topic: string, message: T, partition: number) => unknown | Promise<unknown>
|
|
9
|
+
|
|
10
|
+
export type KafkaConsumer = {
|
|
11
|
+
connect: () => Promise<void>
|
|
12
|
+
disconnect: () => Promise<void>
|
|
13
|
+
subscribe: (arg: {topic: string; fromBeginning: boolean}) => Promise<void>
|
|
14
|
+
run: (arg: {
|
|
15
|
+
autoCommit: boolean
|
|
16
|
+
eachMessage: (arg: {
|
|
17
|
+
topic: string
|
|
18
|
+
partition: number
|
|
19
|
+
message: {
|
|
20
|
+
value: string | Buffer | null
|
|
21
|
+
offset: string
|
|
22
|
+
}
|
|
23
|
+
}) => Promise<void>
|
|
24
|
+
}) => Promise<void>
|
|
25
|
+
commitOffsets: (offsets: {topic: string; partition: number; offset: string}[]) => Promise<void>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type KafkaProducer = {
|
|
29
|
+
connect: () => Promise<void>
|
|
30
|
+
send: (arg: unknown) => Promise<unknown>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type KafkaAdmin = {
|
|
34
|
+
createTopics: (arg: {
|
|
35
|
+
topics: {
|
|
36
|
+
topic: string
|
|
37
|
+
numPartitions: number
|
|
38
|
+
replicationFactor: number
|
|
39
|
+
}[]
|
|
40
|
+
}) => Promise<unknown>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type KafkaClient = {
|
|
44
|
+
consumer: (arg: {groupId: string}) => KafkaConsumer
|
|
45
|
+
producer: (arg: {allowAutoTopicCreation: boolean}) => KafkaProducer
|
|
46
|
+
admin: () => KafkaAdmin
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type KafkaStartArg = {
|
|
50
|
+
clientId?: string
|
|
51
|
+
brokers?: string[]
|
|
52
|
+
sasl?: false | {
|
|
53
|
+
mechanism: string
|
|
54
|
+
username?: string
|
|
55
|
+
password?: string
|
|
56
|
+
}
|
|
57
|
+
logLevel?: unknown
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
let kafka: KafkaClient
|
|
62
|
+
let producer: KafkaProducer
|
|
63
|
+
let admin: KafkaAdmin
|
|
64
|
+
|
|
65
|
+
let appName: string
|
|
10
66
|
let topicPrefix = ''
|
|
11
67
|
|
|
12
|
-
|
|
68
|
+
declare global {
|
|
69
|
+
var app: {
|
|
70
|
+
name?: string
|
|
71
|
+
} | undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const start = (arg?: KafkaStartArg) => {
|
|
13
75
|
arg = !arg && !process.env.APP_DEPLOYMENT_NAME ? { clientId: (global.app?.name ?? 'app') + '-dev', brokers: ['127.0.0.1:9092'], sasl: false, logLevel: logLevel.ERROR } : arg
|
|
14
76
|
appName = arg?.clientId ?? (process.env.APP_DEPLOYMENT_NAME ?? (global.app?.name ?? 'app'))
|
|
15
77
|
topicPrefix = `${appName}.`
|
|
@@ -28,7 +90,7 @@ export const start = (arg) => {
|
|
|
28
90
|
}
|
|
29
91
|
|
|
30
92
|
|
|
31
|
-
export const createTopic = async (topic, partition, replicas) => {
|
|
93
|
+
export const createTopic = async (topic: string, partition?: number, replicas?: number) => {
|
|
32
94
|
topic = topicPrefix + topic
|
|
33
95
|
return await admin.createTopics({ topics: [{topic, numPartitions: partition ?? 1, replicationFactor: replicas ?? 1}] })
|
|
34
96
|
}
|
|
@@ -39,11 +101,10 @@ export const createTopic = async (topic, partition, replicas) => {
|
|
|
39
101
|
|
|
40
102
|
let consumers = []
|
|
41
103
|
|
|
42
|
-
const start_consumer = async function
|
|
43
|
-
|
|
44
|
-
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}`
|
|
45
106
|
|
|
46
|
-
const consumer = kafka.consumer({ groupId: groupId })
|
|
107
|
+
const consumer: KafkaConsumer = kafka.consumer({ groupId: groupId })
|
|
47
108
|
consumers.push(consumer)
|
|
48
109
|
|
|
49
110
|
await consumer.connect()
|
|
@@ -55,9 +116,9 @@ const start_consumer = async function (topic, groupId, callback?: Function) {
|
|
|
55
116
|
// callback(topic, message.value.toString(), partition) // toString() returns array so won't parse if json.
|
|
56
117
|
let success
|
|
57
118
|
try {
|
|
58
|
-
success = await callback(topic, JSON.parse(message.value), partition)
|
|
119
|
+
success = await callback(topic, JSON.parse(message.value as string), partition)
|
|
59
120
|
} catch (ex) {
|
|
60
|
-
success = await callback(topic, message.value, partition)
|
|
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
|
|
61
122
|
}
|
|
62
123
|
|
|
63
124
|
await consumer.commitOffsets([{ topic, partition, offset: (Number(message.offset) + 1).toString() }]);
|
|
@@ -71,7 +132,7 @@ const start_consumer = async function (topic, groupId, callback?: Function) {
|
|
|
71
132
|
// Producer
|
|
72
133
|
// ===========
|
|
73
134
|
let producerInvoked = false
|
|
74
|
-
const start_producer = async function (topic, message, key, options) {
|
|
135
|
+
const start_producer = async function (topic: string, message: object, key: string, options?: object) {
|
|
75
136
|
|
|
76
137
|
let isJson = Object.prototype.toString.call(message) === '[object Object]' || Object.prototype.toString.call(message) === '[object Array]'
|
|
77
138
|
|
|
@@ -97,13 +158,13 @@ const start_producer = async function (topic, message, key, options) {
|
|
|
97
158
|
// start_consumer('quickstart-events')
|
|
98
159
|
// start_producer('quickstart-events', 'Hello KafkaJS user! Little')
|
|
99
160
|
|
|
100
|
-
export const receive = async function(topic, groupId, callback
|
|
161
|
+
export const receive = async function<T>(topic: string, groupId: string, callback: KafkaCallback<T>): Promise<KafkaConsumer> {
|
|
101
162
|
topic = topicPrefix + topic
|
|
102
|
-
return
|
|
163
|
+
return await start_consumer(topic, groupId, callback)
|
|
103
164
|
}
|
|
104
165
|
|
|
105
166
|
|
|
106
|
-
export const send = async function(topic, message, key, options) {
|
|
167
|
+
export const send = async function(topic: string, message: object, key: string, options?: object) {
|
|
107
168
|
topic = topicPrefix + topic
|
|
108
169
|
await start_producer(topic, message, key, options)
|
|
109
170
|
}
|