@superblocksteam/sabs-client 0.127.0

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.
@@ -0,0 +1,423 @@
1
+ import { ApplicationMetadata, BuildStatus } from '@superblocksteam/sabs-types';
2
+ import axios, { AxiosError, AxiosHeaders } from 'axios';
3
+
4
+ import { SabsClient } from './sabs';
5
+
6
+ describe('sabs service', () => {
7
+ let anyBuildKey: string;
8
+
9
+ beforeEach(() => {
10
+ anyBuildKey = 'any-secret-build-key';
11
+ });
12
+
13
+ afterEach(() => {
14
+ jest.restoreAllMocks();
15
+ });
16
+
17
+ describe('build', () => {
18
+ test.each([
19
+ { accessToken: 'anyScopedJwt', expectedHeaders: { Authorization: 'Bearer anyScopedJwt' } },
20
+ { accessToken: undefined, expectedHeaders: undefined }
21
+ ])('returns expected response with accessToken=$accessToken', async ({ accessToken, expectedHeaders }) => {
22
+ const expectedBuildId = 'expectedBuildId';
23
+ const expectedCreated = new Date();
24
+ const expectedUpdated = new Date();
25
+
26
+ const mockAxios = jest.spyOn(axios, 'request');
27
+ mockAxios.mockResolvedValue({
28
+ data: {
29
+ buildId: expectedBuildId,
30
+ created: expectedCreated,
31
+ updated: expectedUpdated
32
+ }
33
+ });
34
+
35
+ const anyDirectoryHash = 'anyDirectoryHash';
36
+ const anyApplicationMetadata = new ApplicationMetadata({
37
+ id: 'anyApplicationId',
38
+ organizationId: 'anyOrganizationId'
39
+ });
40
+
41
+ const sabs = new SabsClient('http://localhost:3000');
42
+ const result = await sabs.build({
43
+ directoryHash: anyDirectoryHash,
44
+ meta: anyApplicationMetadata,
45
+ buildKey: anyBuildKey,
46
+ accessToken
47
+ });
48
+
49
+ expect(result).toEqual({
50
+ buildId: expectedBuildId,
51
+ created: expectedCreated,
52
+ updated: expectedUpdated
53
+ });
54
+ expect(mockAxios).toHaveBeenCalledWith({
55
+ method: 'POST',
56
+ url: 'http://localhost:3000/v1/builds',
57
+ headers: expectedHeaders,
58
+ data: {
59
+ directoryHash: anyDirectoryHash,
60
+ applicationMetadata: anyApplicationMetadata,
61
+ buildKey: anyBuildKey
62
+ }
63
+ });
64
+ });
65
+
66
+ test('raises error when request fails', async () => {
67
+ const mockAxios = jest.spyOn(axios, 'request');
68
+ mockAxios.mockRejectedValue(new Error('any error'));
69
+
70
+ const anyDirectoryHash = 'anyDirectoryHash';
71
+ const anyApplicationMetadata = new ApplicationMetadata({
72
+ id: 'anyApplicationId',
73
+ organizationId: 'anyOrganizationId'
74
+ });
75
+
76
+ const sabs = new SabsClient('http://localhost:3000');
77
+ await expect(sabs.build({ directoryHash: anyDirectoryHash, meta: anyApplicationMetadata, buildKey: anyBuildKey })).rejects.toThrow();
78
+
79
+ expect(mockAxios).toHaveBeenCalledWith({
80
+ method: 'POST',
81
+ url: 'http://localhost:3000/v1/builds',
82
+ data: {
83
+ directoryHash: anyDirectoryHash,
84
+ applicationMetadata: anyApplicationMetadata,
85
+ buildKey: anyBuildKey
86
+ }
87
+ });
88
+ });
89
+ });
90
+
91
+ describe('status', () => {
92
+ test.each([
93
+ { accessToken: 'anyScopedJwt', expectedHeaders: { Authorization: 'Bearer anyScopedJwt' } },
94
+ { accessToken: undefined, expectedHeaders: undefined }
95
+ ])('returns expected response with accessToken=$accessToken', async ({ accessToken, expectedHeaders }) => {
96
+ const expectedBuildId = 'expectedBuildId';
97
+ const expectedStatus = BuildStatus.SUCCESS;
98
+ const expectedCreated = new Date();
99
+ const expectedUpdated = new Date();
100
+
101
+ const mockAxios = jest.spyOn(axios, 'request');
102
+ mockAxios.mockResolvedValue({
103
+ data: {
104
+ buildId: expectedBuildId,
105
+ status: expectedStatus,
106
+ created: expectedCreated,
107
+ updated: expectedUpdated
108
+ }
109
+ });
110
+
111
+ const anyBuildId = 'anyBuildId';
112
+
113
+ const sabs = new SabsClient('http://localhost:3000');
114
+ const result = await sabs.status({ buildId: anyBuildId, accessToken });
115
+
116
+ expect(result).toEqual({
117
+ buildId: expectedBuildId,
118
+ status: expectedStatus,
119
+ created: expectedCreated,
120
+ updated: expectedUpdated
121
+ });
122
+ expect(mockAxios).toHaveBeenCalledWith({
123
+ method: 'GET',
124
+ url: `http://localhost:3000/v1/builds/${anyBuildId}`,
125
+ headers: expectedHeaders
126
+ });
127
+ });
128
+
129
+ test('raises error when request fails', async () => {
130
+ const mockAxios = jest.spyOn(axios, 'request');
131
+ mockAxios.mockRejectedValue(new Error('any error'));
132
+
133
+ const anyBuildId = 'anyBuildId';
134
+
135
+ const sabs = new SabsClient('http://localhost:3000');
136
+ await expect(sabs.status({ buildId: anyBuildId })).rejects.toThrow();
137
+
138
+ expect(mockAxios).toHaveBeenCalledWith({
139
+ method: 'GET',
140
+ url: `http://localhost:3000/v1/builds/${anyBuildId}`
141
+ });
142
+ });
143
+ });
144
+
145
+ describe('list', () => {
146
+ test.each([
147
+ { accessToken: 'anyScopedJwt', expectedHeaders: { Authorization: 'Bearer anyScopedJwt' } },
148
+ { accessToken: undefined, expectedHeaders: undefined }
149
+ ])('returns expected response with accessToken=$accessToken', async ({ accessToken, expectedHeaders }) => {
150
+ const mockAxios = jest.spyOn(axios, 'request');
151
+ mockAxios.mockResolvedValue({
152
+ data: {
153
+ builds: [
154
+ {
155
+ buildId: 'id1',
156
+ status: BuildStatus.SUCCESS,
157
+ created: new Date('2023-01-01'),
158
+ updated: new Date('2023-01-02')
159
+ },
160
+ {
161
+ buildId: 'id2',
162
+ status: BuildStatus.RUNNING,
163
+ created: new Date('2023-01-03'),
164
+ updated: new Date('2023-01-04')
165
+ },
166
+ {
167
+ buildId: 'id3',
168
+ status: BuildStatus.FAILED,
169
+ error: 'Build failed',
170
+ created: new Date('2023-01-05'),
171
+ updated: new Date('2023-01-06')
172
+ }
173
+ ]
174
+ }
175
+ });
176
+
177
+ const anyOrganizationId = 'anyOrganizationId';
178
+ const anyApplicationId = 'anyApplicationId';
179
+ const anyDirectoryHash = 'anyDirectoryHash';
180
+
181
+ const sabs = new SabsClient('http://localhost:3000');
182
+ const result = await sabs.list({
183
+ organizationId: anyOrganizationId,
184
+ applicationId: anyApplicationId,
185
+ directoryHash: anyDirectoryHash,
186
+ accessToken
187
+ });
188
+
189
+ expect(result).toEqual({
190
+ builds: [
191
+ {
192
+ buildId: 'id1',
193
+ status: BuildStatus.SUCCESS,
194
+ created: new Date('2023-01-01'),
195
+ updated: new Date('2023-01-02')
196
+ },
197
+ {
198
+ buildId: 'id2',
199
+ status: BuildStatus.RUNNING,
200
+ created: new Date('2023-01-03'),
201
+ updated: new Date('2023-01-04')
202
+ },
203
+ {
204
+ buildId: 'id3',
205
+ status: BuildStatus.FAILED,
206
+ error: 'Build failed',
207
+ created: new Date('2023-01-05'),
208
+ updated: new Date('2023-01-06')
209
+ }
210
+ ]
211
+ });
212
+ expect(mockAxios).toHaveBeenCalledWith({
213
+ method: 'GET',
214
+ url: `http://localhost:3000/v1/build`,
215
+ headers: expectedHeaders,
216
+ params: {
217
+ organizationId: anyOrganizationId,
218
+ applicationId: anyApplicationId,
219
+ directoryHash: anyDirectoryHash
220
+ }
221
+ });
222
+ });
223
+
224
+ test('raises error when request fails', async () => {
225
+ const mockAxios = jest.spyOn(axios, 'request');
226
+ mockAxios.mockRejectedValue(new Error('any error'));
227
+
228
+ const anyOrganizationId = 'anyOrganizationId';
229
+ const anyApplicationId = 'anyApplicationId';
230
+ const anyDirectoryHash = 'anyDirectoryHash';
231
+
232
+ const sabs = new SabsClient('http://localhost:3000');
233
+ await expect(
234
+ sabs.list({
235
+ organizationId: anyOrganizationId,
236
+ applicationId: anyApplicationId,
237
+ directoryHash: anyDirectoryHash
238
+ })
239
+ ).rejects.toThrow();
240
+
241
+ expect(mockAxios).toHaveBeenCalledWith({
242
+ method: 'GET',
243
+ url: `http://localhost:3000/v1/build`,
244
+ headers: undefined,
245
+ params: {
246
+ organizationId: anyOrganizationId,
247
+ applicationId: anyApplicationId,
248
+ directoryHash: anyDirectoryHash
249
+ }
250
+ });
251
+ });
252
+ });
253
+
254
+ describe('terminate', () => {
255
+ test.each([
256
+ { accessToken: 'anyScopedJwt', expectedHeaders: { Authorization: 'Bearer anyScopedJwt' } },
257
+ { accessToken: undefined, expectedHeaders: undefined }
258
+ ])('returns expected response with accessToken=$accessToken', async ({ accessToken, expectedHeaders }) => {
259
+ const expectedBuildId = 'expectedBuildId';
260
+ const expectedStatus = BuildStatus.TIMED_OUT;
261
+ const expectedError = 'build timed out';
262
+ const expectedCreated = new Date();
263
+ const expectedUpdated = new Date();
264
+
265
+ const mockAxios = jest.spyOn(axios, 'request');
266
+ mockAxios.mockResolvedValue({
267
+ data: {
268
+ buildId: expectedBuildId,
269
+ status: expectedStatus,
270
+ error: expectedError,
271
+ created: expectedCreated,
272
+ updated: expectedUpdated
273
+ }
274
+ });
275
+
276
+ const anyBuildId = 'anyBuildId';
277
+ const anyStatus = BuildStatus.TIMED_OUT;
278
+ const anyError = 'build timed out';
279
+
280
+ const sabs = new SabsClient('http://localhost:3000');
281
+ const result = await sabs.terminate({ buildId: anyBuildId, status: anyStatus, buildKey: anyBuildKey, error: anyError, accessToken });
282
+
283
+ expect(result).toEqual({
284
+ buildId: expectedBuildId,
285
+ status: expectedStatus,
286
+ error: expectedError,
287
+ created: expectedCreated,
288
+ updated: expectedUpdated
289
+ });
290
+ expect(mockAxios).toHaveBeenCalledWith({
291
+ method: 'POST',
292
+ url: `http://localhost:3000/v1/builds/${anyBuildId}/terminate`,
293
+ headers: expectedHeaders,
294
+ data: {
295
+ buildId: anyBuildId,
296
+ status: anyStatus,
297
+ error: anyError,
298
+ buildKey: anyBuildKey
299
+ }
300
+ });
301
+ });
302
+
303
+ test('raises error when request fails', async () => {
304
+ const mockAxios = jest.spyOn(axios, 'request');
305
+ mockAxios.mockRejectedValue(new Error('any error'));
306
+
307
+ const anyBuildId = 'anyBuildId';
308
+ const anyStatus = BuildStatus.TIMED_OUT;
309
+ const anyError = 'build timed out';
310
+
311
+ const sabs = new SabsClient('http://localhost:3000');
312
+ await expect(sabs.terminate({ buildId: anyBuildId, status: anyStatus, buildKey: anyBuildKey, error: anyError })).rejects.toThrow();
313
+
314
+ expect(mockAxios).toHaveBeenCalledWith({
315
+ method: 'POST',
316
+ url: `http://localhost:3000/v1/builds/${anyBuildId}/terminate`,
317
+ data: {
318
+ buildId: anyBuildId,
319
+ status: anyStatus,
320
+ error: anyError,
321
+ buildKey: anyBuildKey
322
+ }
323
+ });
324
+ });
325
+ });
326
+
327
+ describe('bulkStatus', () => {
328
+ test('returns expected response', async () => {
329
+ const mockAxios = jest.spyOn(axios, 'request');
330
+ mockAxios.mockResolvedValue({
331
+ data: {
332
+ builds: [
333
+ {
334
+ buildId: 'build1',
335
+ status: BuildStatus.SUCCESS,
336
+ created: new Date('2023-01-01'),
337
+ updated: new Date('2023-01-02')
338
+ },
339
+ {
340
+ buildId: 'build2',
341
+ status: BuildStatus.RUNNING,
342
+ created: new Date('2023-01-03'),
343
+ updated: new Date('2023-01-04')
344
+ }
345
+ ]
346
+ }
347
+ });
348
+
349
+ const anyOrganizationId = 'anyOrganizationId';
350
+ const anyApplicationId = 'anyApplicationId';
351
+ const anyDirectoryHashes = ['hash1', 'hash2'];
352
+
353
+ const sabs = new SabsClient('http://localhost:3000');
354
+ const result = await sabs.bulkStatus({
355
+ organizationId: anyOrganizationId,
356
+ applicationId: anyApplicationId,
357
+ directoryHashes: anyDirectoryHashes
358
+ });
359
+
360
+ expect(result).toEqual({
361
+ builds: [
362
+ {
363
+ buildId: 'build1',
364
+ status: BuildStatus.SUCCESS,
365
+ created: new Date('2023-01-01'),
366
+ updated: new Date('2023-01-02')
367
+ },
368
+ {
369
+ buildId: 'build2',
370
+ status: BuildStatus.RUNNING,
371
+ created: new Date('2023-01-03'),
372
+ updated: new Date('2023-01-04')
373
+ }
374
+ ]
375
+ });
376
+ expect(mockAxios).toHaveBeenCalledWith({
377
+ method: 'POST',
378
+ url: `http://localhost:3000/v1/builds/${anyOrganizationId}/${anyApplicationId}/bulk-status`,
379
+ data: {
380
+ organizationId: anyOrganizationId,
381
+ applicationId: anyApplicationId,
382
+ directoryHashes: anyDirectoryHashes
383
+ }
384
+ });
385
+ });
386
+ });
387
+
388
+ describe('executeRequest', () => {
389
+ test('raies expected error when error is axios error', async () => {
390
+ const mockAxios = jest.spyOn(axios, 'request');
391
+ mockAxios.mockRejectedValue(
392
+ new AxiosError('request failed', 'internal', undefined, undefined, {
393
+ headers: {},
394
+ config: {
395
+ headers: new AxiosHeaders()
396
+ },
397
+ status: 500,
398
+ statusText: 'internal server error',
399
+ data: 'failed to process request'
400
+ })
401
+ );
402
+
403
+ const anyBuildId = 'anyBuildId';
404
+
405
+ const expectedError = 'sabs service request failed: request failed\n[500] internal server error: "failed to process request"';
406
+
407
+ const sabs = new SabsClient('http://localhost:3000');
408
+ await expect(sabs.status({ buildId: anyBuildId })).rejects.toThrow(expectedError);
409
+ });
410
+
411
+ test('re-raises error when error is not axios error', async () => {
412
+ const mockAxios = jest.spyOn(axios, 'request');
413
+ mockAxios.mockRejectedValue(new Error('unexpected error'));
414
+
415
+ const anyBuildId = 'anyBuildId';
416
+
417
+ const expectedError = 'sabs service request failed: unexpected error';
418
+
419
+ const sabs = new SabsClient('http://localhost:3000');
420
+ await expect(sabs.status({ buildId: anyBuildId })).rejects.toThrow(expectedError);
421
+ });
422
+ });
423
+ });
package/src/sabs.ts ADDED
@@ -0,0 +1,203 @@
1
+ import {
2
+ ApplicationMetadata,
3
+ BuildRequest,
4
+ BuildResponse,
5
+ BuildStatus,
6
+ ListRequest,
7
+ ListResponse,
8
+ StatusResponse,
9
+ TerminateRequest,
10
+ TerminateResponse,
11
+ BulkStatusRequest,
12
+ BulkStatusResponse,
13
+ CreateLiveEditRequest,
14
+ CreateLiveEditResponse
15
+ } from '@superblocksteam/sabs-types';
16
+ import axios, { AxiosRequestConfig, RawAxiosRequestHeaders } from 'axios';
17
+
18
+ export class SabsClient {
19
+ private readonly baseUrl: string;
20
+
21
+ public constructor(baseUrl: string) {
22
+ this.baseUrl = baseUrl;
23
+ }
24
+
25
+ public async build({
26
+ directoryHash,
27
+ meta,
28
+ buildKey,
29
+ accessToken
30
+ }: {
31
+ directoryHash: string;
32
+ meta: ApplicationMetadata;
33
+ buildKey: string;
34
+ accessToken?: string;
35
+ }): Promise<BuildResponse> {
36
+ const data = new BuildRequest({
37
+ directoryHash: directoryHash,
38
+ applicationMetadata: meta,
39
+ buildKey
40
+ });
41
+
42
+ return this.executeRequest<BuildResponse>(
43
+ {
44
+ method: 'POST',
45
+ url: `${this.baseUrl}/v1/builds`,
46
+ data
47
+ },
48
+ accessToken
49
+ );
50
+ }
51
+
52
+ public async status({ buildId, accessToken }: { buildId: string; accessToken?: string }): Promise<StatusResponse> {
53
+ return this.executeRequest<StatusResponse>(
54
+ {
55
+ method: 'GET',
56
+ url: `${this.baseUrl}/v1/builds/${buildId}`
57
+ },
58
+ accessToken
59
+ );
60
+ }
61
+
62
+ public async bulkStatus({
63
+ organizationId,
64
+ applicationId,
65
+ directoryHashes,
66
+ accessToken
67
+ }: {
68
+ organizationId: string;
69
+ applicationId: string;
70
+ directoryHashes: string[];
71
+ accessToken?: string;
72
+ }): Promise<BulkStatusResponse> {
73
+ const data = new BulkStatusRequest({
74
+ organizationId,
75
+ applicationId,
76
+ directoryHashes
77
+ });
78
+
79
+ return this.executeRequest<BulkStatusResponse>(
80
+ {
81
+ method: 'POST',
82
+ url: `${this.baseUrl}/v1/builds/${organizationId}/${applicationId}/bulk-status`,
83
+ data
84
+ },
85
+ accessToken
86
+ );
87
+ }
88
+
89
+ public async list({
90
+ organizationId,
91
+ applicationId,
92
+ directoryHash,
93
+ accessToken
94
+ }: {
95
+ organizationId: string;
96
+ applicationId: string;
97
+ directoryHash: string;
98
+ accessToken?: string;
99
+ }): Promise<ListResponse> {
100
+ const data = new ListRequest({
101
+ organizationId,
102
+ applicationId,
103
+ directoryHash
104
+ });
105
+
106
+ return this.executeRequest<ListResponse>(
107
+ {
108
+ method: 'GET',
109
+ url: `${this.baseUrl}/v1/build`,
110
+ params: data
111
+ },
112
+ accessToken
113
+ );
114
+ }
115
+
116
+ public async terminate({
117
+ buildId,
118
+ status,
119
+ buildKey,
120
+ error,
121
+ accessToken
122
+ }: {
123
+ buildId: string;
124
+ status: BuildStatus;
125
+ buildKey?: string;
126
+ error?: string;
127
+ accessToken?: string;
128
+ }): Promise<TerminateResponse> {
129
+ const data = new TerminateRequest({
130
+ buildId,
131
+ status,
132
+ error,
133
+ buildKey
134
+ });
135
+
136
+ return this.executeRequest<TerminateResponse>(
137
+ {
138
+ method: 'POST',
139
+ url: `${this.baseUrl}/v1/builds/${buildId}/terminate`,
140
+ data
141
+ },
142
+ accessToken
143
+ );
144
+ }
145
+
146
+ public async createLiveEdit({
147
+ applicationId,
148
+ organizationId,
149
+ branch,
150
+ expiresIn,
151
+ accessToken
152
+ }: {
153
+ applicationId: string;
154
+ organizationId: string;
155
+ branch: string;
156
+ expiresIn: number;
157
+ accessToken: string;
158
+ }): Promise<CreateLiveEditResponse> {
159
+ const data = new CreateLiveEditRequest({
160
+ application: {
161
+ applicationId,
162
+ organizationId: organizationId,
163
+ branch: branch
164
+ },
165
+ sessionJwt: accessToken,
166
+ expiresIn: BigInt(expiresIn)
167
+ });
168
+
169
+ return this.executeRequest<CreateLiveEditResponse>(
170
+ {
171
+ method: 'POST',
172
+ url: `${this.baseUrl}/v1/live-edit`,
173
+ data
174
+ },
175
+ accessToken
176
+ );
177
+ }
178
+
179
+ private async executeRequest<T>(config: AxiosRequestConfig, accessToken?: string): Promise<T> {
180
+ let headers: RawAxiosRequestHeaders | undefined;
181
+ if (accessToken || config.headers) {
182
+ headers = {
183
+ ...config.headers,
184
+ Authorization: accessToken ? `Bearer ${accessToken}` : undefined
185
+ };
186
+ }
187
+
188
+ try {
189
+ const response = await axios.request<T>({
190
+ ...config,
191
+ headers
192
+ });
193
+ return response.data;
194
+ } catch (error) {
195
+ let errMsg = `sabs service request failed: ${error.message}`;
196
+ if (axios.isAxiosError(error)) {
197
+ errMsg += `\n[${error.response?.status}] ${error.response?.statusText}: ${JSON.stringify(error.response?.data)}`;
198
+ }
199
+
200
+ throw new Error(errMsg);
201
+ }
202
+ }
203
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "commonjs",
5
+ "composite": true,
6
+ "incremental": true,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "moduleResolution": "node",
11
+ "esModuleInterop": true,
12
+ "resolveJsonModule": true,
13
+ "rootDir": "./src",
14
+ "outDir": "./dist",
15
+ "allowSyntheticDefaultImports": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "resolveJsonModule": true,
18
+ "strictNullChecks": true,
19
+ "allowJs": false,
20
+ "skipLibCheck": true
21
+ },
22
+ "exclude": ["./dist", "./node_modules"],
23
+ "include": ["./**/*.ts"]
24
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "noEmit": true
6
+ },
7
+ "include": ["./test/**/*.ts", "./src/**/*.ts", "./*.config.js", "./src/**/*.json"],
8
+ "exclude": ["node_modules"]
9
+ }