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 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
- phoneNumber: fallbackMapperPair('', ''),
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] multipleLookupValues type field is readonly'); },
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 multipleLookupValues type field to a single string, as it was blank');
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 multipleLookupValues to a single string, as there were ${value?.length} entries`);
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 multipleLookupValues to a single string, as it was of type ${typeof value[0]}`);
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
- phoneNumber: fallbackMapperPair(null, null),
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] multipleLookupValues type field is readonly'); },
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 multipleLookupValues to a single string, as there were ${value?.length} entries`);
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 multipleLookupValues to a single string, as it was of type ${typeof value[0]}`);
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] multipleLookupValues type field is readonly'); },
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 multipleLookupValues type field to a single boolean, as it was blank');
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 multipleLookupValues to a single boolean, as there were ${value?.length} entries`);
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 multipleLookupValues to a single boolean, as it was of type ${typeof value[0]}`);
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] multipleLookupValues type field is readonly'); },
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 multipleLookupValues to a single boolean, as there were ${value?.length} entries`);
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 multipleLookupValues to a single boolean, as it was of type ${typeof value[0]}`);
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] multipleLookupValues type field is readonly'); },
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 multipleLookupValues type field to a single number, as it was blank');
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 multipleLookupValues to a single number, as there were ${value?.length} entries`);
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 multipleLookupValues to a single number, as it was of type ${typeof value[0]}`);
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] multipleLookupValues type field is readonly'); },
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 multipleLookupValues to a single number, as there were ${value?.length} entries`);
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 multipleLookupValues to a single number, as it was of type ${typeof value[0]}`);
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] multipleLookupValues type field is readonly'); },
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 multipleLookupValues type field to a string array, as it was not an array');
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 multipleLookupValues to a string array, as it had non string type');
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] multipleLookupValues type field is readonly'); },
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 multipleLookupValues type field to a string array, as it was not an array');
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 multipleLookupValues to a string array, as it had non string type');
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
- [fieldName: string]: TsTypeString;
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
- [fieldName: string]: TsTypeString;
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(([fieldName, tsType]) => {
25
- const value = record.fields[fieldName];
24
+ Object.entries(tsTypes).forEach(([fieldNameOrId, tsType]) => {
26
25
  // eslint-disable-next-line no-underscore-dangle
27
- const airtableType = record._table.fields.find((f) => f.name === fieldName)?.type;
28
- if (!airtableType) {
29
- throw new Error(`[airtable-ts] Failed to get airtable type for field ${fieldName}`);
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[airtableType]?.fromAirtable;
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}.${fieldName} to be able to map to ts type ${tsType}, but got airtable type ${airtableType} which can't.`);
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[fieldName] = specificMapper(value);
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}.${fieldName}: ${error.message}`;
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}.${fieldName}: ${error.stack?.startsWith('Error: ') ? error.stack.slice('Error: '.length) : error.stack}`;
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(([fieldName, tsType]) => {
77
- const value = tsRecord[fieldName];
78
- if (!(fieldName in tsRecord)) {
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}.${fieldName} 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.`);
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
- const airtableType = airtableTable.fields.find((f) => f.name === fieldName)?.type;
87
- if (!airtableType) {
88
- throw new Error(`[airtable-ts] Failed to get airtable type for field ${fieldName}`);
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[airtableType]?.toAirtable;
95
+ const specificMapper = tsMapper[fieldDefinition.type]?.toAirtable;
95
96
  if (!specificMapper) {
96
- throw new Error(`[airtable-ts] Expected field ${airtableTable.name}.${fieldName} to be able to map to airtable type \`${airtableType}\`, but got ts type \`${tsType}\` which can't.`);
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[fieldName] = specificMapper(value);
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 = 'singleLineText' | 'email' | 'url' | 'multilineText' | 'phoneNumber' | 'checkbox' | 'number' | 'percent' | 'currency' | 'count' | 'autoNumber' | 'rating' | 'richText' | 'duration' | 'multipleRecordLinks' | 'dateTime' | 'multipleLookupValues';
6
- export type FromAirtableTypeString<T> = null | (T extends 'singleLineText' ? string : T extends 'email' ? string : T extends 'url' ? string : T extends 'multilineText' ? string : T extends 'richText' ? string : T extends 'phoneNumber' ? string : T extends 'checkbox' ? boolean : T extends 'number' ? number : T extends 'percent' ? number : T extends 'currency' ? number : T extends 'rating' ? number : T extends 'duration' ? number : T extends 'count' ? number : T extends 'autoNumber' ? number : T extends 'multipleRecordLinks' ? string[] : T extends 'multipleLookupValues' ? FromAirtableTypeString<any>[] : T extends 'dateTime' ? string : never);
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
@@ -4,6 +4,7 @@ export type AirtableRecord = Omit<AirtableSdkRecord<FieldSet>, '_table'> & {
4
4
  };
5
5
  export type AirtableTable = AirtableSdkTable<FieldSet> & {
6
6
  fields: {
7
+ id: string;
7
8
  name: string;
8
9
  type: string;
9
10
  }[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airtable-ts",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "A type-safe Airtable SDK",
5
5
  "license": "MIT",
6
6
  "author": "Adam Jones (domdomegg)",