@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 ADDED
@@ -0,0 +1,6 @@
1
+ Library for Forge environment.
2
+
3
+ Usage example:
4
+ ```typescript
5
+ import os from "@forge/os";
6
+ ```
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=os.test.d.ts.map
@@ -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,5 @@
1
+ export declare const errorCodes: {
2
+ readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
3
+ readonly APP_NOT_ENABLED: "APP_NOT_ENABLED";
4
+ };
5
+ //# sourceMappingURL=errorCodes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorCodes.d.ts","sourceRoot":"","sources":["../src/errorCodes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU;;;CAGb,CAAC"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.errorCodes = void 0;
4
+ exports.errorCodes = {
5
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
6
+ APP_NOT_ENABLED: 'APP_NOT_ENABLED'
7
+ };
@@ -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,6 @@
1
+ import { os } from './os';
2
+ import { errorCodes } from './errorCodes';
3
+ import { ForgeError } from './errors';
4
+ export { errorCodes, os, ForgeError };
5
+ export default os;
6
+ //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=error-handling.test.d.ts.map
@@ -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,3 @@
1
+ import { APIResponse } from '@forge/api';
2
+ export declare type Response = Pick<APIResponse, 'text' | 'ok' | 'status'>;
3
+ //# sourceMappingURL=types.d.ts.map
@@ -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"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ }