@forge/os 1.1.0-next.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.
- package/README.md +6 -0
- package/out/__test__/os.test.d.ts +2 -0
- package/out/__test__/os.test.d.ts.map +1 -0
- package/out/__test__/os.test.js +172 -0
- package/out/errorCodes.d.ts +5 -0
- package/out/errorCodes.d.ts.map +1 -0
- package/out/errorCodes.js +7 -0
- package/out/errors.d.ts +21 -0
- package/out/errors.d.ts.map +1 -0
- package/out/errors.js +26 -0
- package/out/index.d.ts +6 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +8 -0
- package/out/os.d.ts +13 -0
- package/out/os.d.ts.map +1 -0
- package/out/os.js +77 -0
- package/out/utils/__test__/error-handling.test.d.ts +2 -0
- package/out/utils/__test__/error-handling.test.d.ts.map +1 -0
- package/out/utils/__test__/error-handling.test.js +188 -0
- package/out/utils/error-handling.d.ts +7 -0
- package/out/utils/error-handling.d.ts.map +1 -0
- package/out/utils/error-handling.js +43 -0
- package/out/utils/types.d.ts +3 -0
- package/out/utils/types.d.ts.map +1 -0
- package/out/utils/types.js +2 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"os.test.d.ts","sourceRoot":"","sources":["../../src/__test__/os.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const api_1 = require("@forge/api");
|
|
4
|
+
const os_1 = require("../os");
|
|
5
|
+
jest.mock('@forge/api');
|
|
6
|
+
const DEFAULT_HEADERS = {
|
|
7
|
+
Accept: 'application/json',
|
|
8
|
+
'Content-Type': 'application/json'
|
|
9
|
+
};
|
|
10
|
+
const createMockBody = (body) => {
|
|
11
|
+
return jest.fn().mockResolvedValue(body);
|
|
12
|
+
};
|
|
13
|
+
describe('ObjectStoreClient', () => {
|
|
14
|
+
let osClient;
|
|
15
|
+
let mockFetch;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
osClient = new os_1.ObjectStoreClient();
|
|
18
|
+
mockFetch = jest.fn();
|
|
19
|
+
api_1.__fetchProduct.mockReturnValue(mockFetch);
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
describe('sendRequest', () => {
|
|
23
|
+
it('should send a request with the correct options and return the response', async () => {
|
|
24
|
+
const mockResponse = { ok: true, status: 200 };
|
|
25
|
+
mockFetch.mockResolvedValue(mockResponse);
|
|
26
|
+
const path = 'api/v1/objects/1';
|
|
27
|
+
const options = { method: 'GET', headers: { ...DEFAULT_HEADERS, 'Custom-Header': 'value' } };
|
|
28
|
+
const response = await osClient['sendRequest'](path, options);
|
|
29
|
+
expect(mockFetch).toHaveBeenCalledWith(path, {
|
|
30
|
+
...options,
|
|
31
|
+
redirect: 'follow',
|
|
32
|
+
headers: {
|
|
33
|
+
...options.headers,
|
|
34
|
+
...DEFAULT_HEADERS
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
expect(response).toBe(mockResponse);
|
|
38
|
+
});
|
|
39
|
+
it('should handle requests without options', async () => {
|
|
40
|
+
const mockResponse = { ok: true, status: 200 };
|
|
41
|
+
mockFetch.mockResolvedValue(mockResponse);
|
|
42
|
+
const path = 'api/v1/objects/1';
|
|
43
|
+
const response = await osClient['sendRequest'](path);
|
|
44
|
+
expect(mockFetch).toHaveBeenCalledWith(path, {
|
|
45
|
+
redirect: 'follow',
|
|
46
|
+
headers: {
|
|
47
|
+
...DEFAULT_HEADERS
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
expect(response).toBe(mockResponse);
|
|
51
|
+
});
|
|
52
|
+
it('should handle requests that override the default headers', async () => {
|
|
53
|
+
const mockResponse = { ok: true, status: 200 };
|
|
54
|
+
mockFetch.mockResolvedValue(mockResponse);
|
|
55
|
+
const path = 'api/v1/objects/1';
|
|
56
|
+
const options = { method: 'GET', headers: { 'Content-Type': 'application/octet-stream' } };
|
|
57
|
+
const response = await osClient['sendRequest'](path, options);
|
|
58
|
+
expect(mockFetch).toHaveBeenCalledWith(path, {
|
|
59
|
+
redirect: 'follow',
|
|
60
|
+
method: 'GET',
|
|
61
|
+
headers: {
|
|
62
|
+
...DEFAULT_HEADERS,
|
|
63
|
+
'Content-Type': 'application/octet-stream'
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
expect(response).toBe(mockResponse);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('echo', () => {
|
|
70
|
+
it('should send a GET request to the echo endpoint', async () => {
|
|
71
|
+
const mockResponse = {
|
|
72
|
+
ok: true,
|
|
73
|
+
status: 200,
|
|
74
|
+
text: createMockBody(JSON.stringify({ message: 'Blåhaj' }))
|
|
75
|
+
};
|
|
76
|
+
mockFetch.mockResolvedValue(mockResponse);
|
|
77
|
+
const response = await osClient.echo();
|
|
78
|
+
expect(mockFetch).toHaveBeenCalledWith('api/v1/echo', {
|
|
79
|
+
method: 'GET',
|
|
80
|
+
redirect: 'follow',
|
|
81
|
+
headers: {
|
|
82
|
+
...DEFAULT_HEADERS
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
expect(mockResponse.status).toBe(200);
|
|
86
|
+
expect(mockResponse.text).toHaveBeenCalled();
|
|
87
|
+
expect(response).toEqual({ message: 'Blåhaj' });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('put', () => {
|
|
91
|
+
it('should send a PUT request with the correct headers and body', async () => {
|
|
92
|
+
const mockResponse = { ok: true, status: 201, text: createMockBody(null) };
|
|
93
|
+
mockFetch.mockResolvedValue(mockResponse);
|
|
94
|
+
const objectId = '1';
|
|
95
|
+
const buffer = Buffer.from('hello world');
|
|
96
|
+
await osClient.put(objectId, buffer);
|
|
97
|
+
expect(mockFetch).toHaveBeenCalledWith('api/v1/objects/1', {
|
|
98
|
+
method: 'PUT',
|
|
99
|
+
redirect: 'follow',
|
|
100
|
+
headers: {
|
|
101
|
+
...DEFAULT_HEADERS,
|
|
102
|
+
'Content-Type': 'application/octet-stream'
|
|
103
|
+
},
|
|
104
|
+
body: buffer
|
|
105
|
+
});
|
|
106
|
+
expect(mockResponse.status).toBe(201);
|
|
107
|
+
expect(mockResponse.text).toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('get', () => {
|
|
111
|
+
it('should send a GET request with the correct headers and b', async () => {
|
|
112
|
+
const mockResponse = {
|
|
113
|
+
ok: true,
|
|
114
|
+
status: 200,
|
|
115
|
+
text: createMockBody(JSON.stringify({ message: 'Blåhaj' }))
|
|
116
|
+
};
|
|
117
|
+
mockFetch.mockResolvedValue(mockResponse);
|
|
118
|
+
const objectId = '1';
|
|
119
|
+
const response = await osClient.get(objectId);
|
|
120
|
+
expect(mockFetch).toHaveBeenCalledWith('api/v1/objects/1', {
|
|
121
|
+
method: 'GET',
|
|
122
|
+
redirect: 'follow',
|
|
123
|
+
headers: {
|
|
124
|
+
...DEFAULT_HEADERS
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
expect(mockResponse.status).toBe(200);
|
|
128
|
+
expect(mockResponse.text).toHaveBeenCalled();
|
|
129
|
+
expect(response).toEqual({ message: 'Blåhaj' });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('delete', () => {
|
|
133
|
+
it('should send a DELETE request with the correct headers', async () => {
|
|
134
|
+
const mockResponse = { ok: true, status: 204, text: createMockBody(null) };
|
|
135
|
+
mockFetch.mockResolvedValue(mockResponse);
|
|
136
|
+
const objectId = '1';
|
|
137
|
+
await osClient.delete(objectId);
|
|
138
|
+
expect(mockFetch).toHaveBeenCalledWith('api/v1/objects/1', {
|
|
139
|
+
method: 'DELETE',
|
|
140
|
+
redirect: 'follow',
|
|
141
|
+
headers: {
|
|
142
|
+
...DEFAULT_HEADERS
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
expect(mockResponse.status).toBe(204);
|
|
146
|
+
expect(mockResponse.text).toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('download', () => {
|
|
150
|
+
it('should send a GET request with the correct headers and return a buffer', async () => {
|
|
151
|
+
const mockResponse = {
|
|
152
|
+
ok: true,
|
|
153
|
+
status: 200,
|
|
154
|
+
arrayBuffer: createMockBody(Buffer.from('I shall be downloaded as buffer!'))
|
|
155
|
+
};
|
|
156
|
+
mockFetch.mockResolvedValue(mockResponse);
|
|
157
|
+
const objectId = '1';
|
|
158
|
+
const response = await osClient.download(objectId);
|
|
159
|
+
expect(mockFetch).toHaveBeenCalledWith('api/v1/objects/1', {
|
|
160
|
+
method: 'GET',
|
|
161
|
+
redirect: 'follow',
|
|
162
|
+
headers: {
|
|
163
|
+
...DEFAULT_HEADERS,
|
|
164
|
+
Accept: 'application/octet-stream'
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
expect(mockResponse.status).toBe(200);
|
|
168
|
+
expect(mockResponse.arrayBuffer).toHaveBeenCalled();
|
|
169
|
+
expect(response?.toString('utf-8')).toEqual('I shall be downloaded as buffer!');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errorCodes.d.ts","sourceRoot":"","sources":["../src/errorCodes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU;;;CAGb,CAAC"}
|
package/out/errors.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ForgeError {
|
|
2
|
+
code: string;
|
|
3
|
+
message: string;
|
|
4
|
+
context?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export interface APIErrorResponseDetails {
|
|
7
|
+
status: number;
|
|
8
|
+
statusText: string;
|
|
9
|
+
traceId?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare class ForgeObjectStoreError extends Error {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class ForgeObjectStoreAPIError extends ForgeObjectStoreError {
|
|
15
|
+
responseDetails: APIErrorResponseDetails;
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
context: Record<string, unknown>;
|
|
19
|
+
constructor(responseDetails: APIErrorResponseDetails, forgeError: ForgeError);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,wBAAyB,SAAQ,qBAAqB;IACjE,eAAe,EAAE,uBAAuB,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAErB,eAAe,EAAE,uBAAuB,EAAE,UAAU,EAAE,UAAU;CAU7E"}
|
package/out/errors.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ForgeObjectStoreAPIError = exports.ForgeObjectStoreError = void 0;
|
|
4
|
+
class ForgeObjectStoreError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ForgeObjectStoreError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.ForgeObjectStoreError = ForgeObjectStoreError;
|
|
11
|
+
class ForgeObjectStoreAPIError extends ForgeObjectStoreError {
|
|
12
|
+
responseDetails;
|
|
13
|
+
code;
|
|
14
|
+
message;
|
|
15
|
+
context;
|
|
16
|
+
constructor(responseDetails, forgeError) {
|
|
17
|
+
super(forgeError.message);
|
|
18
|
+
const { status, statusText, traceId } = responseDetails;
|
|
19
|
+
this.responseDetails = { status, statusText, traceId };
|
|
20
|
+
const { code, message, context, ...bodyData } = forgeError;
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.message = message;
|
|
23
|
+
this.context = { ...context, ...bodyData };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.ForgeObjectStoreAPIError = ForgeObjectStoreAPIError;
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,MAAM,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC;AAEtC,eAAe,EAAE,CAAC"}
|
package/out/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.os = exports.errorCodes = void 0;
|
|
4
|
+
const os_1 = require("./os");
|
|
5
|
+
Object.defineProperty(exports, "os", { enumerable: true, get: function () { return os_1.os; } });
|
|
6
|
+
const errorCodes_1 = require("./errorCodes");
|
|
7
|
+
Object.defineProperty(exports, "errorCodes", { enumerable: true, get: function () { return errorCodes_1.errorCodes; } });
|
|
8
|
+
exports.default = os_1.os;
|
package/out/os.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
export declare class ObjectStoreClient {
|
|
3
|
+
private sendRequest;
|
|
4
|
+
private storageApi;
|
|
5
|
+
echo(): Promise<string | null>;
|
|
6
|
+
put(objectId: string, buffer: Buffer): Promise<void>;
|
|
7
|
+
get<DataType>(objectId: string): Promise<DataType | null>;
|
|
8
|
+
delete(objectId: string): Promise<void>;
|
|
9
|
+
listVersions(objectId: string): Promise<unknown>;
|
|
10
|
+
download(objectId: string): Promise<Buffer | null>;
|
|
11
|
+
}
|
|
12
|
+
export declare const os: ObjectStoreClient;
|
|
13
|
+
//# sourceMappingURL=os.d.ts.map
|
package/out/os.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"os.d.ts","sourceRoot":"","sources":["../src/os.ts"],"names":[],"mappings":";AAgBA,qBAAa,iBAAiB;YAEd,WAAW;YAYX,UAAU;IA8BX,IAAI;IAIJ,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQpD,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAIzD,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,YAAY,CAAC,QAAQ,EAAE,MAAM;IAI7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAOhE;AAED,eAAO,MAAM,EAAE,mBAA0B,CAAC"}
|
package/out/os.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.os = exports.ObjectStoreClient = void 0;
|
|
4
|
+
const api_1 = require("@forge/api");
|
|
5
|
+
const error_handling_1 = require("./utils/error-handling");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
var ValidHttpMethod;
|
|
8
|
+
(function (ValidHttpMethod) {
|
|
9
|
+
ValidHttpMethod["PUT"] = "PUT";
|
|
10
|
+
ValidHttpMethod["GET"] = "GET";
|
|
11
|
+
ValidHttpMethod["DELETE"] = "DELETE";
|
|
12
|
+
})(ValidHttpMethod || (ValidHttpMethod = {}));
|
|
13
|
+
const DEFAULT_HEADERS = {
|
|
14
|
+
Accept: 'application/json',
|
|
15
|
+
'Content-Type': 'application/json'
|
|
16
|
+
};
|
|
17
|
+
class ObjectStoreClient {
|
|
18
|
+
async sendRequest(path, options) {
|
|
19
|
+
return await (0, api_1.__fetchProduct)({ provider: 'app', remote: 'os' })(path, {
|
|
20
|
+
...options,
|
|
21
|
+
redirect: 'follow',
|
|
22
|
+
headers: {
|
|
23
|
+
...DEFAULT_HEADERS,
|
|
24
|
+
...options?.headers
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async storageApi(method, path, headers, body) {
|
|
29
|
+
const response = await this.sendRequest(path, {
|
|
30
|
+
method,
|
|
31
|
+
headers,
|
|
32
|
+
body
|
|
33
|
+
});
|
|
34
|
+
await (0, error_handling_1.checkResponseError)(response);
|
|
35
|
+
if (headers?.Accept === 'application/octet-stream') {
|
|
36
|
+
return response.arrayBuffer().then((buffer) => Buffer.from(buffer));
|
|
37
|
+
}
|
|
38
|
+
const responseText = await response.text();
|
|
39
|
+
if (responseText) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(responseText);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new errors_1.ForgeObjectStoreError(`Unexpected error. Response was not valid JSON: ${responseText}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
async echo() {
|
|
50
|
+
return this.storageApi(ValidHttpMethod.GET, `api/v1/echo`, { 'Content-Type': 'application/json' });
|
|
51
|
+
}
|
|
52
|
+
async put(objectId, buffer) {
|
|
53
|
+
const headers = {
|
|
54
|
+
...DEFAULT_HEADERS,
|
|
55
|
+
'Content-Type': 'application/octet-stream'
|
|
56
|
+
};
|
|
57
|
+
await this.storageApi(ValidHttpMethod.PUT, `api/v1/objects/${objectId}`, headers, buffer);
|
|
58
|
+
}
|
|
59
|
+
async get(objectId) {
|
|
60
|
+
return this.storageApi(ValidHttpMethod.GET, `api/v1/objects/${objectId}`, DEFAULT_HEADERS);
|
|
61
|
+
}
|
|
62
|
+
async delete(objectId) {
|
|
63
|
+
await this.storageApi(ValidHttpMethod.DELETE, `api/v1/objects/${objectId}`, DEFAULT_HEADERS);
|
|
64
|
+
}
|
|
65
|
+
async listVersions(objectId) {
|
|
66
|
+
return this.storageApi(ValidHttpMethod.GET, `api/v1/objects/${objectId}/versions`, DEFAULT_HEADERS);
|
|
67
|
+
}
|
|
68
|
+
async download(objectId) {
|
|
69
|
+
const headers = {
|
|
70
|
+
...DEFAULT_HEADERS,
|
|
71
|
+
Accept: 'application/octet-stream'
|
|
72
|
+
};
|
|
73
|
+
return this.storageApi(ValidHttpMethod.GET, `api/v1/objects/${objectId}`, headers);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.ObjectStoreClient = ObjectStoreClient;
|
|
77
|
+
exports.os = new ObjectStoreClient();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-handling.test.d.ts","sourceRoot":"","sources":["../../../src/utils/__test__/error-handling.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_fetch_1 = require("node-fetch");
|
|
4
|
+
const errorCodes_1 = require("../../errorCodes");
|
|
5
|
+
const error_handling_1 = require("../error-handling");
|
|
6
|
+
const errors_1 = require("../../errors");
|
|
7
|
+
describe('error-handling', () => {
|
|
8
|
+
it('isForgeError', () => {
|
|
9
|
+
expect((0, error_handling_1.isForgeError)({ code: 'code', message: 'message' })).toBe(true);
|
|
10
|
+
expect((0, error_handling_1.isForgeError)({ code: 'code' })).toBe(false);
|
|
11
|
+
expect((0, error_handling_1.isForgeError)({ message: 'message' })).toBe(false);
|
|
12
|
+
expect((0, error_handling_1.isForgeError)({})).toBe(false);
|
|
13
|
+
expect((0, error_handling_1.isForgeError)(null)).toBe(false);
|
|
14
|
+
expect((0, error_handling_1.isForgeError)(undefined)).toBe(false);
|
|
15
|
+
expect((0, error_handling_1.isForgeError)('string')).toBe(false);
|
|
16
|
+
expect((0, error_handling_1.isForgeError)(123)).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
it('safeGetParsedBody', () => {
|
|
19
|
+
expect((0, error_handling_1.safeGetParsedBody)('{"valid": "json"}')).toEqual({ valid: 'json' });
|
|
20
|
+
expect((0, error_handling_1.safeGetParsedBody)('invalid json')).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
describe('checkResponseError', () => {
|
|
23
|
+
const traceId = 'trace-id';
|
|
24
|
+
it('should do nothing if response is ok', async () => {
|
|
25
|
+
const mockResponse = new node_fetch_1.Response('OK', {
|
|
26
|
+
status: 200,
|
|
27
|
+
statusText: 'OK'
|
|
28
|
+
});
|
|
29
|
+
await expect((0, error_handling_1.checkResponseError)(mockResponse)).resolves.toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
describe('Forge errors - ForgeObjectStoreError', () => {
|
|
32
|
+
const message = 'A test error has occurred';
|
|
33
|
+
const code = 'ERROR_CODE';
|
|
34
|
+
it('should return a ForgeObjectStoreError when response body is a Forge error', async () => {
|
|
35
|
+
const mockResponse = new node_fetch_1.Response(JSON.stringify({ code, message }), {
|
|
36
|
+
status: 400,
|
|
37
|
+
statusText: 'Bad Request',
|
|
38
|
+
headers: { 'x-trace-id': traceId }
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
await (0, error_handling_1.checkResponseError)(mockResponse);
|
|
42
|
+
throw new Error('Expected error to be thrown');
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
expect(error).toBeInstanceOf(errors_1.ForgeObjectStoreError);
|
|
46
|
+
expect(error).toMatchObject({
|
|
47
|
+
responseDetails: {
|
|
48
|
+
status: 400,
|
|
49
|
+
statusText: 'Bad Request',
|
|
50
|
+
traceId
|
|
51
|
+
},
|
|
52
|
+
code,
|
|
53
|
+
message
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
it('should include context if present in the Forge error', async () => {
|
|
58
|
+
const context = { key: 'value' };
|
|
59
|
+
const mockResponse = new node_fetch_1.Response(JSON.stringify({ code, message, context }), {
|
|
60
|
+
status: 400,
|
|
61
|
+
statusText: 'Bad Request',
|
|
62
|
+
headers: { 'x-trace-id': traceId }
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
await (0, error_handling_1.checkResponseError)(mockResponse);
|
|
66
|
+
throw new Error('Expected error to be thrown');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
expect(error).toBeInstanceOf(errors_1.ForgeObjectStoreError);
|
|
70
|
+
expect(error).toMatchObject({
|
|
71
|
+
code,
|
|
72
|
+
message,
|
|
73
|
+
context,
|
|
74
|
+
responseDetails: {
|
|
75
|
+
status: 400,
|
|
76
|
+
statusText: 'Bad Request',
|
|
77
|
+
traceId
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
it('should include top level additional fields if present in the Forge error', async () => {
|
|
83
|
+
const extraFields = { extraValue: 'value', debug: true };
|
|
84
|
+
const mockResponse = new node_fetch_1.Response(JSON.stringify({ code, message, ...extraFields }), {
|
|
85
|
+
status: 400,
|
|
86
|
+
statusText: 'Bad Request',
|
|
87
|
+
headers: { 'x-trace-id': traceId }
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
await (0, error_handling_1.checkResponseError)(mockResponse);
|
|
91
|
+
throw new Error('Expected error to be thrown');
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
expect(error).toBeInstanceOf(errors_1.ForgeObjectStoreError);
|
|
95
|
+
expect(error).toMatchObject({
|
|
96
|
+
code,
|
|
97
|
+
message,
|
|
98
|
+
context: extraFields,
|
|
99
|
+
responseDetails: {
|
|
100
|
+
status: 400,
|
|
101
|
+
statusText: 'Bad Request',
|
|
102
|
+
traceId
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
it('should merge context and additional top level fields if both present in the Forge error', async () => {
|
|
108
|
+
const context = { key: 'value' };
|
|
109
|
+
const extraFields = { extraValue: 'value', debug: true };
|
|
110
|
+
const mockResponse = new node_fetch_1.Response(JSON.stringify({ code, message, context, ...extraFields }), {
|
|
111
|
+
status: 400,
|
|
112
|
+
statusText: 'Bad Request',
|
|
113
|
+
headers: { 'x-trace-id': traceId }
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
await (0, error_handling_1.checkResponseError)(mockResponse);
|
|
117
|
+
throw new Error('Expected error to be thrown');
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
expect(error).toBeInstanceOf(errors_1.ForgeObjectStoreError);
|
|
121
|
+
expect(error).toMatchObject({
|
|
122
|
+
code,
|
|
123
|
+
message,
|
|
124
|
+
context: { ...context, ...extraFields },
|
|
125
|
+
responseDetails: {
|
|
126
|
+
status: 400,
|
|
127
|
+
statusText: 'Bad Request',
|
|
128
|
+
traceId
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
describe('Handle non forge errors - UNKNOWN_ERROR', () => {
|
|
134
|
+
it('returns an when no response body', async () => {
|
|
135
|
+
const mockResponse = new node_fetch_1.Response(undefined, {
|
|
136
|
+
status: 404,
|
|
137
|
+
statusText: 'Not Found',
|
|
138
|
+
headers: { 'x-trace-id': traceId }
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
await (0, error_handling_1.checkResponseError)(mockResponse);
|
|
142
|
+
throw new Error('Expected error to be thrown');
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
expect(error).toBeInstanceOf(errors_1.ForgeObjectStoreError);
|
|
146
|
+
expect(error).toMatchObject({
|
|
147
|
+
code: errorCodes_1.errorCodes.UNKNOWN_ERROR,
|
|
148
|
+
context: {
|
|
149
|
+
responseText: ''
|
|
150
|
+
},
|
|
151
|
+
responseDetails: {
|
|
152
|
+
traceId,
|
|
153
|
+
status: 404,
|
|
154
|
+
statusText: 'Not Found'
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
it("returns UNKNOWN_ERROR if there is a JSON body that isn't a forge error", async () => {
|
|
160
|
+
const body = JSON.stringify({ not: 'a forge error' });
|
|
161
|
+
const mockResponse = new node_fetch_1.Response(body, {
|
|
162
|
+
status: 500,
|
|
163
|
+
statusText: 'Internal Server Error',
|
|
164
|
+
headers: { 'x-trace-id': traceId }
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
await (0, error_handling_1.checkResponseError)(mockResponse);
|
|
168
|
+
throw new Error('Expected error to be thrown');
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
expect(error).toBeInstanceOf(errors_1.ForgeObjectStoreError);
|
|
172
|
+
expect(error).toMatchObject({
|
|
173
|
+
code: errorCodes_1.errorCodes.UNKNOWN_ERROR,
|
|
174
|
+
context: {
|
|
175
|
+
responseText: body
|
|
176
|
+
},
|
|
177
|
+
responseDetails: {
|
|
178
|
+
traceId,
|
|
179
|
+
status: 500,
|
|
180
|
+
statusText: 'Internal Server Error'
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { APIResponse } from '@forge/api';
|
|
2
|
+
import { ForgeError } from '../errors';
|
|
3
|
+
export declare function isForgeError(body: unknown): body is ForgeError;
|
|
4
|
+
export declare function safeGetParsedBody(text: string): unknown | undefined;
|
|
5
|
+
export declare function extractTraceId(response: APIResponse): string | null;
|
|
6
|
+
export declare function checkResponseError(response: APIResponse, message?: string): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=error-handling.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-handling.d.ts","sourceRoot":"","sources":["../../src/utils/error-handling.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAA2B,UAAU,EAA4B,MAAM,WAAW,CAAC;AAG1F,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,UAAU,CAE9D;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAMnE;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB/F"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkResponseError = exports.extractTraceId = exports.safeGetParsedBody = exports.isForgeError = void 0;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
const errorCodes_1 = require("../errorCodes");
|
|
6
|
+
function isForgeError(body) {
|
|
7
|
+
return typeof body === 'object' && body !== null && 'code' in body && 'message' in body;
|
|
8
|
+
}
|
|
9
|
+
exports.isForgeError = isForgeError;
|
|
10
|
+
function safeGetParsedBody(text) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(text);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.safeGetParsedBody = safeGetParsedBody;
|
|
19
|
+
function extractTraceId(response) {
|
|
20
|
+
return response.headers.get('x-b3-traceid') || response.headers.get('x-trace-id');
|
|
21
|
+
}
|
|
22
|
+
exports.extractTraceId = extractTraceId;
|
|
23
|
+
async function checkResponseError(response, message) {
|
|
24
|
+
if (response.ok) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const responseText = await response.text();
|
|
28
|
+
const details = {
|
|
29
|
+
status: response.status,
|
|
30
|
+
statusText: response.statusText,
|
|
31
|
+
traceId: extractTraceId(response)
|
|
32
|
+
};
|
|
33
|
+
const parsedBody = safeGetParsedBody(responseText);
|
|
34
|
+
if (parsedBody && isForgeError(parsedBody)) {
|
|
35
|
+
throw new errors_1.ForgeObjectStoreAPIError(details, parsedBody);
|
|
36
|
+
}
|
|
37
|
+
throw new errors_1.ForgeObjectStoreAPIError(details, {
|
|
38
|
+
code: errorCodes_1.errorCodes.UNKNOWN_ERROR,
|
|
39
|
+
message: message || 'Unexpected error in Forge SQL API request',
|
|
40
|
+
context: { responseText }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
exports.checkResponseError = checkResponseError;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/utils/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,oBAAY,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forge/os",
|
|
3
|
+
"version": "1.1.0-next.0",
|
|
4
|
+
"description": "Forge Object Store SDK",
|
|
5
|
+
"author": "Atlassian",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"main": "out/index.js",
|
|
8
|
+
"types": "out/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"out"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "yarn run clean && yarn run compile",
|
|
14
|
+
"clean": "rm -rf ./out && rm -f tsconfig.tsbuildinfo",
|
|
15
|
+
"compile": "tsc -b -v"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "14.18.63",
|
|
19
|
+
"@types/node-fetch": "^2.6.11",
|
|
20
|
+
"expect-type": "^0.17.3",
|
|
21
|
+
"node-fetch": "2.7.0",
|
|
22
|
+
"jest-when": "^3.6.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@forge/api": "^4.1.2-next.0"
|
|
26
|
+
}
|
|
27
|
+
}
|