@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +32 -26
  3. package/out/__test__/appEvents.test.d.ts +2 -0
  4. package/out/__test__/appEvents.test.d.ts.map +1 -0
  5. package/out/__test__/appEvents.test.js +137 -0
  6. package/out/__test__/jobProgress.test.js +3 -5
  7. package/out/__test__/queue.test.js +88 -39
  8. package/out/__test__/utils.js +1 -1
  9. package/out/appEvents.d.ts +22 -0
  10. package/out/appEvents.d.ts.map +1 -0
  11. package/out/appEvents.js +50 -0
  12. package/out/errors.d.ts +17 -19
  13. package/out/errors.d.ts.map +1 -1
  14. package/out/errors.js +21 -34
  15. package/out/index.d.ts +3 -1
  16. package/out/index.d.ts.map +1 -1
  17. package/out/index.js +3 -1
  18. package/out/jobProgress.d.ts +8 -3
  19. package/out/jobProgress.d.ts.map +1 -1
  20. package/out/jobProgress.js +2 -2
  21. package/out/queries.d.ts +1 -2
  22. package/out/queries.d.ts.map +1 -1
  23. package/out/queue.d.ts +2 -2
  24. package/out/queue.d.ts.map +1 -1
  25. package/out/queue.js +7 -12
  26. package/out/text.d.ts +2 -0
  27. package/out/text.d.ts.map +1 -1
  28. package/out/text.js +2 -0
  29. package/out/types.d.ts +24 -9
  30. package/out/types.d.ts.map +1 -1
  31. package/out/validators.d.ts +4 -4
  32. package/out/validators.d.ts.map +1 -1
  33. package/out/validators.js +22 -11
  34. package/package.json +1 -1
  35. package/src/__test__/appEvents.test.ts +201 -0
  36. package/src/__test__/jobProgress.test.ts +3 -5
  37. package/src/__test__/queue.test.ts +102 -43
  38. package/src/__test__/utils.ts +1 -1
  39. package/src/appEvents.ts +107 -0
  40. package/src/errors.ts +20 -43
  41. package/src/index.ts +11 -1
  42. package/src/jobProgress.ts +11 -5
  43. package/src/queries.ts +2 -2
  44. package/src/queue.ts +9 -15
  45. package/src/text.ts +2 -0
  46. package/src/types.ts +30 -7
  47. package/src/validators.ts +27 -10
  48. 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
- Library for asynchronous data processing.
1
+ # Forge Events
2
2
 
3
- Usage example:
3
+ Library for [asynchronous data processing](https://developer.atlassian.com/platform/forge/runtime-reference/async-events-api/) in Forge.
4
4
 
5
- ```javascript
6
- import fetch, { RequestInit } from 'node-fetch';
5
+ ## Requirements
7
6
 
8
- import { Queue } from './index';
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
- const API_BASE = 'https://api.atlassian.com';
9
+ ## Usage
11
10
 
12
- // For Async service auth
13
- const appContextAri = 'ari:cloud:jira::site/...';
14
- const token = '...';
11
+ ### Pushing events to the queue
15
12
 
16
- async function apiClient(path: string, init: RequestInit): Promise<APIResponse> {
17
- const url = API_BASE + path;
13
+ ```typescript
18
14
 
19
- const extraHeaders = {
20
- // See add-forge-user-agent.ts
21
- 'X-Forge-Context': appContextAri,
15
+ async function pushEvent() {
16
+ const { jobId } = await queue.push({
17
+ body: {
18
+ hello: 'world'
19
+ }
20
+ });
22
21
 
23
- Authorization: `Bearer ${token}`
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 demo() {
31
- const queue = new Queue({key: "queue-name"}, apiClient);
32
- const payloads = {
33
- page: 1
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=appEvents.test.d.ts.map
@@ -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)('done', 200)
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 response = await jobProgress.getStats();
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
- const response = await jobProgress.cancel();
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('should call the queue/publish endpoint', async () => {
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
- const payload = {
24
- page: 1
25
- };
26
- await queue.push([payload]);
27
- (0, utils_1.verifyApiClientCalledPushPathWith)(apiClientMock, [payload], 'name');
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(1, { delayInSeconds: 901 })).rejects.toThrow(new errors_1.InvalidPushSettingsError(`The delayInSeconds setting must be between 0 and 900.`));
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([...Array(51).keys()])).rejects.toThrow(new errors_1.TooManyEventsError(`This push contains more than the 50 events allowed.`));
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
- const payload = [...Array(10).keys()];
57
- await expect(queue.push(payload)).rejects.toThrow(new errors_1.RateLimitError(`Too many requests.`));
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
- const payload = [...Array(5).keys()];
64
- await expect(queue.push(payload)).rejects.toThrow(new errors_1.InvocationLimitReachedError(`The limit on cyclic invocation has been reached.`));
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
- content: 'payload-1'
112
+ body: {
113
+ content: 'payload-1'
114
+ }
84
115
  },
85
116
  {
86
- content: 'payload-2'
117
+ body: {
118
+ content: 'payload-2'
119
+ }
87
120
  },
88
121
  {
89
- content: 'payload-3'
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
- payload: {
96
- content: 'payload-1'
130
+ event: {
131
+ body: {
132
+ content: 'payload-1'
133
+ }
97
134
  }
98
135
  },
99
136
  {
100
137
  errorMessage: 'failed-3',
101
- payload: {
102
- content: 'payload-3'
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
- content: 'payload-1'
154
+ body: {
155
+ content: 'payload-1'
156
+ }
116
157
  },
117
158
  {
118
- content: 'payload-2'
159
+ body: {
160
+ content: 'payload-2'
161
+ }
119
162
  },
120
163
  {
121
- content: 'payload-3'
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
- content: 'payload-1'
185
+ body: {
186
+ content: 'payload-1'
187
+ }
141
188
  },
142
189
  {
143
- content: 'payload-2'
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
- payload: {
150
- content: 'payload-1'
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
- const payload = [...Array(9).keys()];
164
- 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'));
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
- const payload = [1];
171
- await expect(queue.push(1)).rejects.toThrow(new errors_1.InternalServerError(`504 Gateway Timeout`, 504));
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
  });
@@ -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"}
@@ -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 InvalidPushSettingsError extends Error {
1
+ import { FailedEvent, PushResult } from './types';
2
+ export declare class EventsError extends Error {
3
3
  constructor(message: string);
4
4
  }
5
- export declare class InvalidQueueNameError extends Error {
6
- constructor(message: string);
5
+ export declare class InvalidPushSettingsError extends EventsError {
7
6
  }
8
- export declare class TooManyEventsError extends Error {
9
- constructor(message: string);
7
+ export declare class InvalidQueueNameError extends EventsError {
10
8
  }
11
- export declare class PayloadTooBigError extends Error {
12
- constructor(message: string);
9
+ export declare class InvalidPayloadError extends EventsError {
13
10
  }
14
- export declare class NoEventsToPushError extends Error {
15
- constructor(message: string);
11
+ export declare class TooManyEventsError extends EventsError {
16
12
  }
17
- export declare class RateLimitError extends Error {
18
- constructor(message: string);
13
+ export declare class PayloadTooBigError extends EventsError {
19
14
  }
20
- export declare class PartialSuccessError extends Error {
21
- constructor(message: string, failedEvents: FailedEvent[]);
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 Error {
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 Error {
30
- constructor(message: string);
29
+ export declare class JobDoesNotExistError extends EventsError {
31
30
  }
32
- export declare class InvocationLimitReachedError extends Error {
33
- constructor(message: string);
31
+ export declare class InvocationLimitReachedError extends EventsError {
34
32
  }
35
33
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE;IAKxD,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,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,KAAK;gBACjC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAG5B"}
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"}