airtable-ts 1.0.0 → 1.2.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 +3 -0
- package/dist/assertMatchesSchema.js +0 -3
- package/dist/mapping/fieldMappers.js +254 -37
- package/dist/mapping/recordMapper.d.ts +2 -2
- package/dist/mapping/recordMapper.js +21 -20
- package/dist/mapping/typeUtils.d.ts +2 -2
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ export const studentTable: Table<{ id: string, name: string, classes: string[] }
|
|
|
32
32
|
baseId: 'app1234',
|
|
33
33
|
tableId: 'tbl1234',
|
|
34
34
|
schema: { name: 'string', classes: 'string[]' },
|
|
35
|
+
// optional: use mappings with field ids to prevent renamings breaking your app,
|
|
36
|
+
// or with field names to make handling renamings easy
|
|
37
|
+
mappings: { name: 'fld1234', classes: 'Classes student is enrolled in' },
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
export const classTable: Table<{ id: string, name: string }> = {
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.assertMatchesSchema = void 0;
|
|
4
4
|
const typeUtils_1 = require("./mapping/typeUtils");
|
|
5
|
-
// In theory, this should never catch stuff because our type mapping logic should
|
|
6
|
-
// verify the types are compatible. However, "In theory there is no difference
|
|
7
|
-
// between theory and practice - in practice there is"
|
|
8
5
|
/**
|
|
9
6
|
* In theory, this should never catch stuff because our type mapping logic should
|
|
10
7
|
* verify the types are compatible.
|
|
@@ -17,12 +17,31 @@ const requiredMapperPair = {
|
|
|
17
17
|
};
|
|
18
18
|
exports.fieldMappers = {
|
|
19
19
|
string: {
|
|
20
|
-
singleLineText: fallbackMapperPair('', ''),
|
|
21
|
-
email: fallbackMapperPair('', ''),
|
|
22
20
|
url: fallbackMapperPair('', ''),
|
|
21
|
+
email: fallbackMapperPair('', ''),
|
|
22
|
+
phoneNumber: fallbackMapperPair('', ''),
|
|
23
|
+
singleLineText: fallbackMapperPair('', ''),
|
|
23
24
|
multilineText: fallbackMapperPair('', ''),
|
|
24
25
|
richText: fallbackMapperPair('', ''),
|
|
25
|
-
|
|
26
|
+
singleSelect: fallbackMapperPair('', ''),
|
|
27
|
+
externalSyncSource: {
|
|
28
|
+
toAirtable: () => { throw new Error('[airtable-ts] externalSyncSource type field is readonly'); },
|
|
29
|
+
fromAirtable: (value) => value ?? '',
|
|
30
|
+
},
|
|
31
|
+
multipleSelects: {
|
|
32
|
+
toAirtable: (value) => {
|
|
33
|
+
return [value];
|
|
34
|
+
},
|
|
35
|
+
fromAirtable: (value) => {
|
|
36
|
+
if (!value) {
|
|
37
|
+
throw new Error('[airtable-ts] Failed to coerce multipleSelects type field to a single string, as it was blank');
|
|
38
|
+
}
|
|
39
|
+
if (value.length !== 1) {
|
|
40
|
+
throw new Error(`[airtable-ts] Can't coerce multipleSelects to a single string, as there were ${value?.length} entries`);
|
|
41
|
+
}
|
|
42
|
+
return value[0];
|
|
43
|
+
},
|
|
44
|
+
},
|
|
26
45
|
multipleRecordLinks: {
|
|
27
46
|
toAirtable: (value) => {
|
|
28
47
|
return [value];
|
|
@@ -37,6 +56,22 @@ exports.fieldMappers = {
|
|
|
37
56
|
return value[0];
|
|
38
57
|
},
|
|
39
58
|
},
|
|
59
|
+
date: {
|
|
60
|
+
toAirtable: (value) => {
|
|
61
|
+
const date = new Date(value);
|
|
62
|
+
if (Number.isNaN(date.getTime())) {
|
|
63
|
+
throw new Error('[airtable-ts] Invalid date string');
|
|
64
|
+
}
|
|
65
|
+
return date.toJSON().slice(0, 10);
|
|
66
|
+
},
|
|
67
|
+
fromAirtable: (value) => {
|
|
68
|
+
const date = new Date(value ?? '');
|
|
69
|
+
if (Number.isNaN(date.getTime())) {
|
|
70
|
+
throw new Error('[airtable-ts] Invalid date string');
|
|
71
|
+
}
|
|
72
|
+
return date.toJSON();
|
|
73
|
+
},
|
|
74
|
+
},
|
|
40
75
|
dateTime: {
|
|
41
76
|
toAirtable: (value) => {
|
|
42
77
|
const date = new Date(value);
|
|
@@ -54,28 +89,67 @@ exports.fieldMappers = {
|
|
|
54
89
|
},
|
|
55
90
|
},
|
|
56
91
|
multipleLookupValues: {
|
|
57
|
-
toAirtable: () => { throw new Error('[airtable-ts]
|
|
92
|
+
toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
|
|
58
93
|
fromAirtable: (value) => {
|
|
59
94
|
if (!value) {
|
|
60
|
-
throw new Error('[airtable-ts] Failed to coerce
|
|
95
|
+
throw new Error('[airtable-ts] Failed to coerce lookup type field to a single string, as it was blank');
|
|
61
96
|
}
|
|
62
97
|
if (value.length !== 1) {
|
|
63
|
-
throw new Error(`[airtable-ts] Can't coerce
|
|
98
|
+
throw new Error(`[airtable-ts] Can't coerce lookup to a single string, as there were ${value?.length} entries`);
|
|
64
99
|
}
|
|
65
100
|
if (typeof value[0] !== 'string') {
|
|
66
|
-
throw new Error(`[airtable-ts] Can't coerce singular
|
|
101
|
+
throw new Error(`[airtable-ts] Can't coerce singular lookup to a single string, as it was of type ${typeof value[0]}`);
|
|
67
102
|
}
|
|
68
103
|
return value[0];
|
|
69
104
|
},
|
|
70
105
|
},
|
|
106
|
+
rollup: {
|
|
107
|
+
toAirtable: () => { throw new Error('[airtable-ts] rollup type field is readonly'); },
|
|
108
|
+
fromAirtable: (value) => {
|
|
109
|
+
if (typeof value === 'string')
|
|
110
|
+
return value;
|
|
111
|
+
if (value === undefined || value === null)
|
|
112
|
+
return '';
|
|
113
|
+
throw new Error(`[airtable-ts] Can't coerce rollup to a string, as it was of type ${typeof value}`);
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
formula: {
|
|
117
|
+
toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
|
|
118
|
+
fromAirtable: (value) => {
|
|
119
|
+
if (typeof value === 'string')
|
|
120
|
+
return value;
|
|
121
|
+
if (value === undefined || value === null)
|
|
122
|
+
return '';
|
|
123
|
+
throw new Error(`[airtable-ts] Can't coerce formula to a string, as it was of type ${typeof value}`);
|
|
124
|
+
},
|
|
125
|
+
},
|
|
71
126
|
},
|
|
72
127
|
'string | null': {
|
|
73
|
-
singleLineText: fallbackMapperPair(null, null),
|
|
74
|
-
email: fallbackMapperPair(null, null),
|
|
75
128
|
url: fallbackMapperPair(null, null),
|
|
129
|
+
email: fallbackMapperPair(null, null),
|
|
130
|
+
phoneNumber: fallbackMapperPair(null, null),
|
|
131
|
+
singleLineText: fallbackMapperPair(null, null),
|
|
76
132
|
multilineText: fallbackMapperPair(null, null),
|
|
77
133
|
richText: fallbackMapperPair(null, null),
|
|
78
|
-
|
|
134
|
+
singleSelect: fallbackMapperPair(null, null),
|
|
135
|
+
externalSyncSource: {
|
|
136
|
+
toAirtable: () => { throw new Error('[airtable-ts] externalSyncSource type field is readonly'); },
|
|
137
|
+
fromAirtable: (value) => value ?? null,
|
|
138
|
+
},
|
|
139
|
+
multipleSelects: {
|
|
140
|
+
toAirtable: (value) => {
|
|
141
|
+
return value ? [value] : [];
|
|
142
|
+
},
|
|
143
|
+
fromAirtable: (value) => {
|
|
144
|
+
if (!value || value.length === 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (value.length !== 1) {
|
|
148
|
+
throw new Error(`[airtable-ts] Can't coerce multipleSelects to a single string, as there were ${value?.length} entries`);
|
|
149
|
+
}
|
|
150
|
+
return value[0];
|
|
151
|
+
},
|
|
152
|
+
},
|
|
79
153
|
multipleRecordLinks: {
|
|
80
154
|
toAirtable: (value) => {
|
|
81
155
|
return value ? [value] : [];
|
|
@@ -90,6 +164,26 @@ exports.fieldMappers = {
|
|
|
90
164
|
return value[0];
|
|
91
165
|
},
|
|
92
166
|
},
|
|
167
|
+
date: {
|
|
168
|
+
toAirtable: (value) => {
|
|
169
|
+
if (value === null)
|
|
170
|
+
return null;
|
|
171
|
+
const date = new Date(value);
|
|
172
|
+
if (Number.isNaN(date.getTime())) {
|
|
173
|
+
throw new Error('[airtable-ts] Invalid date');
|
|
174
|
+
}
|
|
175
|
+
return date.toJSON().slice(0, 10);
|
|
176
|
+
},
|
|
177
|
+
fromAirtable: (value) => {
|
|
178
|
+
if (value === null || value === undefined)
|
|
179
|
+
return null;
|
|
180
|
+
const date = new Date(value);
|
|
181
|
+
if (Number.isNaN(date.getTime())) {
|
|
182
|
+
throw new Error('[airtable-ts] Invalid date');
|
|
183
|
+
}
|
|
184
|
+
return date.toJSON();
|
|
185
|
+
},
|
|
186
|
+
},
|
|
93
187
|
dateTime: {
|
|
94
188
|
toAirtable: (value) => {
|
|
95
189
|
if (value === null)
|
|
@@ -111,34 +205,54 @@ exports.fieldMappers = {
|
|
|
111
205
|
},
|
|
112
206
|
},
|
|
113
207
|
multipleLookupValues: {
|
|
114
|
-
toAirtable: () => { throw new Error('[airtable-ts]
|
|
208
|
+
toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
|
|
115
209
|
fromAirtable: (value) => {
|
|
116
210
|
if (!value || value.length === 0) {
|
|
117
211
|
return null;
|
|
118
212
|
}
|
|
119
213
|
if (value.length !== 1) {
|
|
120
|
-
throw new Error(`[airtable-ts] Can't coerce
|
|
214
|
+
throw new Error(`[airtable-ts] Can't coerce lookup to a single string, as there were ${value?.length} entries`);
|
|
121
215
|
}
|
|
122
216
|
if (typeof value[0] !== 'string') {
|
|
123
|
-
throw new Error(`[airtable-ts] Can't coerce singular
|
|
217
|
+
throw new Error(`[airtable-ts] Can't coerce singular lookup to a single string, as it was of type ${typeof value[0]}`);
|
|
124
218
|
}
|
|
125
219
|
return value[0];
|
|
126
220
|
},
|
|
127
221
|
},
|
|
222
|
+
rollup: {
|
|
223
|
+
toAirtable: () => { throw new Error('[airtable-ts] rollup type field is readonly'); },
|
|
224
|
+
fromAirtable: (value) => {
|
|
225
|
+
if (typeof value === 'string')
|
|
226
|
+
return value;
|
|
227
|
+
if (value === undefined || value === null)
|
|
228
|
+
return null;
|
|
229
|
+
throw new Error(`[airtable-ts] Can't coerce rollup to a string, as it was of type ${typeof value}`);
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
formula: {
|
|
233
|
+
toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
|
|
234
|
+
fromAirtable: (value) => {
|
|
235
|
+
if (typeof value === 'string')
|
|
236
|
+
return value;
|
|
237
|
+
if (value === undefined || value === null)
|
|
238
|
+
return null;
|
|
239
|
+
throw new Error(`[airtable-ts] Can't coerce formula to a string, as it was of type ${typeof value}`);
|
|
240
|
+
},
|
|
241
|
+
},
|
|
128
242
|
},
|
|
129
243
|
boolean: {
|
|
130
244
|
checkbox: fallbackMapperPair(false, false),
|
|
131
245
|
multipleLookupValues: {
|
|
132
|
-
toAirtable: () => { throw new Error('[airtable-ts]
|
|
246
|
+
toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
|
|
133
247
|
fromAirtable: (value) => {
|
|
134
248
|
if (!value) {
|
|
135
|
-
throw new Error('[airtable-ts] Failed to coerce
|
|
249
|
+
throw new Error('[airtable-ts] Failed to coerce lookup type field to a single boolean, as it was blank');
|
|
136
250
|
}
|
|
137
251
|
if (value.length !== 1) {
|
|
138
|
-
throw new Error(`[airtable-ts] Can't coerce
|
|
252
|
+
throw new Error(`[airtable-ts] Can't coerce lookup to a single boolean, as there were ${value?.length} entries`);
|
|
139
253
|
}
|
|
140
254
|
if (typeof value[0] !== 'boolean') {
|
|
141
|
-
throw new Error(`[airtable-ts] Can't coerce singular
|
|
255
|
+
throw new Error(`[airtable-ts] Can't coerce singular lookup to a single boolean, as it was of type ${typeof value[0]}`);
|
|
142
256
|
}
|
|
143
257
|
return value[0];
|
|
144
258
|
},
|
|
@@ -147,16 +261,16 @@ exports.fieldMappers = {
|
|
|
147
261
|
'boolean | null': {
|
|
148
262
|
checkbox: fallbackMapperPair(null, null),
|
|
149
263
|
multipleLookupValues: {
|
|
150
|
-
toAirtable: () => { throw new Error('[airtable-ts]
|
|
264
|
+
toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
|
|
151
265
|
fromAirtable: (value) => {
|
|
152
266
|
if (!value || value.length === 0) {
|
|
153
267
|
return null;
|
|
154
268
|
}
|
|
155
269
|
if (value.length !== 1) {
|
|
156
|
-
throw new Error(`[airtable-ts] Can't coerce
|
|
270
|
+
throw new Error(`[airtable-ts] Can't coerce lookup to a single boolean, as there were ${value?.length} entries`);
|
|
157
271
|
}
|
|
158
272
|
if (typeof value[0] !== 'boolean') {
|
|
159
|
-
throw new Error(`[airtable-ts] Can't coerce singular
|
|
273
|
+
throw new Error(`[airtable-ts] Can't coerce singular lookup to a single boolean, as it was of type ${typeof value[0]}`);
|
|
160
274
|
}
|
|
161
275
|
return value[0];
|
|
162
276
|
},
|
|
@@ -164,10 +278,10 @@ exports.fieldMappers = {
|
|
|
164
278
|
},
|
|
165
279
|
number: {
|
|
166
280
|
number: requiredMapperPair,
|
|
167
|
-
percent: requiredMapperPair,
|
|
168
|
-
currency: requiredMapperPair,
|
|
169
281
|
rating: requiredMapperPair,
|
|
170
282
|
duration: requiredMapperPair,
|
|
283
|
+
currency: requiredMapperPair,
|
|
284
|
+
percent: requiredMapperPair,
|
|
171
285
|
count: {
|
|
172
286
|
toAirtable: () => { throw new Error('[airtable-ts] count type field is readonly'); },
|
|
173
287
|
fromAirtable: (value) => required(value),
|
|
@@ -177,6 +291,23 @@ exports.fieldMappers = {
|
|
|
177
291
|
fromAirtable: (value) => required(value),
|
|
178
292
|
},
|
|
179
293
|
// Number assumed to be unix time in seconds
|
|
294
|
+
date: {
|
|
295
|
+
toAirtable: (value) => {
|
|
296
|
+
const date = new Date(value * 1000);
|
|
297
|
+
if (Number.isNaN(date.getTime())) {
|
|
298
|
+
throw new Error('[airtable-ts] Invalid date');
|
|
299
|
+
}
|
|
300
|
+
return date.toJSON().slice(0, 10);
|
|
301
|
+
},
|
|
302
|
+
fromAirtable: (value) => {
|
|
303
|
+
const date = new Date(value ?? '');
|
|
304
|
+
if (Number.isNaN(date.getTime())) {
|
|
305
|
+
throw new Error('[airtable-ts] Invalid date');
|
|
306
|
+
}
|
|
307
|
+
return Math.floor(date.getTime() / 1000);
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
// Number assumed to be unix time in seconds
|
|
180
311
|
dateTime: {
|
|
181
312
|
toAirtable: (value) => {
|
|
182
313
|
const date = new Date(value * 1000);
|
|
@@ -194,27 +325,43 @@ exports.fieldMappers = {
|
|
|
194
325
|
},
|
|
195
326
|
},
|
|
196
327
|
multipleLookupValues: {
|
|
197
|
-
toAirtable: () => { throw new Error('[airtable-ts]
|
|
328
|
+
toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
|
|
198
329
|
fromAirtable: (value) => {
|
|
199
330
|
if (!value) {
|
|
200
|
-
throw new Error('[airtable-ts] Failed to coerce
|
|
331
|
+
throw new Error('[airtable-ts] Failed to coerce lookup type field to a single number, as it was blank');
|
|
201
332
|
}
|
|
202
333
|
if (value.length !== 1) {
|
|
203
|
-
throw new Error(`[airtable-ts] Can't coerce
|
|
334
|
+
throw new Error(`[airtable-ts] Can't coerce lookup to a single number, as there were ${value?.length} entries`);
|
|
204
335
|
}
|
|
205
336
|
if (typeof value[0] !== 'number') {
|
|
206
|
-
throw new Error(`[airtable-ts] Can't coerce singular
|
|
337
|
+
throw new Error(`[airtable-ts] Can't coerce singular lookup to a single number, as it was of type ${typeof value[0]}`);
|
|
207
338
|
}
|
|
208
339
|
return value[0];
|
|
209
340
|
},
|
|
210
341
|
},
|
|
342
|
+
rollup: {
|
|
343
|
+
toAirtable: () => { throw new Error('[airtable-ts] rollup type field is readonly'); },
|
|
344
|
+
fromAirtable: (value) => {
|
|
345
|
+
if (typeof value === 'number')
|
|
346
|
+
return value;
|
|
347
|
+
throw new Error(`[airtable-ts] Can't coerce rollup to a number, as it was of type ${typeof value}`);
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
formula: {
|
|
351
|
+
toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
|
|
352
|
+
fromAirtable: (value) => {
|
|
353
|
+
if (typeof value === 'number')
|
|
354
|
+
return value;
|
|
355
|
+
throw new Error(`[airtable-ts] Can't coerce formula to a number, as it was of type ${typeof value}`);
|
|
356
|
+
},
|
|
357
|
+
},
|
|
211
358
|
},
|
|
212
359
|
'number | null': {
|
|
213
360
|
number: fallbackMapperPair(null, null),
|
|
214
|
-
percent: fallbackMapperPair(null, null),
|
|
215
|
-
currency: fallbackMapperPair(null, null),
|
|
216
361
|
rating: fallbackMapperPair(null, null),
|
|
217
362
|
duration: fallbackMapperPair(null, null),
|
|
363
|
+
currency: fallbackMapperPair(null, null),
|
|
364
|
+
percent: fallbackMapperPair(null, null),
|
|
218
365
|
count: {
|
|
219
366
|
fromAirtable: (value) => value ?? null,
|
|
220
367
|
toAirtable: () => { throw new Error('[airtable-ts] count type field is readonly'); },
|
|
@@ -224,6 +371,27 @@ exports.fieldMappers = {
|
|
|
224
371
|
toAirtable: () => { throw new Error('[airtable-ts] autoNumber field is readonly'); },
|
|
225
372
|
},
|
|
226
373
|
// Number assumed to be unix time in seconds
|
|
374
|
+
date: {
|
|
375
|
+
toAirtable: (value) => {
|
|
376
|
+
if (value === null)
|
|
377
|
+
return null;
|
|
378
|
+
const date = new Date(value * 1000);
|
|
379
|
+
if (Number.isNaN(date.getTime())) {
|
|
380
|
+
throw new Error('[airtable-ts] Invalid date');
|
|
381
|
+
}
|
|
382
|
+
return date.toJSON().slice(0, 10);
|
|
383
|
+
},
|
|
384
|
+
fromAirtable: (value) => {
|
|
385
|
+
if (value === null || value === undefined)
|
|
386
|
+
return null;
|
|
387
|
+
const date = new Date(value);
|
|
388
|
+
if (Number.isNaN(date.getTime())) {
|
|
389
|
+
throw new Error('[airtable-ts] Invalid date');
|
|
390
|
+
}
|
|
391
|
+
return Math.floor(date.getTime() / 1000);
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
// Number assumed to be unix time in seconds
|
|
227
395
|
dateTime: {
|
|
228
396
|
toAirtable: (value) => {
|
|
229
397
|
if (value === null)
|
|
@@ -245,49 +413,98 @@ exports.fieldMappers = {
|
|
|
245
413
|
},
|
|
246
414
|
},
|
|
247
415
|
multipleLookupValues: {
|
|
248
|
-
toAirtable: () => { throw new Error('[airtable-ts]
|
|
416
|
+
toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
|
|
249
417
|
fromAirtable: (value) => {
|
|
250
418
|
if (!value || value.length === 0) {
|
|
251
419
|
return null;
|
|
252
420
|
}
|
|
253
421
|
if (value.length !== 1) {
|
|
254
|
-
throw new Error(`[airtable-ts] Can't coerce
|
|
422
|
+
throw new Error(`[airtable-ts] Can't coerce lookup to a single number, as there were ${value?.length} entries`);
|
|
255
423
|
}
|
|
256
424
|
if (typeof value[0] !== 'number') {
|
|
257
|
-
throw new Error(`[airtable-ts] Can't coerce singular
|
|
425
|
+
throw new Error(`[airtable-ts] Can't coerce singular lookup to a single number, as it was of type ${typeof value[0]}`);
|
|
258
426
|
}
|
|
259
427
|
return value[0];
|
|
260
428
|
},
|
|
261
429
|
},
|
|
430
|
+
rollup: {
|
|
431
|
+
toAirtable: () => { throw new Error('[airtable-ts] rollup type field is readonly'); },
|
|
432
|
+
fromAirtable: (value) => {
|
|
433
|
+
if (typeof value === 'number')
|
|
434
|
+
return value;
|
|
435
|
+
if (value === null || value === undefined)
|
|
436
|
+
return null;
|
|
437
|
+
throw new Error(`[airtable-ts] Can't coerce rollup to a number, as it was of type ${typeof value}`);
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
formula: {
|
|
441
|
+
toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
|
|
442
|
+
fromAirtable: (value) => {
|
|
443
|
+
if (typeof value === 'number')
|
|
444
|
+
return value;
|
|
445
|
+
if (value === null || value === undefined)
|
|
446
|
+
return null;
|
|
447
|
+
throw new Error(`[airtable-ts] Can't coerce formula to a number, as it was of type ${typeof value}`);
|
|
448
|
+
},
|
|
449
|
+
},
|
|
262
450
|
},
|
|
263
451
|
'string[]': {
|
|
452
|
+
multipleSelects: fallbackMapperPair([], []),
|
|
264
453
|
multipleRecordLinks: fallbackMapperPair([], []),
|
|
265
454
|
multipleLookupValues: {
|
|
266
|
-
toAirtable: () => { throw new Error('[airtable-ts]
|
|
455
|
+
toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
|
|
267
456
|
fromAirtable: (value) => {
|
|
268
457
|
if (!Array.isArray(value)) {
|
|
269
|
-
throw new Error('[airtable-ts] Failed to coerce
|
|
458
|
+
throw new Error('[airtable-ts] Failed to coerce lookup type field to a string array, as it was not an array');
|
|
270
459
|
}
|
|
271
460
|
if (value.some((v) => typeof v !== 'string')) {
|
|
272
|
-
throw new Error('[airtable-ts] Can\'t coerce
|
|
461
|
+
throw new Error('[airtable-ts] Can\'t coerce lookup to a string array, as it had non string type');
|
|
462
|
+
}
|
|
463
|
+
return value;
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
formula: {
|
|
467
|
+
toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
|
|
468
|
+
fromAirtable: (value) => {
|
|
469
|
+
if (!Array.isArray(value)) {
|
|
470
|
+
throw new Error('[airtable-ts] Failed to coerce formula type field to a string array, as it was not an array');
|
|
471
|
+
}
|
|
472
|
+
if (value.some((v) => typeof v !== 'string')) {
|
|
473
|
+
throw new Error('[airtable-ts] Can\'t coerce formula to a string array, as it had non string type');
|
|
273
474
|
}
|
|
274
475
|
return value;
|
|
275
476
|
},
|
|
276
477
|
},
|
|
277
478
|
},
|
|
278
479
|
'string[] | null': {
|
|
480
|
+
multipleSelects: fallbackMapperPair(null, null),
|
|
279
481
|
multipleRecordLinks: fallbackMapperPair(null, null),
|
|
280
482
|
multipleLookupValues: {
|
|
281
|
-
toAirtable: () => { throw new Error('[airtable-ts]
|
|
483
|
+
toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
|
|
484
|
+
fromAirtable: (value) => {
|
|
485
|
+
if (!value && !Array.isArray(value)) {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
if (!Array.isArray(value)) {
|
|
489
|
+
throw new Error('[airtable-ts] Failed to coerce lookup type field to a string array, as it was not an array');
|
|
490
|
+
}
|
|
491
|
+
if (value.some((v) => typeof v !== 'string')) {
|
|
492
|
+
throw new Error('[airtable-ts] Can\'t coerce lookup to a string array, as it had non string type');
|
|
493
|
+
}
|
|
494
|
+
return value;
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
formula: {
|
|
498
|
+
toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
|
|
282
499
|
fromAirtable: (value) => {
|
|
283
500
|
if (!value && !Array.isArray(value)) {
|
|
284
501
|
return null;
|
|
285
502
|
}
|
|
286
503
|
if (!Array.isArray(value)) {
|
|
287
|
-
throw new Error('[airtable-ts] Failed to coerce
|
|
504
|
+
throw new Error('[airtable-ts] Failed to coerce formula type field to a string array, as it was not an array');
|
|
288
505
|
}
|
|
289
506
|
if (value.some((v) => typeof v !== 'string')) {
|
|
290
|
-
throw new Error('[airtable-ts] Can\'t coerce
|
|
507
|
+
throw new Error('[airtable-ts] Can\'t coerce formula to a string array, as it had non string type');
|
|
291
508
|
}
|
|
292
509
|
return value;
|
|
293
510
|
},
|
|
@@ -5,12 +5,12 @@ export declare const mapRecordFromAirtable: <T extends Item>(table: Table<T>, re
|
|
|
5
5
|
export declare const mapRecordToAirtable: <T extends Item>(table: Table<T>, item: Partial<T>, airtableTable: AirtableTable) => FieldSet;
|
|
6
6
|
export declare const visibleForTesting: {
|
|
7
7
|
mapRecordTypeAirtableToTs: <T extends {
|
|
8
|
-
[
|
|
8
|
+
[fieldNameOrId: string]: TsTypeString;
|
|
9
9
|
}>(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
16
|
}>(tsTypes: T_1, tsRecord: R, airtableTable: AirtableTable) => FieldSet;
|
|
@@ -21,32 +21,32 @@ const typeUtils_1 = require("./typeUtils");
|
|
|
21
21
|
*/
|
|
22
22
|
const mapRecordTypeAirtableToTs = (tsTypes, record) => {
|
|
23
23
|
const item = {};
|
|
24
|
-
Object.entries(tsTypes).forEach(([
|
|
25
|
-
const value = record.fields[fieldName];
|
|
24
|
+
Object.entries(tsTypes).forEach(([fieldNameOrId, tsType]) => {
|
|
26
25
|
// eslint-disable-next-line no-underscore-dangle
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
throw new Error(`[airtable-ts] Failed to get
|
|
26
|
+
const fieldDefinition = record._table.fields.find((f) => f.id === fieldNameOrId || f.name === fieldNameOrId);
|
|
27
|
+
if (!fieldDefinition) {
|
|
28
|
+
throw new Error(`[airtable-ts] Failed to get Airtable field ${fieldNameOrId}`);
|
|
30
29
|
}
|
|
30
|
+
const value = record.fields[fieldDefinition.name];
|
|
31
31
|
const tsMapper = fieldMappers_1.fieldMappers[tsType];
|
|
32
32
|
if (!tsMapper) {
|
|
33
33
|
throw new Error(`[airtable-ts] No mappers for ts type ${tsType}`);
|
|
34
34
|
}
|
|
35
|
-
const specificMapper = tsMapper[
|
|
35
|
+
const specificMapper = tsMapper[fieldDefinition.type]?.fromAirtable;
|
|
36
36
|
if (!specificMapper) {
|
|
37
37
|
// eslint-disable-next-line no-underscore-dangle
|
|
38
|
-
throw new Error(`[airtable-ts] Expected field ${record._table.name}.${
|
|
38
|
+
throw new Error(`[airtable-ts] Expected field ${record._table.name}.${fieldNameOrId} to be able to map to ts type ${tsType}, but got airtable type ${fieldDefinition.type} which can't.`);
|
|
39
39
|
}
|
|
40
40
|
try {
|
|
41
41
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
-
item[
|
|
42
|
+
item[fieldNameOrId] = specificMapper(value);
|
|
43
43
|
}
|
|
44
44
|
catch (error) {
|
|
45
45
|
if (error instanceof Error) {
|
|
46
46
|
// eslint-disable-next-line no-underscore-dangle
|
|
47
|
-
error.message = `Failed to map field ${record._table.name}.${
|
|
47
|
+
error.message = `Failed to map field ${record._table.name}.${fieldNameOrId}: ${error.message}`;
|
|
48
48
|
// eslint-disable-next-line no-underscore-dangle
|
|
49
|
-
error.stack = `Error: Failed to map field ${record._table.name}.${
|
|
49
|
+
error.stack = `Error: Failed to map field ${record._table.name}.${fieldNameOrId}: ${error.stack?.startsWith('Error: ') ? error.stack.slice('Error: '.length) : error.stack}`;
|
|
50
50
|
}
|
|
51
51
|
throw error;
|
|
52
52
|
}
|
|
@@ -73,30 +73,31 @@ const mapRecordTypeAirtableToTs = (tsTypes, record) => {
|
|
|
73
73
|
*/
|
|
74
74
|
const mapRecordTypeTsToAirtable = (tsTypes, tsRecord, airtableTable) => {
|
|
75
75
|
const item = {};
|
|
76
|
-
Object.entries(tsTypes).forEach(([
|
|
77
|
-
const value = tsRecord[
|
|
78
|
-
if (!(
|
|
76
|
+
Object.entries(tsTypes).forEach(([fieldNameOrId, tsType]) => {
|
|
77
|
+
const value = tsRecord[fieldNameOrId];
|
|
78
|
+
if (!(fieldNameOrId in tsRecord)) {
|
|
79
79
|
// If we don't have the field, just skip: this allows us to support partial updates
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
if (!(0, typeUtils_1.matchesType)(value, tsType)) {
|
|
83
83
|
// This should be unreachable because of our types
|
|
84
|
-
throw new Error(`[airtable-ts] Expected field ${airtableTable.name}.${
|
|
84
|
+
throw new Error(`[airtable-ts] Expected field ${airtableTable.name}.${fieldNameOrId} to match type \`${tsType}\` but got value \`${JSON.stringify(value)}\`. This should never happen in normal operation as it should be caught before this point.`);
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
87
|
+
const fieldDefinition = airtableTable.fields.find((f) => f.id === fieldNameOrId || f.name === fieldNameOrId);
|
|
88
|
+
if (!fieldDefinition) {
|
|
89
|
+
throw new Error(`[airtable-ts] Failed to get Airtable field ${fieldNameOrId}`);
|
|
89
90
|
}
|
|
90
91
|
const tsMapper = fieldMappers_1.fieldMappers[tsType];
|
|
91
92
|
if (!tsMapper) {
|
|
92
93
|
throw new Error(`[airtable-ts] No mappers for ts type ${tsType}`);
|
|
93
94
|
}
|
|
94
|
-
const specificMapper = tsMapper[
|
|
95
|
+
const specificMapper = tsMapper[fieldDefinition.type]?.toAirtable;
|
|
95
96
|
if (!specificMapper) {
|
|
96
|
-
throw new Error(`[airtable-ts] Expected field ${airtableTable.name}.${
|
|
97
|
+
throw new Error(`[airtable-ts] Expected field ${airtableTable.name}.${fieldNameOrId} to be able to map to airtable type \`${fieldDefinition.type}\`, but got ts type \`${tsType}\` which can't.`);
|
|
97
98
|
}
|
|
98
99
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
-
item[
|
|
100
|
+
item[fieldNameOrId] = specificMapper(value);
|
|
100
101
|
});
|
|
101
102
|
return Object.assign(item, { id: tsRecord.id });
|
|
102
103
|
};
|
|
@@ -2,8 +2,8 @@ export type TsTypeString = NonNullToString<any> | ToTsTypeString<any>;
|
|
|
2
2
|
type NonNullToString<T> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends number[] ? 'number[]' : T extends string[] ? 'string[]' : T extends boolean[] ? 'boolean[]' : never;
|
|
3
3
|
export type ToTsTypeString<T> = null extends T ? `${NonNullToString<T>} | null` : NonNullToString<T>;
|
|
4
4
|
export type FromTsTypeString<T> = T extends 'string' ? string : T extends 'string | null' ? string | null : T extends 'number' ? number : T extends 'number | null' ? number | null : T extends 'boolean' ? boolean : T extends 'boolean | null' ? boolean | null : T extends 'string[]' ? string[] : T extends 'string[] | null' ? string[] | null : T extends 'number[]' ? number[] : T extends 'number[] | null' ? number[] | null : T extends 'boolean[]' ? boolean[] : T extends 'boolean[] | null' ? boolean[] | null : never;
|
|
5
|
-
export type AirtableTypeString = '
|
|
6
|
-
export type FromAirtableTypeString<T> = null | (T extends '
|
|
5
|
+
export type AirtableTypeString = 'aiText' | 'autoNumber' | 'barcode' | 'button' | 'checkbox' | 'count' | 'createdBy' | 'createdTime' | 'currency' | 'date' | 'dateTime' | 'duration' | 'email' | 'externalSyncSource' | 'formula' | 'lastModifiedBy' | 'lastModifiedTime' | 'lookup' | 'multipleLookupValues' | 'multilineText' | 'multipleAttachments' | 'multipleCollaborators' | 'multipleRecordLinks' | 'multipleSelects' | 'number' | 'percent' | 'phoneNumber' | 'rating' | 'richText' | 'rollup' | 'singleCollaborator' | 'singleLineText' | 'singleSelect' | 'url';
|
|
6
|
+
export type FromAirtableTypeString<T extends AirtableTypeString> = null | (T extends 'url' | 'email' | 'phoneNumber' | 'singleLineText' | 'multilineText' | 'richText' | 'singleSelect' | 'externalSyncSource' | 'date' | 'dateTime' | 'createdTime' | 'lastModifiedTime' ? string : T extends 'multipleRecordLinks' | 'multipleSelects' ? string[] : T extends 'number' | 'rating' | 'duration' | 'currency' | 'percent' | 'count' | 'autoNumber' ? number : T extends 'checkbox' ? boolean : T extends 'lookup' | 'multipleLookupValues' | 'rollup' | 'formula' ? FromAirtableTypeString<any>[] : never);
|
|
7
7
|
/**
|
|
8
8
|
* Verifies whether the given value is assignable to the given type
|
|
9
9
|
*
|
package/dist/types.d.ts
CHANGED