airtable-ts 1.3.2 → 1.5.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 +172 -1
- package/dist/AirtableTs.d.ts +18 -0
- package/dist/AirtableTs.js +84 -0
- package/dist/AirtableTsError.d.ts +22 -0
- package/dist/AirtableTsError.js +37 -0
- package/dist/assertMatchesSchema.js +13 -3
- package/dist/getAirtableTsTable.d.ts +4 -0
- package/dist/getAirtableTsTable.js +100 -0
- package/dist/index.d.ts +4 -20
- package/dist/index.js +5 -62
- package/dist/mapping/fieldMappers.js +147 -31
- package/dist/mapping/nameMapper.js +14 -3
- package/dist/mapping/recordMapper.d.ts +4 -4
- package/dist/mapping/recordMapper.js +55 -33
- package/dist/mapping/typeUtils.d.ts +1 -1
- package/dist/mapping/typeUtils.js +15 -6
- package/dist/types.d.ts +7 -2
- package/dist/wrapToCatchAirtableErrors.d.ts +13 -0
- package/dist/wrapToCatchAirtableErrors.js +65 -0
- package/package.json +1 -1
- package/dist/getAirtableTable.d.ts +0 -4
- package/dist/getAirtableTable.js +0 -60
package/README.md
CHANGED
|
@@ -15,7 +15,12 @@ All of these problems are solved with airtable-ts.
|
|
|
15
15
|
|
|
16
16
|
In development, you'll define the expected types for different fields: enabling you to leverage type hints in your code editor. After deployment, if people make breaking changes to your base schema you'll get clear runtime errors that pinpoint the problem (rather than your app silently failing, or worse: doing something dangerous!) We also fix unintuitive API behavior, like not being able to tell whether a checkbox field has been deleted or the values are just unticked.
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## Related libraries
|
|
19
|
+
|
|
20
|
+
- [airtable-ts-codegen](https://github.com/domdomegg/airtable-ts-codegen): Autogenerate TypeScript definitions for your Airtable base, with perfect compatibility with airtable-ts
|
|
21
|
+
- [airtable-ts-formula](https://github.com/domdomegg/airtable-ts-formula): Type-safe, securely-escaped and rename-robust formulae for Airtable (e.g. for `filterByFormula`)
|
|
22
|
+
|
|
23
|
+
## Example
|
|
19
24
|
|
|
20
25
|
Install it with `npm install airtable-ts`. Then, use it like:
|
|
21
26
|
|
|
@@ -24,6 +29,7 @@ import { AirtableTs, Table } from 'airtable-ts';
|
|
|
24
29
|
|
|
25
30
|
const db = new AirtableTs({
|
|
26
31
|
// Create your own at https://airtable.com/create/tokens
|
|
32
|
+
// Recommended scopes: schema.bases:read, data.records:read, data.records:write
|
|
27
33
|
apiKey: 'pat1234.abcdef',
|
|
28
34
|
});
|
|
29
35
|
|
|
@@ -68,6 +74,171 @@ async function prefixTitleOfFirstClassOfFirstStudent(prefix: string) {
|
|
|
68
74
|
const rawSdk: Airtable = db.airtable;
|
|
69
75
|
```
|
|
70
76
|
|
|
77
|
+
## AirtableTs Class Reference
|
|
78
|
+
|
|
79
|
+
The `AirtableTs` class provides several methods to interact with your Airtable base:
|
|
80
|
+
|
|
81
|
+
### Constructor
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
new AirtableTs(options: AirtableTsOptions)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Creates a new instance of the AirtableTs client.
|
|
88
|
+
|
|
89
|
+
**Parameters:**
|
|
90
|
+
- `options`: Configuration options
|
|
91
|
+
- `apiKey`: Your Airtable API key (required)
|
|
92
|
+
- Create one at https://airtable.com/create/tokens
|
|
93
|
+
- Recommended scopes: schema.bases:read, data.records:read, data.records:write
|
|
94
|
+
- `baseSchemaCacheDurationMs`: Duration in milliseconds to cache base schema (default: 120,000ms = 2 minutes)
|
|
95
|
+
- Other options from Airtable.js are supported, including: `apiVersion`, `customHeaders`, `endpointUrl`, `noRetryIfRateLimited`, `requestTimeout`
|
|
96
|
+
|
|
97
|
+
**Example:**
|
|
98
|
+
```ts
|
|
99
|
+
const db = new AirtableTs({
|
|
100
|
+
apiKey: 'pat1234.abcdef',
|
|
101
|
+
baseSchemaCacheDurationMs: 300000, // 5 minutes
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### get
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
async get<T extends Item>(table: Table<T>, id: string): Promise<T>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Retrieves a single record from a table by its ID.
|
|
112
|
+
|
|
113
|
+
**Parameters:**
|
|
114
|
+
- `table`: Table definition object
|
|
115
|
+
- `id`: The ID of the record to retrieve
|
|
116
|
+
|
|
117
|
+
**Example:**
|
|
118
|
+
```ts
|
|
119
|
+
const student = await db.get(studentTable, 'rec1234');
|
|
120
|
+
console.log(student.firstName); // Access fields with type safety
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### scan
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
async scan<T extends Item>(table: Table<T>, params?: ScanParams): Promise<T[]>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Retrieves all records from a table, with optional filtering parameters.
|
|
130
|
+
|
|
131
|
+
**Parameters:**
|
|
132
|
+
- `table`: Table definition object
|
|
133
|
+
- `params` (optional): Parameters for filtering, sorting, and limiting results
|
|
134
|
+
- `filterByFormula`: An Airtable formula to filter records
|
|
135
|
+
- Tip: use [airtable-ts-formula](https://github.com/domdomegg/airtable-ts-formula) for type-safe, securely-escaped and rename-robust formulae!
|
|
136
|
+
- `sort`: Array of sort objects (e.g., `[{field: 'firstName', direction: 'asc'}]`)
|
|
137
|
+
- `maxRecords`: Maximum number of records to return
|
|
138
|
+
- `view`: Name of a view to use for record selection
|
|
139
|
+
- `timeZone`: Timezone for interpreting date values
|
|
140
|
+
- `userLocale`: Locale for formatting date values
|
|
141
|
+
|
|
142
|
+
**Example:**
|
|
143
|
+
```ts
|
|
144
|
+
// Get all records
|
|
145
|
+
const allStudents = await db.scan(studentTable);
|
|
146
|
+
|
|
147
|
+
// Get records with filtering and sorting
|
|
148
|
+
const topStudents = await db.scan(studentTable, {
|
|
149
|
+
filterByFormula: '{grade} >= 90',
|
|
150
|
+
sort: [{field: 'grade', direction: 'desc'}],
|
|
151
|
+
maxRecords: 10
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### insert
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
async insert<T extends Item>(table: Table<T>, data: Partial<Omit<T, 'id'>>): Promise<T>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Creates a new record in a table. Returns the new record.
|
|
162
|
+
|
|
163
|
+
**Parameters:**
|
|
164
|
+
- `table`: Table definition object
|
|
165
|
+
- `data`: The data for the new record (without an ID, as Airtable will generate one)
|
|
166
|
+
|
|
167
|
+
**Example:**
|
|
168
|
+
```ts
|
|
169
|
+
const newStudent = await db.insert(studentTable, {
|
|
170
|
+
firstName: 'Jane',
|
|
171
|
+
classes: ['rec5678', 'rec9012']
|
|
172
|
+
});
|
|
173
|
+
console.log(newStudent.id); // The new record ID generated by Airtable
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### update
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
async update<T extends Item>(table: Table<T>, data: Partial<T> & { id: string }): Promise<T>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Updates an existing record in a table. Returns the updated record.
|
|
183
|
+
|
|
184
|
+
**Parameters:**
|
|
185
|
+
- `table`: Table definition object
|
|
186
|
+
- `data`: The data to update, must include the record ID
|
|
187
|
+
|
|
188
|
+
**Example:**
|
|
189
|
+
```ts
|
|
190
|
+
const updatedStudent = await db.update(studentTable, {
|
|
191
|
+
id: 'rec1234',
|
|
192
|
+
firstName: 'John',
|
|
193
|
+
// Only include fields you want to update
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### remove
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
async remove<T extends Item>(table: Table<T>, id: string): Promise<{ id: string }>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Deletes a record from a table.
|
|
204
|
+
|
|
205
|
+
**Parameters:**
|
|
206
|
+
- `table`: Table definition object
|
|
207
|
+
- `id`: The ID of the record to delete
|
|
208
|
+
|
|
209
|
+
**Example:**
|
|
210
|
+
```ts
|
|
211
|
+
await db.remove(studentTable, 'rec1234');
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### table
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
async table<T extends Item>(table: Table<T>): Promise<AirtableTsTable<T>>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Retrieves the AirtableTsTable object for the given table definition. This is the Airtable.js table, enriched with a `fields` key that includes details of the Airtable schema for this table.
|
|
221
|
+
|
|
222
|
+
This is useful for advanced use cases where you need direct access to the Airtable table object.
|
|
223
|
+
|
|
224
|
+
**Parameters:**
|
|
225
|
+
- `table`: Table definition object
|
|
226
|
+
|
|
227
|
+
**Example:**
|
|
228
|
+
```ts
|
|
229
|
+
const airtableTsTable = await db.table(studentTable);
|
|
230
|
+
// Now you can use the raw Airtable table object with field information
|
|
231
|
+
console.log(airtableTsTable.fields); // Access the table's field definitions
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### airtable
|
|
235
|
+
|
|
236
|
+
The underlying Airtable.js SDK is exposed in the `airtable` property.
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const rawSdk: Airtable = db.airtable;
|
|
240
|
+
```
|
|
241
|
+
|
|
71
242
|
## Contributing
|
|
72
243
|
|
|
73
244
|
Pull requests are welcomed on GitHub! To get started:
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Airtable from 'airtable';
|
|
2
|
+
import { Item, Table } from './mapping/typeUtils';
|
|
3
|
+
import { AirtableTsTable, AirtableTsOptions, ScanParams } from './types';
|
|
4
|
+
export declare class AirtableTs {
|
|
5
|
+
airtable: Airtable;
|
|
6
|
+
private options;
|
|
7
|
+
constructor(options: AirtableTsOptions);
|
|
8
|
+
get<T extends Item>(table: Table<T>, id: string): Promise<T>;
|
|
9
|
+
scan<T extends Item>(table: Table<T>, params?: ScanParams): Promise<T[]>;
|
|
10
|
+
insert<T extends Item>(table: Table<T>, data: Partial<Omit<T, 'id'>>): Promise<T>;
|
|
11
|
+
update<T extends Item>(table: Table<T>, data: Partial<T> & {
|
|
12
|
+
id: string;
|
|
13
|
+
}): Promise<T>;
|
|
14
|
+
remove<T extends Item>(table: Table<T>, id: string): Promise<{
|
|
15
|
+
id: string;
|
|
16
|
+
}>;
|
|
17
|
+
table<T extends Item>(table: Table<T>): Promise<AirtableTsTable<T>>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AirtableTs = void 0;
|
|
7
|
+
const airtable_1 = __importDefault(require("airtable"));
|
|
8
|
+
const getAirtableTsTable_1 = require("./getAirtableTsTable");
|
|
9
|
+
const assertMatchesSchema_1 = require("./assertMatchesSchema");
|
|
10
|
+
const recordMapper_1 = require("./mapping/recordMapper");
|
|
11
|
+
const getFields_1 = require("./getFields");
|
|
12
|
+
const wrapToCatchAirtableErrors_1 = require("./wrapToCatchAirtableErrors");
|
|
13
|
+
const AirtableTsError_1 = require("./AirtableTsError");
|
|
14
|
+
class AirtableTs {
|
|
15
|
+
airtable;
|
|
16
|
+
options;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.airtable = new airtable_1.default(options);
|
|
19
|
+
this.options = {
|
|
20
|
+
...airtable_1.default.default_config(),
|
|
21
|
+
...options,
|
|
22
|
+
baseSchemaCacheDurationMs: options.baseSchemaCacheDurationMs ?? 120000,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async get(table, id) {
|
|
26
|
+
if (!id) {
|
|
27
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
28
|
+
message: `The record ID must be supplied when getting a record. This was thrown when trying to get a '${table.name}' (${table.tableId}) record.`,
|
|
29
|
+
type: AirtableTsError_1.ErrorType.INVALID_PARAMETER,
|
|
30
|
+
suggestion: 'Provide a valid record ID when calling the get method.',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const airtableTsTable = await (0, getAirtableTsTable_1.getAirtableTsTable)(this.airtable, table, this.options);
|
|
34
|
+
const record = await airtableTsTable.find(id);
|
|
35
|
+
if (!record) {
|
|
36
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
37
|
+
message: `No record with ID '${id}' exists in table '${table.name}'.`,
|
|
38
|
+
type: AirtableTsError_1.ErrorType.RESOURCE_NOT_FOUND,
|
|
39
|
+
suggestion: 'Verify that the record ID is correct and that the record exists in the table.',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
|
|
43
|
+
}
|
|
44
|
+
async scan(table, params) {
|
|
45
|
+
const airtableTsTable = await (0, getAirtableTsTable_1.getAirtableTsTable)(this.airtable, table, this.options);
|
|
46
|
+
const records = await airtableTsTable.select({
|
|
47
|
+
fields: (0, getFields_1.getFields)(table),
|
|
48
|
+
...params,
|
|
49
|
+
}).all();
|
|
50
|
+
return records.map((record) => (0, recordMapper_1.mapRecordFromAirtable)(table, record));
|
|
51
|
+
}
|
|
52
|
+
async insert(table, data) {
|
|
53
|
+
(0, assertMatchesSchema_1.assertMatchesSchema)(table, { ...data, id: 'placeholder' });
|
|
54
|
+
const airtableTsTable = await (0, getAirtableTsTable_1.getAirtableTsTable)(this.airtable, table, this.options);
|
|
55
|
+
const record = await airtableTsTable.create((0, recordMapper_1.mapRecordToAirtable)(table, data, airtableTsTable));
|
|
56
|
+
return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
|
|
57
|
+
}
|
|
58
|
+
async update(table, data) {
|
|
59
|
+
(0, assertMatchesSchema_1.assertMatchesSchema)(table, { ...data });
|
|
60
|
+
const { id, ...withoutId } = data;
|
|
61
|
+
const airtableTsTable = await (0, getAirtableTsTable_1.getAirtableTsTable)(this.airtable, table, this.options);
|
|
62
|
+
const record = await airtableTsTable.update(data.id, (0, recordMapper_1.mapRecordToAirtable)(table, withoutId, airtableTsTable));
|
|
63
|
+
return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
|
|
64
|
+
}
|
|
65
|
+
async remove(table, id) {
|
|
66
|
+
if (!id) {
|
|
67
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
68
|
+
message: `The record ID must be supplied when removing a record. This was thrown when trying to get a '${table.name}' (${table.tableId}) record.`,
|
|
69
|
+
type: AirtableTsError_1.ErrorType.INVALID_PARAMETER,
|
|
70
|
+
suggestion: 'Provide a valid record ID when calling the remove method.',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const airtableTsTable = await (0, getAirtableTsTable_1.getAirtableTsTable)(this.airtable, table, this.options);
|
|
74
|
+
const record = await airtableTsTable.destroy(id);
|
|
75
|
+
return { id: record.id };
|
|
76
|
+
}
|
|
77
|
+
async table(table) {
|
|
78
|
+
return (0, getAirtableTsTable_1.getAirtableTsTable)(this.airtable, table, this.options);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.AirtableTs = AirtableTs;
|
|
82
|
+
// Wrap all methods of AirtableTs with error handling
|
|
83
|
+
// See https://github.com/Airtable/airtable.js/issues/294
|
|
84
|
+
(0, wrapToCatchAirtableErrors_1.wrapToCatchAirtableErrors)(AirtableTs);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types for categorizing different kinds of errors
|
|
3
|
+
*/
|
|
4
|
+
export declare enum ErrorType {
|
|
5
|
+
SCHEMA_VALIDATION = "SCHEMA_VALIDATION",
|
|
6
|
+
RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND",
|
|
7
|
+
INVALID_PARAMETER = "INVALID_PARAMETER",
|
|
8
|
+
API_ERROR = "API_ERROR"
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Base error class for all airtable-ts errors
|
|
12
|
+
*/
|
|
13
|
+
export declare class AirtableTsError extends Error {
|
|
14
|
+
/** Error type for categorization */
|
|
15
|
+
type: ErrorType;
|
|
16
|
+
constructor(options: {
|
|
17
|
+
message: string;
|
|
18
|
+
type: ErrorType;
|
|
19
|
+
suggestion?: string;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export declare const prependError: (error: unknown, prefix: string) => unknown;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prependError = exports.AirtableTsError = exports.ErrorType = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Error types for categorizing different kinds of errors
|
|
6
|
+
*/
|
|
7
|
+
var ErrorType;
|
|
8
|
+
(function (ErrorType) {
|
|
9
|
+
ErrorType["SCHEMA_VALIDATION"] = "SCHEMA_VALIDATION";
|
|
10
|
+
ErrorType["RESOURCE_NOT_FOUND"] = "RESOURCE_NOT_FOUND";
|
|
11
|
+
ErrorType["INVALID_PARAMETER"] = "INVALID_PARAMETER";
|
|
12
|
+
ErrorType["API_ERROR"] = "API_ERROR";
|
|
13
|
+
})(ErrorType || (exports.ErrorType = ErrorType = {}));
|
|
14
|
+
/**
|
|
15
|
+
* Base error class for all airtable-ts errors
|
|
16
|
+
*/
|
|
17
|
+
class AirtableTsError extends Error {
|
|
18
|
+
/** Error type for categorization */
|
|
19
|
+
type;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
const { message, suggestion, type } = options;
|
|
22
|
+
super(suggestion ? `${message} Suggestion: ${suggestion}` : message);
|
|
23
|
+
this.type = type;
|
|
24
|
+
this.name = 'AirtableTsError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.AirtableTsError = AirtableTsError;
|
|
28
|
+
const prependError = (error, prefix) => {
|
|
29
|
+
if (error instanceof AirtableTsError) {
|
|
30
|
+
// eslint-disable-next-line no-param-reassign
|
|
31
|
+
error.message = `${prefix}: ${error.message}`;
|
|
32
|
+
// eslint-disable-next-line no-param-reassign
|
|
33
|
+
error.stack = `Error: ${prefix}: ${error.stack?.startsWith('Error: ') ? error.stack.slice('Error: '.length) : error.stack}`;
|
|
34
|
+
}
|
|
35
|
+
return error;
|
|
36
|
+
};
|
|
37
|
+
exports.prependError = prependError;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.assertMatchesSchema = void 0;
|
|
4
4
|
const typeUtils_1 = require("./mapping/typeUtils");
|
|
5
|
+
const AirtableTsError_1 = require("./AirtableTsError");
|
|
5
6
|
/**
|
|
6
7
|
* In theory, this should never catch stuff because our type mapping logic should
|
|
7
8
|
* verify the types are compatible.
|
|
@@ -14,7 +15,10 @@ const typeUtils_1 = require("./mapping/typeUtils");
|
|
|
14
15
|
*/
|
|
15
16
|
function assertMatchesSchema(table, data, mode = 'partial') {
|
|
16
17
|
if (typeof data !== 'object' || data === null) {
|
|
17
|
-
throw new
|
|
18
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
19
|
+
message: `Data passed in to airtable-ts should be an object but received ${data === null ? 'null' : typeof data}.`,
|
|
20
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
21
|
+
});
|
|
18
22
|
}
|
|
19
23
|
Object.entries(table.schema).forEach(([fieldName, type]) => {
|
|
20
24
|
const value = data[fieldName];
|
|
@@ -22,10 +26,16 @@ function assertMatchesSchema(table, data, mode = 'partial') {
|
|
|
22
26
|
if (mode === 'partial') {
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
25
|
-
throw new
|
|
29
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
30
|
+
message: `Data passed in to airtable-ts is missing required field '${fieldName}' in table '${table.name}' (expected type: ${type}).`,
|
|
31
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
32
|
+
});
|
|
26
33
|
}
|
|
27
34
|
if (!(0, typeUtils_1.matchesType)(value, type)) {
|
|
28
|
-
throw new
|
|
35
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
36
|
+
message: `Invalid value passed in to airtable-ts for field '${fieldName}' in table '${table.name}' (received type: ${typeof value}, expected type: ${type}).`,
|
|
37
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
38
|
+
});
|
|
29
39
|
}
|
|
30
40
|
});
|
|
31
41
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import Airtable from 'airtable';
|
|
2
|
+
import { Item, Table } from './mapping/typeUtils';
|
|
3
|
+
import { AirtableTsTable, CompleteAirtableTsOptions } from './types';
|
|
4
|
+
export declare const getAirtableTsTable: <T extends Item>(airtable: Airtable, table: Table<T>, options: CompleteAirtableTsOptions) => Promise<AirtableTsTable<T>>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.getAirtableTsTable = void 0;
|
|
27
|
+
const axios_1 = __importStar(require("axios"));
|
|
28
|
+
const AirtableTsError_1 = require("./AirtableTsError");
|
|
29
|
+
const getAirtableTsTable = async (airtable, table, options) => {
|
|
30
|
+
const airtableTable = airtable.base(table.baseId).table(table.tableId);
|
|
31
|
+
// We need the schema so we know which type mapper to use.
|
|
32
|
+
// Even if we were inferring a type mapper from the schema, we'd still have to
|
|
33
|
+
// do this as otherwise we can't distinguish a column with all null values from
|
|
34
|
+
// a column that is missing entirely. We'd like to do that as for safety, we'd
|
|
35
|
+
// rather throw an error if the column is missing entirely; this suggests a
|
|
36
|
+
// misconfiguration. But an all-null column is okay. The particular case that
|
|
37
|
+
// this is likely for is checkbox columns.
|
|
38
|
+
const baseSchema = await getAirtableBaseSchema(table.baseId, options);
|
|
39
|
+
const tableDefinition = baseSchema.find((t) => t.id === table.tableId);
|
|
40
|
+
if (!tableDefinition) {
|
|
41
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
42
|
+
message: `Table '${table.name}' (${table.tableId}) does not exist in base ${table.baseId}.`,
|
|
43
|
+
type: AirtableTsError_1.ErrorType.RESOURCE_NOT_FOUND,
|
|
44
|
+
suggestion: 'Verify that the base ID and table ID are correct.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return Object.assign(airtableTable, {
|
|
48
|
+
fields: tableDefinition.fields,
|
|
49
|
+
tsDefinition: table,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
exports.getAirtableTsTable = getAirtableTsTable;
|
|
53
|
+
const baseSchemaCache = new Map();
|
|
54
|
+
/**
|
|
55
|
+
* Get the schemas from the cache or Airtable API for the tables in the given base.
|
|
56
|
+
* @see https://airtable.com/developers/web/api/get-base-schema
|
|
57
|
+
* @param baseId The base id to get the schemas for
|
|
58
|
+
*/
|
|
59
|
+
const getAirtableBaseSchema = async (baseId, options) => {
|
|
60
|
+
const fromCache = baseSchemaCache.get(baseId);
|
|
61
|
+
if (fromCache && Date.now() - fromCache.at < options.baseSchemaCacheDurationMs) {
|
|
62
|
+
return fromCache.data;
|
|
63
|
+
}
|
|
64
|
+
if (!options.apiKey) {
|
|
65
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
66
|
+
message: 'API key is required but was not provided.',
|
|
67
|
+
type: AirtableTsError_1.ErrorType.INVALID_PARAMETER,
|
|
68
|
+
suggestion: 'Provide a valid Airtable API key when initializing AirtableTs.',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const res = await (0, axios_1.default)({
|
|
72
|
+
baseURL: options.endpointUrl ?? 'https://api.airtable.com',
|
|
73
|
+
url: `/v0/meta/bases/${baseId}/tables`,
|
|
74
|
+
...(options.requestTimeout ? { timeout: options.requestTimeout } : {}),
|
|
75
|
+
headers: {
|
|
76
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
77
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
78
|
+
...options.customHeaders,
|
|
79
|
+
},
|
|
80
|
+
}).catch((err) => {
|
|
81
|
+
const normalizedErrorMessage = err instanceof axios_1.AxiosError
|
|
82
|
+
? `${err.message}. Status: ${err.status}. Data: ${JSON.stringify(err.response?.data)}`
|
|
83
|
+
: err;
|
|
84
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
85
|
+
message: `Failed to get base schema: ${normalizedErrorMessage}`,
|
|
86
|
+
type: AirtableTsError_1.ErrorType.API_ERROR,
|
|
87
|
+
suggestion: 'Ensure the API token is correct, and has `schema.bases:read` permission to the target base.',
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
const baseSchema = res.data.tables;
|
|
91
|
+
if (baseSchemaCache.size > 100) {
|
|
92
|
+
baseSchemaCache.clear();
|
|
93
|
+
// If you're seeing this warning, then we probably either need to:
|
|
94
|
+
// - Update the maximum limit before clearing the cache, provided we have memory headroom; or
|
|
95
|
+
// - Use a last recently used cache or similar
|
|
96
|
+
console.warn('[airtable-ts] baseSchemaCache cleared to avoid a memory leak: this code is not currently optimized for accessing over 100 different bases from a single instance');
|
|
97
|
+
}
|
|
98
|
+
baseSchemaCache.set(baseId, { at: Date.now(), data: baseSchema });
|
|
99
|
+
return baseSchema;
|
|
100
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { AirtableTsOptions } from './types';
|
|
5
|
-
export declare class AirtableTs {
|
|
6
|
-
airtable: Airtable;
|
|
7
|
-
private options;
|
|
8
|
-
constructor(options: AirtableTsOptions);
|
|
9
|
-
get<T extends Item>(table: Table<T>, id: string): Promise<T>;
|
|
10
|
-
scan<T extends Item>(table: Table<T>, params?: ScanParams): Promise<T[]>;
|
|
11
|
-
insert<T extends Item>(table: Table<T>, data: Partial<Omit<T, 'id'>>): Promise<T>;
|
|
12
|
-
update<T extends Item>(table: Table<T>, data: Partial<T> & {
|
|
13
|
-
id: string;
|
|
14
|
-
}): Promise<T>;
|
|
15
|
-
remove<T extends Item>(table: Table<T>, id: string): Promise<{
|
|
16
|
-
id: string;
|
|
17
|
-
}>;
|
|
18
|
-
}
|
|
19
|
-
export type ScanParams = Omit<QueryParams<unknown>, 'fields' | 'cellFormat' | 'method' | 'returnFieldsByFieldId' | 'pageSize' | 'offset'>;
|
|
20
|
-
export type { AirtableTsOptions } from './types';
|
|
1
|
+
export { AirtableTs } from './AirtableTs';
|
|
2
|
+
export { AirtableTsError } from './AirtableTsError';
|
|
3
|
+
export type { AirtableTsOptions, ScanParams, AirtableTsTable } from './types';
|
|
21
4
|
export type { Item, Table } from './mapping/typeUtils';
|
|
5
|
+
export type { WrappedAirtableError } from './wrapToCatchAirtableErrors';
|
package/dist/index.js
CHANGED
|
@@ -1,64 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.AirtableTs = void 0;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const getFields_1 = require("./getFields");
|
|
12
|
-
class AirtableTs {
|
|
13
|
-
airtable;
|
|
14
|
-
options;
|
|
15
|
-
constructor(options) {
|
|
16
|
-
this.airtable = new airtable_1.default(options);
|
|
17
|
-
this.options = {
|
|
18
|
-
...airtable_1.default.default_config(),
|
|
19
|
-
...options,
|
|
20
|
-
baseSchemaCacheDurationMs: options.baseSchemaCacheDurationMs ?? 120000,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
async get(table, id) {
|
|
24
|
-
if (!id) {
|
|
25
|
-
throw new Error(`[airtable-ts] Tried to get record in ${table.name} with no id`);
|
|
26
|
-
}
|
|
27
|
-
const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
|
|
28
|
-
const record = await airtableTable.find(id);
|
|
29
|
-
if (!record) {
|
|
30
|
-
throw new Error(`[airtable-ts] Failed to find record in ${table.name} with key ${id}`);
|
|
31
|
-
}
|
|
32
|
-
return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
|
|
33
|
-
}
|
|
34
|
-
async scan(table, params) {
|
|
35
|
-
const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
|
|
36
|
-
const records = await airtableTable.select({
|
|
37
|
-
fields: (0, getFields_1.getFields)(table),
|
|
38
|
-
...params,
|
|
39
|
-
}).all();
|
|
40
|
-
return records.map((record) => (0, recordMapper_1.mapRecordFromAirtable)(table, record));
|
|
41
|
-
}
|
|
42
|
-
async insert(table, data) {
|
|
43
|
-
(0, assertMatchesSchema_1.assertMatchesSchema)(table, { ...data, id: 'placeholder' });
|
|
44
|
-
const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
|
|
45
|
-
const record = await airtableTable.create((0, recordMapper_1.mapRecordToAirtable)(table, data, airtableTable));
|
|
46
|
-
return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
|
|
47
|
-
}
|
|
48
|
-
async update(table, data) {
|
|
49
|
-
(0, assertMatchesSchema_1.assertMatchesSchema)(table, { ...data });
|
|
50
|
-
const { id, ...withoutId } = data;
|
|
51
|
-
const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
|
|
52
|
-
const record = await airtableTable.update(data.id, (0, recordMapper_1.mapRecordToAirtable)(table, withoutId, airtableTable));
|
|
53
|
-
return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
|
|
54
|
-
}
|
|
55
|
-
async remove(table, id) {
|
|
56
|
-
if (!id) {
|
|
57
|
-
throw new Error(`[airtable-ts] Tried to remove record in ${table.name} with no id`);
|
|
58
|
-
}
|
|
59
|
-
const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
|
|
60
|
-
const record = await airtableTable.destroy(id);
|
|
61
|
-
return { id: record.id };
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
exports.AirtableTs = AirtableTs;
|
|
3
|
+
exports.AirtableTsError = exports.AirtableTs = void 0;
|
|
4
|
+
var AirtableTs_1 = require("./AirtableTs");
|
|
5
|
+
Object.defineProperty(exports, "AirtableTs", { enumerable: true, get: function () { return AirtableTs_1.AirtableTs; } });
|
|
6
|
+
var AirtableTsError_1 = require("./AirtableTsError");
|
|
7
|
+
Object.defineProperty(exports, "AirtableTsError", { enumerable: true, get: function () { return AirtableTsError_1.AirtableTsError; } });
|