From 39cee68fbf5e879433a9eddb14f6afebdd114330 Mon Sep 17 00:00:00 2001 From: Vladimir Ubogovich Date: Thu, 27 Jan 2022 22:11:25 +0100 Subject: [PATCH] Add stack trace for Airtable errors Official client error class does not extend `Error` so no stack trace :( See https://github.com/Airtable/airtable.js/issues/294 --- .changeset/eighty-wombats-visit.md | 6 +++++ integration.ts | 9 ++++--- src/error.ts | 20 ++++++++++++++ src/index.ts | 1 + src/official-client-wrapper.ts | 42 ++++++++++++++++++------------ src/table.ts | 5 ++-- 6 files changed, 59 insertions(+), 24 deletions(-) create mode 100644 .changeset/eighty-wombats-visit.md create mode 100644 src/error.ts diff --git a/.changeset/eighty-wombats-visit.md b/.changeset/eighty-wombats-visit.md new file mode 100644 index 0000000..90ecf5d --- /dev/null +++ b/.changeset/eighty-wombats-visit.md @@ -0,0 +1,6 @@ +--- +"@qualifyze/airtable": major +--- + +[BREAKING CHANGE] Introduce `AirtableError` extending `Error` to bring the stack trace very useful for debugging. +The consumers should check their code for usage of `Airtable.Error` from the official client and replace it with the one from this library. diff --git a/integration.ts b/integration.ts index b825ccd..eb92886 100644 --- a/integration.ts +++ b/integration.ts @@ -1,5 +1,5 @@ import Airtable from "airtable"; -import { AirtableRecord, Base, UnknownFields } from "./src"; +import { AirtableError, AirtableRecord, Base, UnknownFields } from "./src"; const apiKey = process.env.AIRTABLE_API_KEY; const baseId = process.env.AIRTABLE_BASE_ID; @@ -55,11 +55,12 @@ const validateRecord = ( const validateNotFound = async (target: () => Promise) => { try { await target(); - throw new Error("Expected an error here"); } catch (err: unknown) { - if (err instanceof Airtable.Error && err.error === "NOT_FOUND") return; + if (err instanceof AirtableError && err.error === "NOT_FOUND") return; throw err; } + + throw new Error("Expected an error here"); }; const main = async () => { @@ -216,6 +217,6 @@ const main = async () => { }; main().catch((error) => { - console.error(error); + console.error(error.stack); process.exitCode = 1; }); diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..32b86cd --- /dev/null +++ b/src/error.ts @@ -0,0 +1,20 @@ +import type { Error as OfficialClientError } from "airtable"; + +// Use a custom error to bring the proper stack trace +export class AirtableError extends Error { + constructor( + public error: string, + message: string, + public statusCode: number + ) { + super(message); + } + + static fromOfficialClientError({ + error, + message, + statusCode, + }: OfficialClientError) { + return new AirtableError(error, message, statusCode); + } +} diff --git a/src/index.ts b/src/index.ts index 610fc65..e96a634 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,5 +4,6 @@ export * from "./record"; export * from "./select-query"; export * from "./validator"; export * from "./official-client-wrapper"; +export * from "./error"; export * from "./fields"; export * from "./endpoint"; diff --git a/src/official-client-wrapper.ts b/src/official-client-wrapper.ts index 7a1ed21..bda8ba6 100644 --- a/src/official-client-wrapper.ts +++ b/src/official-client-wrapper.ts @@ -1,4 +1,5 @@ import Airtable from "airtable"; +import { AirtableError } from "./error"; import { Endpoint, EndpointOptions, @@ -19,25 +20,32 @@ export class OfficialClientWrapper implements Endpoint { method: RestMethod, { path, payload }: EndpointOptions

): Promise { - const { statusCode, headers, body } = await this.officialClient.makeRequest( - { - method, - path: path === null ? undefined : `/${path}`, - qs: payload?.query, - body: payload?.body, + try { + const { statusCode, headers, body } = + await this.officialClient.makeRequest({ + method, + path: path === null ? undefined : `/${path}`, + qs: payload?.query, + body: payload?.body, + }); + + if (!(+statusCode >= 200 && +statusCode < 300)) { + throw new Error( + `Airtable API responded with status code "${statusCode}, but no semantic error in response: ${JSON.stringify( + { headers, body }, + null, + 2 + )}` + ); } - ); - if (!(+statusCode >= 200 && +statusCode < 300)) { - throw new Error( - `Airtable API responded with status code "${statusCode}, but no semantic error in response: ${JSON.stringify( - { headers, body }, - null, - 2 - )}` - ); + return body; + } catch (err: unknown) { + // Because official client error is not extended from Error so no stack trace + if (err instanceof Airtable.Error) { + throw AirtableError.fromOfficialClientError(err); + } + throw err; } - - return body; } } diff --git a/src/table.ts b/src/table.ts index 521b7de..c35d49b 100644 --- a/src/table.ts +++ b/src/table.ts @@ -1,9 +1,8 @@ -import Airtable from "airtable"; - import { FieldsValidator, UnknownFields } from "./fields"; import { ActionPoint, ActionPointOptions } from "./action-point"; import { ValidationContext } from "./validator"; import { RestMethod, UnknownActionPayload } from "./endpoint"; +import { AirtableError } from "./error"; import { AirtableRecord } from "./record"; import { AirtableRecordDraft } from "./record-draft"; import { SelectQuery, SelectQueryParams } from "./select-query"; @@ -81,7 +80,7 @@ export class Table // async/await are needed here to catch the error return await new AirtableRecordDraft(this, recordId).fetch(); } catch (err: unknown) { - if (err instanceof Airtable.Error && err.error === "NOT_FOUND") { + if (err instanceof AirtableError && err.error === "NOT_FOUND") { return null; } throw err;