@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @forge/events
|
|
2
2
|
|
|
3
|
+
## 2.0.0-next.1
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- a89e05a: Restrict async events payload to objects
|
|
8
|
+
- 81174c3: Change `queue.push()` to accept events containing body and delay
|
|
9
|
+
- 9227948: Simplify JobProgress.getStats to return stats directly and remove unneeded response from JobProgress.cancel
|
|
10
|
+
|
|
11
|
+
## 1.1.0-next.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 34386db: App events API
|
|
16
|
+
|
|
3
17
|
## 1.0.3
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,39 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
# Forge Events
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Library for [asynchronous data processing](https://developer.atlassian.com/platform/forge/runtime-reference/async-events-api/) in Forge.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
import fetch, { RequestInit } from 'node-fetch';
|
|
5
|
+
## Requirements
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
See [Set up Forge](https://developer.atlassian.com/platform/forge/set-up-forge/) for details of the software required to develop Forge apps.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
## Usage
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
const appContextAri = 'ari:cloud:jira::site/...';
|
|
14
|
-
const token = '...';
|
|
11
|
+
### Pushing events to the queue
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
const url = API_BASE + path;
|
|
13
|
+
```typescript
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
async function pushEvent() {
|
|
16
|
+
const { jobId } = await queue.push({
|
|
17
|
+
body: {
|
|
18
|
+
hello: 'world'
|
|
19
|
+
}
|
|
20
|
+
});
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
init.headers = Object.assign(init.headers!, extraHeaders);
|
|
27
|
-
return fetch(url, init);
|
|
22
|
+
const jobProgress = queue.getJob(jobId);
|
|
23
|
+
const { success, inProgress, failed } = await jobProgress.getStats();
|
|
28
24
|
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Consuming events from the queue
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { AsyncEvent } from '@forge/events';
|
|
29
31
|
|
|
30
|
-
async function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
export async function eventListener(event: AsyncEvent, context) {
|
|
33
|
+
const jobProgress = queue.getJob(event.jobId);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// process the event
|
|
37
|
+
} catch (error) {
|
|
38
|
+
await jobProgress.cancel();
|
|
34
39
|
}
|
|
35
|
-
await queue.push([payloads])
|
|
36
40
|
}
|
|
37
|
-
|
|
38
|
-
demo();
|
|
39
41
|
```
|
|
42
|
+
|
|
43
|
+
## Support
|
|
44
|
+
|
|
45
|
+
See [Get help](https://developer.atlassian.com/platform/forge/get-help/) for how to get help and provide feedback.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appEvents.test.d.ts","sourceRoot":"","sources":["../../src/__test__/appEvents.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const appEvents_1 = require("../appEvents");
|
|
4
|
+
const queries_1 = require("../queries");
|
|
5
|
+
const api_1 = require("@forge/api");
|
|
6
|
+
jest.mock('../queries', () => ({
|
|
7
|
+
post: jest.fn()
|
|
8
|
+
}));
|
|
9
|
+
jest.mock('@forge/api', () => ({
|
|
10
|
+
__requestAtlassianAsApp: jest.fn()
|
|
11
|
+
}));
|
|
12
|
+
describe('appEvents', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
describe('publishEvent', () => {
|
|
17
|
+
it('should call post with the correct parameters', async () => {
|
|
18
|
+
const event = { key: 'test-event' };
|
|
19
|
+
const mockResponse = {
|
|
20
|
+
ok: true,
|
|
21
|
+
json: jest.fn().mockResolvedValue({
|
|
22
|
+
failedEvents: []
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
queries_1.post.mockResolvedValue(mockResponse);
|
|
26
|
+
const result = await appEvents_1.appEvents.publish(event);
|
|
27
|
+
expect(queries_1.post).toHaveBeenCalledTimes(1);
|
|
28
|
+
expect(queries_1.post).toHaveBeenCalledWith('/forge/events/v1/app-events', {
|
|
29
|
+
events: [
|
|
30
|
+
{
|
|
31
|
+
key: event.key
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}, api_1.__requestAtlassianAsApp);
|
|
35
|
+
expect(result).toEqual({ type: 'success', failedEvents: [] });
|
|
36
|
+
});
|
|
37
|
+
it('should return a success result for successful requests', async () => {
|
|
38
|
+
const event = { key: 'test-event' };
|
|
39
|
+
const mockResponse = {
|
|
40
|
+
ok: true,
|
|
41
|
+
json: jest.fn().mockResolvedValue({
|
|
42
|
+
failedEvents: []
|
|
43
|
+
})
|
|
44
|
+
};
|
|
45
|
+
queries_1.post.mockResolvedValue(mockResponse);
|
|
46
|
+
const result = await appEvents_1.appEvents.publish(event);
|
|
47
|
+
expect(result).toEqual({ type: 'success', failedEvents: [] });
|
|
48
|
+
});
|
|
49
|
+
it('should return an error result for failed requests', async () => {
|
|
50
|
+
const event = { key: 'test-event' };
|
|
51
|
+
const mockResponse = {
|
|
52
|
+
ok: false,
|
|
53
|
+
status: 500,
|
|
54
|
+
json: jest.fn().mockResolvedValue({
|
|
55
|
+
errorMessages: ['Internal server error']
|
|
56
|
+
})
|
|
57
|
+
};
|
|
58
|
+
queries_1.post.mockResolvedValue(mockResponse);
|
|
59
|
+
const result = await appEvents_1.appEvents.publish(event);
|
|
60
|
+
expect(result).toEqual({
|
|
61
|
+
type: 'error',
|
|
62
|
+
errorType: 'SERVICE_ERROR',
|
|
63
|
+
errorMessage: 'Internal server error'
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it('should handle validation errors', async () => {
|
|
67
|
+
const event = { key: 'test-event' };
|
|
68
|
+
const mockResponse = {
|
|
69
|
+
ok: false,
|
|
70
|
+
status: 400,
|
|
71
|
+
json: jest.fn().mockResolvedValue({
|
|
72
|
+
errorMessages: ['Invalid event type']
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
queries_1.post.mockResolvedValue(mockResponse);
|
|
76
|
+
const result = await appEvents_1.appEvents.publish(event);
|
|
77
|
+
expect(result).toEqual({
|
|
78
|
+
type: 'error',
|
|
79
|
+
errorType: 'VALIDATION_ERROR',
|
|
80
|
+
errorMessage: 'Invalid event type'
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
it('should handle errors object in the error response', async () => {
|
|
84
|
+
const event = { key: 'test-event' };
|
|
85
|
+
const errorResponse = {
|
|
86
|
+
errorMessages: [],
|
|
87
|
+
errors: { key: 'Event key is required' }
|
|
88
|
+
};
|
|
89
|
+
const mockResponse = {
|
|
90
|
+
ok: false,
|
|
91
|
+
status: 400,
|
|
92
|
+
json: jest.fn().mockResolvedValue(errorResponse)
|
|
93
|
+
};
|
|
94
|
+
queries_1.post.mockResolvedValue(mockResponse);
|
|
95
|
+
const result = await appEvents_1.appEvents.publish(event);
|
|
96
|
+
expect(result).toEqual({
|
|
97
|
+
type: 'error',
|
|
98
|
+
errorType: 'VALIDATION_ERROR',
|
|
99
|
+
errorMessage: 'key: Event key is required'
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it('should use OTHER as errorType for unknown status codes', async () => {
|
|
103
|
+
const event = { key: 'test-event' };
|
|
104
|
+
const mockResponse = {
|
|
105
|
+
ok: false,
|
|
106
|
+
status: 418,
|
|
107
|
+
json: jest.fn().mockResolvedValue({
|
|
108
|
+
errorMessages: ['I refuse to brew coffee']
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
queries_1.post.mockResolvedValue(mockResponse);
|
|
112
|
+
const result = await appEvents_1.appEvents.publish(event);
|
|
113
|
+
expect(result).toEqual({
|
|
114
|
+
type: 'error',
|
|
115
|
+
errorType: 'OTHER',
|
|
116
|
+
errorMessage: 'I refuse to brew coffee'
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
it('should handle empty error messages', async () => {
|
|
120
|
+
const event = { key: 'test-event' };
|
|
121
|
+
const mockResponse = {
|
|
122
|
+
ok: false,
|
|
123
|
+
status: 500,
|
|
124
|
+
json: jest.fn().mockResolvedValue({
|
|
125
|
+
errorMessages: []
|
|
126
|
+
})
|
|
127
|
+
};
|
|
128
|
+
queries_1.post.mockResolvedValue(mockResponse);
|
|
129
|
+
const result = await appEvents_1.appEvents.publish(event);
|
|
130
|
+
expect(result).toEqual({
|
|
131
|
+
type: 'error',
|
|
132
|
+
errorType: 'SERVICE_ERROR',
|
|
133
|
+
errorMessage: '{"errorMessages":[]}'
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -5,7 +5,7 @@ const errors_1 = require("../errors");
|
|
|
5
5
|
const jobProgress_1 = require("../jobProgress");
|
|
6
6
|
const api_1 = require("@forge/api");
|
|
7
7
|
jest.mock('@forge/api', () => ({
|
|
8
|
-
__requestAtlassianAsApp: (0, utils_1.getMockFetchMethod)(
|
|
8
|
+
__requestAtlassianAsApp: (0, utils_1.getMockFetchMethod)({ done: true }, 200)
|
|
9
9
|
}));
|
|
10
10
|
const getJobProgress = (jobId, apiClientMock) => new jobProgress_1.JobProgress(jobId, apiClientMock);
|
|
11
11
|
describe('JobProgress methods', () => {
|
|
@@ -17,8 +17,7 @@ describe('JobProgress methods', () => {
|
|
|
17
17
|
failed: 1
|
|
18
18
|
}, 200);
|
|
19
19
|
const jobProgress = getJobProgress('test-queue-name#test-job-id', apiClientMock);
|
|
20
|
-
const
|
|
21
|
-
const { success, inProgress, failed } = await response.json();
|
|
20
|
+
const { success, inProgress, failed } = await jobProgress.getStats();
|
|
22
21
|
(0, utils_1.verifyApiClientCalledWith)(apiClientMock, '/webhook/queue/stats/{contextAri}/{environmentId}/{appId}/{appVersion}', {
|
|
23
22
|
queueName: 'test-queue-name',
|
|
24
23
|
jobId: 'test-job-id'
|
|
@@ -66,12 +65,11 @@ describe('JobProgress methods', () => {
|
|
|
66
65
|
it('should call the queue/cancel endpoint', async () => {
|
|
67
66
|
const apiClientMock = (0, utils_1.getMockFetchMethod)({}, 204);
|
|
68
67
|
const jobProgress = getJobProgress('test-queue-name#test-job-id', apiClientMock);
|
|
69
|
-
|
|
68
|
+
await jobProgress.cancel();
|
|
70
69
|
(0, utils_1.verifyApiClientCalledWith)(apiClientMock, '/webhook/queue/cancel/{contextAri}/{environmentId}/{appId}/{appVersion}', {
|
|
71
70
|
queueName: 'test-queue-name',
|
|
72
71
|
jobId: 'test-job-id'
|
|
73
72
|
});
|
|
74
|
-
expect(response.status).toEqual(204);
|
|
75
73
|
});
|
|
76
74
|
it('should throw JobDoesNotExistError', async () => {
|
|
77
75
|
const apiClientMock = (0, utils_1.getMockFetchMethod)({
|
|
@@ -16,20 +16,51 @@ describe('Queue methods', () => {
|
|
|
16
16
|
expect(() => getQueue('invalid name', apiClientMock)).toThrowError(new errors_1.InvalidQueueNameError('Queue names can only contain alphanumeric characters, dashes and underscores.'));
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
|
+
const PAYLOAD = { body: { page: 1 } };
|
|
19
20
|
describe('push', () => {
|
|
20
|
-
it(
|
|
21
|
+
it.each([
|
|
22
|
+
['single payload', PAYLOAD, [PAYLOAD]],
|
|
23
|
+
['payload array', [PAYLOAD, PAYLOAD], [PAYLOAD, PAYLOAD]]
|
|
24
|
+
])('should call the queue/publish endpoint when given %s', async (_, payload, expectedPayload) => {
|
|
21
25
|
const apiClientMock = (0, utils_1.getMockFetchMethod)();
|
|
22
26
|
const queue = getQueue('name', apiClientMock);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
await queue.push(payload);
|
|
28
|
+
(0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, expectedPayload, 'name');
|
|
29
|
+
});
|
|
30
|
+
it.each([
|
|
31
|
+
['number', 1],
|
|
32
|
+
['string', 'test'],
|
|
33
|
+
['boolean', true],
|
|
34
|
+
['array', [1, 2, 3]],
|
|
35
|
+
['null', null],
|
|
36
|
+
['undefined', undefined],
|
|
37
|
+
['array with non-object', [1]],
|
|
38
|
+
['array with an array', [[2, 3]]]
|
|
39
|
+
])('rejects non-object (%s) event', async (_, payload) => {
|
|
40
|
+
const apiClientMock = (0, utils_1.getMockFetchMethod)();
|
|
41
|
+
const queue = getQueue('name', apiClientMock);
|
|
42
|
+
await expect(queue.push(payload)).rejects.toThrow(new errors_1.InvalidPayloadError(`Event must be an object.`));
|
|
43
|
+
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
44
|
+
});
|
|
45
|
+
it.each([
|
|
46
|
+
['number', 1],
|
|
47
|
+
['string', 'test'],
|
|
48
|
+
['boolean', true],
|
|
49
|
+
['array', [1, 2, 3]],
|
|
50
|
+
['null', null],
|
|
51
|
+
['undefined', undefined],
|
|
52
|
+
['array with non-object', [1]],
|
|
53
|
+
['array with an array', [[2, 3]]]
|
|
54
|
+
])('rejects non-object (%s) event body', async (_, body) => {
|
|
55
|
+
const apiClientMock = (0, utils_1.getMockFetchMethod)();
|
|
56
|
+
const queue = getQueue('name', apiClientMock);
|
|
57
|
+
await expect(queue.push({ body })).rejects.toThrow(new errors_1.InvalidPayloadError(`Event body must be an object.`));
|
|
58
|
+
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
28
59
|
});
|
|
29
60
|
it('should throw InvalidPushSettingsError for delay', async () => {
|
|
30
61
|
const apiClientMock = (0, utils_1.getMockFetchMethod)();
|
|
31
62
|
const queue = getQueue('name', apiClientMock);
|
|
32
|
-
await expect(queue.push(
|
|
63
|
+
await expect(queue.push({ ...PAYLOAD, delayInSeconds: 901 })).rejects.toThrow(new errors_1.InvalidPushSettingsError(`The delayInSeconds setting must be between 0 and 900.`));
|
|
33
64
|
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
34
65
|
});
|
|
35
66
|
it('should throw NoEventsToPushError', async () => {
|
|
@@ -41,28 +72,26 @@ describe('Queue methods', () => {
|
|
|
41
72
|
it('should throw TooManyEventsError', async () => {
|
|
42
73
|
const apiClientMock = (0, utils_1.getMockFetchMethod)();
|
|
43
74
|
const queue = getQueue('name', apiClientMock);
|
|
44
|
-
await expect(queue.push(
|
|
75
|
+
await expect(queue.push(Array(51).fill(PAYLOAD))).rejects.toThrow(new errors_1.TooManyEventsError(`This push contains more than the 50 events allowed.`));
|
|
45
76
|
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
46
77
|
});
|
|
47
78
|
it('should throw PayloadTooBigError', async () => {
|
|
48
79
|
const apiClientMock = (0, utils_1.getMockFetchMethod)();
|
|
49
80
|
const queue = getQueue('name', apiClientMock);
|
|
50
|
-
await expect(queue.push('x'.repeat(201 * 1024))).rejects.toThrow(new errors_1.PayloadTooBigError(`The maximum payload size is 200KB.`));
|
|
81
|
+
await expect(queue.push({ body: { content: 'x'.repeat(201 * 1024) } })).rejects.toThrow(new errors_1.PayloadTooBigError(`The maximum payload size is 200KB.`));
|
|
51
82
|
expect(apiClientMock).toHaveBeenCalledTimes(0);
|
|
52
83
|
});
|
|
53
84
|
it('should throw RateLimitError', async () => {
|
|
54
85
|
const apiClientMock = (0, utils_1.getMockFetchMethod)({}, 429);
|
|
55
86
|
const queue = getQueue('name', apiClientMock);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
(0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, payload, 'name');
|
|
87
|
+
await expect(queue.push(PAYLOAD)).rejects.toThrow(new errors_1.RateLimitError(`Too many requests.`));
|
|
88
|
+
(0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, [PAYLOAD], 'name');
|
|
59
89
|
});
|
|
60
90
|
it('should throw InvocationLimitReachedError', async () => {
|
|
61
91
|
const apiClientMock = (0, utils_1.getMockFetchMethod)({}, 405);
|
|
62
92
|
const queue = getQueue('name', apiClientMock);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
(0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, payload, 'name');
|
|
93
|
+
await expect(queue.push(PAYLOAD)).rejects.toThrow(new errors_1.InvocationLimitReachedError(`The limit on cyclic invocation has been reached.`));
|
|
94
|
+
(0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, [PAYLOAD], 'name');
|
|
66
95
|
});
|
|
67
96
|
it('should throw PartialSuccessError when there are failed events', async () => {
|
|
68
97
|
const apiClientMock = (0, utils_1.getMockFetchMethod)({
|
|
@@ -80,26 +109,36 @@ describe('Queue methods', () => {
|
|
|
80
109
|
const queue = getQueue('name', apiClientMock);
|
|
81
110
|
const payload = [
|
|
82
111
|
{
|
|
83
|
-
|
|
112
|
+
body: {
|
|
113
|
+
content: 'payload-1'
|
|
114
|
+
}
|
|
84
115
|
},
|
|
85
116
|
{
|
|
86
|
-
|
|
117
|
+
body: {
|
|
118
|
+
content: 'payload-2'
|
|
119
|
+
}
|
|
87
120
|
},
|
|
88
121
|
{
|
|
89
|
-
|
|
122
|
+
body: {
|
|
123
|
+
content: 'payload-3'
|
|
124
|
+
}
|
|
90
125
|
}
|
|
91
126
|
];
|
|
92
|
-
await expect(queue.push(payload)).rejects.toThrow(new errors_1.PartialSuccessError(`Failed to process 2 event(s).`, [
|
|
127
|
+
await expect(queue.push(payload)).rejects.toThrow(new errors_1.PartialSuccessError(`Failed to process 2 event(s).`, { jobId: 'some-job-id' }, [
|
|
93
128
|
{
|
|
94
129
|
errorMessage: 'failed-1',
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
event: {
|
|
131
|
+
body: {
|
|
132
|
+
content: 'payload-1'
|
|
133
|
+
}
|
|
97
134
|
}
|
|
98
135
|
},
|
|
99
136
|
{
|
|
100
137
|
errorMessage: 'failed-3',
|
|
101
|
-
|
|
102
|
-
|
|
138
|
+
event: {
|
|
139
|
+
body: {
|
|
140
|
+
content: 'payload-3'
|
|
141
|
+
}
|
|
103
142
|
}
|
|
104
143
|
}
|
|
105
144
|
]));
|
|
@@ -112,16 +151,22 @@ describe('Queue methods', () => {
|
|
|
112
151
|
const queue = getQueue('name', apiClientMock);
|
|
113
152
|
const payload = [
|
|
114
153
|
{
|
|
115
|
-
|
|
154
|
+
body: {
|
|
155
|
+
content: 'payload-1'
|
|
156
|
+
}
|
|
116
157
|
},
|
|
117
158
|
{
|
|
118
|
-
|
|
159
|
+
body: {
|
|
160
|
+
content: 'payload-2'
|
|
161
|
+
}
|
|
119
162
|
},
|
|
120
163
|
{
|
|
121
|
-
|
|
164
|
+
body: {
|
|
165
|
+
content: 'payload-3'
|
|
166
|
+
}
|
|
122
167
|
}
|
|
123
168
|
];
|
|
124
|
-
await expect(queue.push(payload)).rejects.toThrow(new errors_1.PartialSuccessError(`Failed to create stats for job name#12345`, []));
|
|
169
|
+
await expect(queue.push(payload)).rejects.toThrow(new errors_1.PartialSuccessError(`Failed to create stats for job name#12345`, { jobId: 'some-job-id' }, []));
|
|
125
170
|
(0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, payload, 'name');
|
|
126
171
|
});
|
|
127
172
|
it('should throw PartialSuccessError when there are failed events and backend failed to create job stats', async () => {
|
|
@@ -137,17 +182,23 @@ describe('Queue methods', () => {
|
|
|
137
182
|
const queue = getQueue('name', apiClientMock);
|
|
138
183
|
const payload = [
|
|
139
184
|
{
|
|
140
|
-
|
|
185
|
+
body: {
|
|
186
|
+
content: 'payload-1'
|
|
187
|
+
}
|
|
141
188
|
},
|
|
142
189
|
{
|
|
143
|
-
|
|
190
|
+
body: {
|
|
191
|
+
content: 'payload-2'
|
|
192
|
+
}
|
|
144
193
|
}
|
|
145
194
|
];
|
|
146
|
-
await expect(queue.push(payload)).rejects.toThrow(new errors_1.PartialSuccessError(`Failed to process 1 event(s). Failed to create stats for job name#12345`, [
|
|
195
|
+
await expect(queue.push(payload)).rejects.toThrow(new errors_1.PartialSuccessError(`Failed to process 1 event(s). Failed to create stats for job name#12345`, { jobId: 'some-job-id' }, [
|
|
147
196
|
{
|
|
148
197
|
errorMessage: 'failed-1',
|
|
149
|
-
|
|
150
|
-
|
|
198
|
+
event: {
|
|
199
|
+
body: {
|
|
200
|
+
content: 'payload-1'
|
|
201
|
+
}
|
|
151
202
|
}
|
|
152
203
|
}
|
|
153
204
|
]));
|
|
@@ -160,20 +211,18 @@ describe('Queue methods', () => {
|
|
|
160
211
|
details: 'The request processing has failed because of an unknown error, exception or failure'
|
|
161
212
|
}, 500);
|
|
162
213
|
const queue = getQueue('name', apiClientMock);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
(0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, payload, 'name');
|
|
214
|
+
await expect(queue.push(PAYLOAD)).rejects.toThrow(new errors_1.InternalServerError(`500 Status Text: AWS SQS timed out`, 500, 'The request processing has failed because of an unknown error, exception or failure'));
|
|
215
|
+
(0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, [PAYLOAD], 'name');
|
|
166
216
|
});
|
|
167
217
|
it('should throw InternalServerError for error response without response body', async () => {
|
|
168
218
|
const apiClientMock = (0, utils_1.getApiClientMockWithoutResponseBody)(504, 'Gateway Timeout');
|
|
169
219
|
const queue = getQueue('name', apiClientMock);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
(0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, payload, 'name');
|
|
220
|
+
await expect(queue.push(PAYLOAD)).rejects.toThrow(new errors_1.InternalServerError(`504 Gateway Timeout`, 504));
|
|
221
|
+
(0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, [PAYLOAD], 'name');
|
|
173
222
|
});
|
|
174
223
|
it('requests stargate if no api client is provided', async () => {
|
|
175
224
|
const queue = getQueue('queue');
|
|
176
|
-
await queue.push({ test: 'stargate' });
|
|
225
|
+
await queue.push({ body: { test: 'stargate' } });
|
|
177
226
|
expect(api_1.__requestAtlassianAsApp).toHaveBeenCalledTimes(1);
|
|
178
227
|
});
|
|
179
228
|
});
|
package/out/__test__/utils.js
CHANGED
|
@@ -33,7 +33,7 @@ exports.verifyApiClientCalledWith = verifyApiClientCalledWith;
|
|
|
33
33
|
const verifyApiClientCalledPushPathWith = (apiClientMock, payloads, name) => {
|
|
34
34
|
(0, exports.verifyApiClientCalledWith)(apiClientMock, '/webhook/queue/publish/{contextAri}/{environmentId}/{appId}/{appVersion}', {
|
|
35
35
|
queueName: name,
|
|
36
|
-
schema: 'ari:cloud:ecosystem::forge/app-event',
|
|
36
|
+
schema: 'ari:cloud:ecosystem::forge/app-event-2',
|
|
37
37
|
type: 'avi:forge:app:event',
|
|
38
38
|
payload: payloads
|
|
39
39
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface AppEvent {
|
|
2
|
+
key: string;
|
|
3
|
+
}
|
|
4
|
+
export declare type AppEventPublishResult = AppEventPublishSuccess | AppEventPublishError;
|
|
5
|
+
export declare type AppEventPublishSuccess = {
|
|
6
|
+
type: 'success';
|
|
7
|
+
failedEvents: AppEventPublishFailure[];
|
|
8
|
+
};
|
|
9
|
+
export interface AppEventPublishFailure {
|
|
10
|
+
event: AppEvent;
|
|
11
|
+
errorMessage: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AppEventPublishError {
|
|
14
|
+
type: 'error';
|
|
15
|
+
errorType: AppEventErrorType;
|
|
16
|
+
errorMessage: string;
|
|
17
|
+
}
|
|
18
|
+
export declare type AppEventErrorType = 'VALIDATION_ERROR' | 'AUTHENTICATION_ERROR' | 'AUTHORIZATION_ERROR' | 'RATE_LIMIT' | 'SERVICE_ERROR' | 'SERVICE_UNAVAILABLE' | `OTHER`;
|
|
19
|
+
export declare const appEvents: {
|
|
20
|
+
publish(events: AppEvent | AppEvent[]): Promise<AppEventPublishResult>;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=appEvents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appEvents.d.ts","sourceRoot":"","sources":["../src/appEvents.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,oBAAY,qBAAqB,GAAG,sBAAsB,GAAG,oBAAoB,CAAC;AASlF,oBAAY,sBAAsB,GAAG;IACnC,IAAI,EAAE,SAAS,CAAC;IAEhB,YAAY,EAAE,sBAAsB,EAAE,CAAC;CACxC,CAAC;AAKF,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,QAAQ,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAMD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,iBAAiB,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,oBAAY,iBAAiB,GACzB,kBAAkB,GAClB,sBAAsB,GACtB,qBAAqB,GACrB,YAAY,GACZ,eAAe,GACf,qBAAqB,GACrB,OAAO,CAAC;AAsBZ,eAAO,MAAM,SAAS;oBACE,QAAQ,GAAG,QAAQ,EAAE,GAAG,QAAQ,qBAAqB,CAAC;CAwB7E,CAAC"}
|
package/out/appEvents.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.appEvents = void 0;
|
|
4
|
+
const queries_1 = require("./queries");
|
|
5
|
+
const api_1 = require("@forge/api");
|
|
6
|
+
const errorTypes = {
|
|
7
|
+
400: 'VALIDATION_ERROR',
|
|
8
|
+
401: 'AUTHENTICATION_ERROR',
|
|
9
|
+
403: 'AUTHORIZATION_ERROR',
|
|
10
|
+
429: 'RATE_LIMIT',
|
|
11
|
+
500: 'SERVICE_ERROR',
|
|
12
|
+
503: 'SERVICE_UNAVAILABLE'
|
|
13
|
+
};
|
|
14
|
+
const endpoint = '/forge/events/v1/app-events';
|
|
15
|
+
exports.appEvents = {
|
|
16
|
+
async publish(events) {
|
|
17
|
+
const eventsArray = Array.isArray(events) ? events : [events];
|
|
18
|
+
const body = {
|
|
19
|
+
events: eventsArray.map((e) => ({
|
|
20
|
+
key: e.key
|
|
21
|
+
}))
|
|
22
|
+
};
|
|
23
|
+
const response = await (0, queries_1.post)(endpoint, body, api_1.__requestAtlassianAsApp);
|
|
24
|
+
const responseBody = await response.json();
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
return {
|
|
27
|
+
type: 'error',
|
|
28
|
+
errorType: errorTypes[response.status] ?? 'OTHER',
|
|
29
|
+
errorMessage: getErrorMessage(responseBody)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
type: 'success',
|
|
34
|
+
failedEvents: responseBody.failedEvents ?? []
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function getErrorMessage(responseBody) {
|
|
39
|
+
if (responseBody.errorMessages && responseBody.errorMessages.length > 0) {
|
|
40
|
+
return responseBody.errorMessages.join(', ');
|
|
41
|
+
}
|
|
42
|
+
else if (responseBody.errors) {
|
|
43
|
+
return Object.entries(responseBody.errors)
|
|
44
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
45
|
+
.join(', ');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
return JSON.stringify(responseBody);
|
|
49
|
+
}
|
|
50
|
+
}
|
package/out/errors.d.ts
CHANGED
|
@@ -1,35 +1,33 @@
|
|
|
1
|
-
import { FailedEvent } from './types';
|
|
2
|
-
export declare class
|
|
1
|
+
import { FailedEvent, PushResult } from './types';
|
|
2
|
+
export declare class EventsError extends Error {
|
|
3
3
|
constructor(message: string);
|
|
4
4
|
}
|
|
5
|
-
export declare class
|
|
6
|
-
constructor(message: string);
|
|
5
|
+
export declare class InvalidPushSettingsError extends EventsError {
|
|
7
6
|
}
|
|
8
|
-
export declare class
|
|
9
|
-
constructor(message: string);
|
|
7
|
+
export declare class InvalidQueueNameError extends EventsError {
|
|
10
8
|
}
|
|
11
|
-
export declare class
|
|
12
|
-
constructor(message: string);
|
|
9
|
+
export declare class InvalidPayloadError extends EventsError {
|
|
13
10
|
}
|
|
14
|
-
export declare class
|
|
15
|
-
constructor(message: string);
|
|
11
|
+
export declare class TooManyEventsError extends EventsError {
|
|
16
12
|
}
|
|
17
|
-
export declare class
|
|
18
|
-
constructor(message: string);
|
|
13
|
+
export declare class PayloadTooBigError extends EventsError {
|
|
19
14
|
}
|
|
20
|
-
export declare class
|
|
21
|
-
|
|
15
|
+
export declare class NoEventsToPushError extends EventsError {
|
|
16
|
+
}
|
|
17
|
+
export declare class RateLimitError extends EventsError {
|
|
18
|
+
}
|
|
19
|
+
export declare class PartialSuccessError extends EventsError {
|
|
20
|
+
result: PushResult;
|
|
22
21
|
failedEvents: FailedEvent[];
|
|
22
|
+
constructor(message: string, result: PushResult, failedEvents: FailedEvent[]);
|
|
23
23
|
}
|
|
24
|
-
export declare class InternalServerError extends
|
|
24
|
+
export declare class InternalServerError extends EventsError {
|
|
25
25
|
constructor(message: string, errorCode?: number, details?: string);
|
|
26
26
|
errorCode?: number;
|
|
27
27
|
details?: string;
|
|
28
28
|
}
|
|
29
|
-
export declare class JobDoesNotExistError extends
|
|
30
|
-
constructor(message: string);
|
|
29
|
+
export declare class JobDoesNotExistError extends EventsError {
|
|
31
30
|
}
|
|
32
|
-
export declare class InvocationLimitReachedError extends
|
|
33
|
-
constructor(message: string);
|
|
31
|
+
export declare class InvocationLimitReachedError extends EventsError {
|
|
34
32
|
}
|
|
35
33
|
//# sourceMappingURL=errors.d.ts.map
|
package/out/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAElD,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,wBAAyB,SAAQ,WAAW;CAAG;AAE5D,qBAAa,qBAAsB,SAAQ,WAAW;CAAG;AAEzD,qBAAa,mBAAoB,SAAQ,WAAW;CAAG;AAEvD,qBAAa,kBAAmB,SAAQ,WAAW;CAAG;AAEtD,qBAAa,kBAAmB,SAAQ,WAAW;CAAG;AAEtD,qBAAa,mBAAoB,SAAQ,WAAW;CAAG;AAEvD,qBAAa,cAAe,SAAQ,WAAW;CAAG;AAElD,qBAAa,mBAAoB,SAAQ,WAAW;IAGzC,MAAM,EAAE,UAAU;IAClB,YAAY,EAAE,WAAW,EAAE;gBAFlC,OAAO,EAAE,MAAM,EACR,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,WAAW,EAAE;CAIrC;AAED,qBAAa,mBAAoB,SAAQ,WAAW;gBACtC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;IAMjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,oBAAqB,SAAQ,WAAW;CAAG;AAExD,qBAAa,2BAA4B,SAAQ,WAAW;CAAG"}
|