airtable-ts 1.4.0 → 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 +63 -13
- 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.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; } });
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fieldMappers = void 0;
|
|
4
4
|
const typeUtils_1 = require("./typeUtils");
|
|
5
|
+
const AirtableTsError_1 = require("../AirtableTsError");
|
|
5
6
|
const fallbackMapperPair = (toFallback, fromFallback) => ({
|
|
6
7
|
toAirtable: (value) => value ?? toFallback,
|
|
7
8
|
fromAirtable: (value) => value ?? fromFallback,
|
|
8
9
|
});
|
|
9
|
-
const readonly = (airtableType) => () => {
|
|
10
|
+
const readonly = (airtableType) => () => {
|
|
11
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
12
|
+
message: `Cannot modify a field of type '${airtableType}' as it is read-only.`,
|
|
13
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
14
|
+
});
|
|
15
|
+
};
|
|
10
16
|
const coerce = (airtableType, tsType) => (value) => {
|
|
11
17
|
const parsedType = (0, typeUtils_1.parseType)(tsType);
|
|
12
18
|
if (!parsedType.array && typeof value === parsedType.single) {
|
|
@@ -25,9 +31,17 @@ const coerce = (airtableType, tsType) => (value) => {
|
|
|
25
31
|
return value[0];
|
|
26
32
|
}
|
|
27
33
|
if (!parsedType.array && Array.isArray(value) && value.length !== 1) {
|
|
28
|
-
throw new
|
|
34
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
35
|
+
message: `Cannot convert array with ${value.length} entries from airtable type '${airtableType} to TypeScript type '${tsType}'.`,
|
|
36
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
37
|
+
suggestion: `Change the type from '${tsType}' to '${tsType}[]' in your table definition.`,
|
|
38
|
+
});
|
|
29
39
|
}
|
|
30
|
-
throw new
|
|
40
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
41
|
+
message: `Cannot convert value from airtable type '${airtableType}' to '${tsType}', as the Airtable API provided a '${typeof value}'.`,
|
|
42
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
43
|
+
suggestion: 'Update the types in your table definition to compatible types for your Airtable base.',
|
|
44
|
+
});
|
|
31
45
|
};
|
|
32
46
|
const dateTimeMapperPair = {
|
|
33
47
|
// Number assumed to be unix time in seconds
|
|
@@ -36,7 +50,10 @@ const dateTimeMapperPair = {
|
|
|
36
50
|
return null;
|
|
37
51
|
const date = new Date(typeof value === 'number' ? value * 1000 : value);
|
|
38
52
|
if (Number.isNaN(date.getTime())) {
|
|
39
|
-
throw new
|
|
53
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
54
|
+
message: 'Invalid date/time value provided.',
|
|
55
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
56
|
+
});
|
|
40
57
|
}
|
|
41
58
|
return date.toJSON();
|
|
42
59
|
},
|
|
@@ -45,7 +62,10 @@ const dateTimeMapperPair = {
|
|
|
45
62
|
return null;
|
|
46
63
|
const date = new Date(value);
|
|
47
64
|
if (Number.isNaN(date.getTime())) {
|
|
48
|
-
throw new
|
|
65
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
66
|
+
message: 'Invalid date/time value received from Airtable.',
|
|
67
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
68
|
+
});
|
|
49
69
|
}
|
|
50
70
|
return date.toJSON();
|
|
51
71
|
},
|
|
@@ -196,7 +216,10 @@ const numberOrNull = {
|
|
|
196
216
|
return null;
|
|
197
217
|
const date = new Date(nullableValue);
|
|
198
218
|
if (Number.isNaN(date.getTime())) {
|
|
199
|
-
throw new
|
|
219
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
220
|
+
message: 'Invalid date/time value received from Airtable.',
|
|
221
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
222
|
+
});
|
|
200
223
|
}
|
|
201
224
|
return Math.floor(date.getTime() / 1000);
|
|
202
225
|
},
|
|
@@ -209,7 +232,10 @@ const numberOrNull = {
|
|
|
209
232
|
return null;
|
|
210
233
|
const date = new Date(nullableValue);
|
|
211
234
|
if (Number.isNaN(date.getTime())) {
|
|
212
|
-
throw new
|
|
235
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
236
|
+
message: 'Invalid date/time value received from Airtable.',
|
|
237
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
238
|
+
});
|
|
213
239
|
}
|
|
214
240
|
return Math.floor(date.getTime() / 1000);
|
|
215
241
|
},
|
|
@@ -222,7 +248,10 @@ const numberOrNull = {
|
|
|
222
248
|
return null;
|
|
223
249
|
const date = new Date(nullableValue);
|
|
224
250
|
if (Number.isNaN(date.getTime())) {
|
|
225
|
-
throw new
|
|
251
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
252
|
+
message: 'Invalid date/time value received from Airtable.',
|
|
253
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
254
|
+
});
|
|
226
255
|
}
|
|
227
256
|
return Math.floor(date.getTime() / 1000);
|
|
228
257
|
},
|
|
@@ -235,7 +264,10 @@ const numberOrNull = {
|
|
|
235
264
|
return null;
|
|
236
265
|
const date = new Date(nullableValue);
|
|
237
266
|
if (Number.isNaN(date.getTime())) {
|
|
238
|
-
throw new
|
|
267
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
268
|
+
message: 'Invalid date/time value received from Airtable.',
|
|
269
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
270
|
+
});
|
|
239
271
|
}
|
|
240
272
|
return Math.floor(date.getTime() / 1000);
|
|
241
273
|
},
|
|
@@ -265,11 +297,21 @@ const stringArrayOrNull = {
|
|
|
265
297
|
multipleCollaborators: multipleCollaboratorsMapperPair,
|
|
266
298
|
multipleAttachments: multipleAttachmentsMapperPair,
|
|
267
299
|
multipleLookupValues: {
|
|
268
|
-
toAirtable: () => {
|
|
300
|
+
toAirtable: () => {
|
|
301
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
302
|
+
message: 'Lookup fields are read-only and cannot be modified.',
|
|
303
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
304
|
+
});
|
|
305
|
+
},
|
|
269
306
|
fromAirtable: coerce('multipleLookupValues', 'string[] | null'),
|
|
270
307
|
},
|
|
271
308
|
formula: {
|
|
272
|
-
toAirtable: () => {
|
|
309
|
+
toAirtable: () => {
|
|
310
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
311
|
+
message: 'Formula fields are read-only and cannot be modified.',
|
|
312
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
313
|
+
});
|
|
314
|
+
},
|
|
273
315
|
fromAirtable: coerce('multipleLookupValues', 'string[] | null'),
|
|
274
316
|
},
|
|
275
317
|
unknown: {
|
|
@@ -287,7 +329,11 @@ exports.fieldMappers = {
|
|
|
287
329
|
fromAirtable: (value) => {
|
|
288
330
|
const nullableValue = nullablePair.fromAirtable(value);
|
|
289
331
|
if (nullableValue === null && ['multipleRecordLinks', 'dateTime', 'createdTime', 'lastModifiedTime'].includes(airtableType)) {
|
|
290
|
-
throw new
|
|
332
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
333
|
+
message: `Cannot convert null value to string for field type '${airtableType}'.`,
|
|
334
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
335
|
+
suggestion: 'Provide a non-null value for this field or update your schema to allow null values.',
|
|
336
|
+
});
|
|
291
337
|
}
|
|
292
338
|
return nullableValue ?? '';
|
|
293
339
|
},
|
|
@@ -311,7 +357,11 @@ exports.fieldMappers = {
|
|
|
311
357
|
fromAirtable: (value) => {
|
|
312
358
|
const nullableValue = nullablePair.fromAirtable(value);
|
|
313
359
|
if (nullableValue === null) {
|
|
314
|
-
throw new
|
|
360
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
361
|
+
message: `Cannot convert null value to number for field type '${airtableType}'.`,
|
|
362
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
363
|
+
suggestion: 'Provide a non-null value for this field or update your schema to allow null values.',
|
|
364
|
+
});
|
|
315
365
|
}
|
|
316
366
|
return nullableValue;
|
|
317
367
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.mapRecordFieldNamesTsToAirtable = exports.mapRecordFieldNamesAirtableToTs = void 0;
|
|
4
|
+
const AirtableTsError_1 = require("../AirtableTsError");
|
|
4
5
|
/**
|
|
5
6
|
* Maps a TS object (matching table.mappings) to another TS object (matching table.schema),
|
|
6
7
|
* mapping columns based on the table definition.
|
|
@@ -86,13 +87,23 @@ const mapRecordFieldNamesTsToAirtable = (table, item) => {
|
|
|
86
87
|
return mappingToAirtable.map((airtableFieldName) => [airtableFieldName, null]);
|
|
87
88
|
}
|
|
88
89
|
// This should be unreachable because of our types
|
|
89
|
-
throw new
|
|
90
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
91
|
+
message: `Received null for non-nullable field '${outputFieldName}' (${mappingToAirtable}) with type '${tsType}' in table '${table.name}' (${table.tableId}). This should never happen in normal operation as it should be caught before this point.`,
|
|
92
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
93
|
+
});
|
|
90
94
|
}
|
|
91
95
|
if (!Array.isArray(value)) {
|
|
92
|
-
throw new
|
|
96
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
97
|
+
message: `Expected an array for field '${outputFieldName}' (${mappingToAirtable}) in table '${table.name}' (${table.tableId}), but received ${typeof value}.`,
|
|
98
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
99
|
+
});
|
|
93
100
|
}
|
|
94
101
|
if (value.length !== mappingToAirtable.length) {
|
|
95
|
-
throw new
|
|
102
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
103
|
+
message: `Array length mismatch for field '${outputFieldName}' (${JSON.stringify(mappingToAirtable)}) in table '${table.name}' (${table.tableId}): received ${value.length} values but had mappings for ${mappingToAirtable.length}.`,
|
|
104
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
105
|
+
suggestion: 'Ensure the array length matches the number of mapped fields in your table definition.',
|
|
106
|
+
});
|
|
96
107
|
}
|
|
97
108
|
return mappingToAirtable.map((airtableFieldName, index) => [airtableFieldName, value[index]]);
|
|
98
109
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { FieldSet } from 'airtable';
|
|
2
|
-
import { AirtableRecord,
|
|
2
|
+
import { AirtableRecord, AirtableTsTable } from '../types';
|
|
3
3
|
import { FromTsTypeString, Item, Table, TsTypeString } from './typeUtils';
|
|
4
4
|
export declare const mapRecordFromAirtable: <T extends Item>(table: Table<T>, record: AirtableRecord) => T;
|
|
5
|
-
export declare const mapRecordToAirtable: <T extends Item>(table: Table<T>, item: Partial<T>,
|
|
5
|
+
export declare const mapRecordToAirtable: <T extends Item>(table: Table<T>, item: Partial<T>, airtableTsTable: AirtableTsTable) => FieldSet;
|
|
6
6
|
export declare const visibleForTesting: {
|
|
7
7
|
mapRecordTypeAirtableToTs: <T extends {
|
|
8
8
|
[fieldNameOrId: string]: TsTypeString;
|
|
9
|
-
}>(tsTypes: T, record: AirtableRecord) => { [F in keyof T]: FromTsTypeString<T[F]>; } & {
|
|
9
|
+
}>(table: Table<Item>, tsTypes: T, record: AirtableRecord) => { [F in keyof T]: FromTsTypeString<T[F]>; } & {
|
|
10
10
|
id: string;
|
|
11
11
|
};
|
|
12
12
|
mapRecordTypeTsToAirtable: <T_1 extends {
|
|
13
13
|
[fieldNameOrId: string]: TsTypeString;
|
|
14
14
|
}, R extends { [K in keyof T_1]?: FromTsTypeString<T_1[K]>; } & {
|
|
15
15
|
id?: string;
|
|
16
|
-
}>(tsTypes: T_1, tsRecord: R,
|
|
16
|
+
}>(table: Table<Item>, tsTypes: T_1, tsRecord: R, airtableTsTable: AirtableTsTable) => FieldSet;
|
|
17
17
|
};
|
|
@@ -4,19 +4,28 @@ exports.visibleForTesting = exports.mapRecordToAirtable = exports.mapRecordFromA
|
|
|
4
4
|
const fieldMappers_1 = require("./fieldMappers");
|
|
5
5
|
const nameMapper_1 = require("./nameMapper");
|
|
6
6
|
const typeUtils_1 = require("./typeUtils");
|
|
7
|
+
const AirtableTsError_1 = require("../AirtableTsError");
|
|
7
8
|
const getMapper = (tsType, airtableType) => {
|
|
8
9
|
const tsMapper = fieldMappers_1.fieldMappers[tsType];
|
|
9
10
|
if (!tsMapper) {
|
|
10
|
-
throw new
|
|
11
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
12
|
+
message: `No mapper exists for TypeScript type '${tsType}'.`,
|
|
13
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
14
|
+
suggestion: 'Check that you are using a supported TypeScript type in your schema definition.',
|
|
15
|
+
});
|
|
11
16
|
}
|
|
12
17
|
if (tsMapper[airtableType]) {
|
|
13
18
|
return tsMapper[airtableType];
|
|
14
19
|
}
|
|
15
20
|
if (tsMapper.unknown) {
|
|
16
|
-
console.warn(`[airtable-ts] Unknown airtable type ${airtableType}. This is not fully supported and exact mapping behaviour may change in a future release.`);
|
|
21
|
+
console.warn(`[airtable-ts] Unknown airtable type ${airtableType} for tsType ${tsType}. This is not fully supported and exact mapping behaviour may change in a future release.`);
|
|
17
22
|
return tsMapper.unknown;
|
|
18
23
|
}
|
|
19
|
-
throw new
|
|
24
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
25
|
+
message: `Cannot map Airtable type '${airtableType}' to TypeScript type '${tsType}'.`,
|
|
26
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
27
|
+
suggestion: 'Check that your schema definition uses TypeScript types that are compatible with the Airtable field types.',
|
|
28
|
+
});
|
|
20
29
|
};
|
|
21
30
|
/**
|
|
22
31
|
* This function coerces an Airtable record to a TypeScript object, given an
|
|
@@ -33,13 +42,17 @@ const getMapper = (tsType, airtableType) => {
|
|
|
33
42
|
* @returns An object matching the TypeScript type passed in, based on the Airtable record. Throws if cannot coerce to requested type.
|
|
34
43
|
* @example { id: 'rec012', a: 'Some text', b: 123, c: false, d: 'rec345' }
|
|
35
44
|
*/
|
|
36
|
-
const mapRecordTypeAirtableToTs = (tsTypes, record) => {
|
|
45
|
+
const mapRecordTypeAirtableToTs = (table, tsTypes, record) => {
|
|
37
46
|
const item = {};
|
|
38
47
|
Object.entries(tsTypes).forEach(([fieldNameOrId, tsType]) => {
|
|
39
48
|
// eslint-disable-next-line no-underscore-dangle
|
|
40
49
|
const fieldDefinition = record._table.fields.find((f) => f.id === fieldNameOrId || f.name === fieldNameOrId);
|
|
41
50
|
if (!fieldDefinition) {
|
|
42
|
-
|
|
51
|
+
// This should not happen normally, as we should only be trying to map fields that are in the table definition
|
|
52
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
53
|
+
message: `Field '${fieldNameOrId}' does not exist in the table definition. This error should not happen in normal operation.`,
|
|
54
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
55
|
+
});
|
|
43
56
|
}
|
|
44
57
|
const value = record.fields[fieldDefinition.name];
|
|
45
58
|
try {
|
|
@@ -48,13 +61,8 @@ const mapRecordTypeAirtableToTs = (tsTypes, record) => {
|
|
|
48
61
|
item[fieldNameOrId] = fromAirtable(value);
|
|
49
62
|
}
|
|
50
63
|
catch (error) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
error.message = `Failed to map field ${record._table.name}.${fieldNameOrId}: ${error.message}`;
|
|
54
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
55
|
-
error.stack = `Error: Failed to map field ${record._table.name}.${fieldNameOrId}: ${error.stack?.startsWith('Error: ') ? error.stack.slice('Error: '.length) : error.stack}`;
|
|
56
|
-
}
|
|
57
|
-
throw error;
|
|
64
|
+
const tsName = table.mappings ? Object.entries(table.mappings).find((e) => e[1] === fieldNameOrId)?.[0] : undefined;
|
|
65
|
+
throw (0, AirtableTsError_1.prependError)(error, `Failed to map field ${tsName ? `${tsName} (${fieldNameOrId})` : fieldNameOrId} from Airtable`);
|
|
58
66
|
}
|
|
59
67
|
});
|
|
60
68
|
return Object.assign(item, { id: record.id });
|
|
@@ -71,13 +79,13 @@ const mapRecordTypeAirtableToTs = (tsTypes, record) => {
|
|
|
71
79
|
* @param tsRecord TypeScript object to convert.
|
|
72
80
|
* @example { a: 'Some text', b: 123, c: false, d: 'rec123' }
|
|
73
81
|
*
|
|
74
|
-
* @param
|
|
82
|
+
* @param airtableTsTable An Airtable table.
|
|
75
83
|
* @example { fields: { a: 'singleLineText', b: 'number', c: 'checkbox', d: 'multipleRecordLinks' }, ... }
|
|
76
84
|
*
|
|
77
85
|
* @returns An Airtable FieldSet. Throws if cannot coerce to requested type.
|
|
78
86
|
* @example { a: 'Some text', b: 123, d: ['rec123'] } // (c is an un-ticked checkbox, d is a multipleRecordLinks)
|
|
79
87
|
*/
|
|
80
|
-
const mapRecordTypeTsToAirtable = (tsTypes, tsRecord,
|
|
88
|
+
const mapRecordTypeTsToAirtable = (table, tsTypes, tsRecord, airtableTsTable) => {
|
|
81
89
|
const item = {};
|
|
82
90
|
Object.entries(tsTypes).forEach(([fieldNameOrId, tsType]) => {
|
|
83
91
|
const value = tsRecord[fieldNameOrId];
|
|
@@ -87,12 +95,21 @@ const mapRecordTypeTsToAirtable = (tsTypes, tsRecord, airtableTable) => {
|
|
|
87
95
|
}
|
|
88
96
|
if (!(0, typeUtils_1.matchesType)(value, tsType)) {
|
|
89
97
|
// This should be unreachable because of our types
|
|
90
|
-
throw new
|
|
98
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
99
|
+
message: `Type mismatch for field '${fieldNameOrId}': expected ${tsType} but got a ${typeof value}.`,
|
|
100
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
101
|
+
suggestion: 'Ensure the value matches the expected type in your schema definition.',
|
|
102
|
+
});
|
|
91
103
|
}
|
|
92
104
|
// eslint-disable-next-line no-underscore-dangle
|
|
93
|
-
const fieldDefinition =
|
|
105
|
+
const fieldDefinition = airtableTsTable.fields.find((f) => f.id === fieldNameOrId || f.name === fieldNameOrId);
|
|
94
106
|
if (!fieldDefinition) {
|
|
95
|
-
|
|
107
|
+
const tsName = table.mappings ? Object.entries(table.mappings).find((e) => e[1] === fieldNameOrId)?.[0] : undefined;
|
|
108
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
109
|
+
message: `Field ${tsName ? `${tsName} (${fieldNameOrId})` : fieldNameOrId} does not exist in the Airtable table.`,
|
|
110
|
+
type: AirtableTsError_1.ErrorType.RESOURCE_NOT_FOUND,
|
|
111
|
+
suggestion: 'Verify that the field exists in your Airtable base and that you are using the correct field name or ID.',
|
|
112
|
+
});
|
|
96
113
|
}
|
|
97
114
|
try {
|
|
98
115
|
const { toAirtable } = getMapper(tsType, fieldDefinition.type);
|
|
@@ -100,29 +117,34 @@ const mapRecordTypeTsToAirtable = (tsTypes, tsRecord, airtableTable) => {
|
|
|
100
117
|
item[fieldNameOrId] = toAirtable(value);
|
|
101
118
|
}
|
|
102
119
|
catch (error) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
error.message = `Failed to map field ${airtableTable.name}.${fieldNameOrId}: ${error.message}`;
|
|
106
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
107
|
-
error.stack = `Error: Failed to map field ${airtableTable.name}.${fieldNameOrId}: ${error.stack?.startsWith('Error: ') ? error.stack.slice('Error: '.length) : error.stack}`;
|
|
108
|
-
}
|
|
109
|
-
throw error;
|
|
120
|
+
const tsName = table.mappings ? Object.entries(table.mappings).find((e) => e[1] === fieldNameOrId)?.[0] : undefined;
|
|
121
|
+
throw (0, AirtableTsError_1.prependError)(error, `Failed to map field ${tsName ? `${tsName} (${fieldNameOrId})` : fieldNameOrId} to Airtable`);
|
|
110
122
|
}
|
|
111
123
|
});
|
|
112
124
|
return Object.assign(item, { id: tsRecord.id });
|
|
113
125
|
};
|
|
114
126
|
const mapRecordFromAirtable = (table, record) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
try {
|
|
128
|
+
const tsTypes = (0, typeUtils_1.airtableFieldNameTsTypes)(table);
|
|
129
|
+
const tsRecord = mapRecordTypeAirtableToTs(table, tsTypes, record);
|
|
130
|
+
const mappedRecord = (0, nameMapper_1.mapRecordFieldNamesAirtableToTs)(table, tsRecord);
|
|
131
|
+
return mappedRecord;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw (0, AirtableTsError_1.prependError)(error, `Failed to map record from Airtable format for table '${table.name}' (${table.tableId}) and record ${record.id}`);
|
|
135
|
+
}
|
|
119
136
|
};
|
|
120
137
|
exports.mapRecordFromAirtable = mapRecordFromAirtable;
|
|
121
|
-
const mapRecordToAirtable = (table, item,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
const mapRecordToAirtable = (table, item, airtableTsTable) => {
|
|
139
|
+
try {
|
|
140
|
+
const mappedItem = (0, nameMapper_1.mapRecordFieldNamesTsToAirtable)(table, item);
|
|
141
|
+
const tsTypes = (0, typeUtils_1.airtableFieldNameTsTypes)(table);
|
|
142
|
+
const fieldSet = mapRecordTypeTsToAirtable(table, tsTypes, mappedItem, airtableTsTable);
|
|
143
|
+
return fieldSet;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw (0, AirtableTsError_1.prependError)(error, `Failed to map record to Airtable format for table '${table.name}' (${table.tableId})`);
|
|
147
|
+
}
|
|
126
148
|
};
|
|
127
149
|
exports.mapRecordToAirtable = mapRecordToAirtable;
|
|
128
150
|
exports.visibleForTesting = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.airtableFieldNameTsTypes = exports.matchesType = exports.parseType = void 0;
|
|
4
|
+
const AirtableTsError_1 = require("../AirtableTsError");
|
|
4
5
|
const parseType = (t) => {
|
|
5
6
|
if (t.endsWith('[] | null')) {
|
|
6
7
|
return {
|
|
@@ -74,7 +75,10 @@ const arrayToSingleType = (tsType) => {
|
|
|
74
75
|
if (tsType.endsWith('[]')) {
|
|
75
76
|
return tsType.slice(0, -'[]'.length);
|
|
76
77
|
}
|
|
77
|
-
throw new
|
|
78
|
+
throw new AirtableTsError_1.AirtableTsError({
|
|
79
|
+
message: `The type '${tsType}' is not an array type.`,
|
|
80
|
+
type: AirtableTsError_1.ErrorType.SCHEMA_VALIDATION,
|
|
81
|
+
});
|
|
78
82
|
};
|
|
79
83
|
/**
|
|
80
84
|
* Constructs a TypeScript object type definition given a table definition
|
|
@@ -98,13 +102,18 @@ const airtableFieldNameTsTypes = (table) => {
|
|
|
98
102
|
const schemaEntries = Object.entries(table.schema);
|
|
99
103
|
return Object.fromEntries(schemaEntries.map(([outputFieldName, tsType]) => {
|
|
100
104
|
const mappingToAirtable = table.mappings?.[outputFieldName];
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
try {
|
|
106
|
+
if (!mappingToAirtable) {
|
|
107
|
+
return [[outputFieldName, tsType]];
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(mappingToAirtable)) {
|
|
110
|
+
return mappingToAirtable.map((airtableFieldName) => [airtableFieldName, arrayToSingleType(tsType)]);
|
|
111
|
+
}
|
|
112
|
+
return [[mappingToAirtable, tsType]];
|
|
103
113
|
}
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw (0, AirtableTsError_1.prependError)(error, `Error with field ${JSON.stringify(outputFieldName)} (${mappingToAirtable})`);
|
|
106
116
|
}
|
|
107
|
-
return [[mappingToAirtable, tsType]];
|
|
108
117
|
}).flat(1));
|
|
109
118
|
};
|
|
110
119
|
exports.airtableFieldNameTsTypes = airtableFieldNameTsTypes;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { FieldSet, Table as AirtableSdkTable, Record as AirtableSdkRecord, AirtableOptions } from 'airtable';
|
|
2
|
+
import { QueryParams } from 'airtable/lib/query_params';
|
|
3
|
+
import { Item, Table } from './mapping/typeUtils';
|
|
2
4
|
export type AirtableRecord = Omit<AirtableSdkRecord<FieldSet>, '_table'> & {
|
|
3
|
-
_table:
|
|
5
|
+
_table: AirtableTsTable;
|
|
4
6
|
};
|
|
5
|
-
export type
|
|
7
|
+
export type AirtableTsTable<T extends Item = Item> = AirtableSdkTable<FieldSet> & {
|
|
6
8
|
fields: {
|
|
7
9
|
id: string;
|
|
8
10
|
name: string;
|
|
9
11
|
type: string;
|
|
10
12
|
}[];
|
|
13
|
+
tsDefinition: Table<T>;
|
|
14
|
+
__brand?: T;
|
|
11
15
|
};
|
|
12
16
|
interface AirtableTsSpecificOptions {
|
|
13
17
|
/** The Airtable base schema is used to determine the appropriate type mapper for the field type (for example converting a number to a string representing a date is different to converting a number to a singleLineText). For performance reasons, airtable-ts caches base schemas so we don't refetch it for every request. Note that we always still do validation against the expected type at runtime so the library is always type-safe. @default 120_000 */
|
|
@@ -15,4 +19,5 @@ interface AirtableTsSpecificOptions {
|
|
|
15
19
|
}
|
|
16
20
|
export type AirtableTsOptions = AirtableOptions & AirtableTsSpecificOptions;
|
|
17
21
|
export type CompleteAirtableTsOptions = AirtableTsOptions & Required<AirtableTsSpecificOptions>;
|
|
22
|
+
export type ScanParams = Omit<QueryParams<unknown>, 'fields' | 'cellFormat' | 'method' | 'returnFieldsByFieldId' | 'pageSize' | 'offset'>;
|
|
18
23
|
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import AirtableError from 'airtable/lib/airtable_error';
|
|
2
|
+
export declare class WrappedAirtableError extends Error {
|
|
3
|
+
/** The original error thrown by Airtable.js */
|
|
4
|
+
originalError: AirtableError;
|
|
5
|
+
/** The error type from Airtable.js */
|
|
6
|
+
error?: string;
|
|
7
|
+
/** The HTTP status code if applicable */
|
|
8
|
+
statusCode?: number;
|
|
9
|
+
constructor(originalError: AirtableError);
|
|
10
|
+
}
|
|
11
|
+
export declare const wrapToCatchAirtableErrors: <T extends {
|
|
12
|
+
prototype: object;
|
|
13
|
+
}>(c: T) => void;
|
|
@@ -0,0 +1,65 @@
|
|
|
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.wrapToCatchAirtableErrors = exports.WrappedAirtableError = void 0;
|
|
7
|
+
const airtable_error_1 = __importDefault(require("airtable/lib/airtable_error"));
|
|
8
|
+
class WrappedAirtableError extends Error {
|
|
9
|
+
/** The original error thrown by Airtable.js */
|
|
10
|
+
originalError;
|
|
11
|
+
/** The error type from Airtable.js */
|
|
12
|
+
error;
|
|
13
|
+
/** The HTTP status code if applicable */
|
|
14
|
+
statusCode;
|
|
15
|
+
constructor(originalError) {
|
|
16
|
+
super(originalError.message);
|
|
17
|
+
this.name = 'WrappedAirtableError';
|
|
18
|
+
this.originalError = originalError;
|
|
19
|
+
this.error = originalError.error;
|
|
20
|
+
this.statusCode = originalError.statusCode;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.WrappedAirtableError = WrappedAirtableError;
|
|
24
|
+
/**
|
|
25
|
+
* Wraps any error thrown that isn't a proper Error object to ensure it has a stack trace for debugging.
|
|
26
|
+
* @see https://github.com/Airtable/airtable.js/issues/294
|
|
27
|
+
*/
|
|
28
|
+
function wrapAirtableError(error) {
|
|
29
|
+
if (error instanceof Error) {
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
if (error instanceof airtable_error_1.default) {
|
|
33
|
+
return new WrappedAirtableError(error);
|
|
34
|
+
}
|
|
35
|
+
return new Error(String(error));
|
|
36
|
+
}
|
|
37
|
+
const wrapToCatchAirtableErrors = (c) => {
|
|
38
|
+
// Cast to any to bypass TypeScript's type checking, as unfortunately this is too funky for TypeScript
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
const prototype = c.prototype;
|
|
41
|
+
const methods = Object.getOwnPropertyNames(prototype).filter((prop) => {
|
|
42
|
+
return prop !== 'constructor' && typeof prototype[prop] === 'function';
|
|
43
|
+
});
|
|
44
|
+
methods.forEach((method) => {
|
|
45
|
+
const original = prototype[method];
|
|
46
|
+
if (typeof original === 'function') {
|
|
47
|
+
// eslint-disable-next-line func-names
|
|
48
|
+
prototype[method] = function (...args) {
|
|
49
|
+
try {
|
|
50
|
+
const result = original.apply(this, args);
|
|
51
|
+
if (result instanceof Promise) {
|
|
52
|
+
return result.catch((error) => {
|
|
53
|
+
throw wrapAirtableError(error);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw wrapAirtableError(error);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
exports.wrapToCatchAirtableErrors = wrapToCatchAirtableErrors;
|
package/package.json
CHANGED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import Airtable from 'airtable';
|
|
2
|
-
import { Item, Table } from './mapping/typeUtils';
|
|
3
|
-
import { AirtableTable, CompleteAirtableTsOptions } from './types';
|
|
4
|
-
export declare const getAirtableTable: <T extends Item>(airtable: Airtable, table: Table<T>, options: CompleteAirtableTsOptions) => Promise<AirtableTable>;
|
package/dist/getAirtableTable.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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.getAirtableTable = void 0;
|
|
7
|
-
const axios_1 = __importDefault(require("axios"));
|
|
8
|
-
const getAirtableTable = async (airtable, table, options) => {
|
|
9
|
-
const airtableTable = airtable.base(table.baseId).table(table.tableId);
|
|
10
|
-
// We need the schema so we know which type mapper to use.
|
|
11
|
-
// Even if we were inferring a type mapper from the schema, we'd still have to
|
|
12
|
-
// do this as otherwise we can't distinguish a column with all null values from
|
|
13
|
-
// a column that is missing entirely. We'd like to do that as for safety, we'd
|
|
14
|
-
// rather throw an error if the column is missing entirely; this suggests a
|
|
15
|
-
// misconfiguration. But an all-null column is okay. The particular case that
|
|
16
|
-
// this is likely for is checkbox columns.
|
|
17
|
-
const baseSchema = await getAirtableBaseSchema(table.baseId, options);
|
|
18
|
-
const tableDefinition = baseSchema.find((t) => t.id === table.tableId);
|
|
19
|
-
if (!tableDefinition) {
|
|
20
|
-
throw new Error(`[airtable-ts] Failed to find table ${table.tableId} in base ${table.baseId}`);
|
|
21
|
-
}
|
|
22
|
-
return Object.assign(airtableTable, { fields: tableDefinition.fields });
|
|
23
|
-
};
|
|
24
|
-
exports.getAirtableTable = getAirtableTable;
|
|
25
|
-
const baseSchemaCache = new Map();
|
|
26
|
-
/**
|
|
27
|
-
* Get the schemas from the cache or Airtable API for the tables in the given base.
|
|
28
|
-
* @see https://airtable.com/developers/web/api/get-base-schema
|
|
29
|
-
* @param baseId The base id to get the schemas for
|
|
30
|
-
*/
|
|
31
|
-
const getAirtableBaseSchema = async (baseId, options) => {
|
|
32
|
-
const fromCache = baseSchemaCache.get(baseId);
|
|
33
|
-
if (fromCache && Date.now() - fromCache.at < options.baseSchemaCacheDurationMs) {
|
|
34
|
-
return fromCache.data;
|
|
35
|
-
}
|
|
36
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
37
|
-
if (!options.apiKey) {
|
|
38
|
-
throw new Error('[airtable-ts] Missing API key');
|
|
39
|
-
}
|
|
40
|
-
const res = await (0, axios_1.default)({
|
|
41
|
-
baseURL: options.endpointUrl ?? 'https://api.airtable.com',
|
|
42
|
-
url: `/v0/meta/bases/${baseId}/tables`,
|
|
43
|
-
...(options.requestTimeout ? { timeout: options.requestTimeout } : {}),
|
|
44
|
-
headers: {
|
|
45
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
46
|
-
Authorization: `Bearer ${options.apiKey}`,
|
|
47
|
-
...options.customHeaders,
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
const baseSchema = res.data.tables;
|
|
51
|
-
if (baseSchemaCache.size > 100) {
|
|
52
|
-
baseSchemaCache.clear();
|
|
53
|
-
// If you're seeing this warning, then we probably either need to:
|
|
54
|
-
// - Update the maximum limit before clearing the cache, provided we have memory headroom; or
|
|
55
|
-
// - Use a last recently used cache or similar
|
|
56
|
-
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');
|
|
57
|
-
}
|
|
58
|
-
baseSchemaCache.set(baseId, { at: Date.now(), data: baseSchema });
|
|
59
|
-
return baseSchema;
|
|
60
|
-
};
|