@forge/events 2.0.0 → 2.0.1-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/package.json +3 -3
- package/out/__test__/appEvents.test.d.ts +0 -2
- package/out/__test__/appEvents.test.d.ts.map +0 -1
- package/out/__test__/appEvents.test.js +0 -137
- package/out/__test__/invocationError.test.d.ts +0 -2
- package/out/__test__/invocationError.test.d.ts.map +0 -1
- package/out/__test__/invocationError.test.js +0 -36
- package/out/__test__/jobProgress.test.d.ts +0 -2
- package/out/__test__/jobProgress.test.d.ts.map +0 -1
- package/out/__test__/jobProgress.test.js +0 -118
- package/out/__test__/queue.test.d.ts +0 -2
- package/out/__test__/queue.test.d.ts.map +0 -1
- package/out/__test__/queue.test.js +0 -237
- package/out/__test__/queueResponse.test.d.ts +0 -2
- package/out/__test__/queueResponse.test.d.ts.map +0 -1
- package/out/__test__/queueResponse.test.js +0 -14
- package/out/__test__/utils.d.ts +0 -8
- package/out/__test__/utils.d.ts.map +0 -1
- package/out/__test__/utils.js +0 -41
- package/src/__test__/appEvents.test.ts +0 -201
- package/src/__test__/invocationError.test.ts +0 -40
- package/src/__test__/jobProgress.test.ts +0 -184
- package/src/__test__/queue.test.ts +0 -306
- package/src/__test__/queueResponse.test.ts +0 -14
- package/src/__test__/utils.ts +0 -47
- package/src/appEvents.ts +0 -107
- package/src/errors.ts +0 -46
- package/src/index.ts +0 -28
- package/src/invocationError.ts +0 -22
- package/src/invocationErrorCode.ts +0 -8
- package/src/jobProgress.ts +0 -51
- package/src/queries.ts +0 -17
- package/src/queue.ts +0 -39
- package/src/queueResponse.ts +0 -13
- package/src/retryOptions.ts +0 -14
- package/src/text.ts +0 -17
- package/src/types.ts +0 -62
- package/src/validators.ts +0 -160
- package/tsconfig.json +0 -14
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
InternalServerError,
|
|
3
|
-
InvalidQueueNameError,
|
|
4
|
-
InvalidPayloadError,
|
|
5
|
-
NoEventsToPushError,
|
|
6
|
-
PartialSuccessError,
|
|
7
|
-
PayloadTooBigError,
|
|
8
|
-
RateLimitError,
|
|
9
|
-
TooManyEventsError,
|
|
10
|
-
InvalidPushSettingsError,
|
|
11
|
-
InvocationLimitReachedError
|
|
12
|
-
} from '../errors';
|
|
13
|
-
import { getMockFetchMethod, getApiClientMockWithoutResponseBody, verifyApiClientCalledPushPathWith } from './utils';
|
|
14
|
-
import { Queue } from '../queue';
|
|
15
|
-
import { JobProgress } from '../jobProgress';
|
|
16
|
-
import { __requestAtlassianAsApp } from '@forge/api';
|
|
17
|
-
|
|
18
|
-
jest.mock('@forge/api', () => ({
|
|
19
|
-
__requestAtlassianAsApp: getMockFetchMethod('done', 201)
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
const getQueue = (queueName: string, apiClientMock?: any) => new Queue({ key: queueName }, apiClientMock);
|
|
23
|
-
|
|
24
|
-
describe('Queue methods', () => {
|
|
25
|
-
describe('constructor', () => {
|
|
26
|
-
it('should throw InvalidQueueNameError', async () => {
|
|
27
|
-
const apiClientMock = getMockFetchMethod();
|
|
28
|
-
expect(() => getQueue('invalid name', apiClientMock)).toThrowError(
|
|
29
|
-
new InvalidQueueNameError('Queue names can only contain alphanumeric characters, dashes and underscores.')
|
|
30
|
-
);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const PAYLOAD = { body: { page: 1 } } as const;
|
|
35
|
-
|
|
36
|
-
describe('push', () => {
|
|
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) => {
|
|
74
|
-
const apiClientMock = getMockFetchMethod();
|
|
75
|
-
const queue = getQueue('name', apiClientMock);
|
|
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);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should throw InvalidPushSettingsError for delay', async () => {
|
|
82
|
-
const apiClientMock = getMockFetchMethod();
|
|
83
|
-
const queue = getQueue('name', apiClientMock);
|
|
84
|
-
await expect(queue.push({ ...PAYLOAD, delayInSeconds: 901 })).rejects.toThrow(
|
|
85
|
-
new InvalidPushSettingsError(`The delayInSeconds setting must be between 0 and 900.`)
|
|
86
|
-
);
|
|
87
|
-
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should throw NoEventsToPushError', async () => {
|
|
91
|
-
const apiClientMock = getMockFetchMethod();
|
|
92
|
-
const queue = getQueue('name', apiClientMock);
|
|
93
|
-
await expect(queue.push([])).rejects.toThrow(new NoEventsToPushError(`No events pushed.`));
|
|
94
|
-
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should throw TooManyEventsError', async () => {
|
|
98
|
-
const apiClientMock = getMockFetchMethod();
|
|
99
|
-
const queue = getQueue('name', apiClientMock);
|
|
100
|
-
await expect(queue.push(Array(51).fill(PAYLOAD))).rejects.toThrow(
|
|
101
|
-
new TooManyEventsError(`This push contains more than the 50 events allowed.`)
|
|
102
|
-
);
|
|
103
|
-
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should throw PayloadTooBigError', async () => {
|
|
107
|
-
const apiClientMock = getMockFetchMethod();
|
|
108
|
-
const queue = getQueue('name', apiClientMock);
|
|
109
|
-
await expect(queue.push({ body: { content: 'x'.repeat(201 * 1024) } })).rejects.toThrow(
|
|
110
|
-
new PayloadTooBigError(`The maximum payload size is 200KB.`)
|
|
111
|
-
);
|
|
112
|
-
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should throw RateLimitError', async () => {
|
|
116
|
-
const apiClientMock = getMockFetchMethod({}, 429);
|
|
117
|
-
const queue = getQueue('name', apiClientMock);
|
|
118
|
-
await expect(queue.push(PAYLOAD)).rejects.toThrow(new RateLimitError(`Too many requests.`));
|
|
119
|
-
verifyApiClientCalledPushPathWith(apiClientMock, [PAYLOAD], 'name');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should throw InvocationLimitReachedError', async () => {
|
|
123
|
-
const apiClientMock = getMockFetchMethod({}, 405);
|
|
124
|
-
const queue = getQueue('name', apiClientMock);
|
|
125
|
-
await expect(queue.push(PAYLOAD)).rejects.toThrow(
|
|
126
|
-
new InvocationLimitReachedError(`The limit on cyclic invocation has been reached.`)
|
|
127
|
-
);
|
|
128
|
-
verifyApiClientCalledPushPathWith(apiClientMock, [PAYLOAD], 'name');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should throw PartialSuccessError when there are failed events', async () => {
|
|
132
|
-
const apiClientMock = getMockFetchMethod(
|
|
133
|
-
{
|
|
134
|
-
failedEvents: [
|
|
135
|
-
{
|
|
136
|
-
index: '0',
|
|
137
|
-
errorMessage: 'failed-1'
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
index: 2,
|
|
141
|
-
errorMessage: 'failed-3'
|
|
142
|
-
}
|
|
143
|
-
]
|
|
144
|
-
},
|
|
145
|
-
202
|
|
146
|
-
);
|
|
147
|
-
const queue = getQueue('name', apiClientMock);
|
|
148
|
-
const payload = [
|
|
149
|
-
{
|
|
150
|
-
body: {
|
|
151
|
-
content: 'payload-1'
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
body: {
|
|
156
|
-
content: 'payload-2'
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
body: {
|
|
161
|
-
content: 'payload-3'
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
];
|
|
165
|
-
await expect(queue.push(payload)).rejects.toThrow(
|
|
166
|
-
new PartialSuccessError(`Failed to process 2 event(s).`, { jobId: 'some-job-id' }, [
|
|
167
|
-
{
|
|
168
|
-
errorMessage: 'failed-1',
|
|
169
|
-
event: {
|
|
170
|
-
body: {
|
|
171
|
-
content: 'payload-1'
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
errorMessage: 'failed-3',
|
|
177
|
-
event: {
|
|
178
|
-
body: {
|
|
179
|
-
content: 'payload-3'
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
])
|
|
184
|
-
);
|
|
185
|
-
verifyApiClientCalledPushPathWith(apiClientMock, payload, 'name');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should throw PartialSuccessError when backend failed to create job stats', async () => {
|
|
189
|
-
const apiClientMock = getMockFetchMethod(
|
|
190
|
-
{
|
|
191
|
-
errorMessage: 'Failed to create stats for job name#12345'
|
|
192
|
-
},
|
|
193
|
-
202
|
|
194
|
-
);
|
|
195
|
-
const queue = getQueue('name', apiClientMock);
|
|
196
|
-
const payload = [
|
|
197
|
-
{
|
|
198
|
-
body: {
|
|
199
|
-
content: 'payload-1'
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
body: {
|
|
204
|
-
content: 'payload-2'
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
body: {
|
|
209
|
-
content: 'payload-3'
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
];
|
|
213
|
-
await expect(queue.push(payload)).rejects.toThrow(
|
|
214
|
-
new PartialSuccessError(`Failed to create stats for job name#12345`, { jobId: 'some-job-id' }, [])
|
|
215
|
-
);
|
|
216
|
-
verifyApiClientCalledPushPathWith(apiClientMock, payload, 'name');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('should throw PartialSuccessError when there are failed events and backend failed to create job stats', async () => {
|
|
220
|
-
const apiClientMock = getMockFetchMethod(
|
|
221
|
-
{
|
|
222
|
-
errorMessage: 'Failed to create stats for job name#12345',
|
|
223
|
-
failedEvents: [
|
|
224
|
-
{
|
|
225
|
-
index: '0',
|
|
226
|
-
errorMessage: 'failed-1'
|
|
227
|
-
}
|
|
228
|
-
]
|
|
229
|
-
},
|
|
230
|
-
202
|
|
231
|
-
);
|
|
232
|
-
const queue = getQueue('name', apiClientMock);
|
|
233
|
-
const payload = [
|
|
234
|
-
{
|
|
235
|
-
body: {
|
|
236
|
-
content: 'payload-1'
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
body: {
|
|
241
|
-
content: 'payload-2'
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
];
|
|
245
|
-
await expect(queue.push(payload)).rejects.toThrow(
|
|
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
|
-
}
|
|
257
|
-
}
|
|
258
|
-
]
|
|
259
|
-
)
|
|
260
|
-
);
|
|
261
|
-
verifyApiClientCalledPushPathWith(apiClientMock, payload, 'name');
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('should throw InternalServerError', async () => {
|
|
265
|
-
const apiClientMock = getMockFetchMethod(
|
|
266
|
-
{
|
|
267
|
-
message: 'AWS SQS timed out',
|
|
268
|
-
code: 500,
|
|
269
|
-
details: 'The request processing has failed because of an unknown error, exception or failure'
|
|
270
|
-
},
|
|
271
|
-
500
|
|
272
|
-
);
|
|
273
|
-
const queue = getQueue('name', apiClientMock);
|
|
274
|
-
await expect(queue.push(PAYLOAD)).rejects.toThrow(
|
|
275
|
-
new InternalServerError(
|
|
276
|
-
`500 Status Text: AWS SQS timed out`,
|
|
277
|
-
500,
|
|
278
|
-
'The request processing has failed because of an unknown error, exception or failure'
|
|
279
|
-
)
|
|
280
|
-
);
|
|
281
|
-
verifyApiClientCalledPushPathWith(apiClientMock, [PAYLOAD], 'name');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should throw InternalServerError for error response without response body', async () => {
|
|
285
|
-
const apiClientMock = getApiClientMockWithoutResponseBody(504, 'Gateway Timeout');
|
|
286
|
-
const queue = getQueue('name', apiClientMock);
|
|
287
|
-
await expect(queue.push(PAYLOAD)).rejects.toThrow(new InternalServerError(`504 Gateway Timeout`, 504));
|
|
288
|
-
verifyApiClientCalledPushPathWith(apiClientMock, [PAYLOAD], 'name');
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('requests stargate if no api client is provided', async () => {
|
|
292
|
-
const queue = getQueue('queue');
|
|
293
|
-
await queue.push({ body: { test: 'stargate' } });
|
|
294
|
-
expect(__requestAtlassianAsApp).toHaveBeenCalledTimes(1);
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
describe('getJob', () => {
|
|
299
|
-
it('should create a JobProgress by jobId', async () => {
|
|
300
|
-
const apiClientMock = getMockFetchMethod();
|
|
301
|
-
const queue = getQueue('name', apiClientMock);
|
|
302
|
-
const jobProgress = queue.getJob('test-job-id');
|
|
303
|
-
expect(jobProgress).toEqual(new JobProgress({ key: 'name' }, 'test-job-id', apiClientMock));
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { QueueResponse } from '../queueResponse';
|
|
2
|
-
|
|
3
|
-
describe('QueueResponse', () => {
|
|
4
|
-
it('should contain property retry=false by default', async () => {
|
|
5
|
-
const res = new QueueResponse();
|
|
6
|
-
expect(JSON.stringify(res)).toEqual('{"_retry":false}');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it('doRetry() should change property retry=true', async () => {
|
|
10
|
-
const res = new QueueResponse();
|
|
11
|
-
res.retry();
|
|
12
|
-
expect(JSON.stringify(res)).toEqual('{"_retry":true}');
|
|
13
|
-
});
|
|
14
|
-
});
|
package/src/__test__/utils.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { Payload } from '../types';
|
|
2
|
-
|
|
3
|
-
export const getMockFetchMethod = (response?: any, statusCode = 201) => {
|
|
4
|
-
return jest.fn().mockReturnValue({
|
|
5
|
-
created: statusCode === 201,
|
|
6
|
-
status: statusCode,
|
|
7
|
-
statusText: 'Status Text',
|
|
8
|
-
json: jest.fn().mockResolvedValue(response)
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const getApiClientMockWithoutResponseBody = (statusCode: number, statusText: string) => {
|
|
13
|
-
return jest.fn().mockReturnValue({
|
|
14
|
-
ok: statusCode === 200,
|
|
15
|
-
status: statusCode,
|
|
16
|
-
statusText: statusText
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const verifyApiClientCalledWith = (apiClientMock: jest.Mock, path: string, expectedBody: any) => {
|
|
21
|
-
expect(apiClientMock).toHaveBeenCalledWith(
|
|
22
|
-
path,
|
|
23
|
-
expect.objectContaining({
|
|
24
|
-
method: 'POST',
|
|
25
|
-
body: expect.any(String),
|
|
26
|
-
headers: {
|
|
27
|
-
'content-type': 'application/json'
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
const [, { body }] = apiClientMock.mock.calls[0];
|
|
33
|
-
expect(JSON.parse(body)).toEqual(expect.objectContaining(expectedBody));
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const verifyApiClientCalledPushPathWith = (
|
|
37
|
-
apiClientMock: jest.Mock,
|
|
38
|
-
payloads: Payload | Payload[],
|
|
39
|
-
name: string
|
|
40
|
-
) => {
|
|
41
|
-
verifyApiClientCalledWith(apiClientMock, '/webhook/queue/publish/{contextAri}/{environmentId}/{appId}/{appVersion}', {
|
|
42
|
-
queueName: name,
|
|
43
|
-
schema: 'ari:cloud:ecosystem::forge/app-event-2',
|
|
44
|
-
type: 'avi:forge:app:event',
|
|
45
|
-
payload: payloads
|
|
46
|
-
});
|
|
47
|
-
};
|
package/src/appEvents.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { FailedEvent, PushResult } from './types';
|
|
2
|
-
|
|
3
|
-
export class EventsError extends Error {
|
|
4
|
-
constructor(message: string) {
|
|
5
|
-
super(message);
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class InvalidPushSettingsError extends EventsError {}
|
|
10
|
-
|
|
11
|
-
export class InvalidQueueNameError extends EventsError {}
|
|
12
|
-
|
|
13
|
-
export class InvalidPayloadError extends EventsError {}
|
|
14
|
-
|
|
15
|
-
export class TooManyEventsError extends EventsError {}
|
|
16
|
-
|
|
17
|
-
export class PayloadTooBigError extends EventsError {}
|
|
18
|
-
|
|
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
|
-
) {
|
|
29
|
-
super(message);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class InternalServerError extends EventsError {
|
|
34
|
-
constructor(message: string, errorCode?: number, details?: string) {
|
|
35
|
-
super(message);
|
|
36
|
-
this.errorCode = errorCode;
|
|
37
|
-
this.details = details;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
errorCode?: number;
|
|
41
|
-
details?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class JobDoesNotExistError extends EventsError {}
|
|
45
|
-
|
|
46
|
-
export class InvocationLimitReachedError extends EventsError {}
|
package/src/index.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export { Queue } from './queue';
|
|
2
|
-
export {
|
|
3
|
-
InvalidQueueNameError,
|
|
4
|
-
TooManyEventsError,
|
|
5
|
-
PayloadTooBigError,
|
|
6
|
-
NoEventsToPushError,
|
|
7
|
-
RateLimitError,
|
|
8
|
-
PartialSuccessError,
|
|
9
|
-
InternalServerError,
|
|
10
|
-
JobDoesNotExistError,
|
|
11
|
-
InvalidPushSettingsError,
|
|
12
|
-
InvocationLimitReachedError
|
|
13
|
-
} from './errors';
|
|
14
|
-
export { JobProgress, JobStats } from './jobProgress';
|
|
15
|
-
export { QueueResponse } from './queueResponse';
|
|
16
|
-
export { InvocationError } from './invocationError';
|
|
17
|
-
export { InvocationErrorCode } from './invocationErrorCode';
|
|
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/invocationError.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_RETRY_OPTIONS, MIN_RETRY_AFTER, RetryOptions } from './retryOptions';
|
|
2
|
-
import { Response } from './queueResponse';
|
|
3
|
-
|
|
4
|
-
export class InvocationError extends Response {
|
|
5
|
-
constructor(public retryOptions: RetryOptions = DEFAULT_RETRY_OPTIONS) {
|
|
6
|
-
super(true);
|
|
7
|
-
if (this.retryOptions.retryAfter !== undefined && this.retryOptions.retryAfter <= 0) {
|
|
8
|
-
this.retryOptions.retryAfter = MIN_RETRY_AFTER;
|
|
9
|
-
}
|
|
10
|
-
return this.toJSON() as unknown as InvocationError;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/***
|
|
14
|
-
* Function to convert class object to JSON object so that app function return this as part of lambda invocation.
|
|
15
|
-
*/
|
|
16
|
-
public toJSON(): { _retry: boolean; retryOptions: RetryOptions } {
|
|
17
|
-
return {
|
|
18
|
-
_retry: this._retry,
|
|
19
|
-
retryOptions: this.retryOptions
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export enum InvocationErrorCode {
|
|
2
|
-
FUNCTION_OUT_OF_MEMORY = 'FUNCTION_OUT_OF_MEMORY',
|
|
3
|
-
FUNCTION_TIME_OUT = 'FUNCTION_TIME_OUT',
|
|
4
|
-
FUNCTION_PLATFORM_UNKNOWN_ERROR = 'FUNCTION_PLATFORM_UNKNOWN_ERROR',
|
|
5
|
-
FUNCTION_PLATFORM_RATE_LIMITED = 'FUNCTION_PLATFORM_RATE_LIMITED',
|
|
6
|
-
FUNCTION_UPSTREAM_RATE_LIMITED = 'FUNCTION_UPSTREAM_RATE_LIMITED',
|
|
7
|
-
FUNCTION_RETRY_REQUEST = 'FUNCTION_RETRY_REQUEST'
|
|
8
|
-
}
|
package/src/jobProgress.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { __requestAtlassianAsApp, FetchMethod } from '@forge/api';
|
|
2
|
-
import { CANCEL_JOB_PATH, GET_STATS_PATH, post } from './queries';
|
|
3
|
-
import {
|
|
4
|
-
validateCancelJobAPIResponse,
|
|
5
|
-
validateCancelJobRequest,
|
|
6
|
-
validateGetStatsAPIResponse,
|
|
7
|
-
validateGetStatsPayload
|
|
8
|
-
} from './validators';
|
|
9
|
-
import { CancelJobRequest, GetStatsRequest, QueueParams } from './types';
|
|
10
|
-
|
|
11
|
-
export type JobStats = {
|
|
12
|
-
success: number;
|
|
13
|
-
inProgress: number;
|
|
14
|
-
failed: number;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export class JobProgress {
|
|
18
|
-
constructor(
|
|
19
|
-
private readonly queueParams: QueueParams,
|
|
20
|
-
private readonly id: string,
|
|
21
|
-
private readonly apiClient: FetchMethod = __requestAtlassianAsApp
|
|
22
|
-
) {}
|
|
23
|
-
|
|
24
|
-
async getStats(): Promise<JobStats> {
|
|
25
|
-
const getStatsRequest: GetStatsRequest = {
|
|
26
|
-
queueName: this.queueParams.key,
|
|
27
|
-
jobId: this.id,
|
|
28
|
-
time: new Date().toISOString()
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
validateGetStatsPayload(getStatsRequest);
|
|
32
|
-
|
|
33
|
-
const response = await post(GET_STATS_PATH, getStatsRequest, this.apiClient);
|
|
34
|
-
await validateGetStatsAPIResponse(response, getStatsRequest);
|
|
35
|
-
const { success, inProgress, failed } = await response.json();
|
|
36
|
-
return { success, inProgress, failed };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async cancel(): Promise<void> {
|
|
40
|
-
const cancelJobRequest: CancelJobRequest = {
|
|
41
|
-
queueName: this.queueParams.key,
|
|
42
|
-
jobId: this.id,
|
|
43
|
-
time: new Date().toISOString()
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
validateCancelJobRequest(cancelJobRequest);
|
|
47
|
-
|
|
48
|
-
const response = await post(CANCEL_JOB_PATH, cancelJobRequest, this.apiClient);
|
|
49
|
-
await validateCancelJobAPIResponse(response, cancelJobRequest);
|
|
50
|
-
}
|
|
51
|
-
}
|
package/src/queries.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { APIResponse, FetchMethod } from '@forge/api';
|
|
2
|
-
|
|
3
|
-
export const PUSH_PATH = '/webhook/queue/publish/{contextAri}/{environmentId}/{appId}/{appVersion}';
|
|
4
|
-
export const GET_STATS_PATH = '/webhook/queue/stats/{contextAri}/{environmentId}/{appId}/{appVersion}';
|
|
5
|
-
export const CANCEL_JOB_PATH = '/webhook/queue/cancel/{contextAri}/{environmentId}/{appId}/{appVersion}';
|
|
6
|
-
|
|
7
|
-
export const post = async (endpoint: string, body: unknown, apiClient: FetchMethod): Promise<APIResponse> => {
|
|
8
|
-
const request = {
|
|
9
|
-
method: 'POST',
|
|
10
|
-
body: JSON.stringify(body),
|
|
11
|
-
headers: {
|
|
12
|
-
'content-type': 'application/json'
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
return await apiClient(endpoint, request);
|
|
17
|
-
};
|
package/src/queue.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { FetchMethod, __requestAtlassianAsApp } from '@forge/api';
|
|
2
|
-
import { PUSH_PATH, post } from './queries';
|
|
3
|
-
import { validatePushAPIResponse, validatePushEvents, validateQueueKey } from './validators';
|
|
4
|
-
import { QueueParams, PushEvent, PushRequest, PushResult } from './types';
|
|
5
|
-
import { v4 as uuid } from 'uuid';
|
|
6
|
-
import { JobProgress } from './jobProgress';
|
|
7
|
-
|
|
8
|
-
export class Queue {
|
|
9
|
-
constructor(
|
|
10
|
-
private readonly queueParams: QueueParams,
|
|
11
|
-
private readonly apiClient: FetchMethod = __requestAtlassianAsApp
|
|
12
|
-
) {
|
|
13
|
-
validateQueueKey(this.queueParams.key);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async push(events: PushEvent | PushEvent[]): Promise<PushResult> {
|
|
17
|
-
const validEvents = validatePushEvents(events);
|
|
18
|
-
const queueName = this.queueParams.key;
|
|
19
|
-
const jobId = uuid();
|
|
20
|
-
|
|
21
|
-
const pushRequest: PushRequest = {
|
|
22
|
-
queueName: queueName,
|
|
23
|
-
jobId: jobId,
|
|
24
|
-
type: 'avi:forge:app:event',
|
|
25
|
-
schema: 'ari:cloud:ecosystem::forge/app-event-2',
|
|
26
|
-
payload: validEvents,
|
|
27
|
-
time: new Date().toISOString()
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const response = await post(PUSH_PATH, pushRequest, this.apiClient);
|
|
31
|
-
const result = { jobId };
|
|
32
|
-
await validatePushAPIResponse(pushRequest, response, result);
|
|
33
|
-
return result;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
getJob(jobId: string): JobProgress {
|
|
37
|
-
return new JobProgress(this.queueParams, jobId, this.apiClient);
|
|
38
|
-
}
|
|
39
|
-
}
|