Skip to main content
Module

x/airtable/airtable.ts

Unofficial Airtable client for Deno 🦕
Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
import { AirtableOptions, AirtableRequestOptions, DeletedRecord, DeletedRecords, FieldSet, RecordOptions, SelectOptions, SelectResult, TableRecord, TableRecords,} from "./types.ts";
import { AirtableError } from "./error.ts";import { hasAnyKey } from "./utils.ts";import { stringify } from "./deps.ts";
/** * Unofficial Airtable API client for Deno * * @author Griko Nibras <hello@griko.id> * @copyright MIT License Copyright (c) 2020 [Griko Nibras](https://github.com/grikomsn) * @export * @class Airtable */export class Airtable { #options: AirtableOptions;
/** * Creates an instance of Airtable client * * ```ts * const airtable = new Airtable() * const airtable = new Airtable({ useEnv: true }) * const airtable = new Airtable({ * apiKey: "keyXXXXXXXXXXXXXX", * baseId: "appXXXXXXXXXXXXXX", * tableName: "Some table name", * }) * ``` * * @param {AirtableOptions} options Airtable client configuration * @memberof Airtable */ constructor(options: AirtableOptions = {}) { const d = Airtable.defaultOptions; this.#options = { ...d, ...(options.useEnv ? { apiKey: Deno.env.get("AIRTABLE_API_KEY"), endpointUrl: Deno.env.get("AIRTABLE_ENDPOINT_URL"), baseId: Deno.env.get("AIRTABLE_BASE_ID"), tableName: Deno.env.get("AIRTABLE_TABLE_NAME"), } : {}), ...options, }; }
/** * Reconfigure the Airtable client * * ```ts * airtable.configure({ * apiKey: "keyXXXXXXXXXXXXXX", * baseId: "appXXXXXXXXXXXXXX", * tableName: "Some table name", * }) * ``` * * @param {AirtableOptions} options Airtable client configuration * @returns {Airtable} current Airtable client * @memberof Airtable */ configure(options: AirtableOptions): Airtable { this.#options = { ...this.#options, ...options, }; return this; }
/** * Returns new Airtable client with defined base ID * * ```ts * const airtable = new Airtable() * const airtableWithNewBaseId = airtable.base("appXXXXXXXXXXXXXX") * ``` * * @param {string} baseId Airtable base * @returns {Airtable} current Airtable client * @memberof Airtable */ base(baseId: string): Airtable { return new Airtable({ ...this.#options, baseId, }); }
/** * Returns new Airtable client with defined table name * * ```ts * const airtable = new Airtable() * const airtableWithNewTableName = airtable.table("Some table name") * ``` * * @param {string} tableName * @returns {Airtable} current Airtable client * @memberof Airtable */ table(tableName: string): Airtable { return new Airtable({ ...this.#options, tableName, }); }
/** * List records from selected base and table * * ```ts * const results = await airtable.select() * ``` * * @template T table field types * @param {SelectOptions} [options={}] select query options, read more on the [Airtable API documentation](https://airtable.com/api) * @returns {Promise<SelectResult<T>>} select query result * @memberof Airtable */ select<T extends FieldSet>( options: SelectOptions<T> = {} ): Promise<SelectResult<T>> { return this.request<SelectResult<T>>({ url: this.getRequestUrl(options), }); }
/** * Retrieve record from selected base and table * * ```ts * const record = await airtable.find("recXXXXXXXXXXXXXX") * const { id, fields, createdTime } = await airtable.find("recXXXXXXXXXXXXXX") * ``` * * @template T table field types * @param {string} id table record id * @returns {Promise<TableRecord<T>>} table record result * @memberof Airtable */ find<T extends FieldSet>(id: string): Promise<TableRecord<T>> { return this.request<TableRecord<T>>({ url: this.getRequestUrl({}, id), }); }
/** * Create record for selected base and table * * ```ts * const createOne = await airtable.create({ * Name: "Griko Nibras", * Age: 25, * }); * ``` * * @template T table field types * @param {T} data record values * @param {RecordOptions} [options] record creations options, read more on the [Airtable API documentation](https://airtable.com/api) * @returns {Promise<TableRecord<T>>} created record values * @memberof Airtable */ create<T extends FieldSet>( data: T, options?: RecordOptions ): Promise<TableRecord<T>>;
/** * Create multiple records for selected base and table * * ```ts * const createMultiple = await airtable.create( * [ * { Name: "Foo", Age: 20 }, * { Name: "Bar", Age: 15 }, * ], * { typecast: true } * ); * ``` * * @template T table field types * @param {T[]} data array of record values * @param {RecordOptions} [options] record creations options, read more on the [Airtable API documentation](https://airtable.com/api) * @returns {Promise<TableRecords<T>>} array of created record values * @memberof Airtable */ create<T extends FieldSet>( data: T[], options?: RecordOptions ): Promise<TableRecords<T>>;
create<T extends FieldSet>( data: T | T[], options: RecordOptions = {} ): Promise<TableRecord<T>> | Promise<TableRecords<T>> { if (data instanceof Array) { return this.jsonRequest<TableRecords<T>>({ url: this.getRequestUrl(), method: "POST", jsonBody: { records: data.map((fields) => ({ fields })), ...options, }, }); }
return this.jsonRequest<TableRecord<T>>({ url: this.getRequestUrl(), method: "POST", jsonBody: { fields: data, ...options, }, }); }
/** * Update multiple for selected base and table * * ```ts * const updateMultiple = await airtable.update( * [ * { * id: "recXXXXXXXXXXXXXX", * fields: { Name: "Adult boi", Age: 30 }, * }, * { * id: "recXXXXXXXXXXXXXX", * fields: { Name: "Yung boi", Age: 15 }, * }, * ], * { typecast: true } * ); * ``` * * @template T table field types * @param {TableRecord<T>[]} records array of record values to be updated * @param {RecordOptions} [options] record updating options, read more on the [Airtable API documentation](https://airtable.com/api) * @returns {Promise<TableRecords<T>>} array of updated record values * @memberof Airtable */ update<T extends FieldSet>( records: TableRecord<T>[], options?: RecordOptions ): Promise<TableRecords<T>>;
/** * Update single records for selected base and table * * ```ts * const updateOne = await airtable.update("recXXXXXXXXXXXXXX", { * Name: "Adult boi", * Age: 30, * }); * ``` * * @template T table field types * @param {string} id record id * @param {T} record record values * @param {RecordOptions} [options] record updating options, read more on the [Airtable API documentation](https://airtable.com/api) * @returns {Promise<TableRecords<T>>} updated record values * @memberof Airtable */ update<T extends FieldSet>( id: string, record: T, options?: RecordOptions ): Promise<TableRecord<T>>;
update<T extends FieldSet>( idOrRecords: string | TableRecord<T>[], record?: T | RecordOptions, options?: RecordOptions ): Promise<TableRecord<T>> | Promise<TableRecords<T>> { if (typeof idOrRecords === "string") { const id = idOrRecords; const fields = record; return this.updateOrReplace<T>({ id, data: { fields } as TableRecord<T>, options, replace: false, }); }
const records = idOrRecords; return this.updateOrReplace<T>({ data: { records }, options, replace: false, }); }
/** * Replace multiple for selected base and table * * ```ts * const replaceMultiple = await airtable.replace( * [ * { * id: "recXXXXXXXXXXXXXX", * fields: { Name: "Adult boi", Age: 30 }, * }, * { * id: "recXXXXXXXXXXXXXX", * fields: { Name: "Yung boi", Age: 15 }, * }, * ], * { typecast: true } * ); * ``` * * @template T table field types * @param {TableRecord<T>[]} records array of record values to be replaced * @param {RecordOptions} [options] record replacing options, read more on the [Airtable API documentation](https://airtable.com/api) * @returns {Promise<TableRecords<T>>} array of replaced record values * @memberof Airtable */ replace<T extends FieldSet>( records: TableRecord<T>[], options?: RecordOptions ): Promise<TableRecords<T>>;
/** * Replace single records for selected base and table * * ```ts * const replaceOne = await airtable.update("recXXXXXXXXXXXXXX", { * Name: "Adult boi", * Age: 30, * }); * ``` * * @template T table field types * @param {string} id record id * @param {T} record record values * @param {RecordOptions} [options] record replacing options, read more on the [Airtable API documentation](https://airtable.com/api) * @returns {Promise<TableRecords<T>>} replaced record values * @memberof Airtable */ replace<T extends FieldSet>( id: string, record: T, options?: RecordOptions ): Promise<TableRecord<T>>;
replace<T extends FieldSet>( idOrRecords: string | TableRecord<T>[], record?: T | RecordOptions, options?: RecordOptions ): Promise<TableRecord<T>> | Promise<TableRecords<T>> { if (typeof idOrRecords === "string") { const id = idOrRecords; const fields = record; return this.updateOrReplace<T>({ id, data: { fields } as TableRecord<T>, options, replace: true, }); }
const records = idOrRecords; return this.updateOrReplace<T>({ data: { records }, options, replace: true, }); }
/** * Delete record from selected base and table * * ```ts * const deleteOne = await airtable.delete("recXXXXXXXXXXXXXX"); * ``` * * @param {string} id record id * @returns {Promise<DeletedRecord>} deleted record result * @memberof Airtable */ delete(id: string): Promise<DeletedRecord>;
/** * Delete multiple records from selected base and table * * ```ts * const deleteMultiple = await airtable.delete([ * "recXXXXXXXXXXXXXX", * "recXXXXXXXXXXXXXX", * ]); * ``` * * @param {string[]} ids record ids * @returns {Promise<DeletedRecords>} deleted records result * @memberof Airtable */ delete(ids: string[]): Promise<DeletedRecords>;
delete(ids: string | string[]): Promise<DeletedRecord | DeletedRecords> { return this.request<DeletedRecord | DeletedRecords>({ url: this.getRequestUrl({}, Array.isArray(ids) ? "" : ids), method: "DELETE", headers: { "Content-Type": "application/x-www-form-urlencoded", }, ...(Array.isArray(ids) ? { body: ids.map((id) => `records[]=${id}`).join("&") } : {}), }); }
private getAuthHeader(): HeadersInit { return { Authorization: `Bearer ${this.#options.apiKey}`, }; }
private getRequestUrl( query: Record<string, any> = {}, ...segments: string[] ): string { const { apiKey, endpointUrl, baseId, tableName } = this.#options;
if (!apiKey) { throw new AirtableError({ message: "An API key is required to connect to Airtable", }); }
if (!endpointUrl) { throw new AirtableError({ message: "Endpoint URL is not defined", }); }
if (!baseId) { throw new AirtableError({ message: "Base ID is not defined", }); }
if (!tableName) { throw new AirtableError({ message: "Table Name is not defined", }); }
const urlSegments = [ endpointUrl, baseId, encodeURIComponent(tableName), ...segments, ...(hasAnyKey(query) ? ["?", stringify(query)] : []), ];
return urlSegments.join("/"); }
private async request<T>({ url, headers, ...options }: AirtableRequestOptions): Promise<T> { const response = await fetch(url, { headers: { ...this.getAuthHeader(), ...headers, }, ...options, });
if (!response.ok) { throw new AirtableError({ message: `${response.status} ${response.statusText}`, stackObject: await response.text(), status: response.status, }); }
return response.json(); }
private async jsonRequest<T>({ headers, jsonBody, ...options }: AirtableRequestOptions): Promise<T> { return this.request<T>({ headers: { ...headers, "Content-Type": "application/json", }, body: JSON.stringify(jsonBody), ...options, }); }
private updateOrReplace<T extends FieldSet>({ id, data, replace, options, }: UpdateOrReplaceOptions<T>): | Promise<TableRecord<T>> | Promise<TableRecords<T>> { const method = replace ? "PUT" : "PATCH";
if (typeof id === "string") { return this.jsonRequest<TableRecord<T>>({ url: this.getRequestUrl({}, id), method, jsonBody: { ...data, ...options, }, }); }
return this.jsonRequest<TableRecords<T>>({ url: this.getRequestUrl(), method, jsonBody: { ...data, ...options, }, }); }
static defaultOptions: Partial<AirtableOptions> = { endpointUrl: "https://api.airtable.com/v0", };}
interface UpdateOrReplaceOptions<T extends FieldSet> { id?: string; data?: TableRecord<T> | TableRecords<T>; replace?: boolean; options?: RecordOptions;}