@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.
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/init-templates/azure-function/README.md +3 -0
- package/dist/init-templates/azure-function/host.json +10 -1
- package/dist/init-templates/azure-function/package.json +1 -1
- package/dist/init-templates/azure-function/scripts/dev-queue-setup.js +12 -0
- package/dist/init-templates/azure-function/src/functions/queueExample.ts +45 -12
- package/dist/init-templates/azure-function/src/functions/seekaAppWebhook.ts +8 -27
- package/dist/init-templates/azure-function/src/lib/jobs/index.ts +45 -3
- package/package.json +1 -1
|
@@ -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":
|
|
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": "
|
|
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
|
|
16
|
-
|
|
18
|
+
export interface MyQueueItem extends BackgroundJobRequestContext {
|
|
19
|
+
items: SeekaActivityAcceptedWebhookContent[];
|
|
20
|
+
}
|
|
17
21
|
|
|
18
|
-
|
|
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
|
-
|
|
29
|
+
// Group by applicationInstallId
|
|
30
|
+
const grouped = groupBy(payload, e => e.applicationInstallId);
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
logger.verbose('Received queue batch to handle queue message', { batchSize: payload.length });
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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>(
|
|
34
|
-
|
|
35
|
-
|
|
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
|
}
|