@seeka-labs/cli-apps 1.1.17 → 1.1.18

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.
@@ -100,6 +100,9 @@ This project comes ready to deploy for free to Azure functions with database bac
100
100
  ### Continuous delivery
101
101
  This template includes a GitLab CD pipeline that can be used to trigger deployments of your app when changes are pushed to your Git repository.
102
102
 
103
+ ### Configuring queues
104
+ See [this article](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue?tabs=isolated-process%2Cextensionv5%2Cextensionv3&pivots=programming-language-typescript#host-json) on settings that are settable in the `host.json` file.
105
+
103
106
  ## References
104
107
  - https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite
105
108
  - https://learn.microsoft.com/en-us/azure/storage/queues/storage-quickstart-queues-nodejs?tabs=connection-string%2Croles-azure-portal%2Cenvironment-variable-windows%2Csign-in-azure-cli
@@ -14,7 +14,16 @@
14
14
  "version": "[4.*, 5.0.0)"
15
15
  },
16
16
  "concurrency": {
17
- "dynamicConcurrencyEnabled": true,
17
+ "dynamicConcurrencyEnabled": false,
18
18
  "snapshotPersistenceEnabled": true
19
+ },
20
+ "extensions": {
21
+ "queues": {
22
+ "maxPollingInterval": "00:00:15",
23
+ "visibilityTimeout": "00:01:00",
24
+ "batchSize": 32,
25
+ "maxDequeueCount": 2,
26
+ "messageEncoding": "base64"
27
+ }
19
28
  }
20
29
  }
@@ -13,7 +13,7 @@
13
13
  "lint": "eslint",
14
14
  "build": "<packageManagerRunPrefix> clean && tsc",
15
15
  "watch": "tsc -w",
16
- "test": "yarn jest",
16
+ "test": "<packageManagerRunPrefix> jest",
17
17
  "clean": "<packageManagerRunPrefix> rimraf dist",
18
18
  "prestart": "<packageManagerRunPrefix> clean && <packageManagerRunPrefix> build",
19
19
  "dev": "<packageManagerRunPrefix> build && func start --port 7072",
@@ -15,4 +15,16 @@ const { QueueClient } = require("@azure/storage-queue");
15
15
  else {
16
16
  console.log("Queue already exists");
17
17
  }
18
+
19
+ // // https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cextensionv5&pivots=programming-language-typescript#poison-messages
20
+ var poisonClient = new QueueClient(
21
+ `UseDevelopmentStorage=true`, `${process.argv[2]}-poison`
22
+ );
23
+ const resPoison = await poisonClient.createIfNotExists();
24
+ if (resPoison.succeeded) {
25
+ console.log("Poison queue created");
26
+ }
27
+ else {
28
+ console.log("Poison queue already exists");
29
+ }
18
30
  })();
@@ -2,9 +2,12 @@ import winston from 'winston';
2
2
 
3
3
  import { app, InvocationContext } from '@azure/functions';
4
4
 
5
- import { BackgroundJobRequestContext, deserialiseQueuePayload, queueNames } from '../lib/jobs';
5
+ import { BackgroundJobRequestContext, deserialiseQueuePayload, queueNames, sendQueueMessageToPoisonQueue } from '../lib/jobs';
6
6
  import { backgroundJobLogger } from '../lib/logging';
7
7
  import { startServices } from '../lib/services';
8
+ import { groupBy } from 'lodash';
9
+ import { SeekaAppInstallState, tryGetInstallation } from '../lib/state/seeka/installations';
10
+ import { SeekaActivityAcceptedWebhookContent } from '@seeka-labs/sdk-apps-server';
8
11
 
9
12
  app.storageQueue('queueExample', {
10
13
  queueName: queueNames.queueItemExampleQueueName,
@@ -12,23 +15,53 @@ app.storageQueue('queueExample', {
12
15
  handler: queueExample
13
16
  });
14
17
 
15
- export async function queueExample(queueItem: string, context: InvocationContext): Promise<void> {
16
- const body = typeof queueItem === 'string' ? deserialiseQueuePayload<BackgroundJobRequestContext>(queueItem) : queueItem as BackgroundJobRequestContext;
18
+ export interface MyQueueItem extends BackgroundJobRequestContext {
19
+ items: SeekaActivityAcceptedWebhookContent[];
20
+ }
17
21
 
18
- const logger = backgroundJobLogger(queueNames.queueItemExampleQueueName, body, context);
22
+ export async function queueExample(queueItem: any, context: InvocationContext): Promise<void> {
23
+ const logger = backgroundJobLogger(queueNames.queueItemExampleQueueName, undefined, context);
19
24
  logger.profile(`queue.${queueNames.queueItemExampleQueueName}`)
25
+ try {
26
+ // queueItem can either be a single item or an array of items
27
+ const payload = deserialiseQueuePayload<MyQueueItem>(queueItem, logger);
20
28
 
21
- await startServices(logger);
29
+ // Group by applicationInstallId
30
+ const grouped = groupBy(payload, e => e.applicationInstallId);
22
31
 
23
- logger.verbose('Received request to trigger background job', { body });
32
+ logger.verbose('Received queue batch to handle queue message', { batchSize: payload.length });
24
33
 
25
- try {
26
- // Execute sync
27
- // await executeLongRunningTask(body, logger);
34
+ // Process each group
35
+ await startServices(logger);
36
+ for (const [applicationInstallId, items] of Object.entries(grouped)) {
37
+ if (items.length === 0) {
38
+ logger.warn('No items to process for applicationInstallId', { applicationInstallId });
39
+ continue;
40
+ }
41
+ const thisLogger = backgroundJobLogger(queueNames.queueItemExampleQueueName, items[0], context);
42
+ try {
43
+ const installation = await tryGetInstallation(applicationInstallId, true, thisLogger) as SeekaAppInstallState;
44
+
45
+ // Execute sync
46
+ // const batchItems = items.flatMap(e => e.items || []).filter(Boolean)
47
+ // await executeLongRunningTask(batchItems, logger);
48
+ }
49
+ catch (err) {
50
+ thisLogger.error('Error handling queue item to handle queue message', { ex: winston.exceptions.getAllInfo(err) })
51
+ await sendQueueMessageToPoisonQueue(queueNames.queueItemExampleQueueName, {
52
+ ...items[0],
53
+ causationId: items[0].causationId,
54
+ correlationId: context.invocationId,
55
+ rows: items.flatMap(e => e.items || []).filter(Boolean)
56
+ } as MyQueueItem, thisLogger);
57
+ }
58
+ }
28
59
  }
29
60
  catch (err) {
30
- logger.error('Error executing background job', { ex: winston.exceptions.getAllInfo(err) })
61
+ logger.error('Error handling queue item to handle queue message', { ex: winston.exceptions.getAllInfo(err) })
62
+ throw err; // Will retry based on host.json > extensions.queues.maxDequeueCount and then push to poison queue
63
+ }
64
+ finally {
65
+ logger.profile(`queue.${queueNames.queueItemExampleQueueName}`)
31
66
  }
32
-
33
- logger.profile(`queue.${queueNames.queueItemExampleQueueName}`)
34
67
  }
@@ -20,6 +20,7 @@ import {
20
20
  } from '../lib/state/seeka/installations';
21
21
 
22
22
  import type { Logger } from 'winston';
23
+ import { MyQueueItem } from './queueExample';
23
24
  app.http('seekaAppWebhook', {
24
25
  methods: ['POST'],
25
26
  authLevel: 'anonymous',
@@ -226,31 +227,11 @@ const onInstallationSettingsUpdate = async (payload: SeekaAppInstallSettingsUpda
226
227
  }
227
228
 
228
229
  const handleSeekaActivity = async (activity: SeekaActivityAcceptedWebhookPayload, logger: Logger) => {
229
- // const context = activity.context as SeekaAppWebhookContext;
230
- // const helper = SeekaAppHelper.create(process.env['SEEKA_APP_SECRET'] as string, {
231
- // organisationId: context.organisationId as string,
232
- // applicationInstallId: context.applicationInstallId as string,
233
- // applicationId: process.env['SEEKA_APP_ID'] as string,
234
- // }, { name, version }, logger);
235
-
236
- // // Append a first name to the identity
237
- // await helper.api.mergeIdentity({
238
- // seekaPId: activity.content?.personId,
239
- // firstName: [
240
- // 'firstname_' + new Date().getTime()
241
- // ]
242
- // }, {
243
- // method: 'toremove',
244
- // origin: TrackingEventSourceOriginType.Server
245
- // })
246
-
247
- // // Fire off a tracking event
248
- // await helper.api.trackActivity({
249
- // activityName: TrackingActivityNames.Custom,
250
- // activityNameCustom: 'seeka-app-activity-accepted',
251
- // activityId: 'act' + new Date().getTime(),
252
- // }, activity.content?.personId as string, {
253
- // method: 'toremove',
254
- // origin: TrackingEventSourceOriginType.Server
255
- // })
230
+ // Will be handled by queueExample
231
+ await triggerBackgroundJob(queueNames.queueItemExampleQueueName, {
232
+ ...activity.context,
233
+ causationId: activity.causationId,
234
+ correlationId: activity.requestId,
235
+ items: [activity]
236
+ } as MyQueueItem, logger)
256
237
  }
@@ -1,5 +1,7 @@
1
1
  import type { Logger } from 'winston';
2
2
  import { QueueClient } from '@azure/storage-queue';
3
+ import { isArray } from 'lodash';
4
+ import winston from 'winston';
3
5
 
4
6
  export interface BackgroundJobRequestContext {
5
7
  organisationId?: string;
@@ -30,9 +32,28 @@ const serialiseQueuePayload = (payload: unknown): string => {
30
32
  return Buffer.from(jsonString).toString('base64')
31
33
  }
32
34
 
33
- export const deserialiseQueuePayload = <TPayload>(payload: string): TPayload => {
34
- const jsonString = Buffer.from(payload, 'base64').toString()
35
- return JSON.parse(jsonString) as TPayload;
35
+ export const deserialiseQueuePayload = <TPayload>(queueItem: any, logger: Logger): TPayload[] => {
36
+ try {
37
+ if (typeof queueItem !== 'string') {
38
+ if (isArray(queueItem)) {
39
+ return queueItem
40
+ }
41
+ return [queueItem]
42
+ }
43
+ else {
44
+ const jsonString = Buffer.from(queueItem, 'base64').toString()
45
+ const parsed = JSON.parse(jsonString);
46
+
47
+ if (isArray(parsed)) {
48
+ return parsed
49
+ }
50
+ return [parsed]
51
+ }
52
+ }
53
+ catch (err) {
54
+ logger.error('Failed to deserialise queue payload', { ex: winston.exceptions.getAllInfo(err), queueItem });
55
+ throw new Error(`Failed to deserialise queue payload`);
56
+ }
36
57
  }
37
58
 
38
59
  export const triggerBackgroundJobWithQueue = async (queueClient: QueueClient, context: BackgroundJobRequestContext, logger: Logger): Promise<void> => {
@@ -51,4 +72,25 @@ export const triggerBackgroundJobWithQueue = async (queueClient: QueueClient, co
51
72
  else {
52
73
  logger.debug("Background job triggered", { body, messageId: response.messageId, context })
53
74
  }
75
+ }
76
+
77
+ export const sendQueueMessageToPoisonQueue = async (queueName: string, context: BackgroundJobRequestContext, logger: Logger): Promise<void> => {
78
+ const body = {
79
+ ...context
80
+ }
81
+ const bodyStr = serialiseQueuePayload(body);
82
+
83
+ // https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cextensionv5&pivots=programming-language-typescript#poison-messages
84
+ const queueClient = new QueueClient(process.env.AzureWebJobsStorage as string, `${queueName}-poison`);
85
+
86
+ const response = await queueClient.sendMessage(bodyStr, { messageTimeToLive: -1 });
87
+
88
+ if (response.errorCode) {
89
+ const { requestId, date, errorCode } = response;
90
+ logger.error("Failed to push to poison queue", { body, requestId, date, errorCode })
91
+ throw new Error(`Failed to push to poison queue: ${response.errorCode}`);
92
+ }
93
+ else {
94
+ logger.verbose("Message pushed to poison queue", { body, messageId: response.messageId, context })
95
+ }
54
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seeka-labs/cli-apps",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
4
4
  "description": "Seeka - Apps CLI",
5
5
  "author": "SEEKA <platform@seeka.co>",
6
6
  "license": "MIT",