@forge/sql 2.2.0-next.1 → 2.2.0-next.3

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.
@@ -4,7 +4,7 @@ const node_fetch_1 = require("node-fetch");
4
4
  const api_1 = require("@forge/api");
5
5
  const sql_1 = require("../sql");
6
6
  const sql_statement_1 = require("../sql-statement");
7
- const response_handler_1 = require("../utils/response-handler");
7
+ const errors_1 = require("../errors");
8
8
  jest.mock('@forge/api');
9
9
  describe('SqlClient', () => {
10
10
  let sqlClient;
@@ -83,11 +83,19 @@ describe('SqlClient', () => {
83
83
  });
84
84
  expect(result).toEqual({ rows: [] });
85
85
  });
86
- it('should handle errors from getResponseBody', async () => {
86
+ it('should handle invalid JSON body', async () => {
87
87
  const responseText = 'Invalid JSON';
88
88
  const response = new node_fetch_1.Response(responseText, { status: 200 });
89
89
  mockFetch.mockResolvedValue(response);
90
- await expect(sqlClient.storageApi('INVALID SQL QUERY')).rejects.toThrow(`Unexpected error. Response was not valid JSON: ${responseText}`);
90
+ await expect(sqlClient.storageApi('SELECT * from strange;')).rejects.toThrow(`Unexpected error. Response was not valid JSON: ${responseText}`);
91
+ });
92
+ it('should throw ForgeSQLAPIError on API error', async () => {
93
+ const forgeError = { code: 'INVALID_QUERY', message: 'Invalid SQL query' };
94
+ const mockResponse = new node_fetch_1.Response(JSON.stringify(forgeError), {
95
+ status: 400
96
+ });
97
+ mockFetch.mockResolvedValue(mockResponse);
98
+ await expect(sqlClient.storageApi('INVALID SQL QUERY')).rejects.toThrow(new errors_1.ForgeSQLAPIError(mockResponse, forgeError));
91
99
  });
92
100
  });
93
101
  describe('prepare', () => {
@@ -112,8 +120,40 @@ describe('SqlClient', () => {
112
120
  expect(result).toEqual(mockResult);
113
121
  });
114
122
  it('should handle API errors', async () => {
115
- mockFetch.mockResolvedValue(new node_fetch_1.Response(JSON.stringify({ code: 'INVALID_QUERY', message: 'Invalid SQL query' }), { status: 400 }));
116
- await expect(sqlClient.executeRaw('INVALID SQL QUERY')).rejects.toThrow(new response_handler_1.ApiError(400, 'INVALID_QUERY', 'Invalid SQL query'));
123
+ const forgeError = { code: 'INVALID_QUERY', message: 'Invalid SQL query' };
124
+ const mockResponse = new node_fetch_1.Response(JSON.stringify(forgeError), {
125
+ status: 400
126
+ });
127
+ mockFetch.mockResolvedValue(mockResponse);
128
+ await expect(sqlClient.executeRaw('INVALID SQL QUERY')).rejects.toThrow(new errors_1.ForgeSQLAPIError(mockResponse, forgeError));
129
+ });
130
+ });
131
+ describe('_provision', () => {
132
+ it('should work if the response is a 201', async () => {
133
+ const mockResponse = new node_fetch_1.Response(JSON.stringify({ success: true }), {
134
+ status: 201,
135
+ statusText: 'OK'
136
+ });
137
+ mockFetch.mockResolvedValue(mockResponse);
138
+ await sqlClient._provision();
139
+ expect(mockFetch).toHaveBeenCalledWith('/api/v1/provision', expect.objectContaining({ method: 'POST' }));
140
+ });
141
+ it('should work if the response is a 204', async () => {
142
+ const mockResponse = new node_fetch_1.Response(undefined, {
143
+ status: 204,
144
+ statusText: 'OK'
145
+ });
146
+ mockFetch.mockResolvedValue(mockResponse);
147
+ await sqlClient._provision();
148
+ expect(mockFetch).toHaveBeenCalledWith('/api/v1/provision', expect.objectContaining({ method: 'POST' }));
149
+ });
150
+ it('should throw an error if the response is not 201 or 204', async () => {
151
+ const mockResponse = new node_fetch_1.Response('Server Error', {
152
+ status: 500,
153
+ statusText: 'Bad Request'
154
+ });
155
+ mockFetch.mockResolvedValue(mockResponse);
156
+ await expect(sqlClient._provision()).rejects.toThrow('Unexpected error in provision request');
117
157
  });
118
158
  });
119
159
  });
@@ -1,7 +1,7 @@
1
1
  export declare const errorCodes: {
2
- SQL_EXECUTION_ERROR: string;
3
- INVALID_SQL_QUERY: string;
4
- QUERY_TIMED_OUT: string;
5
- APP_NOT_ENABLED: string;
2
+ readonly SQL_EXECUTION_ERROR: "SQL_EXECUTION_ERROR";
3
+ readonly INVALID_SQL_QUERY: "INVALID_SQL_QUERY";
4
+ readonly QUERY_TIMED_OUT: "QUERY_TIMED_OUT";
5
+ readonly APP_NOT_ENABLED: "APP_NOT_ENABLED";
6
6
  };
7
7
  //# sourceMappingURL=errorCodes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errorCodes.d.ts","sourceRoot":"","sources":["../src/errorCodes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU;;;;;CAKtB,CAAC"}
1
+ {"version":3,"file":"errorCodes.d.ts","sourceRoot":"","sources":["../src/errorCodes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU;;;;;CAUb,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { APIResponse } from '@forge/api';
2
+ export interface ForgeError {
3
+ code: string;
4
+ message: string;
5
+ suggestion?: string;
6
+ context?: Record<string, unknown>;
7
+ }
8
+ export declare class ForgeSQLError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ export interface APIErrorResponseDetails extends Pick<APIResponse, 'status' | 'statusText'> {
12
+ traceId?: string | null;
13
+ }
14
+ export declare class ForgeSQLAPIUnknownError extends ForgeSQLError {
15
+ code: string;
16
+ responseText?: string;
17
+ status: number;
18
+ statusText: string;
19
+ traceId?: string | null;
20
+ constructor(responseDetails: APIErrorResponseDetails, body?: string, message?: string);
21
+ }
22
+ export declare class ForgeSQLAPIError extends ForgeSQLAPIUnknownError {
23
+ code: string;
24
+ serverMessage: string;
25
+ suggestion?: string;
26
+ context?: Record<string, unknown>;
27
+ constructor(responseDetails: APIErrorResponseDetails, forgeError: ForgeError, message?: string);
28
+ }
29
+ export declare class MigrationExecutionError extends ForgeSQLError {
30
+ readonly migrationName: string;
31
+ readonly migrationsYetToRun: string[];
32
+ constructor(migrationName: string, migrationsYetToRun: string[]);
33
+ }
34
+ export declare class MigrationCheckPointError extends ForgeSQLError {
35
+ readonly migrationName: string;
36
+ readonly migrationsYetToRun: string[];
37
+ constructor(migrationName: string, migrationsYetToRun: string[]);
38
+ }
39
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGzC,MAAM,WAAW,UAAU;IAKzB,IAAI,EAAE,MAAM,CAAC;IAGb,OAAO,EAAE,MAAM,CAAC;IAGhB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAGnC;AAGD,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AAMD,MAAM,WAAW,uBAAwB,SAAQ,IAAI,CAAC,WAAW,EAAE,QAAQ,GAAG,YAAY,CAAC;IAEzF,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAOD,qBAAa,uBAAwB,SAAQ,aAAa;IACxD,IAAI,SAAmB;IAGvB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,MAAM,EAAE,MAAM,CAAC;IAGf,UAAU,EAAE,MAAM,CAAC;IAGnB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEZ,eAAe,EAAE,uBAAuB,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAQtF;AAMD,qBAAa,gBAAiB,SAAQ,uBAAuB;IAK3D,IAAI,EAAE,MAAM,CAAC;IAEb,aAAa,EAAE,MAAM,CAAC;IAEtB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAEtB,eAAe,EAAE,uBAAuB,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM;CAU/F;AAED,qBAAa,uBAAwB,SAAQ,aAAa;IAEtD,QAAQ,CAAC,aAAa,EAAE,MAAM;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,EAAE;gBAD5B,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAAE;CAIxC;AAED,qBAAa,wBAAyB,SAAQ,aAAa;IAEvD,QAAQ,CAAC,aAAa,EAAE,MAAM;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,EAAE;gBAD5B,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAAE;CAIxC"}
package/out/errors.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MigrationCheckPointError = exports.MigrationExecutionError = exports.ForgeSQLAPIError = exports.ForgeSQLAPIUnknownError = exports.ForgeSQLError = void 0;
4
+ class ForgeSQLError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'ForgeSQLError';
8
+ }
9
+ }
10
+ exports.ForgeSQLError = ForgeSQLError;
11
+ class ForgeSQLAPIUnknownError extends ForgeSQLError {
12
+ code = 'UNKNOWN_ERROR';
13
+ responseText;
14
+ status;
15
+ statusText;
16
+ traceId;
17
+ constructor(responseDetails, body, message) {
18
+ super(message || 'Unexpected error in Forge SQL API request');
19
+ this.code = 'UNKNOWN_ERROR';
20
+ this.status = responseDetails.status;
21
+ this.statusText = responseDetails.statusText;
22
+ this.traceId = responseDetails.traceId;
23
+ this.responseText = body;
24
+ }
25
+ }
26
+ exports.ForgeSQLAPIUnknownError = ForgeSQLAPIUnknownError;
27
+ class ForgeSQLAPIError extends ForgeSQLAPIUnknownError {
28
+ code;
29
+ serverMessage;
30
+ suggestion;
31
+ context;
32
+ constructor(responseDetails, forgeError, message) {
33
+ super(responseDetails, undefined, message || forgeError.message);
34
+ const { code, message: serverMessage, suggestion, context = {}, ...additionalData } = forgeError;
35
+ this.code = code;
36
+ this.serverMessage = serverMessage;
37
+ this.suggestion = suggestion;
38
+ this.context = { ...context, ...additionalData };
39
+ }
40
+ }
41
+ exports.ForgeSQLAPIError = ForgeSQLAPIError;
42
+ class MigrationExecutionError extends ForgeSQLError {
43
+ migrationName;
44
+ migrationsYetToRun;
45
+ constructor(migrationName, migrationsYetToRun) {
46
+ super(`Failed to execute migration with name ${migrationName}`);
47
+ this.migrationName = migrationName;
48
+ this.migrationsYetToRun = migrationsYetToRun;
49
+ }
50
+ }
51
+ exports.MigrationExecutionError = MigrationExecutionError;
52
+ class MigrationCheckPointError extends ForgeSQLError {
53
+ migrationName;
54
+ migrationsYetToRun;
55
+ constructor(migrationName, migrationsYetToRun) {
56
+ super(`Failed to checkpoint after running migration with name ${migrationName}`);
57
+ this.migrationName = migrationName;
58
+ this.migrationsYetToRun = migrationsYetToRun;
59
+ }
60
+ }
61
+ exports.MigrationCheckPointError = MigrationCheckPointError;
package/out/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { sql } from './sql';
2
2
  import { errorCodes } from './errorCodes';
3
+ import { ForgeError, ForgeSQLAPIUnknownError, ForgeSQLAPIError } from './errors';
3
4
  import { migrationRunner } from './migration';
4
5
  import type { Result, UpdateQueryResponse } from './utils/types';
5
6
  export type { Result, UpdateQueryResponse };
6
- export { errorCodes, migrationRunner, sql };
7
+ export { errorCodes, migrationRunner, sql, ForgeError, ForgeSQLAPIUnknownError, ForgeSQLAPIError };
7
8
  export default sql;
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEjE,YAAY,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;AAE5C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC;AAC5C,eAAe,GAAG,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEjE,YAAY,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;AAE5C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,UAAU,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,CAAC;AAEnG,eAAe,GAAG,CAAC"}
package/out/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sql = exports.migrationRunner = exports.errorCodes = void 0;
3
+ exports.ForgeSQLAPIError = exports.ForgeSQLAPIUnknownError = exports.sql = exports.migrationRunner = exports.errorCodes = void 0;
4
4
  const sql_1 = require("./sql");
5
5
  Object.defineProperty(exports, "sql", { enumerable: true, get: function () { return sql_1.sql; } });
6
6
  const errorCodes_1 = require("./errorCodes");
7
7
  Object.defineProperty(exports, "errorCodes", { enumerable: true, get: function () { return errorCodes_1.errorCodes; } });
8
+ const errors_1 = require("./errors");
9
+ Object.defineProperty(exports, "ForgeSQLAPIUnknownError", { enumerable: true, get: function () { return errors_1.ForgeSQLAPIUnknownError; } });
10
+ Object.defineProperty(exports, "ForgeSQLAPIError", { enumerable: true, get: function () { return errors_1.ForgeSQLAPIError; } });
8
11
  const migration_1 = require("./migration");
9
12
  Object.defineProperty(exports, "migrationRunner", { enumerable: true, get: function () { return migration_1.migrationRunner; } });
10
13
  exports.default = sql_1.sql;
package/out/migration.js CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.migrationRunner = exports.MigrationRunner = void 0;
4
4
  const sql_1 = require("./sql");
5
- const response_handler_1 = require("./utils/response-handler");
5
+ const errors_1 = require("./errors");
6
6
  const SCHEMA_VERSION_TABLE_CREATE_QUERY = 'CREATE TABLE IF NOT EXISTS __migrations (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, migratedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP);';
7
7
  const INSERT_SCHEMA_VERSION_QUERY = 'INSERT INTO __migrations (name) VALUES (?);';
8
8
  const LIST_MIGRATIONS_QUERY = 'SELECT id, name, migratedAt FROM __migrations;';
@@ -47,13 +47,13 @@ class MigrationRunner {
47
47
  await this.sqlClient.executeRaw(migration.statement);
48
48
  }
49
49
  catch (error) {
50
- throw new response_handler_1.MigrationExecutionError(migration.name, migrationsToRun.map((m) => m.name));
50
+ throw new errors_1.MigrationExecutionError(migration.name, migrationsToRun.map((m) => m.name));
51
51
  }
52
52
  try {
53
53
  await this.sqlClient.prepare(INSERT_SCHEMA_VERSION_QUERY).bindParams(migration.name).execute();
54
54
  }
55
55
  catch (error) {
56
- throw new response_handler_1.MigrationCheckPointError(migration.name, migrationsToRun.map((m) => m.name));
56
+ throw new errors_1.MigrationCheckPointError(migration.name, migrationsToRun.map((m) => m.name));
57
57
  }
58
58
  migrationsSuccessfullyRun.push(migration.name);
59
59
  }
package/out/sql.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAY,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE9D,qBAAa,SAAS;YACN,WAAW;IAYnB,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,aAAkB,EAAE,MAAM,SAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAQhH,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAI1D,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAI9D,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAMlC;AAED,eAAO,MAAM,GAAG,WAAkB,CAAC"}
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAI9D,qBAAa,SAAS;YACN,WAAW;IAYnB,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,aAAkB,EAAE,MAAM,SAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAiBhH,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAI1D,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAK9D,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CASlC;AAED,eAAO,MAAM,GAAG,WAAkB,CAAC"}
package/out/sql.js CHANGED
@@ -2,8 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sql = exports.SqlClient = void 0;
4
4
  const api_1 = require("@forge/api");
5
- const response_handler_1 = require("./utils/response-handler");
6
5
  const sql_statement_1 = require("./sql-statement");
6
+ const error_handling_1 = require("./utils/error-handling");
7
+ const errors_1 = require("./errors");
7
8
  class SqlClient {
8
9
  async sendRequest(path, options) {
9
10
  const response = await (0, api_1.__fetchProduct)({ provider: 'app', remote: 'sql' })(path, {
@@ -21,7 +22,14 @@ class SqlClient {
21
22
  method: 'POST',
22
23
  body: JSON.stringify({ query, params, method })
23
24
  });
24
- return await (0, response_handler_1.getResponseBody)(response);
25
+ await (0, error_handling_1.checkResponseError)(response);
26
+ const responseText = await response.text();
27
+ try {
28
+ return JSON.parse(responseText);
29
+ }
30
+ catch (error) {
31
+ throw new errors_1.ForgeSQLError(`Unexpected error. Response was not valid JSON: ${responseText}`);
32
+ }
25
33
  }
26
34
  prepare(query) {
27
35
  return new sql_statement_1.SqlStatement(query, this.storageApi.bind(this));
@@ -33,7 +41,7 @@ class SqlClient {
33
41
  const response = await this.sendRequest('/api/v1/provision', {
34
42
  method: 'POST'
35
43
  });
36
- await (0, response_handler_1.getResponseBody)(response);
44
+ await (0, error_handling_1.checkResponseError)(response, 'Unexpected error in provision request');
37
45
  }
38
46
  }
39
47
  exports.SqlClient = SqlClient;
@@ -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,201 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_fetch_1 = require("node-fetch");
4
+ const errors_1 = require("../../errors");
5
+ const error_handling_1 = require("../error-handling");
6
+ describe('errorFromResponse', () => {
7
+ const traceId = 'trace-id';
8
+ it('should do nothing if response is ok', async () => {
9
+ const mockResponse = new node_fetch_1.Response('OK', {
10
+ status: 200,
11
+ statusText: 'OK'
12
+ });
13
+ await expect((0, error_handling_1.checkResponseError)(mockResponse)).resolves.toBeUndefined();
14
+ });
15
+ describe('Forge errors - ForgeSQLAPIError', () => {
16
+ const message = 'A test error has occurred';
17
+ const code = 'ERROR_CODE';
18
+ const suggestion = 'Do something different';
19
+ it('should return a ForgeSQLAPIError when response body is a Forge error', async () => {
20
+ const mockResponse = new node_fetch_1.Response(JSON.stringify({ code, message }), {
21
+ status: 400,
22
+ statusText: 'Bad Request',
23
+ headers: { 'x-trace-id': traceId }
24
+ });
25
+ try {
26
+ await (0, error_handling_1.checkResponseError)(mockResponse);
27
+ throw new Error('Expected error to be thrown');
28
+ }
29
+ catch (error) {
30
+ expect(error).toBeInstanceOf(errors_1.ForgeSQLAPIError);
31
+ expect(error).toMatchObject({
32
+ status: 400,
33
+ statusText: 'Bad Request',
34
+ traceId,
35
+ code,
36
+ message,
37
+ serverMessage: message
38
+ });
39
+ }
40
+ });
41
+ it('should include custom message if provided', async () => {
42
+ const customMessage = 'A custom message';
43
+ const mockResponse = new node_fetch_1.Response(JSON.stringify({ code, message }), {
44
+ status: 400,
45
+ statusText: 'Bad Request',
46
+ headers: { 'x-trace-id': traceId }
47
+ });
48
+ try {
49
+ await (0, error_handling_1.checkResponseError)(mockResponse, customMessage);
50
+ throw new Error('Expected error to be thrown');
51
+ }
52
+ catch (error) {
53
+ expect(error).toBeInstanceOf(errors_1.ForgeSQLAPIError);
54
+ expect(error).toMatchObject({
55
+ status: 400,
56
+ statusText: 'Bad Request',
57
+ traceId,
58
+ code,
59
+ message: customMessage,
60
+ serverMessage: message
61
+ });
62
+ }
63
+ });
64
+ it('should include context if present in the Forge error', async () => {
65
+ const context = { key: 'value' };
66
+ const mockResponse = new node_fetch_1.Response(JSON.stringify({ code, message, context }), {
67
+ status: 400,
68
+ statusText: 'Bad Request',
69
+ headers: { 'x-trace-id': traceId }
70
+ });
71
+ try {
72
+ await (0, error_handling_1.checkResponseError)(mockResponse);
73
+ throw new Error('Expected error to be thrown');
74
+ }
75
+ catch (error) {
76
+ expect(error).toBeInstanceOf(errors_1.ForgeSQLAPIError);
77
+ expect(error).toMatchObject({
78
+ code,
79
+ message,
80
+ context,
81
+ status: 400,
82
+ statusText: 'Bad Request',
83
+ traceId
84
+ });
85
+ }
86
+ });
87
+ it('should include top level additional fields if present in the Forge error', async () => {
88
+ const extraFields = { extraValue: 'value', debug: true };
89
+ const mockResponse = new node_fetch_1.Response(JSON.stringify({ code, message, ...extraFields }), {
90
+ status: 400,
91
+ statusText: 'Bad Request',
92
+ headers: { 'x-trace-id': traceId }
93
+ });
94
+ try {
95
+ await (0, error_handling_1.checkResponseError)(mockResponse);
96
+ throw new Error('Expected error to be thrown');
97
+ }
98
+ catch (error) {
99
+ expect(error).toBeInstanceOf(errors_1.ForgeSQLAPIError);
100
+ expect(error).toMatchObject({
101
+ code,
102
+ message,
103
+ context: extraFields,
104
+ status: 400,
105
+ statusText: 'Bad Request',
106
+ traceId
107
+ });
108
+ }
109
+ });
110
+ it('should merge context and additional top level fields if both present in the Forge error', async () => {
111
+ const context = { key: 'value' };
112
+ const extraFields = { extraValue: 'value', debug: true };
113
+ const mockResponse = new node_fetch_1.Response(JSON.stringify({ code, message, context, ...extraFields }), {
114
+ status: 400,
115
+ statusText: 'Bad Request',
116
+ headers: { 'x-trace-id': traceId }
117
+ });
118
+ try {
119
+ await (0, error_handling_1.checkResponseError)(mockResponse);
120
+ throw new Error('Expected error to be thrown');
121
+ }
122
+ catch (error) {
123
+ expect(error).toBeInstanceOf(errors_1.ForgeSQLAPIError);
124
+ expect(error).toMatchObject({
125
+ code,
126
+ message,
127
+ context: { ...context, ...extraFields },
128
+ status: 400,
129
+ statusText: 'Bad Request',
130
+ traceId
131
+ });
132
+ }
133
+ });
134
+ it('should include suggestion if present in the Forge error', async () => {
135
+ const mockResponse = new node_fetch_1.Response(JSON.stringify({ code, message, suggestion }), {
136
+ status: 400,
137
+ statusText: 'Bad Request',
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.ForgeSQLAPIError);
146
+ expect(error).toMatchObject({
147
+ code,
148
+ message,
149
+ suggestion,
150
+ status: 400,
151
+ statusText: 'Bad Request',
152
+ traceId
153
+ });
154
+ }
155
+ });
156
+ });
157
+ describe('Unknown or not forge errors - ForgeSQLAPIUnknownError', () => {
158
+ it('returns an ForgeSQLAPIUnknownError when no response body', async () => {
159
+ const mockResponse = new node_fetch_1.Response(undefined, {
160
+ status: 404,
161
+ statusText: 'Not Found',
162
+ headers: { 'x-trace-id': traceId }
163
+ });
164
+ try {
165
+ await (0, error_handling_1.checkResponseError)(mockResponse);
166
+ throw new Error('Expected error to be thrown');
167
+ }
168
+ catch (error) {
169
+ expect(error).toBeInstanceOf(errors_1.ForgeSQLAPIUnknownError);
170
+ expect(error).toMatchObject({
171
+ code: 'UNKNOWN_ERROR',
172
+ status: 404,
173
+ statusText: 'Not Found',
174
+ traceId
175
+ });
176
+ }
177
+ });
178
+ it("returns ForgeSQLAPIUnknownError if there is a JSON body that isn't a forge error", async () => {
179
+ const body = JSON.stringify({ not: 'a forge error' });
180
+ const mockResponse = new node_fetch_1.Response(body, {
181
+ status: 500,
182
+ statusText: 'Internal Server Error',
183
+ headers: { 'x-trace-id': traceId }
184
+ });
185
+ try {
186
+ await (0, error_handling_1.checkResponseError)(mockResponse);
187
+ throw new Error('Expected error to be thrown');
188
+ }
189
+ catch (error) {
190
+ expect(error).toBeInstanceOf(errors_1.ForgeSQLAPIUnknownError);
191
+ expect(error).toMatchObject({
192
+ code: 'UNKNOWN_ERROR',
193
+ status: 500,
194
+ statusText: 'Internal Server Error',
195
+ responseText: body,
196
+ traceId
197
+ });
198
+ }
199
+ });
200
+ });
201
+ });
@@ -0,0 +1,5 @@
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 checkResponseError(response: APIResponse, message?: string): Promise<void>;
5
+ //# 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,EAA6C,MAAM,WAAW,CAAC;AAG3G,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,UAAU,CAO9D;AASD,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC/F"}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkResponseError = exports.isForgeError = void 0;
4
+ const errors_1 = require("../errors");
5
+ function isForgeError(body) {
6
+ if (typeof body === 'object' && body !== null) {
7
+ if ('code' in body && 'message' in body) {
8
+ return true;
9
+ }
10
+ }
11
+ return false;
12
+ }
13
+ exports.isForgeError = isForgeError;
14
+ async function checkResponseError(response, message) {
15
+ if (response.ok) {
16
+ return;
17
+ }
18
+ const responseText = await response.text();
19
+ const details = {
20
+ status: response.status,
21
+ statusText: response.statusText,
22
+ traceId: response.headers.get('x-trace-id')
23
+ };
24
+ try {
25
+ const parsedBody = JSON.parse(responseText);
26
+ if (isForgeError(parsedBody)) {
27
+ throw new errors_1.ForgeSQLAPIError(details, parsedBody, message);
28
+ }
29
+ }
30
+ catch (error) {
31
+ if (error instanceof SyntaxError) {
32
+ }
33
+ else {
34
+ throw error;
35
+ }
36
+ }
37
+ throw new errors_1.ForgeSQLAPIUnknownError(details, responseText, message);
38
+ }
39
+ exports.checkResponseError = checkResponseError;
@@ -1 +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;AAGnE,MAAM,WAAW,mBAAmB;IAElC,YAAY,EAAE,MAAM,CAAC;IAErB,UAAU,EAAE,MAAM,CAAC;IAEnB,IAAI,EAAE,MAAM,CAAC;IAEb,QAAQ,EAAE,MAAM,CAAC;IAEjB,YAAY,EAAE,MAAM,CAAC;IAErB,aAAa,EAAE,MAAM,CAAC;CACvB;AAWD,MAAM,WAAW,MAAM,CAAC,QAAQ,GAAG,GAAG;IAKpC,IAAI,EAAE,QAAQ,SAAS,mBAAmB,GAAG,mBAAmB,GAAG,QAAQ,EAAE,CAAC;IAM9E,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAChC"}
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;AAGnE,MAAM,WAAW,mBAAmB;IAElC,YAAY,EAAE,MAAM,CAAC;IAErB,UAAU,EAAE,MAAM,CAAC;IAEnB,IAAI,EAAE,MAAM,CAAC;IAEb,QAAQ,EAAE,MAAM,CAAC;IAEjB,YAAY,EAAE,MAAM,CAAC;IAErB,aAAa,EAAE,MAAM,CAAC;CACvB;AAWD,MAAM,WAAW,MAAM,CAAC,QAAQ,GAAG,GAAG;IAKpC,IAAI,EAAE,QAAQ,SAAS,mBAAmB,GAAG,mBAAmB,GAAG,QAAQ,EAAE,CAAC;IAO9E,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAChC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forge/sql",
3
- "version": "2.2.0-next.1",
3
+ "version": "2.2.0-next.3",
4
4
  "description": "Forge SQL sdk",
5
5
  "author": "Atlassian",
6
6
  "license": "UNLICENSED",
@@ -22,6 +22,6 @@
22
22
  "jest-when": "^3.6.0"
23
23
  },
24
24
  "dependencies": {
25
- "@forge/api": "^4.0.0"
25
+ "@forge/api": "^4.1.0-next.0"
26
26
  }
27
27
  }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=response-handler.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"response-handler.test.d.ts","sourceRoot":"","sources":["../../../src/utils/__test__/response-handler.test.ts"],"names":[],"mappings":""}
@@ -1,60 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const response_handler_1 = require("../response-handler");
4
- describe('getResponseBody', () => {
5
- it('should return parsed response when response is ok', async () => {
6
- const mockResponse = {
7
- text: jest.fn().mockResolvedValue(JSON.stringify({ rows: [] })),
8
- ok: true,
9
- status: 200
10
- };
11
- const result = await (0, response_handler_1.getResponseBody)(mockResponse);
12
- expect(result).toEqual({ rows: [] });
13
- });
14
- it('should throw ApiError with parsed error when response is not ok and error JSON is valid', async () => {
15
- const mockResponse = {
16
- text: jest
17
- .fn()
18
- .mockResolvedValue(JSON.stringify({ code: 'ERROR_CODE', message: 'Error Title', debug: { info: 'detail' } })),
19
- ok: false,
20
- status: 400
21
- };
22
- await expect((0, response_handler_1.getResponseBody)(mockResponse)).rejects.toThrow(response_handler_1.ApiError);
23
- await expect((0, response_handler_1.getResponseBody)(mockResponse)).rejects.toMatchObject({
24
- status: 400,
25
- code: 'ERROR_CODE',
26
- message: 'Error Title',
27
- debug: { info: 'detail' }
28
- });
29
- });
30
- it('should throw ApiError with UNKNOWN_ERROR when response is not ok and error JSON is invalid', async () => {
31
- const mockResponse = {
32
- text: jest.fn().mockResolvedValue('Invalid JSON'),
33
- ok: false,
34
- status: 500
35
- };
36
- await expect((0, response_handler_1.getResponseBody)(mockResponse)).rejects.toThrow(response_handler_1.ApiError);
37
- await expect((0, response_handler_1.getResponseBody)(mockResponse)).rejects.toMatchObject({
38
- status: 500,
39
- code: 'UNKNOWN_ERROR',
40
- message: 'Invalid JSON'
41
- });
42
- });
43
- it('should throw Error when response text is not valid JSON', async () => {
44
- const mockResponse = {
45
- text: jest.fn().mockResolvedValue('Invalid JSON'),
46
- ok: true,
47
- status: 200
48
- };
49
- await expect((0, response_handler_1.getResponseBody)(mockResponse)).rejects.toThrowError(new Error('Unexpected error. Response was not valid JSON: Invalid JSON'));
50
- });
51
- });
52
- describe('ApiError', () => {
53
- it('should create an ApiError with correct properties', () => {
54
- const error = new response_handler_1.ApiError(404, 'NOT_FOUND', 'Resource not found');
55
- expect(error).toBeInstanceOf(Error);
56
- expect(error.status).toBe(404);
57
- expect(error.code).toBe('NOT_FOUND');
58
- expect(error.message).toBe('Resource not found');
59
- });
60
- });
@@ -1,20 +0,0 @@
1
- import { Response, Result } from './types';
2
- export declare class ApiError extends Error {
3
- readonly status: number;
4
- readonly code: string;
5
- readonly suggestion?: string | undefined;
6
- readonly debug?: any;
7
- constructor(status: number, code: string, message: string, suggestion?: string | undefined, debug?: any);
8
- }
9
- export declare class MigrationExecutionError extends Error {
10
- readonly migrationName: string;
11
- readonly migrationsYetToRun: string[];
12
- constructor(migrationName: string, migrationsYetToRun: string[]);
13
- }
14
- export declare class MigrationCheckPointError extends Error {
15
- readonly migrationName: string;
16
- readonly migrationsYetToRun: string[];
17
- constructor(migrationName: string, migrationsYetToRun: string[]);
18
- }
19
- export declare function getResponseBody<DataType>(response: Response): Promise<Result<DataType>>;
20
- //# sourceMappingURL=response-handler.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"response-handler.d.ts","sourceRoot":"","sources":["../../src/utils/response-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAE3C,qBAAa,QAAS,SAAQ,KAAK;IAE/B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM;IAErB,QAAQ,CAAC,UAAU,CAAC;IACpB,QAAQ,CAAC,KAAK,CAAC;gBAJN,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACN,UAAU,CAAC,oBAAQ,EACnB,KAAK,CAAC,KAAK;CAIvB;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IAE9C,QAAQ,CAAC,aAAa,EAAE,MAAM;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,EAAE;gBAD5B,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAAE;CAIxC;AAED,qBAAa,wBAAyB,SAAQ,KAAK;IAE/C,QAAQ,CAAC,aAAa,EAAE,MAAM;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,EAAE;gBAD5B,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAAE;CAIxC;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CA2B7F"}
@@ -1,61 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getResponseBody = exports.MigrationCheckPointError = exports.MigrationExecutionError = exports.ApiError = void 0;
4
- class ApiError extends Error {
5
- status;
6
- code;
7
- suggestion;
8
- debug;
9
- constructor(status, code, message, suggestion, debug) {
10
- super(message);
11
- this.status = status;
12
- this.code = code;
13
- this.suggestion = suggestion;
14
- this.debug = debug;
15
- }
16
- }
17
- exports.ApiError = ApiError;
18
- class MigrationExecutionError extends Error {
19
- migrationName;
20
- migrationsYetToRun;
21
- constructor(migrationName, migrationsYetToRun) {
22
- super(`Failed to execute migration with name ${migrationName}`);
23
- this.migrationName = migrationName;
24
- this.migrationsYetToRun = migrationsYetToRun;
25
- }
26
- }
27
- exports.MigrationExecutionError = MigrationExecutionError;
28
- class MigrationCheckPointError extends Error {
29
- migrationName;
30
- migrationsYetToRun;
31
- constructor(migrationName, migrationsYetToRun) {
32
- super(`Failed to checkpoint after running migration with name ${migrationName}`);
33
- this.migrationName = migrationName;
34
- this.migrationsYetToRun = migrationsYetToRun;
35
- }
36
- }
37
- exports.MigrationCheckPointError = MigrationCheckPointError;
38
- async function getResponseBody(response) {
39
- const responseText = await response.text();
40
- if (!response.ok) {
41
- try {
42
- const parsedError = JSON.parse(responseText);
43
- throw new ApiError(response.status, parsedError?.code, parsedError?.message, parsedError?.suggestion, parsedError?.debug);
44
- }
45
- catch (e) {
46
- if (!(e instanceof ApiError)) {
47
- throw new ApiError(response.status, 'UNKNOWN_ERROR', `${responseText}`);
48
- }
49
- else {
50
- throw e;
51
- }
52
- }
53
- }
54
- try {
55
- return JSON.parse(responseText);
56
- }
57
- catch (error) {
58
- throw new Error(`Unexpected error. Response was not valid JSON: ${responseText}`);
59
- }
60
- }
61
- exports.getResponseBody = getResponseBody;