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/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
- export type Message = unknown
12
- export type Req = unknown
13
- export type Res = unknown
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: Message, req?: Req, res?: Res) => Promise<void>
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].toLowerCase()
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, feature, data, params) => {
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
- let payload = { ...meta, data, feature, txn: Utils.uid() }
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 (getFeature(feature)) { // Local call
175
+ if (baseFeature) { // Local call
95
176
  let response;
96
- let req = {meta,body: payload, params,method}
177
+ let req: ExpressRequest = { body: payload, params: params ?? {}, method, path: url, url: ''}
97
178
  req = JSON.parse(JSON.stringify(req))
98
- let res = {json: payload => response = payload, end: () => true }
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
- if (features[key].featureless && features[key].api === req.method.toUpperCase() + ' ' + req.path) {
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
- req.meta = meta
259
+ let meta = {...req.body, data: undefined} as FeatureMeta // TODO: Must also include invoiceTxn
260
+
166
261
  if (process.env.USE_DEFAULT_COMPANY) {
167
- req.meta.company = 'DEFAULT_COMPANY'
168
- req.meta.outlet = 'DEFAULT_OUTLET'
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 = {...req.body, topic}
174
- await (feature.handler as Query)(req, res)
175
- } catch (err) {
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.log('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
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, prepareMessage(req), req, res)
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.log('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
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
- await commandAction(req, res, topic)
190
- } catch (err) {
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.log('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
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].headers = {JWT: SERVICE_ACCESS_TOKEN, service: true, NDCURVE_DEVELOPER_LICENSE_ACCESS_TOKEN}
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 (let name in features) {
267
- let feature = features[name]
268
- if (feature.api.split(' ')[0].toLowerCase() === "get" )
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
- Kafka.receive(`Features.${topic}`, async (topic, message) => {
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
- await (features[message.feature].handler as Command)(topic, message)
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.id})
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
- for (let key in SLYP_FEATURES_LIST)
365
- if (!Utils.isEmpty(SLYP_FEATURES_LIST[key].subscribe) && subscriptions[key] !== SLYP_FEATURES_LIST[key].subscribe && subscribed_consumers[key]) {
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
- if (!Utils.isEmpty(SLYP_FEATURES_LIST[key].subscribe)) {
373
- subscriptions[key] = SLYP_FEATURES_LIST[key].subscribe
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
- for (let [key, {subscribe}] of Object.entries(features))
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
- let {topic} = getFeature(publisher)
510
+ let publisher = subscriptions[consumer]!
511
+
512
+ const publisherFeature = getFeature(publisher)
384
513
 
385
- if (subscribed_consumers[consumer] || !getFeature(publisher))
514
+ if (subscribed_consumers[consumer] || !publisherFeature)
386
515
  continue
387
516
 
388
- let kafka_consumer = Kafka.receive(`Features.${topic}`, consumer, async (topic, message) => {
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
- subscribed_consumers[consumer] = kafka_consumer
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
- let appName
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
- export const start = (arg) => {
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 (topic, groupId, callback?: Function) {
43
- callback = typeof groupId === "string" ? callback : groupId
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?: Function) {
161
+ export const receive = async function<T>(topic: string, groupId: string, callback: KafkaCallback<T>): Promise<KafkaConsumer> {
101
162
  topic = topicPrefix + topic
102
- return (await start_consumer(topic, groupId, callback))
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
  }