@forge/events 1.0.3 → 2.0.0-next.1
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__/appEvents.test.d.ts +2 -0
- package/out/__test__/appEvents.test.d.ts.map +1 -0
- package/out/__test__/appEvents.test.js +137 -0
- package/out/__test__/jobProgress.test.js +3 -5
- package/out/__test__/queue.test.js +88 -39
- package/out/__test__/utils.js +1 -1
- package/out/appEvents.d.ts +22 -0
- package/out/appEvents.d.ts.map +1 -0
- package/out/appEvents.js +50 -0
- 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 +3 -1
- package/out/index.d.ts.map +1 -1
- package/out/index.js +3 -1
- package/out/jobProgress.d.ts +8 -3
- package/out/jobProgress.d.ts.map +1 -1
- package/out/jobProgress.js +2 -2
- package/out/queries.d.ts +1 -2
- package/out/queries.d.ts.map +1 -1
- package/out/queue.d.ts +2 -2
- package/out/queue.d.ts.map +1 -1
- package/out/queue.js +7 -12
- 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__/appEvents.test.ts +201 -0
- package/src/__test__/jobProgress.test.ts +3 -5
- package/src/__test__/queue.test.ts +102 -43
- package/src/__test__/utils.ts +1 -1
- package/src/appEvents.ts +107 -0
- package/src/errors.ts +20 -43
- package/src/index.ts +11 -1
- package/src/jobProgress.ts +11 -5
- package/src/queries.ts +2 -2
- package/src/queue.ts +9 -15
- package/src/text.ts +2 -0
- package/src/types.ts +30 -7
- package/src/validators.ts +27 -10
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -4,7 +4,7 @@ import { JobProgress } from '../jobProgress';
|
|
|
4
4
|
import { __requestAtlassianAsApp } from '@forge/api';
|
|
5
5
|
|
|
6
6
|
jest.mock('@forge/api', () => ({
|
|
7
|
-
__requestAtlassianAsApp: getMockFetchMethod(
|
|
7
|
+
__requestAtlassianAsApp: getMockFetchMethod({ done: true }, 200)
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
10
|
const getJobProgress = (jobId: string, apiClientMock?: any) => new JobProgress(jobId, apiClientMock);
|
|
@@ -21,8 +21,7 @@ describe('JobProgress methods', () => {
|
|
|
21
21
|
200
|
|
22
22
|
);
|
|
23
23
|
const jobProgress = getJobProgress('test-queue-name#test-job-id', apiClientMock);
|
|
24
|
-
const
|
|
25
|
-
const { success, inProgress, failed } = await response.json();
|
|
24
|
+
const { success, inProgress, failed } = await jobProgress.getStats();
|
|
26
25
|
verifyApiClientCalledWith(
|
|
27
26
|
apiClientMock,
|
|
28
27
|
'/webhook/queue/stats/{contextAri}/{environmentId}/{appId}/{appVersion}',
|
|
@@ -100,7 +99,7 @@ describe('JobProgress methods', () => {
|
|
|
100
99
|
it('should call the queue/cancel endpoint', async () => {
|
|
101
100
|
const apiClientMock = getMockFetchMethod({}, 204);
|
|
102
101
|
const jobProgress = getJobProgress('test-queue-name#test-job-id', apiClientMock);
|
|
103
|
-
|
|
102
|
+
await jobProgress.cancel();
|
|
104
103
|
verifyApiClientCalledWith(
|
|
105
104
|
apiClientMock,
|
|
106
105
|
'/webhook/queue/cancel/{contextAri}/{environmentId}/{appId}/{appVersion}',
|
|
@@ -109,7 +108,6 @@ describe('JobProgress methods', () => {
|
|
|
109
108
|
jobId: 'test-job-id'
|
|
110
109
|
}
|
|
111
110
|
);
|
|
112
|
-
expect(response.status).toEqual(204);
|
|
113
111
|
});
|
|
114
112
|
|
|
115
113
|
it('should throw JobDoesNotExistError', async () => {
|
|
@@ -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
|
});
|
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/appEvents.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { post } from './queries';
|
|
2
|
+
import { __requestAtlassianAsApp } from '@forge/api';
|
|
3
|
+
|
|
4
|
+
export interface AppEvent {
|
|
5
|
+
key: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type AppEventPublishResult = AppEventPublishSuccess | AppEventPublishError;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents the successful result of the publishing operation.
|
|
12
|
+
*
|
|
13
|
+
* It may happen that publishing some of the events fails even though the request was generally successful.
|
|
14
|
+
* Inspect the `failedEvents` field to see which events failed to be published and why
|
|
15
|
+
* (if the list is empty then the operation was fully successful).
|
|
16
|
+
*/
|
|
17
|
+
export type AppEventPublishSuccess = {
|
|
18
|
+
type: 'success';
|
|
19
|
+
/** A list of events that failed to be published along with error messages. */
|
|
20
|
+
failedEvents: AppEventPublishFailure[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A publishing failure of a specific event.
|
|
25
|
+
*/
|
|
26
|
+
export interface AppEventPublishFailure {
|
|
27
|
+
event: AppEvent;
|
|
28
|
+
errorMessage: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A general error of the event publishing operation.
|
|
33
|
+
* No events were published if this is returned.
|
|
34
|
+
*/
|
|
35
|
+
export interface AppEventPublishError {
|
|
36
|
+
type: 'error';
|
|
37
|
+
errorType: AppEventErrorType;
|
|
38
|
+
errorMessage: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type AppEventErrorType =
|
|
42
|
+
| 'VALIDATION_ERROR'
|
|
43
|
+
| 'AUTHENTICATION_ERROR'
|
|
44
|
+
| 'AUTHORIZATION_ERROR'
|
|
45
|
+
| 'RATE_LIMIT'
|
|
46
|
+
| 'SERVICE_ERROR'
|
|
47
|
+
| 'SERVICE_UNAVAILABLE'
|
|
48
|
+
| `OTHER`;
|
|
49
|
+
|
|
50
|
+
interface AppEventErrorResponse {
|
|
51
|
+
errorMessages: string[];
|
|
52
|
+
errors: Map<string, string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface AppEventSuccessResponse {
|
|
56
|
+
failedEvents: AppEventPublishFailure[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const errorTypes: Record<number, AppEventErrorType> = {
|
|
60
|
+
400: 'VALIDATION_ERROR',
|
|
61
|
+
401: 'AUTHENTICATION_ERROR',
|
|
62
|
+
403: 'AUTHORIZATION_ERROR',
|
|
63
|
+
429: 'RATE_LIMIT',
|
|
64
|
+
500: 'SERVICE_ERROR',
|
|
65
|
+
503: 'SERVICE_UNAVAILABLE'
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const endpoint = '/forge/events/v1/app-events';
|
|
69
|
+
|
|
70
|
+
export const appEvents = {
|
|
71
|
+
async publish(events: AppEvent | AppEvent[]): Promise<AppEventPublishResult> {
|
|
72
|
+
const eventsArray = Array.isArray(events) ? events : [events];
|
|
73
|
+
const body = {
|
|
74
|
+
events: eventsArray.map((e) => ({
|
|
75
|
+
key: e.key
|
|
76
|
+
}))
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const response = await post(endpoint, body, __requestAtlassianAsApp);
|
|
80
|
+
const responseBody = await response.json();
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
return {
|
|
84
|
+
type: 'error',
|
|
85
|
+
errorType: errorTypes[response.status] ?? 'OTHER',
|
|
86
|
+
errorMessage: getErrorMessage(responseBody)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
type: 'success',
|
|
92
|
+
failedEvents: (responseBody as AppEventSuccessResponse).failedEvents ?? []
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function getErrorMessage(responseBody: AppEventErrorResponse) {
|
|
98
|
+
if (responseBody.errorMessages && responseBody.errorMessages.length > 0) {
|
|
99
|
+
return responseBody.errorMessages.join(', ');
|
|
100
|
+
} else if (responseBody.errors) {
|
|
101
|
+
return Object.entries(responseBody.errors)
|
|
102
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
103
|
+
.join(', ');
|
|
104
|
+
} else {
|
|
105
|
+
return JSON.stringify(responseBody);
|
|
106
|
+
}
|
|
107
|
+
}
|
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,8 +11,18 @@ 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';
|
|
20
|
+
export {
|
|
21
|
+
appEvents,
|
|
22
|
+
AppEvent,
|
|
23
|
+
AppEventPublishResult,
|
|
24
|
+
AppEventPublishSuccess,
|
|
25
|
+
AppEventPublishFailure,
|
|
26
|
+
AppEventPublishError,
|
|
27
|
+
AppEventErrorType
|
|
28
|
+
} from './appEvents';
|
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,
|
|
@@ -8,13 +8,19 @@ import {
|
|
|
8
8
|
} from './validators';
|
|
9
9
|
import { CancelJobRequest, GetStatsRequest } from './types';
|
|
10
10
|
|
|
11
|
+
export type JobStats = {
|
|
12
|
+
success: number;
|
|
13
|
+
inProgress: number;
|
|
14
|
+
failed: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
11
17
|
export class JobProgress {
|
|
12
18
|
constructor(
|
|
13
19
|
private readonly id: string,
|
|
14
20
|
private readonly apiClient: FetchMethod = __requestAtlassianAsApp
|
|
15
21
|
) {}
|
|
16
22
|
|
|
17
|
-
async getStats(): Promise<
|
|
23
|
+
async getStats(): Promise<JobStats> {
|
|
18
24
|
const [queueName, jobId] = this.id.split('#');
|
|
19
25
|
const getStatsRequest: GetStatsRequest = {
|
|
20
26
|
queueName: queueName,
|
|
@@ -26,10 +32,11 @@ 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<
|
|
39
|
+
async cancel(): Promise<void> {
|
|
33
40
|
const [queueName, jobId] = this.id.split('#');
|
|
34
41
|
const cancelJobRequest: CancelJobRequest = {
|
|
35
42
|
queueName: queueName,
|
|
@@ -41,6 +48,5 @@ export class JobProgress {
|
|
|
41
48
|
|
|
42
49
|
const response = await post(CANCEL_JOB_PATH, cancelJobRequest, this.apiClient);
|
|
43
50
|
await validateCancelJobAPIResponse(response, cancelJobRequest);
|
|
44
|
-
return response;
|
|
45
51
|
}
|
|
46
52
|
}
|
package/src/queries.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { APIResponse, FetchMethod } from '@forge/api';
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
export const PUSH_PATH = '/webhook/queue/publish/{contextAri}/{environmentId}/{appId}/{appVersion}';
|
|
4
4
|
export const GET_STATS_PATH = '/webhook/queue/stats/{contextAri}/{environmentId}/{appId}/{appVersion}';
|
|
5
5
|
export const CANCEL_JOB_PATH = '/webhook/queue/cancel/{contextAri}/{environmentId}/{appId}/{appVersion}';
|
|
6
6
|
|
|
7
|
-
export const post = async (endpoint: string, body:
|
|
7
|
+
export const post = async (endpoint: string, body: unknown, apiClient: FetchMethod): Promise<APIResponse> => {
|
|
8
8
|
const request = {
|
|
9
9
|
method: 'POST',
|
|
10
10
|
body: JSON.stringify(body),
|
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,21 +22,15 @@ 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 {
|
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.`,
|