@forge/events 1.1.0-next.0 → 2.0.0-next.2
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/CHANGELOG.md +14 -0
- package/README.md +32 -26
- package/out/__test__/jobProgress.test.js +15 -16
- package/out/__test__/queue.test.js +89 -40
- package/out/__test__/utils.js +1 -1
- package/out/errors.d.ts +17 -19
- package/out/errors.d.ts.map +1 -1
- package/out/errors.js +21 -34
- package/out/index.d.ts +2 -1
- package/out/index.d.ts.map +1 -1
- package/out/jobProgress.d.ts +11 -4
- package/out/jobProgress.d.ts.map +1 -1
- package/out/jobProgress.js +9 -9
- package/out/queue.d.ts +2 -2
- package/out/queue.d.ts.map +1 -1
- package/out/queue.js +8 -13
- package/out/text.d.ts +2 -0
- package/out/text.d.ts.map +1 -1
- package/out/text.js +2 -0
- package/out/types.d.ts +24 -9
- package/out/types.d.ts.map +1 -1
- package/out/validators.d.ts +4 -4
- package/out/validators.d.ts.map +1 -1
- package/out/validators.js +22 -11
- package/package.json +1 -1
- package/src/__test__/jobProgress.test.ts +16 -16
- package/src/__test__/queue.test.ts +103 -44
- package/src/__test__/utils.ts +1 -1
- package/src/errors.ts +20 -43
- package/src/index.ts +2 -1
- package/src/jobProgress.ts +17 -12
- package/src/queue.ts +10 -16
- package/src/text.ts +2 -0
- package/src/types.ts +30 -7
- package/src/validators.ts +27 -10
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
InternalServerError,
|
|
3
3
|
InvalidQueueNameError,
|
|
4
|
+
InvalidPayloadError,
|
|
4
5
|
NoEventsToPushError,
|
|
5
6
|
PartialSuccessError,
|
|
6
7
|
PayloadTooBigError,
|
|
@@ -30,21 +31,57 @@ describe('Queue methods', () => {
|
|
|
30
31
|
});
|
|
31
32
|
});
|
|
32
33
|
|
|
34
|
+
const PAYLOAD = { body: { page: 1 } } as const;
|
|
35
|
+
|
|
33
36
|
describe('push', () => {
|
|
34
|
-
it(
|
|
37
|
+
it.each([
|
|
38
|
+
['single payload', PAYLOAD, [PAYLOAD]],
|
|
39
|
+
['payload array', [PAYLOAD, PAYLOAD], [PAYLOAD, PAYLOAD]]
|
|
40
|
+
])('should call the queue/publish endpoint when given %s', async (_, payload, expectedPayload) => {
|
|
41
|
+
const apiClientMock = getMockFetchMethod();
|
|
42
|
+
const queue = getQueue('name', apiClientMock);
|
|
43
|
+
await queue.push(payload);
|
|
44
|
+
verifyApiClientCalledPushPathWith(apiClientMock, expectedPayload, 'name');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it.each([
|
|
48
|
+
['number', 1],
|
|
49
|
+
['string', 'test'],
|
|
50
|
+
['boolean', true],
|
|
51
|
+
['array', [1, 2, 3]],
|
|
52
|
+
['null', null],
|
|
53
|
+
['undefined', undefined],
|
|
54
|
+
['array with non-object', [1]],
|
|
55
|
+
['array with an array', [[2, 3]]]
|
|
56
|
+
])('rejects non-object (%s) event', async (_, payload) => {
|
|
57
|
+
const apiClientMock = getMockFetchMethod();
|
|
58
|
+
const queue = getQueue('name', apiClientMock);
|
|
59
|
+
// @ts-expect-error Testing invalid payload type on purpose
|
|
60
|
+
await expect(queue.push(payload)).rejects.toThrow(new InvalidPayloadError(`Event must be an object.`));
|
|
61
|
+
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it.each([
|
|
65
|
+
['number', 1],
|
|
66
|
+
['string', 'test'],
|
|
67
|
+
['boolean', true],
|
|
68
|
+
['array', [1, 2, 3]],
|
|
69
|
+
['null', null],
|
|
70
|
+
['undefined', undefined],
|
|
71
|
+
['array with non-object', [1]],
|
|
72
|
+
['array with an array', [[2, 3]]]
|
|
73
|
+
])('rejects non-object (%s) event body', async (_, body) => {
|
|
35
74
|
const apiClientMock = getMockFetchMethod();
|
|
36
75
|
const queue = getQueue('name', apiClientMock);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await queue.push([payload]);
|
|
41
|
-
verifyApiClientCalledPushPathWith(apiClientMock, [payload], 'name');
|
|
76
|
+
// @ts-expect-error Testing invalid payload type on purpose
|
|
77
|
+
await expect(queue.push({ body })).rejects.toThrow(new InvalidPayloadError(`Event body must be an object.`));
|
|
78
|
+
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
42
79
|
});
|
|
43
80
|
|
|
44
81
|
it('should throw InvalidPushSettingsError for delay', async () => {
|
|
45
82
|
const apiClientMock = getMockFetchMethod();
|
|
46
83
|
const queue = getQueue('name', apiClientMock);
|
|
47
|
-
await expect(queue.push(
|
|
84
|
+
await expect(queue.push({ ...PAYLOAD, delayInSeconds: 901 })).rejects.toThrow(
|
|
48
85
|
new InvalidPushSettingsError(`The delayInSeconds setting must be between 0 and 900.`)
|
|
49
86
|
);
|
|
50
87
|
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
@@ -60,7 +97,7 @@ describe('Queue methods', () => {
|
|
|
60
97
|
it('should throw TooManyEventsError', async () => {
|
|
61
98
|
const apiClientMock = getMockFetchMethod();
|
|
62
99
|
const queue = getQueue('name', apiClientMock);
|
|
63
|
-
await expect(queue.push(
|
|
100
|
+
await expect(queue.push(Array(51).fill(PAYLOAD))).rejects.toThrow(
|
|
64
101
|
new TooManyEventsError(`This push contains more than the 50 events allowed.`)
|
|
65
102
|
);
|
|
66
103
|
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
@@ -69,7 +106,7 @@ describe('Queue methods', () => {
|
|
|
69
106
|
it('should throw PayloadTooBigError', async () => {
|
|
70
107
|
const apiClientMock = getMockFetchMethod();
|
|
71
108
|
const queue = getQueue('name', apiClientMock);
|
|
72
|
-
await expect(queue.push('x'.repeat(201 * 1024))).rejects.toThrow(
|
|
109
|
+
await expect(queue.push({ body: { content: 'x'.repeat(201 * 1024) } })).rejects.toThrow(
|
|
73
110
|
new PayloadTooBigError(`The maximum payload size is 200KB.`)
|
|
74
111
|
);
|
|
75
112
|
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
@@ -78,19 +115,17 @@ describe('Queue methods', () => {
|
|
|
78
115
|
it('should throw RateLimitError', async () => {
|
|
79
116
|
const apiClientMock = getMockFetchMethod({}, 429);
|
|
80
117
|
const queue = getQueue('name', apiClientMock);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
verifyApiClientCalledPushPathWith(apiClientMock, payload, 'name');
|
|
118
|
+
await expect(queue.push(PAYLOAD)).rejects.toThrow(new RateLimitError(`Too many requests.`));
|
|
119
|
+
verifyApiClientCalledPushPathWith(apiClientMock, [PAYLOAD], 'name');
|
|
84
120
|
});
|
|
85
121
|
|
|
86
122
|
it('should throw InvocationLimitReachedError', async () => {
|
|
87
123
|
const apiClientMock = getMockFetchMethod({}, 405);
|
|
88
124
|
const queue = getQueue('name', apiClientMock);
|
|
89
|
-
|
|
90
|
-
await expect(queue.push(payload)).rejects.toThrow(
|
|
125
|
+
await expect(queue.push(PAYLOAD)).rejects.toThrow(
|
|
91
126
|
new InvocationLimitReachedError(`The limit on cyclic invocation has been reached.`)
|
|
92
127
|
);
|
|
93
|
-
verifyApiClientCalledPushPathWith(apiClientMock,
|
|
128
|
+
verifyApiClientCalledPushPathWith(apiClientMock, [PAYLOAD], 'name');
|
|
94
129
|
});
|
|
95
130
|
|
|
96
131
|
it('should throw PartialSuccessError when there are failed events', async () => {
|
|
@@ -112,27 +147,37 @@ describe('Queue methods', () => {
|
|
|
112
147
|
const queue = getQueue('name', apiClientMock);
|
|
113
148
|
const payload = [
|
|
114
149
|
{
|
|
115
|
-
|
|
150
|
+
body: {
|
|
151
|
+
content: 'payload-1'
|
|
152
|
+
}
|
|
116
153
|
},
|
|
117
154
|
{
|
|
118
|
-
|
|
155
|
+
body: {
|
|
156
|
+
content: 'payload-2'
|
|
157
|
+
}
|
|
119
158
|
},
|
|
120
159
|
{
|
|
121
|
-
|
|
160
|
+
body: {
|
|
161
|
+
content: 'payload-3'
|
|
162
|
+
}
|
|
122
163
|
}
|
|
123
164
|
];
|
|
124
165
|
await expect(queue.push(payload)).rejects.toThrow(
|
|
125
|
-
new PartialSuccessError(`Failed to process 2 event(s).`, [
|
|
166
|
+
new PartialSuccessError(`Failed to process 2 event(s).`, { jobId: 'some-job-id' }, [
|
|
126
167
|
{
|
|
127
168
|
errorMessage: 'failed-1',
|
|
128
|
-
|
|
129
|
-
|
|
169
|
+
event: {
|
|
170
|
+
body: {
|
|
171
|
+
content: 'payload-1'
|
|
172
|
+
}
|
|
130
173
|
}
|
|
131
174
|
},
|
|
132
175
|
{
|
|
133
176
|
errorMessage: 'failed-3',
|
|
134
|
-
|
|
135
|
-
|
|
177
|
+
event: {
|
|
178
|
+
body: {
|
|
179
|
+
content: 'payload-3'
|
|
180
|
+
}
|
|
136
181
|
}
|
|
137
182
|
}
|
|
138
183
|
])
|
|
@@ -150,17 +195,23 @@ describe('Queue methods', () => {
|
|
|
150
195
|
const queue = getQueue('name', apiClientMock);
|
|
151
196
|
const payload = [
|
|
152
197
|
{
|
|
153
|
-
|
|
198
|
+
body: {
|
|
199
|
+
content: 'payload-1'
|
|
200
|
+
}
|
|
154
201
|
},
|
|
155
202
|
{
|
|
156
|
-
|
|
203
|
+
body: {
|
|
204
|
+
content: 'payload-2'
|
|
205
|
+
}
|
|
157
206
|
},
|
|
158
207
|
{
|
|
159
|
-
|
|
208
|
+
body: {
|
|
209
|
+
content: 'payload-3'
|
|
210
|
+
}
|
|
160
211
|
}
|
|
161
212
|
];
|
|
162
213
|
await expect(queue.push(payload)).rejects.toThrow(
|
|
163
|
-
new PartialSuccessError(`Failed to create stats for job name#12345`, [])
|
|
214
|
+
new PartialSuccessError(`Failed to create stats for job name#12345`, { jobId: 'some-job-id' }, [])
|
|
164
215
|
);
|
|
165
216
|
verifyApiClientCalledPushPathWith(apiClientMock, payload, 'name');
|
|
166
217
|
});
|
|
@@ -181,21 +232,31 @@ describe('Queue methods', () => {
|
|
|
181
232
|
const queue = getQueue('name', apiClientMock);
|
|
182
233
|
const payload = [
|
|
183
234
|
{
|
|
184
|
-
|
|
235
|
+
body: {
|
|
236
|
+
content: 'payload-1'
|
|
237
|
+
}
|
|
185
238
|
},
|
|
186
239
|
{
|
|
187
|
-
|
|
240
|
+
body: {
|
|
241
|
+
content: 'payload-2'
|
|
242
|
+
}
|
|
188
243
|
}
|
|
189
244
|
];
|
|
190
245
|
await expect(queue.push(payload)).rejects.toThrow(
|
|
191
|
-
new PartialSuccessError(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
246
|
+
new PartialSuccessError(
|
|
247
|
+
`Failed to process 1 event(s). Failed to create stats for job name#12345`,
|
|
248
|
+
{ jobId: 'some-job-id' },
|
|
249
|
+
[
|
|
250
|
+
{
|
|
251
|
+
errorMessage: 'failed-1',
|
|
252
|
+
event: {
|
|
253
|
+
body: {
|
|
254
|
+
content: 'payload-1'
|
|
255
|
+
}
|
|
256
|
+
}
|
|
196
257
|
}
|
|
197
|
-
|
|
198
|
-
|
|
258
|
+
]
|
|
259
|
+
)
|
|
199
260
|
);
|
|
200
261
|
verifyApiClientCalledPushPathWith(apiClientMock, payload, 'name');
|
|
201
262
|
});
|
|
@@ -210,28 +271,26 @@ describe('Queue methods', () => {
|
|
|
210
271
|
500
|
|
211
272
|
);
|
|
212
273
|
const queue = getQueue('name', apiClientMock);
|
|
213
|
-
|
|
214
|
-
await expect(queue.push(payload)).rejects.toThrow(
|
|
274
|
+
await expect(queue.push(PAYLOAD)).rejects.toThrow(
|
|
215
275
|
new InternalServerError(
|
|
216
276
|
`500 Status Text: AWS SQS timed out`,
|
|
217
277
|
500,
|
|
218
278
|
'The request processing has failed because of an unknown error, exception or failure'
|
|
219
279
|
)
|
|
220
280
|
);
|
|
221
|
-
verifyApiClientCalledPushPathWith(apiClientMock,
|
|
281
|
+
verifyApiClientCalledPushPathWith(apiClientMock, [PAYLOAD], 'name');
|
|
222
282
|
});
|
|
223
283
|
|
|
224
284
|
it('should throw InternalServerError for error response without response body', async () => {
|
|
225
285
|
const apiClientMock = getApiClientMockWithoutResponseBody(504, 'Gateway Timeout');
|
|
226
286
|
const queue = getQueue('name', apiClientMock);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
verifyApiClientCalledPushPathWith(apiClientMock, payload, 'name');
|
|
287
|
+
await expect(queue.push(PAYLOAD)).rejects.toThrow(new InternalServerError(`504 Gateway Timeout`, 504));
|
|
288
|
+
verifyApiClientCalledPushPathWith(apiClientMock, [PAYLOAD], 'name');
|
|
230
289
|
});
|
|
231
290
|
|
|
232
291
|
it('requests stargate if no api client is provided', async () => {
|
|
233
292
|
const queue = getQueue('queue');
|
|
234
|
-
await queue.push({ test: 'stargate' });
|
|
293
|
+
await queue.push({ body: { test: 'stargate' } });
|
|
235
294
|
expect(__requestAtlassianAsApp).toHaveBeenCalledTimes(1);
|
|
236
295
|
});
|
|
237
296
|
});
|
|
@@ -241,7 +300,7 @@ describe('Queue methods', () => {
|
|
|
241
300
|
const apiClientMock = getMockFetchMethod();
|
|
242
301
|
const queue = getQueue('name', apiClientMock);
|
|
243
302
|
const jobProgress = queue.getJob('test-job-id');
|
|
244
|
-
expect(jobProgress).toEqual(new JobProgress('test-job-id', apiClientMock));
|
|
303
|
+
expect(jobProgress).toEqual(new JobProgress({ key: 'name' }, 'test-job-id', apiClientMock));
|
|
245
304
|
});
|
|
246
305
|
});
|
|
247
306
|
});
|
package/src/__test__/utils.ts
CHANGED
|
@@ -40,7 +40,7 @@ export const verifyApiClientCalledPushPathWith = (
|
|
|
40
40
|
) => {
|
|
41
41
|
verifyApiClientCalledWith(apiClientMock, '/webhook/queue/publish/{contextAri}/{environmentId}/{appId}/{appVersion}', {
|
|
42
42
|
queueName: name,
|
|
43
|
-
schema: 'ari:cloud:ecosystem::forge/app-event',
|
|
43
|
+
schema: 'ari:cloud:ecosystem::forge/app-event-2',
|
|
44
44
|
type: 'avi:forge:app:event',
|
|
45
45
|
payload: payloads
|
|
46
46
|
});
|
package/src/errors.ts
CHANGED
|
@@ -1,51 +1,36 @@
|
|
|
1
|
-
import { FailedEvent } from './types';
|
|
1
|
+
import { FailedEvent, PushResult } from './types';
|
|
2
2
|
|
|
3
|
-
export class
|
|
3
|
+
export class EventsError extends Error {
|
|
4
4
|
constructor(message: string) {
|
|
5
5
|
super(message);
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export class
|
|
10
|
-
constructor(message: string) {
|
|
11
|
-
super(message);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
9
|
+
export class InvalidPushSettingsError extends EventsError {}
|
|
14
10
|
|
|
15
|
-
export class
|
|
16
|
-
constructor(message: string) {
|
|
17
|
-
super(message);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
11
|
+
export class InvalidQueueNameError extends EventsError {}
|
|
20
12
|
|
|
21
|
-
export class
|
|
22
|
-
constructor(message: string) {
|
|
23
|
-
super(message);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
13
|
+
export class InvalidPayloadError extends EventsError {}
|
|
26
14
|
|
|
27
|
-
export class
|
|
28
|
-
constructor(message: string) {
|
|
29
|
-
super(message);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
15
|
+
export class TooManyEventsError extends EventsError {}
|
|
32
16
|
|
|
33
|
-
export class
|
|
34
|
-
constructor(message: string) {
|
|
35
|
-
super(message);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
17
|
+
export class PayloadTooBigError extends EventsError {}
|
|
38
18
|
|
|
39
|
-
export class
|
|
40
|
-
|
|
19
|
+
export class NoEventsToPushError extends EventsError {}
|
|
20
|
+
|
|
21
|
+
export class RateLimitError extends EventsError {}
|
|
22
|
+
|
|
23
|
+
export class PartialSuccessError extends EventsError {
|
|
24
|
+
constructor(
|
|
25
|
+
message: string,
|
|
26
|
+
public result: PushResult,
|
|
27
|
+
public failedEvents: FailedEvent[]
|
|
28
|
+
) {
|
|
41
29
|
super(message);
|
|
42
|
-
this.failedEvents = failedEvents;
|
|
43
30
|
}
|
|
44
|
-
|
|
45
|
-
failedEvents: FailedEvent[];
|
|
46
31
|
}
|
|
47
32
|
|
|
48
|
-
export class InternalServerError extends
|
|
33
|
+
export class InternalServerError extends EventsError {
|
|
49
34
|
constructor(message: string, errorCode?: number, details?: string) {
|
|
50
35
|
super(message);
|
|
51
36
|
this.errorCode = errorCode;
|
|
@@ -56,14 +41,6 @@ export class InternalServerError extends Error {
|
|
|
56
41
|
details?: string;
|
|
57
42
|
}
|
|
58
43
|
|
|
59
|
-
export class JobDoesNotExistError extends
|
|
60
|
-
constructor(message: string) {
|
|
61
|
-
super(message);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
44
|
+
export class JobDoesNotExistError extends EventsError {}
|
|
64
45
|
|
|
65
|
-
export class InvocationLimitReachedError extends
|
|
66
|
-
constructor(message: string) {
|
|
67
|
-
super(message);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
46
|
+
export class InvocationLimitReachedError extends EventsError {}
|
package/src/index.ts
CHANGED
|
@@ -11,11 +11,12 @@ export {
|
|
|
11
11
|
InvalidPushSettingsError,
|
|
12
12
|
InvocationLimitReachedError
|
|
13
13
|
} from './errors';
|
|
14
|
-
export { JobProgress } from './jobProgress';
|
|
14
|
+
export { JobProgress, JobStats } from './jobProgress';
|
|
15
15
|
export { QueueResponse } from './queueResponse';
|
|
16
16
|
export { InvocationError } from './invocationError';
|
|
17
17
|
export { InvocationErrorCode } from './invocationErrorCode';
|
|
18
18
|
export { RetryOptions } from './retryOptions';
|
|
19
|
+
export { AsyncEvent, PushEvent, PushResult } from './types';
|
|
19
20
|
export {
|
|
20
21
|
appEvents,
|
|
21
22
|
AppEvent,
|
package/src/jobProgress.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __requestAtlassianAsApp, FetchMethod
|
|
1
|
+
import { __requestAtlassianAsApp, FetchMethod } from '@forge/api';
|
|
2
2
|
import { CANCEL_JOB_PATH, GET_STATS_PATH, post } from './queries';
|
|
3
3
|
import {
|
|
4
4
|
validateCancelJobAPIResponse,
|
|
@@ -6,19 +6,25 @@ import {
|
|
|
6
6
|
validateGetStatsAPIResponse,
|
|
7
7
|
validateGetStatsPayload
|
|
8
8
|
} from './validators';
|
|
9
|
-
import { CancelJobRequest, GetStatsRequest } from './types';
|
|
9
|
+
import { CancelJobRequest, GetStatsRequest, QueueParams } from './types';
|
|
10
|
+
|
|
11
|
+
export type JobStats = {
|
|
12
|
+
success: number;
|
|
13
|
+
inProgress: number;
|
|
14
|
+
failed: number;
|
|
15
|
+
};
|
|
10
16
|
|
|
11
17
|
export class JobProgress {
|
|
12
18
|
constructor(
|
|
19
|
+
private readonly queueParams: QueueParams,
|
|
13
20
|
private readonly id: string,
|
|
14
21
|
private readonly apiClient: FetchMethod = __requestAtlassianAsApp
|
|
15
22
|
) {}
|
|
16
23
|
|
|
17
|
-
async getStats(): Promise<
|
|
18
|
-
const [queueName, jobId] = this.id.split('#');
|
|
24
|
+
async getStats(): Promise<JobStats> {
|
|
19
25
|
const getStatsRequest: GetStatsRequest = {
|
|
20
|
-
queueName:
|
|
21
|
-
jobId:
|
|
26
|
+
queueName: this.queueParams.key,
|
|
27
|
+
jobId: this.id,
|
|
22
28
|
time: new Date().toISOString()
|
|
23
29
|
};
|
|
24
30
|
|
|
@@ -26,14 +32,14 @@ export class JobProgress {
|
|
|
26
32
|
|
|
27
33
|
const response = await post(GET_STATS_PATH, getStatsRequest, this.apiClient);
|
|
28
34
|
await validateGetStatsAPIResponse(response, getStatsRequest);
|
|
29
|
-
|
|
35
|
+
const { success, inProgress, failed } = await response.json();
|
|
36
|
+
return { success, inProgress, failed };
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
async cancel(): Promise<
|
|
33
|
-
const [queueName, jobId] = this.id.split('#');
|
|
39
|
+
async cancel(): Promise<void> {
|
|
34
40
|
const cancelJobRequest: CancelJobRequest = {
|
|
35
|
-
queueName:
|
|
36
|
-
jobId:
|
|
41
|
+
queueName: this.queueParams.key,
|
|
42
|
+
jobId: this.id,
|
|
37
43
|
time: new Date().toISOString()
|
|
38
44
|
};
|
|
39
45
|
|
|
@@ -41,6 +47,5 @@ export class JobProgress {
|
|
|
41
47
|
|
|
42
48
|
const response = await post(CANCEL_JOB_PATH, cancelJobRequest, this.apiClient);
|
|
43
49
|
await validateCancelJobAPIResponse(response, cancelJobRequest);
|
|
44
|
-
return response;
|
|
45
50
|
}
|
|
46
51
|
}
|
package/src/queue.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FetchMethod, __requestAtlassianAsApp } from '@forge/api';
|
|
2
2
|
import { PUSH_PATH, post } from './queries';
|
|
3
|
-
import { validatePushAPIResponse,
|
|
4
|
-
import {
|
|
3
|
+
import { validatePushAPIResponse, validatePushEvents, validateQueueKey } from './validators';
|
|
4
|
+
import { QueueParams, PushEvent, PushRequest, PushResult } from './types';
|
|
5
5
|
import { v4 as uuid } from 'uuid';
|
|
6
6
|
import { JobProgress } from './jobProgress';
|
|
7
7
|
|
|
@@ -13,8 +13,8 @@ export class Queue {
|
|
|
13
13
|
validateQueueKey(this.queueParams.key);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
async push(
|
|
17
|
-
|
|
16
|
+
async push(events: PushEvent | PushEvent[]): Promise<PushResult> {
|
|
17
|
+
const validEvents = validatePushEvents(events);
|
|
18
18
|
const queueName = this.queueParams.key;
|
|
19
19
|
const jobId = uuid();
|
|
20
20
|
|
|
@@ -22,24 +22,18 @@ export class Queue {
|
|
|
22
22
|
queueName: queueName,
|
|
23
23
|
jobId: jobId,
|
|
24
24
|
type: 'avi:forge:app:event',
|
|
25
|
-
schema: 'ari:cloud:ecosystem::forge/app-event',
|
|
26
|
-
payload:
|
|
25
|
+
schema: 'ari:cloud:ecosystem::forge/app-event-2',
|
|
26
|
+
payload: validEvents,
|
|
27
27
|
time: new Date().toISOString()
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
if (pushSettings) {
|
|
31
|
-
validatePushSettings(pushSettings);
|
|
32
|
-
if (pushSettings.delayInSeconds) {
|
|
33
|
-
pushRequest.delayInSeconds = pushSettings.delayInSeconds;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
30
|
const response = await post(PUSH_PATH, pushRequest, this.apiClient);
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
const result = { jobId };
|
|
32
|
+
await validatePushAPIResponse(pushRequest, response, result);
|
|
33
|
+
return result;
|
|
40
34
|
}
|
|
41
35
|
|
|
42
36
|
getJob(jobId: string): JobProgress {
|
|
43
|
-
return new JobProgress(jobId, this.apiClient);
|
|
37
|
+
return new JobProgress(this.queueParams, jobId, this.apiClient);
|
|
44
38
|
}
|
|
45
39
|
}
|
package/src/text.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export const Text = {
|
|
2
2
|
error: {
|
|
3
3
|
invalidQueueName: `Queue names can only contain alphanumeric characters, dashes and underscores.`,
|
|
4
|
+
invalidEvent: `Event must be an object.`,
|
|
5
|
+
invalidEventBody: `Event body must be an object.`,
|
|
4
6
|
invalidDelayInSecondsSetting: `The delayInSeconds setting must be between 0 and 900.`,
|
|
5
7
|
maxEventsAllowed: (maxEventsCount: number): string =>
|
|
6
8
|
`This push contains more than the ${maxEventsCount} events allowed.`,
|
package/src/types.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import { InvocationErrorCode } from './invocationErrorCode';
|
|
2
|
+
|
|
3
|
+
export type Payload = Record<string, unknown>;
|
|
2
4
|
|
|
3
|
-
export interface PushSettings {
|
|
4
|
-
delayInSeconds: number;
|
|
5
|
-
}
|
|
6
5
|
export interface QueueParams {
|
|
7
6
|
key: string;
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
export interface PushRequest extends APIRequest {
|
|
11
|
-
payload:
|
|
10
|
+
payload: PushEvent[];
|
|
12
11
|
schema: string;
|
|
13
12
|
type: string;
|
|
14
|
-
|
|
13
|
+
jobId: string;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export interface APIRequest {
|
|
@@ -20,9 +19,33 @@ export interface APIRequest {
|
|
|
20
19
|
time: string;
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
export interface PushEvent {
|
|
23
|
+
body: Body;
|
|
24
|
+
delayInSeconds?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Body = Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
export interface PushResult {
|
|
30
|
+
jobId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
export interface FailedEvent {
|
|
24
34
|
errorMessage: string;
|
|
25
|
-
|
|
35
|
+
event: PushEvent;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AsyncEvent extends PushEvent {
|
|
39
|
+
queueName: string;
|
|
40
|
+
jobId: string;
|
|
41
|
+
eventId: string;
|
|
42
|
+
retryContext?: RetryContext;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RetryContext {
|
|
46
|
+
retryCount: number;
|
|
47
|
+
retryReason: InvocationErrorCode;
|
|
48
|
+
retryData: any;
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
export type GetStatsRequest = APIRequest;
|
package/src/validators.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
InternalServerError,
|
|
4
4
|
InvalidQueueNameError,
|
|
5
5
|
JobDoesNotExistError,
|
|
6
|
+
InvalidPayloadError,
|
|
6
7
|
NoEventsToPushError,
|
|
7
8
|
PartialSuccessError,
|
|
8
9
|
PayloadTooBigError,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
InvocationLimitReachedError
|
|
13
14
|
} from './errors';
|
|
14
15
|
import { Text } from './text';
|
|
15
|
-
import { CancelJobRequest, GetStatsRequest,
|
|
16
|
+
import { CancelJobRequest, GetStatsRequest, PushEvent, PushRequest, PushResult } from './types';
|
|
16
17
|
|
|
17
18
|
const VALID_QUEUE_NAME_PATTERN = /^[a-zA-Z0-9-_]+$/;
|
|
18
19
|
const MAXIMUM_EVENTS = 50;
|
|
@@ -24,26 +25,42 @@ export const validateQueueKey = (queueName: string) => {
|
|
|
24
25
|
}
|
|
25
26
|
};
|
|
26
27
|
|
|
27
|
-
export const validatePushSettings = (
|
|
28
|
-
if ((
|
|
28
|
+
export const validatePushSettings = (event: PushEvent) => {
|
|
29
|
+
if ((event.delayInSeconds && event.delayInSeconds > 900) || (event.delayInSeconds && event.delayInSeconds < 0)) {
|
|
29
30
|
throw new InvalidPushSettingsError(Text.error.invalidDelayInSecondsSetting);
|
|
30
31
|
}
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
export
|
|
34
|
-
|
|
34
|
+
export function validatePushEvents(arg: PushEvent | PushEvent[]): PushEvent[] {
|
|
35
|
+
const events = Array.isArray(arg) ? arg : [arg];
|
|
36
|
+
|
|
37
|
+
if (events.length === 0) {
|
|
35
38
|
throw new NoEventsToPushError(Text.error.noEventsPushed);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
if (
|
|
41
|
+
if (events.length > MAXIMUM_EVENTS) {
|
|
39
42
|
throw new TooManyEventsError(Text.error.maxEventsAllowed(MAXIMUM_EVENTS));
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
const
|
|
45
|
+
for (const event of events) {
|
|
46
|
+
if (typeof event !== 'object' || Array.isArray(event) || event === null) {
|
|
47
|
+
throw new InvalidPayloadError(Text.error.invalidEvent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof event.body !== 'object' || Array.isArray(event.body) || event.body === null) {
|
|
51
|
+
throw new InvalidPayloadError(Text.error.invalidEventBody);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
validatePushSettings(event);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const payloadSizeKB = Buffer.byteLength(JSON.stringify(events)) / 1024;
|
|
43
58
|
if (payloadSizeKB > MAXIMUM_PAYLOAD_SIZE_KB) {
|
|
44
59
|
throw new PayloadTooBigError(Text.error.maxPayloadAllowed(MAXIMUM_PAYLOAD_SIZE_KB));
|
|
45
60
|
}
|
|
46
|
-
|
|
61
|
+
|
|
62
|
+
return events;
|
|
63
|
+
}
|
|
47
64
|
|
|
48
65
|
export const validateGetStatsPayload = (getStatsRequest: GetStatsRequest) => {
|
|
49
66
|
if (!getStatsRequest.jobId) {
|
|
@@ -90,7 +107,7 @@ export const validateAPIResponse = async (response: APIResponse, expectedSuccess
|
|
|
90
107
|
}
|
|
91
108
|
};
|
|
92
109
|
|
|
93
|
-
export const validatePushAPIResponse = async (response: APIResponse,
|
|
110
|
+
export const validatePushAPIResponse = async (requestBody: PushRequest, response: APIResponse, result: PushResult) => {
|
|
94
111
|
if (response.status === 413) {
|
|
95
112
|
//Server can return this error response if it has a different max payload size and max number of events limits
|
|
96
113
|
const responseBody = await response.json();
|
|
@@ -100,7 +117,7 @@ export const validatePushAPIResponse = async (response: APIResponse, requestBody
|
|
|
100
117
|
if (response.status === 202) {
|
|
101
118
|
const responseBody = await response.json();
|
|
102
119
|
const defaultErrorMessage = 'Failed to process some events.';
|
|
103
|
-
const partialSuccessError = new PartialSuccessError(defaultErrorMessage, []);
|
|
120
|
+
const partialSuccessError = new PartialSuccessError(defaultErrorMessage, result, []);
|
|
104
121
|
|
|
105
122
|
if (responseBody.failedEvents && responseBody.failedEvents.length > 0) {
|
|
106
123
|
partialSuccessError.message = `Failed to process ${responseBody.failedEvents.length} event(s).`;
|