airtable-ts 1.3.0 → 1.3.2

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/dist/index.d.ts CHANGED
@@ -12,7 +12,9 @@ export declare class AirtableTs {
12
12
  update<T extends Item>(table: Table<T>, data: Partial<T> & {
13
13
  id: string;
14
14
  }): Promise<T>;
15
- remove<T extends Item>(table: Table<T>, id: string): Promise<T>;
15
+ remove<T extends Item>(table: Table<T>, id: string): Promise<{
16
+ id: string;
17
+ }>;
16
18
  }
17
19
  export type ScanParams = Omit<QueryParams<unknown>, 'fields' | 'cellFormat' | 'method' | 'returnFieldsByFieldId' | 'pageSize' | 'offset'>;
18
20
  export type { AirtableTsOptions } from './types';
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ class AirtableTs {
58
58
  }
59
59
  const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
60
60
  const record = await airtableTable.destroy(id);
61
- return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
61
+ return { id: record.id };
62
62
  }
63
63
  }
64
64
  exports.AirtableTs = AirtableTs;
@@ -1,7 +1,7 @@
1
1
  import { AirtableTypeString, FromAirtableTypeString, FromTsTypeString, TsTypeString } from './typeUtils';
2
2
  type Mapper = {
3
3
  [T in TsTypeString]?: {
4
- [A in AirtableTypeString]?: {
4
+ [A in AirtableTypeString | 'unknown']?: {
5
5
  toAirtable: (value: FromTsTypeString<T>) => FromAirtableTypeString<A>;
6
6
  fromAirtable: (value: FromAirtableTypeString<A> | null | undefined) => FromTsTypeString<T>;
7
7
  };
@@ -1,161 +1,56 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fieldMappers = void 0;
4
- const required = (value) => {
5
- if (value === null || value === undefined) {
6
- throw new Error('[airtable-ts] Missing required value');
7
- }
8
- return value;
9
- };
4
+ const typeUtils_1 = require("./typeUtils");
10
5
  const fallbackMapperPair = (toFallback, fromFallback) => ({
11
6
  toAirtable: (value) => value ?? toFallback,
12
7
  fromAirtable: (value) => value ?? fromFallback,
13
8
  });
14
- const requiredMapperPair = {
15
- toAirtable: (value) => required(value),
16
- fromAirtable: (value) => required(value),
17
- };
18
- exports.fieldMappers = {
19
- string: {
20
- url: fallbackMapperPair('', ''),
21
- email: fallbackMapperPair('', ''),
22
- phoneNumber: fallbackMapperPair('', ''),
23
- singleLineText: fallbackMapperPair('', ''),
24
- multilineText: fallbackMapperPair('', ''),
25
- richText: 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
- },
45
- multipleRecordLinks: {
46
- toAirtable: (value) => {
47
- return [value];
48
- },
49
- fromAirtable: (value) => {
50
- if (!value) {
51
- throw new Error('[airtable-ts] Failed to coerce multipleRecordLinks type field to a single string, as it was blank');
52
- }
53
- if (value.length !== 1) {
54
- throw new Error(`[airtable-ts] Can't coerce multipleRecordLinks to a single string, as there were ${value?.length} entries`);
55
- }
56
- return value[0];
57
- },
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
- },
75
- dateTime: {
76
- toAirtable: (value) => {
77
- const date = new Date(value);
78
- if (Number.isNaN(date.getTime())) {
79
- throw new Error('[airtable-ts] Invalid dateTime string');
80
- }
81
- return date.toJSON();
82
- },
83
- fromAirtable: (value) => {
84
- const date = new Date(value ?? '');
85
- if (Number.isNaN(date.getTime())) {
86
- throw new Error('[airtable-ts] Invalid dateTime string');
87
- }
88
- return date.toJSON();
89
- },
90
- },
91
- createdTime: {
92
- toAirtable: (value) => {
93
- const date = new Date(value);
94
- if (Number.isNaN(date.getTime())) {
95
- throw new Error('[airtable-ts] Invalid dateTime string');
96
- }
97
- return date.toJSON();
98
- },
99
- fromAirtable: (value) => {
100
- const date = new Date(value ?? '');
101
- if (Number.isNaN(date.getTime())) {
102
- throw new Error('[airtable-ts] Invalid dateTime string');
103
- }
104
- return date.toJSON();
105
- },
106
- },
107
- lastModifiedTime: {
108
- toAirtable: (value) => {
109
- const date = new Date(value);
110
- if (Number.isNaN(date.getTime())) {
111
- throw new Error('[airtable-ts] Invalid dateTime string');
112
- }
113
- return date.toJSON();
114
- },
115
- fromAirtable: (value) => {
116
- const date = new Date(value ?? '');
117
- if (Number.isNaN(date.getTime())) {
118
- throw new Error('[airtable-ts] Invalid dateTime string');
119
- }
120
- return date.toJSON();
121
- },
122
- },
123
- multipleLookupValues: {
124
- toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
125
- fromAirtable: (value) => {
126
- if (!value) {
127
- throw new Error('[airtable-ts] Failed to coerce lookup type field to a single string, as it was blank');
128
- }
129
- if (value.length !== 1) {
130
- throw new Error(`[airtable-ts] Can't coerce lookup to a single string, as there were ${value?.length} entries`);
131
- }
132
- if (typeof value[0] !== 'string') {
133
- throw new Error(`[airtable-ts] Can't coerce singular lookup to a single string, as it was of type ${typeof value[0]}`);
134
- }
135
- return value[0];
136
- },
137
- },
138
- rollup: {
139
- toAirtable: () => { throw new Error('[airtable-ts] rollup type field is readonly'); },
140
- fromAirtable: (value) => {
141
- if (typeof value === 'string')
142
- return value;
143
- if (value === undefined || value === null)
144
- return '';
145
- throw new Error(`[airtable-ts] Can't coerce rollup to a string, as it was of type ${typeof value}`);
146
- },
147
- },
148
- formula: {
149
- toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
150
- fromAirtable: (value) => {
151
- if (typeof value === 'string')
152
- return value;
153
- if (value === undefined || value === null)
154
- return '';
155
- throw new Error(`[airtable-ts] Can't coerce formula to a string, as it was of type ${typeof value}`);
156
- },
157
- },
9
+ const dateTimeMapperPair = {
10
+ // Number assumed to be unix time in seconds
11
+ toAirtable: (value) => {
12
+ if (value === null)
13
+ return null;
14
+ const date = new Date(typeof value === 'number' ? value * 1000 : value);
15
+ if (Number.isNaN(date.getTime())) {
16
+ throw new Error('[airtable-ts] Invalid dateTime');
17
+ }
18
+ return date.toJSON();
19
+ },
20
+ fromAirtable: (value) => {
21
+ if (value === null || value === undefined)
22
+ return null;
23
+ const date = new Date(value);
24
+ if (Number.isNaN(date.getTime())) {
25
+ throw new Error('[airtable-ts] Invalid dateTime');
26
+ }
27
+ return date.toJSON();
158
28
  },
29
+ };
30
+ const readonly = (airtableType) => () => { throw new Error(`[airtable-ts] ${airtableType} type field is readonly`); };
31
+ const coerce = (airtableType, tsType) => (value) => {
32
+ const parsedType = (0, typeUtils_1.parseType)(tsType);
33
+ if (!parsedType.array && typeof value === parsedType.single) {
34
+ return value;
35
+ }
36
+ if (parsedType.array && Array.isArray(value) && value.every((v) => typeof v === parsedType.single)) {
37
+ return value;
38
+ }
39
+ if (parsedType.nullable && (value === undefined || value === null || (Array.isArray(value) && value.length === 0))) {
40
+ return null;
41
+ }
42
+ if (parsedType.array && typeof value === parsedType.single) {
43
+ return [value];
44
+ }
45
+ if (!parsedType.array && Array.isArray(value) && value.length === 1 && typeof value[0] === parsedType.single) {
46
+ return value[0];
47
+ }
48
+ if (!parsedType.array && Array.isArray(value) && value.length !== 1) {
49
+ throw new Error(`[airtable-ts] Can't coerce ${airtableType} to a ${tsType}, as there were ${value.length} array entries`);
50
+ }
51
+ throw new Error(`[airtable-ts] Can't coerce ${airtableType} to a ${tsType}, as it was of type ${typeof value}`);
52
+ };
53
+ const stringOrNull = {
159
54
  'string | null': {
160
55
  url: fallbackMapperPair(null, null),
161
56
  email: fallbackMapperPair(null, null),
@@ -164,302 +59,57 @@ exports.fieldMappers = {
164
59
  multilineText: fallbackMapperPair(null, null),
165
60
  richText: fallbackMapperPair(null, null),
166
61
  singleSelect: fallbackMapperPair(null, null),
167
- externalSyncSource: {
168
- toAirtable: () => { throw new Error('[airtable-ts] externalSyncSource type field is readonly'); },
169
- fromAirtable: (value) => value ?? null,
170
- },
171
62
  multipleSelects: {
172
- toAirtable: (value) => {
173
- return value ? [value] : [];
174
- },
175
- fromAirtable: (value) => {
176
- if (!value || value.length === 0) {
177
- return null;
178
- }
179
- if (value.length !== 1) {
180
- throw new Error(`[airtable-ts] Can't coerce multipleSelects to a single string, as there were ${value?.length} entries`);
181
- }
182
- return value[0];
183
- },
63
+ toAirtable: (value) => (value ? [value] : []),
64
+ fromAirtable: coerce('multipleSelects', 'string | null'),
184
65
  },
185
66
  multipleRecordLinks: {
186
- toAirtable: (value) => {
187
- return value ? [value] : [];
188
- },
189
- fromAirtable: (value) => {
190
- if (!value || value.length === 0) {
191
- return null;
192
- }
193
- if (value.length !== 1) {
194
- throw new Error(`[airtable-ts] Can't coerce multipleRecordLinks to a single string, as there were ${value?.length} entries`);
195
- }
196
- return value[0];
197
- },
67
+ toAirtable: (value) => (value ? [value] : []),
68
+ fromAirtable: coerce('multipleRecordLinks', 'string | null'),
198
69
  },
199
70
  date: {
200
- toAirtable: (value) => {
201
- if (value === null)
202
- return null;
203
- const date = new Date(value);
204
- if (Number.isNaN(date.getTime())) {
205
- throw new Error('[airtable-ts] Invalid date');
206
- }
207
- return date.toJSON().slice(0, 10);
208
- },
209
- fromAirtable: (value) => {
210
- if (value === null || value === undefined)
211
- return null;
212
- const date = new Date(value);
213
- if (Number.isNaN(date.getTime())) {
214
- throw new Error('[airtable-ts] Invalid date');
215
- }
216
- return date.toJSON();
217
- },
218
- },
219
- dateTime: {
220
- toAirtable: (value) => {
221
- if (value === null)
222
- return null;
223
- const date = new Date(value);
224
- if (Number.isNaN(date.getTime())) {
225
- throw new Error('[airtable-ts] Invalid dateTime');
226
- }
227
- return date.toJSON();
228
- },
229
- fromAirtable: (value) => {
230
- if (value === null || value === undefined)
231
- return null;
232
- const date = new Date(value);
233
- if (Number.isNaN(date.getTime())) {
234
- throw new Error('[airtable-ts] Invalid dateTime');
235
- }
236
- return date.toJSON();
237
- },
238
- },
239
- createdTime: {
240
- toAirtable: (value) => {
241
- if (value === null)
242
- return null;
243
- const date = new Date(value);
244
- if (Number.isNaN(date.getTime())) {
245
- throw new Error('[airtable-ts] Invalid dateTime');
246
- }
247
- return date.toJSON();
248
- },
249
- fromAirtable: (value) => {
250
- if (value === null || value === undefined)
251
- return null;
252
- const date = new Date(value);
253
- if (Number.isNaN(date.getTime())) {
254
- throw new Error('[airtable-ts] Invalid dateTime');
255
- }
256
- return date.toJSON();
257
- },
258
- },
259
- lastModifiedTime: {
260
- toAirtable: (value) => {
261
- if (value === null)
262
- return null;
263
- const date = new Date(value);
264
- if (Number.isNaN(date.getTime())) {
265
- throw new Error('[airtable-ts] Invalid dateTime');
266
- }
267
- return date.toJSON();
268
- },
269
- fromAirtable: (value) => {
270
- if (value === null || value === undefined)
271
- return null;
272
- const date = new Date(value);
273
- if (Number.isNaN(date.getTime())) {
274
- throw new Error('[airtable-ts] Invalid dateTime');
275
- }
276
- return date.toJSON();
277
- },
71
+ toAirtable: (value) => dateTimeMapperPair.toAirtable(value)?.slice(0, 10) ?? null,
72
+ fromAirtable: dateTimeMapperPair.fromAirtable,
278
73
  },
74
+ dateTime: dateTimeMapperPair,
75
+ createdTime: dateTimeMapperPair,
76
+ lastModifiedTime: dateTimeMapperPair,
279
77
  multipleLookupValues: {
280
- toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
281
- fromAirtable: (value) => {
282
- if (!value || value.length === 0) {
283
- return null;
284
- }
285
- if (value.length !== 1) {
286
- throw new Error(`[airtable-ts] Can't coerce lookup to a single string, as there were ${value?.length} entries`);
287
- }
288
- if (typeof value[0] !== 'string') {
289
- throw new Error(`[airtable-ts] Can't coerce singular lookup to a single string, as it was of type ${typeof value[0]}`);
290
- }
291
- return value[0];
292
- },
78
+ toAirtable: readonly('multipleLookupValues'),
79
+ fromAirtable: coerce('multipleLookupValues', 'string | null'),
80
+ },
81
+ externalSyncSource: {
82
+ toAirtable: readonly('externalSyncSource'),
83
+ fromAirtable: coerce('externalSyncSource', 'string | null'),
293
84
  },
294
85
  rollup: {
295
- toAirtable: () => { throw new Error('[airtable-ts] rollup type field is readonly'); },
296
- fromAirtable: (value) => {
297
- if (typeof value === 'string')
298
- return value;
299
- if (value === undefined || value === null)
300
- return null;
301
- throw new Error(`[airtable-ts] Can't coerce rollup to a string, as it was of type ${typeof value}`);
302
- },
86
+ toAirtable: readonly('rollup'),
87
+ fromAirtable: coerce('rollup', 'string | null'),
303
88
  },
304
89
  formula: {
305
- toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
306
- fromAirtable: (value) => {
307
- if (typeof value === 'string')
308
- return value;
309
- if (value === undefined || value === null)
310
- return null;
311
- throw new Error(`[airtable-ts] Can't coerce formula to a string, as it was of type ${typeof value}`);
312
- },
90
+ toAirtable: readonly('formula'),
91
+ fromAirtable: coerce('formula', 'string | null'),
313
92
  },
314
- },
315
- boolean: {
316
- checkbox: fallbackMapperPair(false, false),
317
- multipleLookupValues: {
318
- toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
319
- fromAirtable: (value) => {
320
- if (!value) {
321
- throw new Error('[airtable-ts] Failed to coerce lookup type field to a single boolean, as it was blank');
322
- }
323
- if (value.length !== 1) {
324
- throw new Error(`[airtable-ts] Can't coerce lookup to a single boolean, as there were ${value?.length} entries`);
325
- }
326
- if (typeof value[0] !== 'boolean') {
327
- throw new Error(`[airtable-ts] Can't coerce singular lookup to a single boolean, as it was of type ${typeof value[0]}`);
328
- }
329
- return value[0];
330
- },
93
+ unknown: {
94
+ toAirtable: (value) => value,
95
+ fromAirtable: coerce('unknown', 'string | null'),
331
96
  },
332
97
  },
98
+ };
99
+ const booleanOrNull = {
333
100
  'boolean | null': {
334
101
  checkbox: fallbackMapperPair(null, null),
335
102
  multipleLookupValues: {
336
- toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
337
- fromAirtable: (value) => {
338
- if (!value || value.length === 0) {
339
- return null;
340
- }
341
- if (value.length !== 1) {
342
- throw new Error(`[airtable-ts] Can't coerce lookup to a single boolean, as there were ${value?.length} entries`);
343
- }
344
- if (typeof value[0] !== 'boolean') {
345
- throw new Error(`[airtable-ts] Can't coerce singular lookup to a single boolean, as it was of type ${typeof value[0]}`);
346
- }
347
- return value[0];
348
- },
349
- },
350
- },
351
- number: {
352
- number: requiredMapperPair,
353
- rating: requiredMapperPair,
354
- duration: requiredMapperPair,
355
- currency: requiredMapperPair,
356
- percent: requiredMapperPair,
357
- count: {
358
- toAirtable: () => { throw new Error('[airtable-ts] count type field is readonly'); },
359
- fromAirtable: (value) => required(value),
360
- },
361
- autoNumber: {
362
- toAirtable: () => { throw new Error('[airtable-ts] autoNumber type field is readonly'); },
363
- fromAirtable: (value) => required(value),
103
+ toAirtable: readonly('multipleLookupValues'),
104
+ fromAirtable: coerce('multipleLookupValues', 'boolean | null'),
364
105
  },
365
- // Number assumed to be unix time in seconds
366
- date: {
367
- toAirtable: (value) => {
368
- const date = new Date(value * 1000);
369
- if (Number.isNaN(date.getTime())) {
370
- throw new Error('[airtable-ts] Invalid date');
371
- }
372
- return date.toJSON().slice(0, 10);
373
- },
374
- fromAirtable: (value) => {
375
- const date = new Date(value ?? '');
376
- if (Number.isNaN(date.getTime())) {
377
- throw new Error('[airtable-ts] Invalid date');
378
- }
379
- return Math.floor(date.getTime() / 1000);
380
- },
381
- },
382
- // Number assumed to be unix time in seconds
383
- dateTime: {
384
- toAirtable: (value) => {
385
- const date = new Date(value * 1000);
386
- if (Number.isNaN(date.getTime())) {
387
- throw new Error('[airtable-ts] Invalid dateTime');
388
- }
389
- return date.toJSON();
390
- },
391
- fromAirtable: (value) => {
392
- const date = new Date(value ?? '');
393
- if (Number.isNaN(date.getTime())) {
394
- throw new Error('[airtable-ts] Invalid dateTime');
395
- }
396
- return Math.floor(date.getTime() / 1000);
397
- },
398
- },
399
- createdTime: {
400
- toAirtable: (value) => {
401
- const date = new Date(value * 1000);
402
- if (Number.isNaN(date.getTime())) {
403
- throw new Error('[airtable-ts] Invalid dateTime');
404
- }
405
- return date.toJSON();
406
- },
407
- fromAirtable: (value) => {
408
- const date = new Date(value ?? '');
409
- if (Number.isNaN(date.getTime())) {
410
- throw new Error('[airtable-ts] Invalid dateTime');
411
- }
412
- return Math.floor(date.getTime() / 1000);
413
- },
414
- },
415
- lastModifiedTime: {
416
- toAirtable: (value) => {
417
- const date = new Date(value * 1000);
418
- if (Number.isNaN(date.getTime())) {
419
- throw new Error('[airtable-ts] Invalid dateTime');
420
- }
421
- return date.toJSON();
422
- },
423
- fromAirtable: (value) => {
424
- const date = new Date(value ?? '');
425
- if (Number.isNaN(date.getTime())) {
426
- throw new Error('[airtable-ts] Invalid dateTime');
427
- }
428
- return Math.floor(date.getTime() / 1000);
429
- },
430
- },
431
- multipleLookupValues: {
432
- toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
433
- fromAirtable: (value) => {
434
- if (!value) {
435
- throw new Error('[airtable-ts] Failed to coerce lookup type field to a single number, as it was blank');
436
- }
437
- if (value.length !== 1) {
438
- throw new Error(`[airtable-ts] Can't coerce lookup to a single number, as there were ${value?.length} entries`);
439
- }
440
- if (typeof value[0] !== 'number') {
441
- throw new Error(`[airtable-ts] Can't coerce singular lookup to a single number, as it was of type ${typeof value[0]}`);
442
- }
443
- return value[0];
444
- },
445
- },
446
- rollup: {
447
- toAirtable: () => { throw new Error('[airtable-ts] rollup type field is readonly'); },
448
- fromAirtable: (value) => {
449
- if (typeof value === 'number')
450
- return value;
451
- throw new Error(`[airtable-ts] Can't coerce rollup to a number, as it was of type ${typeof value}`);
452
- },
453
- },
454
- formula: {
455
- toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
456
- fromAirtable: (value) => {
457
- if (typeof value === 'number')
458
- return value;
459
- throw new Error(`[airtable-ts] Can't coerce formula to a number, as it was of type ${typeof value}`);
460
- },
106
+ unknown: {
107
+ toAirtable: (value) => value,
108
+ fromAirtable: coerce('unknown', 'boolean | null'),
461
109
  },
462
110
  },
111
+ };
112
+ const numberOrNull = {
463
113
  'number | null': {
464
114
  number: fallbackMapperPair(null, null),
465
115
  rating: fallbackMapperPair(null, null),
@@ -468,190 +118,147 @@ exports.fieldMappers = {
468
118
  percent: fallbackMapperPair(null, null),
469
119
  count: {
470
120
  fromAirtable: (value) => value ?? null,
471
- toAirtable: () => { throw new Error('[airtable-ts] count type field is readonly'); },
121
+ toAirtable: readonly('count'),
472
122
  },
473
123
  autoNumber: {
474
124
  fromAirtable: (value) => value ?? null,
475
- toAirtable: () => { throw new Error('[airtable-ts] autoNumber field is readonly'); },
125
+ toAirtable: readonly('autoNumber'),
476
126
  },
477
- // Number assumed to be unix time in seconds
478
127
  date: {
479
- toAirtable: (value) => {
480
- if (value === null)
481
- return null;
482
- const date = new Date(value * 1000);
483
- if (Number.isNaN(date.getTime())) {
484
- throw new Error('[airtable-ts] Invalid date');
485
- }
486
- return date.toJSON().slice(0, 10);
487
- },
128
+ toAirtable: (value) => dateTimeMapperPair.toAirtable(value)?.slice(0, 10) ?? null,
488
129
  fromAirtable: (value) => {
489
- if (value === null || value === undefined)
130
+ const nullableValue = dateTimeMapperPair.fromAirtable(value);
131
+ if (nullableValue === null)
490
132
  return null;
491
- const date = new Date(value);
133
+ const date = new Date(nullableValue);
492
134
  if (Number.isNaN(date.getTime())) {
493
135
  throw new Error('[airtable-ts] Invalid date');
494
136
  }
495
137
  return Math.floor(date.getTime() / 1000);
496
138
  },
497
139
  },
498
- // Number assumed to be unix time in seconds
499
140
  dateTime: {
500
- toAirtable: (value) => {
501
- if (value === null)
502
- return null;
503
- const date = new Date(value * 1000);
504
- if (Number.isNaN(date.getTime())) {
505
- throw new Error('[airtable-ts] Invalid dateTime');
506
- }
507
- return date.toJSON();
508
- },
141
+ toAirtable: dateTimeMapperPair.toAirtable,
509
142
  fromAirtable: (value) => {
510
- if (value === null || value === undefined)
143
+ const nullableValue = dateTimeMapperPair.fromAirtable(value);
144
+ if (nullableValue === null)
511
145
  return null;
512
- const date = new Date(value);
146
+ const date = new Date(nullableValue);
513
147
  if (Number.isNaN(date.getTime())) {
514
- throw new Error('[airtable-ts] Invalid dateTime');
148
+ throw new Error('[airtable-ts] Invalid date');
515
149
  }
516
150
  return Math.floor(date.getTime() / 1000);
517
151
  },
518
152
  },
519
153
  createdTime: {
520
- toAirtable: (value) => {
521
- if (value === null)
522
- return null;
523
- const date = new Date(value * 1000);
524
- if (Number.isNaN(date.getTime())) {
525
- throw new Error('[airtable-ts] Invalid dateTime');
526
- }
527
- return date.toJSON();
528
- },
154
+ toAirtable: dateTimeMapperPair.toAirtable,
529
155
  fromAirtable: (value) => {
530
- if (value === null || value === undefined)
156
+ const nullableValue = dateTimeMapperPair.fromAirtable(value);
157
+ if (nullableValue === null)
531
158
  return null;
532
- const date = new Date(value);
159
+ const date = new Date(nullableValue);
533
160
  if (Number.isNaN(date.getTime())) {
534
- throw new Error('[airtable-ts] Invalid dateTime');
161
+ throw new Error('[airtable-ts] Invalid date');
535
162
  }
536
163
  return Math.floor(date.getTime() / 1000);
537
164
  },
538
165
  },
539
166
  lastModifiedTime: {
540
- toAirtable: (value) => {
541
- if (value === null)
542
- return null;
543
- const date = new Date(value * 1000);
544
- if (Number.isNaN(date.getTime())) {
545
- throw new Error('[airtable-ts] Invalid dateTime');
546
- }
547
- return date.toJSON();
548
- },
167
+ toAirtable: dateTimeMapperPair.toAirtable,
549
168
  fromAirtable: (value) => {
550
- if (value === null || value === undefined)
169
+ const nullableValue = dateTimeMapperPair.fromAirtable(value);
170
+ if (nullableValue === null)
551
171
  return null;
552
- const date = new Date(value);
172
+ const date = new Date(nullableValue);
553
173
  if (Number.isNaN(date.getTime())) {
554
- throw new Error('[airtable-ts] Invalid dateTime');
174
+ throw new Error('[airtable-ts] Invalid date');
555
175
  }
556
176
  return Math.floor(date.getTime() / 1000);
557
177
  },
558
178
  },
559
179
  multipleLookupValues: {
560
- toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
561
- fromAirtable: (value) => {
562
- if (!value || value.length === 0) {
563
- return null;
564
- }
565
- if (value.length !== 1) {
566
- throw new Error(`[airtable-ts] Can't coerce lookup to a single number, as there were ${value?.length} entries`);
567
- }
568
- if (typeof value[0] !== 'number') {
569
- throw new Error(`[airtable-ts] Can't coerce singular lookup to a single number, as it was of type ${typeof value[0]}`);
570
- }
571
- return value[0];
572
- },
180
+ toAirtable: readonly('multipleLookupValues'),
181
+ fromAirtable: coerce('multipleLookupValues', 'number | null'),
573
182
  },
574
183
  rollup: {
575
- toAirtable: () => { throw new Error('[airtable-ts] rollup type field is readonly'); },
576
- fromAirtable: (value) => {
577
- if (typeof value === 'number')
578
- return value;
579
- if (value === null || value === undefined)
580
- return null;
581
- throw new Error(`[airtable-ts] Can't coerce rollup to a number, as it was of type ${typeof value}`);
582
- },
184
+ toAirtable: readonly('rollup'),
185
+ fromAirtable: coerce('rollup', 'number | null'),
583
186
  },
584
187
  formula: {
585
- toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
586
- fromAirtable: (value) => {
587
- if (typeof value === 'number')
588
- return value;
589
- if (value === null || value === undefined)
590
- return null;
591
- throw new Error(`[airtable-ts] Can't coerce formula to a number, as it was of type ${typeof value}`);
592
- },
188
+ toAirtable: readonly('formula'),
189
+ fromAirtable: coerce('formula', 'number | null'),
190
+ },
191
+ unknown: {
192
+ toAirtable: (value) => value,
193
+ fromAirtable: coerce('unknown', 'number | null'),
593
194
  },
594
195
  },
595
- 'string[]': {
196
+ };
197
+ const stringArrayOrNull = {
198
+ 'string[] | null': {
596
199
  multipleSelects: fallbackMapperPair([], []),
597
200
  multipleRecordLinks: fallbackMapperPair([], []),
598
201
  multipleLookupValues: {
599
202
  toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
600
- fromAirtable: (value) => {
601
- if (!Array.isArray(value)) {
602
- throw new Error('[airtable-ts] Failed to coerce lookup type field to a string array, as it was not an array');
603
- }
604
- if (value.some((v) => typeof v !== 'string')) {
605
- throw new Error('[airtable-ts] Can\'t coerce lookup to a string array, as it had non string type');
606
- }
607
- return value;
608
- },
203
+ fromAirtable: coerce('multipleLookupValues', 'string[] | null'),
609
204
  },
610
205
  formula: {
611
206
  toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
612
- fromAirtable: (value) => {
613
- if (!Array.isArray(value)) {
614
- throw new Error('[airtable-ts] Failed to coerce formula type field to a string array, as it was not an array');
615
- }
616
- if (value.some((v) => typeof v !== 'string')) {
617
- throw new Error('[airtable-ts] Can\'t coerce formula to a string array, as it had non string type');
618
- }
619
- return value;
620
- },
207
+ fromAirtable: coerce('multipleLookupValues', 'string[] | null'),
621
208
  },
622
- },
623
- 'string[] | null': {
624
- multipleSelects: fallbackMapperPair(null, null),
625
- multipleRecordLinks: fallbackMapperPair(null, null),
626
- multipleLookupValues: {
627
- toAirtable: () => { throw new Error('[airtable-ts] lookup type field is readonly'); },
628
- fromAirtable: (value) => {
629
- if (!value && !Array.isArray(value)) {
630
- return null;
631
- }
632
- if (!Array.isArray(value)) {
633
- throw new Error('[airtable-ts] Failed to coerce lookup type field to a string array, as it was not an array');
634
- }
635
- if (value.some((v) => typeof v !== 'string')) {
636
- throw new Error('[airtable-ts] Can\'t coerce lookup to a string array, as it had non string type');
637
- }
638
- return value;
639
- },
640
- },
641
- formula: {
642
- toAirtable: () => { throw new Error('[airtable-ts] formula type field is readonly'); },
643
- fromAirtable: (value) => {
644
- if (!value && !Array.isArray(value)) {
645
- return null;
646
- }
647
- if (!Array.isArray(value)) {
648
- throw new Error('[airtable-ts] Failed to coerce formula type field to a string array, as it was not an array');
649
- }
650
- if (value.some((v) => typeof v !== 'string')) {
651
- throw new Error('[airtable-ts] Can\'t coerce formula to a string array, as it had non string type');
652
- }
653
- return value;
654
- },
209
+ unknown: {
210
+ toAirtable: (value) => value,
211
+ fromAirtable: coerce('unknown', 'string[] | null'),
655
212
  },
656
213
  },
657
214
  };
215
+ exports.fieldMappers = {
216
+ ...stringOrNull,
217
+ string: {
218
+ ...Object.fromEntries(Object.entries(stringOrNull['string | null']).map(([airtableType, nullablePair]) => {
219
+ return [airtableType, {
220
+ toAirtable: nullablePair.toAirtable,
221
+ fromAirtable: (value) => {
222
+ const nullableValue = nullablePair.fromAirtable(value);
223
+ if (nullableValue === null && ['multipleRecordLinks', 'dateTime', 'createdTime', 'lastModifiedTime'].includes(airtableType)) {
224
+ throw new Error(`[airtable-ts] Expected non-null or non-empty value to map to string for field type ${airtableType}`);
225
+ }
226
+ return nullableValue ?? '';
227
+ },
228
+ }];
229
+ })),
230
+ },
231
+ ...booleanOrNull,
232
+ boolean: {
233
+ ...Object.fromEntries(Object.entries(booleanOrNull['boolean | null']).map(([airtableType, nullablePair]) => {
234
+ return [airtableType, {
235
+ toAirtable: nullablePair.toAirtable,
236
+ fromAirtable: (value) => nullablePair.fromAirtable(value) ?? false,
237
+ }];
238
+ })),
239
+ },
240
+ ...numberOrNull,
241
+ number: {
242
+ ...Object.fromEntries(Object.entries(numberOrNull['number | null']).map(([airtableType, nullablePair]) => {
243
+ return [airtableType, {
244
+ toAirtable: nullablePair.toAirtable,
245
+ fromAirtable: (value) => {
246
+ const nullableValue = nullablePair.fromAirtable(value);
247
+ if (nullableValue === null) {
248
+ throw new Error(`[airtable-ts] Expected non-null or non-empty value to map to number for field type ${airtableType}`);
249
+ }
250
+ return nullableValue;
251
+ },
252
+ }];
253
+ })),
254
+ },
255
+ ...stringArrayOrNull,
256
+ 'string[]': {
257
+ ...Object.fromEntries(Object.entries(stringArrayOrNull['string[] | null']).map(([airtableType, nullablePair]) => {
258
+ return [airtableType, {
259
+ toAirtable: nullablePair.toAirtable,
260
+ fromAirtable: (value) => nullablePair.fromAirtable(value) ?? [],
261
+ }];
262
+ })),
263
+ },
264
+ };
@@ -4,6 +4,20 @@ 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 getMapper = (tsType, airtableType) => {
8
+ const tsMapper = fieldMappers_1.fieldMappers[tsType];
9
+ if (!tsMapper) {
10
+ throw new Error(`[airtable-ts] No mappers for ts type ${tsType}`);
11
+ }
12
+ if (tsMapper[airtableType]) {
13
+ return tsMapper[airtableType];
14
+ }
15
+ 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.`);
17
+ return tsMapper.unknown;
18
+ }
19
+ throw new Error(`[airtable-ts] Expected to be able to map to ts type ${tsType}, but got airtable type ${airtableType} which can't.`);
20
+ };
7
21
  /**
8
22
  * This function coerces an Airtable record to a TypeScript object, given an
9
23
  * object type definition. It will do this using the field mappers on each
@@ -28,18 +42,10 @@ const mapRecordTypeAirtableToTs = (tsTypes, record) => {
28
42
  throw new Error(`[airtable-ts] Failed to get Airtable field ${fieldNameOrId}`);
29
43
  }
30
44
  const value = record.fields[fieldDefinition.name];
31
- const tsMapper = fieldMappers_1.fieldMappers[tsType];
32
- if (!tsMapper) {
33
- throw new Error(`[airtable-ts] No mappers for ts type ${tsType}`);
34
- }
35
- const specificMapper = tsMapper[fieldDefinition.type]?.fromAirtable;
36
- if (!specificMapper) {
37
- // eslint-disable-next-line no-underscore-dangle
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
- }
40
45
  try {
46
+ const { fromAirtable } = getMapper(tsType, fieldDefinition.type);
41
47
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
- item[fieldNameOrId] = specificMapper(value);
48
+ item[fieldNameOrId] = fromAirtable(value);
43
49
  }
44
50
  catch (error) {
45
51
  if (error instanceof Error) {
@@ -88,16 +94,20 @@ const mapRecordTypeTsToAirtable = (tsTypes, tsRecord, airtableTable) => {
88
94
  if (!fieldDefinition) {
89
95
  throw new Error(`[airtable-ts] Failed to get Airtable field ${fieldNameOrId}`);
90
96
  }
91
- const tsMapper = fieldMappers_1.fieldMappers[tsType];
92
- if (!tsMapper) {
93
- throw new Error(`[airtable-ts] No mappers for ts type ${tsType}`);
97
+ try {
98
+ const { toAirtable } = getMapper(tsType, fieldDefinition.type);
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ item[fieldNameOrId] = toAirtable(value);
94
101
  }
95
- const specificMapper = tsMapper[fieldDefinition.type]?.toAirtable;
96
- if (!specificMapper) {
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.`);
102
+ catch (error) {
103
+ if (error instanceof Error) {
104
+ // eslint-disable-next-line no-underscore-dangle
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;
98
110
  }
99
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
- item[fieldNameOrId] = specificMapper(value);
101
111
  });
102
112
  return Object.assign(item, { id: tsRecord.id });
103
113
  };
@@ -3,7 +3,13 @@ type NonNullToString<T> = T extends string ? 'string' : T extends number ? 'numb
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
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);
6
+ export type FromAirtableTypeString<T extends AirtableTypeString | 'unknown'> = 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>[] : T extends 'unknown' ? unknown : never);
7
+ interface TypeDef {
8
+ single: 'string' | 'number' | 'boolean';
9
+ array: boolean;
10
+ nullable: boolean;
11
+ }
12
+ export declare const parseType: (t: TsTypeString) => TypeDef;
7
13
  /**
8
14
  * Verifies whether the given value is assignable to the given type
9
15
  *
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.airtableFieldNameTsTypes = exports.matchesType = void 0;
3
+ exports.airtableFieldNameTsTypes = exports.matchesType = exports.parseType = void 0;
4
4
  const parseType = (t) => {
5
5
  if (t.endsWith('[] | null')) {
6
6
  return {
@@ -29,6 +29,7 @@ const parseType = (t) => {
29
29
  nullable: false,
30
30
  };
31
31
  };
32
+ exports.parseType = parseType;
32
33
  /**
33
34
  * Verifies whether the given value is assignable to the given type
34
35
  *
@@ -42,7 +43,7 @@ const parseType = (t) => {
42
43
  * @example true
43
44
  */
44
45
  const matchesType = (value, tsType) => {
45
- const expectedType = parseType(tsType);
46
+ const expectedType = (0, exports.parseType)(tsType);
46
47
  if (expectedType.nullable && value === null) {
47
48
  return true;
48
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airtable-ts",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "A type-safe Airtable SDK",
5
5
  "license": "MIT",
6
6
  "author": "Adam Jones (domdomegg)",