airtable-ts 1.0.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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/assertMatchesSchema.d.ts +12 -0
- package/dist/assertMatchesSchema.js +35 -0
- package/dist/getAirtableTable.d.ts +4 -0
- package/dist/getAirtableTable.js +60 -0
- package/dist/getFields.d.ts +2 -0
- package/dist/getFields.js +10 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +64 -0
- package/dist/mapping/fieldMappers.d.ts +11 -0
- package/dist/mapping/fieldMappers.js +296 -0
- package/dist/mapping/nameMapper.d.ts +55 -0
- package/dist/mapping/nameMapper.js +103 -0
- package/dist/mapping/recordMapper.d.ts +17 -0
- package/dist/mapping/recordMapper.js +120 -0
- package/dist/mapping/typeUtils.d.ts +71 -0
- package/dist/mapping/typeUtils.js +109 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.js +2 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Adam Jones (domdomegg)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# airtable-ts
|
|
2
|
+
|
|
3
|
+
🗄️🧱 A type-safe Airtable SDK that makes developing apps on top of Airtable a breeze. We use it in multiple production applications and have found compared to the original SDK it:
|
|
4
|
+
- enables us to develop new applications faster
|
|
5
|
+
- significantly reduces mean time to detect and resolve issues
|
|
6
|
+
- helps us avoid footguns with the Airtable SDK, as well as eliminates boilerplate code
|
|
7
|
+
|
|
8
|
+
If you've ever tried to build applications on top of the Airtable API, you know it can be a pain. The default SDK leads to:
|
|
9
|
+
- apps silently breaking when colleagues edit field definitions in your base
|
|
10
|
+
- an error-prone and difficult coding experience with no type safety or editor hints
|
|
11
|
+
- unintuitive API behavior, like not being able to distinguish between a non-existent field and a field with unticked checkboxes
|
|
12
|
+
- awkward code as each AirtableRecord is a class with many helper methods, so you can't safely stringify them or use [Object.entries()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object#static_methods) etc.
|
|
13
|
+
|
|
14
|
+
All of these problems are solved with airtable-ts.
|
|
15
|
+
|
|
16
|
+
In development, you'll define the expected types for different fields: enabling you to leverage type hints in your code editor. After deployment, if people make breaking changes to your base schema you'll get clear runtime errors that pinpoint the problem (rather than your app silently failing, or worse: doing something dangerous!) We also fix unintuitive API behavior, like not being able to tell whether a checkbox field has been deleted or the values are just unticked.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
Install it with `npm install airtable-ts`. Then, use it like:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { AirtableTs, Table } from 'airtable-ts';
|
|
24
|
+
|
|
25
|
+
const db = new AirtableTs({
|
|
26
|
+
// Create your own at https://airtable.com/create/tokens
|
|
27
|
+
apiKey: 'pat1234.abcdef',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const studentTable: Table<{ id: string, name: string, classes: string[] }> = {
|
|
31
|
+
name: 'student',
|
|
32
|
+
baseId: 'app1234',
|
|
33
|
+
tableId: 'tbl1234',
|
|
34
|
+
schema: { name: 'string', classes: 'string[]' },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const classTable: Table<{ id: string, name: string }> = {
|
|
38
|
+
name: 'class',
|
|
39
|
+
baseId: 'app1234',
|
|
40
|
+
tableId: 'tbl4567',
|
|
41
|
+
schema: { name: 'string' },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Now we can get all the records in a table (a scan)
|
|
45
|
+
const classes = await db.scan(classTable);
|
|
46
|
+
|
|
47
|
+
// Get, update and delete specific records:
|
|
48
|
+
const student = await db.get(studentTable, 'rec1234');
|
|
49
|
+
await db.update(studentTable, { id: 'rec1234', name: 'Adam' });
|
|
50
|
+
await db.remove(studentTable, 'rec5678');
|
|
51
|
+
|
|
52
|
+
// Or for a more involved example:
|
|
53
|
+
async function prefixNameOfFirstClassOfFirstStudent(namePrefix: string) {
|
|
54
|
+
const students = await db.scan(studentTable);
|
|
55
|
+
if (!students[0]) throw new Error('There are no students');
|
|
56
|
+
if (!students[0].classes[0]) throw new Error('First student does not have a class');
|
|
57
|
+
|
|
58
|
+
const currentClass = await db.get(classTable, students[0].classes[0]);
|
|
59
|
+
const newName = namePrefix + currentClass.name;
|
|
60
|
+
await db.update(classTable, { id: currentClass.id, name: newName });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// And should you ever need it, access to the raw Airtable JS SDK
|
|
64
|
+
const rawSdk: Airtable = db.airtable;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Contributing
|
|
68
|
+
|
|
69
|
+
Pull requests are welcomed on GitHub! To get started:
|
|
70
|
+
|
|
71
|
+
1. Install Git and Node.js
|
|
72
|
+
2. Clone the repository
|
|
73
|
+
3. Install dependencies with `npm install`
|
|
74
|
+
4. Run `npm run test` to run tests
|
|
75
|
+
5. Build with `npm run build`
|
|
76
|
+
|
|
77
|
+
## Releases
|
|
78
|
+
|
|
79
|
+
Versions follow the [semantic versioning spec](https://semver.org/).
|
|
80
|
+
|
|
81
|
+
To release:
|
|
82
|
+
|
|
83
|
+
1. Use `npm version <major | minor | patch>` to bump the version
|
|
84
|
+
2. Run `git push --follow-tags` to push with tags
|
|
85
|
+
3. Wait for GitHub Actions to publish to the NPM registry.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Item, Table } from './mapping/typeUtils';
|
|
2
|
+
/**
|
|
3
|
+
* In theory, this should never catch stuff because our type mapping logic should
|
|
4
|
+
* verify the types are compatible.
|
|
5
|
+
*
|
|
6
|
+
* "In theory, there is no difference between theory and practice. But in practice, there is."
|
|
7
|
+
* ~ Benjamin Brewster, probably: https://quoteinvestigator.com/2018/04/14/theory/
|
|
8
|
+
*
|
|
9
|
+
* @param table
|
|
10
|
+
* @param data
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertMatchesSchema<T extends Item>(table: Table<T>, data: unknown, mode?: 'full' | 'partial'): asserts data is T;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertMatchesSchema = void 0;
|
|
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
|
+
/**
|
|
9
|
+
* In theory, this should never catch stuff because our type mapping logic should
|
|
10
|
+
* verify the types are compatible.
|
|
11
|
+
*
|
|
12
|
+
* "In theory, there is no difference between theory and practice. But in practice, there is."
|
|
13
|
+
* ~ Benjamin Brewster, probably: https://quoteinvestigator.com/2018/04/14/theory/
|
|
14
|
+
*
|
|
15
|
+
* @param table
|
|
16
|
+
* @param data
|
|
17
|
+
*/
|
|
18
|
+
function assertMatchesSchema(table, data, mode = 'full') {
|
|
19
|
+
if (typeof data !== 'object' || data === null) {
|
|
20
|
+
throw new Error(`[airtable-ts] Item for ${table.name} is not an object`);
|
|
21
|
+
}
|
|
22
|
+
Object.entries(table.schema).forEach(([fieldName, type]) => {
|
|
23
|
+
const value = data[fieldName];
|
|
24
|
+
if (value === undefined) {
|
|
25
|
+
if (mode === 'partial') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`[airtable-ts] Item for ${table.name} table is missing field '${fieldName}' (expected ${type})`);
|
|
29
|
+
}
|
|
30
|
+
if (!(0, typeUtils_1.matchesType)(value, type)) {
|
|
31
|
+
throw new Error(`[airtable-ts] Item for ${table.name} table has invalid value for field '${fieldName}' (actual type ${typeof value}, but expected ${type})`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
exports.assertMatchesSchema = assertMatchesSchema;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import Airtable from 'airtable';
|
|
2
|
+
import { Item, Table } from './mapping/typeUtils';
|
|
3
|
+
import { AirtableTable, CompleteAirtableTsOptions } from './types';
|
|
4
|
+
export declare const getAirtableTable: <T extends Item>(airtable: Airtable, table: Table<T>, options: CompleteAirtableTsOptions) => Promise<AirtableTable>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getAirtableTable = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const getAirtableTable = async (airtable, table, options) => {
|
|
9
|
+
const airtableTable = airtable.base(table.baseId).table(table.tableId);
|
|
10
|
+
// We need the schema so we know which type mapper to use.
|
|
11
|
+
// Even if we were inferring a type mapper from the schema, we'd still have to
|
|
12
|
+
// do this as otherwise we can't distinguish a column with all null values from
|
|
13
|
+
// a column that is missing entirely. We'd like to do that as for safety, we'd
|
|
14
|
+
// rather throw an error if the column is missing entirely; this suggests a
|
|
15
|
+
// misconfiguration. But an all-null column is okay. The particular case that
|
|
16
|
+
// this is likely for is checkbox columns.
|
|
17
|
+
const baseSchema = await getAirtableBaseSchema(table.baseId, options);
|
|
18
|
+
const tableDefinition = baseSchema.find((t) => t.id === table.tableId);
|
|
19
|
+
if (!tableDefinition) {
|
|
20
|
+
throw new Error(`[airtable-ts] Failed to find table ${table.tableId} in base ${table.baseId}`);
|
|
21
|
+
}
|
|
22
|
+
return Object.assign(airtableTable, { fields: tableDefinition.fields });
|
|
23
|
+
};
|
|
24
|
+
exports.getAirtableTable = getAirtableTable;
|
|
25
|
+
const baseSchemaCache = new Map();
|
|
26
|
+
/**
|
|
27
|
+
* Get the schemas from the cache or Airtable API for the tables in the given base.
|
|
28
|
+
* @see https://airtable.com/developers/web/api/get-base-schema
|
|
29
|
+
* @param baseId The base id to get the schemas for
|
|
30
|
+
*/
|
|
31
|
+
const getAirtableBaseSchema = async (baseId, options) => {
|
|
32
|
+
const fromCache = baseSchemaCache.get(baseId);
|
|
33
|
+
if (fromCache && Date.now() - fromCache.at < options.baseSchemaCacheDurationMs) {
|
|
34
|
+
return fromCache.data;
|
|
35
|
+
}
|
|
36
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
37
|
+
if (!options.apiKey) {
|
|
38
|
+
throw new Error('[airtable-ts] Missing API key');
|
|
39
|
+
}
|
|
40
|
+
const res = await (0, axios_1.default)({
|
|
41
|
+
baseURL: options.endpointUrl ?? 'https://api.airtable.com',
|
|
42
|
+
url: `/v0/meta/bases/${baseId}/tables`,
|
|
43
|
+
...(options.requestTimeout ? { timeout: options.requestTimeout } : {}),
|
|
44
|
+
headers: {
|
|
45
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
46
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
47
|
+
...options.customHeaders,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const baseSchema = res.data.tables;
|
|
51
|
+
if (baseSchemaCache.size > 100) {
|
|
52
|
+
baseSchemaCache.clear();
|
|
53
|
+
// If you're seeing this warning, then we probably either need to:
|
|
54
|
+
// - Update the maximum limit before clearing the cache, provided we have memory headroom; or
|
|
55
|
+
// - Use a last recently used cache or similar
|
|
56
|
+
console.warn('[airtable-ts] baseSchemaCache cleared to avoid a memory leak: this code is not currently optimized for accessing over 100 different bases from a single instance');
|
|
57
|
+
}
|
|
58
|
+
baseSchemaCache.set(baseId, { at: Date.now(), data: baseSchema });
|
|
59
|
+
return baseSchema;
|
|
60
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getFields = void 0;
|
|
4
|
+
const getFields = (table) => {
|
|
5
|
+
if (table.mappings) {
|
|
6
|
+
return Object.values(table.mappings).flat();
|
|
7
|
+
}
|
|
8
|
+
return Object.keys(table.schema);
|
|
9
|
+
};
|
|
10
|
+
exports.getFields = getFields;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Airtable from 'airtable';
|
|
2
|
+
import type { QueryParams } from 'airtable/lib/query_params';
|
|
3
|
+
import { Item, Table } from './mapping/typeUtils';
|
|
4
|
+
import { AirtableTsOptions } from './types';
|
|
5
|
+
export declare class AirtableTs {
|
|
6
|
+
airtable: Airtable;
|
|
7
|
+
private options;
|
|
8
|
+
constructor(options: AirtableTsOptions);
|
|
9
|
+
get<T extends Item>(table: Table<T>, id: string): Promise<T>;
|
|
10
|
+
scan<T extends Item>(table: Table<T>, params?: ScanParams): Promise<T[]>;
|
|
11
|
+
insert<T extends Item>(table: Table<T>, data: Omit<T, 'id'>): Promise<T>;
|
|
12
|
+
update<T extends Item>(table: Table<T>, data: Partial<T> & {
|
|
13
|
+
id: string;
|
|
14
|
+
}): Promise<T>;
|
|
15
|
+
remove<T extends Item>(table: Table<T>, id: string): Promise<T>;
|
|
16
|
+
}
|
|
17
|
+
export type ScanParams = Omit<QueryParams<unknown>, 'fields' | 'cellFormat' | 'method' | 'returnFieldsByFieldId' | 'pageSize' | 'offset'>;
|
|
18
|
+
export type { AirtableTsOptions } from './types';
|
|
19
|
+
export type { Item, Table } from './mapping/typeUtils';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AirtableTs = void 0;
|
|
7
|
+
const airtable_1 = __importDefault(require("airtable"));
|
|
8
|
+
const getAirtableTable_1 = require("./getAirtableTable");
|
|
9
|
+
const assertMatchesSchema_1 = require("./assertMatchesSchema");
|
|
10
|
+
const recordMapper_1 = require("./mapping/recordMapper");
|
|
11
|
+
const getFields_1 = require("./getFields");
|
|
12
|
+
class AirtableTs {
|
|
13
|
+
airtable;
|
|
14
|
+
options;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.airtable = new airtable_1.default(options);
|
|
17
|
+
this.options = {
|
|
18
|
+
...airtable_1.default.default_config(),
|
|
19
|
+
...options,
|
|
20
|
+
baseSchemaCacheDurationMs: options.baseSchemaCacheDurationMs ?? 120000,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async get(table, id) {
|
|
24
|
+
if (!id) {
|
|
25
|
+
throw new Error(`[airtable-ts] Tried to get record in ${table.name} with no id`);
|
|
26
|
+
}
|
|
27
|
+
const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
|
|
28
|
+
const record = await airtableTable.find(id);
|
|
29
|
+
if (!record) {
|
|
30
|
+
throw new Error(`[airtable-ts] Failed to find record in ${table.name} with key ${id}`);
|
|
31
|
+
}
|
|
32
|
+
return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
|
|
33
|
+
}
|
|
34
|
+
async scan(table, params) {
|
|
35
|
+
const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
|
|
36
|
+
const records = await airtableTable.select({
|
|
37
|
+
fields: (0, getFields_1.getFields)(table),
|
|
38
|
+
...params,
|
|
39
|
+
}).all();
|
|
40
|
+
return records.map((record) => (0, recordMapper_1.mapRecordFromAirtable)(table, record));
|
|
41
|
+
}
|
|
42
|
+
async insert(table, data) {
|
|
43
|
+
(0, assertMatchesSchema_1.assertMatchesSchema)(table, { ...data, id: 'placeholder' });
|
|
44
|
+
const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
|
|
45
|
+
const record = await airtableTable.create((0, recordMapper_1.mapRecordToAirtable)(table, data, airtableTable));
|
|
46
|
+
return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
|
|
47
|
+
}
|
|
48
|
+
async update(table, data) {
|
|
49
|
+
(0, assertMatchesSchema_1.assertMatchesSchema)(table, { ...data }, 'partial');
|
|
50
|
+
const { id, ...withoutId } = data;
|
|
51
|
+
const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
|
|
52
|
+
const record = await airtableTable.update(data.id, (0, recordMapper_1.mapRecordToAirtable)(table, withoutId, airtableTable));
|
|
53
|
+
return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
|
|
54
|
+
}
|
|
55
|
+
async remove(table, id) {
|
|
56
|
+
if (!id) {
|
|
57
|
+
throw new Error(`[airtable-ts] Tried to remove record in ${table.name} with no id`);
|
|
58
|
+
}
|
|
59
|
+
const airtableTable = await (0, getAirtableTable_1.getAirtableTable)(this.airtable, table, this.options);
|
|
60
|
+
const record = await airtableTable.destroy(id);
|
|
61
|
+
return (0, recordMapper_1.mapRecordFromAirtable)(table, record);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.AirtableTs = AirtableTs;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AirtableTypeString, FromAirtableTypeString, FromTsTypeString, TsTypeString } from './typeUtils';
|
|
2
|
+
type Mapper = {
|
|
3
|
+
[T in TsTypeString]?: {
|
|
4
|
+
[A in AirtableTypeString]?: {
|
|
5
|
+
toAirtable: (value: FromTsTypeString<T>) => FromAirtableTypeString<A>;
|
|
6
|
+
fromAirtable: (value: FromAirtableTypeString<A> | null | undefined) => FromTsTypeString<T>;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare const fieldMappers: Mapper;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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
|
+
};
|
|
10
|
+
const fallbackMapperPair = (toFallback, fromFallback) => ({
|
|
11
|
+
toAirtable: (value) => value ?? toFallback,
|
|
12
|
+
fromAirtable: (value) => value ?? fromFallback,
|
|
13
|
+
});
|
|
14
|
+
const requiredMapperPair = {
|
|
15
|
+
toAirtable: (value) => required(value),
|
|
16
|
+
fromAirtable: (value) => required(value),
|
|
17
|
+
};
|
|
18
|
+
exports.fieldMappers = {
|
|
19
|
+
string: {
|
|
20
|
+
singleLineText: fallbackMapperPair('', ''),
|
|
21
|
+
email: fallbackMapperPair('', ''),
|
|
22
|
+
url: fallbackMapperPair('', ''),
|
|
23
|
+
multilineText: fallbackMapperPair('', ''),
|
|
24
|
+
richText: fallbackMapperPair('', ''),
|
|
25
|
+
phoneNumber: fallbackMapperPair('', ''),
|
|
26
|
+
multipleRecordLinks: {
|
|
27
|
+
toAirtable: (value) => {
|
|
28
|
+
return [value];
|
|
29
|
+
},
|
|
30
|
+
fromAirtable: (value) => {
|
|
31
|
+
if (!value) {
|
|
32
|
+
throw new Error('[airtable-ts] Failed to coerce multipleRecordLinks type field to a single string, as it was blank');
|
|
33
|
+
}
|
|
34
|
+
if (value.length !== 1) {
|
|
35
|
+
throw new Error(`[airtable-ts] Can't coerce multipleRecordLinks to a single string, as there were ${value?.length} entries`);
|
|
36
|
+
}
|
|
37
|
+
return value[0];
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
dateTime: {
|
|
41
|
+
toAirtable: (value) => {
|
|
42
|
+
const date = new Date(value);
|
|
43
|
+
if (Number.isNaN(date.getTime())) {
|
|
44
|
+
throw new Error('[airtable-ts] Invalid dateTime string');
|
|
45
|
+
}
|
|
46
|
+
return date.toJSON();
|
|
47
|
+
},
|
|
48
|
+
fromAirtable: (value) => {
|
|
49
|
+
const date = new Date(value ?? '');
|
|
50
|
+
if (Number.isNaN(date.getTime())) {
|
|
51
|
+
throw new Error('[airtable-ts] Invalid dateTime string');
|
|
52
|
+
}
|
|
53
|
+
return date.toJSON();
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
multipleLookupValues: {
|
|
57
|
+
toAirtable: () => { throw new Error('[airtable-ts] multipleLookupValues type field is readonly'); },
|
|
58
|
+
fromAirtable: (value) => {
|
|
59
|
+
if (!value) {
|
|
60
|
+
throw new Error('[airtable-ts] Failed to coerce multipleLookupValues type field to a single string, as it was blank');
|
|
61
|
+
}
|
|
62
|
+
if (value.length !== 1) {
|
|
63
|
+
throw new Error(`[airtable-ts] Can't coerce multipleLookupValues to a single string, as there were ${value?.length} entries`);
|
|
64
|
+
}
|
|
65
|
+
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]}`);
|
|
67
|
+
}
|
|
68
|
+
return value[0];
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
'string | null': {
|
|
73
|
+
singleLineText: fallbackMapperPair(null, null),
|
|
74
|
+
email: fallbackMapperPair(null, null),
|
|
75
|
+
url: fallbackMapperPair(null, null),
|
|
76
|
+
multilineText: fallbackMapperPair(null, null),
|
|
77
|
+
richText: fallbackMapperPair(null, null),
|
|
78
|
+
phoneNumber: fallbackMapperPair(null, null),
|
|
79
|
+
multipleRecordLinks: {
|
|
80
|
+
toAirtable: (value) => {
|
|
81
|
+
return value ? [value] : [];
|
|
82
|
+
},
|
|
83
|
+
fromAirtable: (value) => {
|
|
84
|
+
if (!value || value.length === 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (value.length !== 1) {
|
|
88
|
+
throw new Error(`[airtable-ts] Can't coerce multipleRecordLinks to a single string, as there were ${value?.length} entries`);
|
|
89
|
+
}
|
|
90
|
+
return value[0];
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
dateTime: {
|
|
94
|
+
toAirtable: (value) => {
|
|
95
|
+
if (value === null)
|
|
96
|
+
return null;
|
|
97
|
+
const date = new Date(value);
|
|
98
|
+
if (Number.isNaN(date.getTime())) {
|
|
99
|
+
throw new Error('[airtable-ts] Invalid dateTime');
|
|
100
|
+
}
|
|
101
|
+
return date.toJSON();
|
|
102
|
+
},
|
|
103
|
+
fromAirtable: (value) => {
|
|
104
|
+
if (value === null || value === undefined)
|
|
105
|
+
return null;
|
|
106
|
+
const date = new Date(value);
|
|
107
|
+
if (Number.isNaN(date.getTime())) {
|
|
108
|
+
throw new Error('[airtable-ts] Invalid dateTime');
|
|
109
|
+
}
|
|
110
|
+
return date.toJSON();
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
multipleLookupValues: {
|
|
114
|
+
toAirtable: () => { throw new Error('[airtable-ts] multipleLookupValues type field is readonly'); },
|
|
115
|
+
fromAirtable: (value) => {
|
|
116
|
+
if (!value || value.length === 0) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (value.length !== 1) {
|
|
120
|
+
throw new Error(`[airtable-ts] Can't coerce multipleLookupValues to a single string, as there were ${value?.length} entries`);
|
|
121
|
+
}
|
|
122
|
+
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]}`);
|
|
124
|
+
}
|
|
125
|
+
return value[0];
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
boolean: {
|
|
130
|
+
checkbox: fallbackMapperPair(false, false),
|
|
131
|
+
multipleLookupValues: {
|
|
132
|
+
toAirtable: () => { throw new Error('[airtable-ts] multipleLookupValues type field is readonly'); },
|
|
133
|
+
fromAirtable: (value) => {
|
|
134
|
+
if (!value) {
|
|
135
|
+
throw new Error('[airtable-ts] Failed to coerce multipleLookupValues type field to a single boolean, as it was blank');
|
|
136
|
+
}
|
|
137
|
+
if (value.length !== 1) {
|
|
138
|
+
throw new Error(`[airtable-ts] Can't coerce multipleLookupValues to a single boolean, as there were ${value?.length} entries`);
|
|
139
|
+
}
|
|
140
|
+
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]}`);
|
|
142
|
+
}
|
|
143
|
+
return value[0];
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
'boolean | null': {
|
|
148
|
+
checkbox: fallbackMapperPair(null, null),
|
|
149
|
+
multipleLookupValues: {
|
|
150
|
+
toAirtable: () => { throw new Error('[airtable-ts] multipleLookupValues type field is readonly'); },
|
|
151
|
+
fromAirtable: (value) => {
|
|
152
|
+
if (!value || value.length === 0) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (value.length !== 1) {
|
|
156
|
+
throw new Error(`[airtable-ts] Can't coerce multipleLookupValues to a single boolean, as there were ${value?.length} entries`);
|
|
157
|
+
}
|
|
158
|
+
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]}`);
|
|
160
|
+
}
|
|
161
|
+
return value[0];
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
number: {
|
|
166
|
+
number: requiredMapperPair,
|
|
167
|
+
percent: requiredMapperPair,
|
|
168
|
+
currency: requiredMapperPair,
|
|
169
|
+
rating: requiredMapperPair,
|
|
170
|
+
duration: requiredMapperPair,
|
|
171
|
+
count: {
|
|
172
|
+
toAirtable: () => { throw new Error('[airtable-ts] count type field is readonly'); },
|
|
173
|
+
fromAirtable: (value) => required(value),
|
|
174
|
+
},
|
|
175
|
+
autoNumber: {
|
|
176
|
+
toAirtable: () => { throw new Error('[airtable-ts] autoNumber type field is readonly'); },
|
|
177
|
+
fromAirtable: (value) => required(value),
|
|
178
|
+
},
|
|
179
|
+
// Number assumed to be unix time in seconds
|
|
180
|
+
dateTime: {
|
|
181
|
+
toAirtable: (value) => {
|
|
182
|
+
const date = new Date(value * 1000);
|
|
183
|
+
if (Number.isNaN(date.getTime())) {
|
|
184
|
+
throw new Error('[airtable-ts] Invalid dateTime');
|
|
185
|
+
}
|
|
186
|
+
return date.toJSON();
|
|
187
|
+
},
|
|
188
|
+
fromAirtable: (value) => {
|
|
189
|
+
const date = new Date(value ?? '');
|
|
190
|
+
if (Number.isNaN(date.getTime())) {
|
|
191
|
+
throw new Error('[airtable-ts] Invalid dateTime');
|
|
192
|
+
}
|
|
193
|
+
return Math.floor(date.getTime() / 1000);
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
multipleLookupValues: {
|
|
197
|
+
toAirtable: () => { throw new Error('[airtable-ts] multipleLookupValues type field is readonly'); },
|
|
198
|
+
fromAirtable: (value) => {
|
|
199
|
+
if (!value) {
|
|
200
|
+
throw new Error('[airtable-ts] Failed to coerce multipleLookupValues type field to a single number, as it was blank');
|
|
201
|
+
}
|
|
202
|
+
if (value.length !== 1) {
|
|
203
|
+
throw new Error(`[airtable-ts] Can't coerce multipleLookupValues to a single number, as there were ${value?.length} entries`);
|
|
204
|
+
}
|
|
205
|
+
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]}`);
|
|
207
|
+
}
|
|
208
|
+
return value[0];
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
'number | null': {
|
|
213
|
+
number: fallbackMapperPair(null, null),
|
|
214
|
+
percent: fallbackMapperPair(null, null),
|
|
215
|
+
currency: fallbackMapperPair(null, null),
|
|
216
|
+
rating: fallbackMapperPair(null, null),
|
|
217
|
+
duration: fallbackMapperPair(null, null),
|
|
218
|
+
count: {
|
|
219
|
+
fromAirtable: (value) => value ?? null,
|
|
220
|
+
toAirtable: () => { throw new Error('[airtable-ts] count type field is readonly'); },
|
|
221
|
+
},
|
|
222
|
+
autoNumber: {
|
|
223
|
+
fromAirtable: (value) => value ?? null,
|
|
224
|
+
toAirtable: () => { throw new Error('[airtable-ts] autoNumber field is readonly'); },
|
|
225
|
+
},
|
|
226
|
+
// Number assumed to be unix time in seconds
|
|
227
|
+
dateTime: {
|
|
228
|
+
toAirtable: (value) => {
|
|
229
|
+
if (value === null)
|
|
230
|
+
return null;
|
|
231
|
+
const date = new Date(value * 1000);
|
|
232
|
+
if (Number.isNaN(date.getTime())) {
|
|
233
|
+
throw new Error('[airtable-ts] Invalid dateTime');
|
|
234
|
+
}
|
|
235
|
+
return date.toJSON();
|
|
236
|
+
},
|
|
237
|
+
fromAirtable: (value) => {
|
|
238
|
+
if (value === null || value === undefined)
|
|
239
|
+
return null;
|
|
240
|
+
const date = new Date(value);
|
|
241
|
+
if (Number.isNaN(date.getTime())) {
|
|
242
|
+
throw new Error('[airtable-ts] Invalid dateTime');
|
|
243
|
+
}
|
|
244
|
+
return Math.floor(date.getTime() / 1000);
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
multipleLookupValues: {
|
|
248
|
+
toAirtable: () => { throw new Error('[airtable-ts] multipleLookupValues type field is readonly'); },
|
|
249
|
+
fromAirtable: (value) => {
|
|
250
|
+
if (!value || value.length === 0) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
if (value.length !== 1) {
|
|
254
|
+
throw new Error(`[airtable-ts] Can't coerce multipleLookupValues to a single number, as there were ${value?.length} entries`);
|
|
255
|
+
}
|
|
256
|
+
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]}`);
|
|
258
|
+
}
|
|
259
|
+
return value[0];
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
'string[]': {
|
|
264
|
+
multipleRecordLinks: fallbackMapperPair([], []),
|
|
265
|
+
multipleLookupValues: {
|
|
266
|
+
toAirtable: () => { throw new Error('[airtable-ts] multipleLookupValues type field is readonly'); },
|
|
267
|
+
fromAirtable: (value) => {
|
|
268
|
+
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');
|
|
270
|
+
}
|
|
271
|
+
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');
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
'string[] | null': {
|
|
279
|
+
multipleRecordLinks: fallbackMapperPair(null, null),
|
|
280
|
+
multipleLookupValues: {
|
|
281
|
+
toAirtable: () => { throw new Error('[airtable-ts] multipleLookupValues type field is readonly'); },
|
|
282
|
+
fromAirtable: (value) => {
|
|
283
|
+
if (!value && !Array.isArray(value)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
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');
|
|
288
|
+
}
|
|
289
|
+
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');
|
|
291
|
+
}
|
|
292
|
+
return value;
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Item, Table, FromTsTypeString, TsTypeString } from './typeUtils';
|
|
2
|
+
/**
|
|
3
|
+
* Maps a TS object (matching table.mappings) to another TS object (matching table.schema),
|
|
4
|
+
* mapping columns based on the table definition.
|
|
5
|
+
*
|
|
6
|
+
* @param table Table definition
|
|
7
|
+
* @example {
|
|
8
|
+
* schema: { someProp: 'string', otherProps: 'number[]', another: 'boolean' },
|
|
9
|
+
* mappings: { someProp: 'Some_Airtable_Field', otherProps: ['Field1', 'Field2'], another: 'another' },
|
|
10
|
+
* ...
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* @param tsRecord The TS object to map
|
|
14
|
+
* @example {
|
|
15
|
+
* Some_Airtable_Field: 'abcd',
|
|
16
|
+
* Field1: 314,
|
|
17
|
+
* Field2: 159,
|
|
18
|
+
* another: true,
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* @returns The TS object mapped via the table.mappings
|
|
22
|
+
* @example {
|
|
23
|
+
* someProp: 'abcd',
|
|
24
|
+
* otherProps: [314, 159],
|
|
25
|
+
* another: true,
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
export declare const mapRecordFieldNamesAirtableToTs: <T extends Item>(table: Table<T>, tsRecord: Record<string, FromTsTypeString<TsTypeString>>) => T;
|
|
29
|
+
/**
|
|
30
|
+
* Maps a TS object (matching table.schema) to another TS object (matching table.mappings),
|
|
31
|
+
* mapping columns based on the table definition.
|
|
32
|
+
*
|
|
33
|
+
* @param table Table definition
|
|
34
|
+
* @example {
|
|
35
|
+
* schema: { someProp: 'string', otherProps: 'number[]', another: 'boolean' },
|
|
36
|
+
* mappings: { someProp: 'Some_Airtable_Field', otherProps: ['Field1', 'Field2'] },
|
|
37
|
+
* ...
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* @param item The TS object to map
|
|
41
|
+
* @example {
|
|
42
|
+
* someProp: 'abcd',
|
|
43
|
+
* otherProps: [314, 159],
|
|
44
|
+
* another: true,
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* @returns The TS object mapped via the table.mappings
|
|
48
|
+
* @example {
|
|
49
|
+
* Some_Airtable_Field: 'abcd',
|
|
50
|
+
* Field1: 314,
|
|
51
|
+
* Field2: 159,
|
|
52
|
+
* another: true,
|
|
53
|
+
* }
|
|
54
|
+
*/
|
|
55
|
+
export declare const mapRecordFieldNamesTsToAirtable: <T extends Item>(table: Table<T>, item: Partial<T>) => Record<string, FromTsTypeString<TsTypeString>>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapRecordFieldNamesTsToAirtable = exports.mapRecordFieldNamesAirtableToTs = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Maps a TS object (matching table.mappings) to another TS object (matching table.schema),
|
|
6
|
+
* mapping columns based on the table definition.
|
|
7
|
+
*
|
|
8
|
+
* @param table Table definition
|
|
9
|
+
* @example {
|
|
10
|
+
* schema: { someProp: 'string', otherProps: 'number[]', another: 'boolean' },
|
|
11
|
+
* mappings: { someProp: 'Some_Airtable_Field', otherProps: ['Field1', 'Field2'], another: 'another' },
|
|
12
|
+
* ...
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* @param tsRecord The TS object to map
|
|
16
|
+
* @example {
|
|
17
|
+
* Some_Airtable_Field: 'abcd',
|
|
18
|
+
* Field1: 314,
|
|
19
|
+
* Field2: 159,
|
|
20
|
+
* another: true,
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @returns The TS object mapped via the table.mappings
|
|
24
|
+
* @example {
|
|
25
|
+
* someProp: 'abcd',
|
|
26
|
+
* otherProps: [314, 159],
|
|
27
|
+
* another: true,
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
const mapRecordFieldNamesAirtableToTs = (table, tsRecord) => {
|
|
31
|
+
const schemaEntries = Object.entries(table.schema);
|
|
32
|
+
const item = Object.fromEntries(schemaEntries.map(([outputFieldName]) => {
|
|
33
|
+
const mappingToAirtable = table.mappings?.[outputFieldName];
|
|
34
|
+
if (!mappingToAirtable) {
|
|
35
|
+
return [outputFieldName, tsRecord[outputFieldName]];
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(mappingToAirtable)) {
|
|
38
|
+
return [outputFieldName, mappingToAirtable.map((airtableFieldName) => tsRecord[airtableFieldName])];
|
|
39
|
+
}
|
|
40
|
+
return [outputFieldName, tsRecord[mappingToAirtable]];
|
|
41
|
+
}));
|
|
42
|
+
return Object.assign(item, { id: tsRecord['id'] });
|
|
43
|
+
};
|
|
44
|
+
exports.mapRecordFieldNamesAirtableToTs = mapRecordFieldNamesAirtableToTs;
|
|
45
|
+
/**
|
|
46
|
+
* Maps a TS object (matching table.schema) to another TS object (matching table.mappings),
|
|
47
|
+
* mapping columns based on the table definition.
|
|
48
|
+
*
|
|
49
|
+
* @param table Table definition
|
|
50
|
+
* @example {
|
|
51
|
+
* schema: { someProp: 'string', otherProps: 'number[]', another: 'boolean' },
|
|
52
|
+
* mappings: { someProp: 'Some_Airtable_Field', otherProps: ['Field1', 'Field2'] },
|
|
53
|
+
* ...
|
|
54
|
+
* }
|
|
55
|
+
*
|
|
56
|
+
* @param item The TS object to map
|
|
57
|
+
* @example {
|
|
58
|
+
* someProp: 'abcd',
|
|
59
|
+
* otherProps: [314, 159],
|
|
60
|
+
* another: true,
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* @returns The TS object mapped via the table.mappings
|
|
64
|
+
* @example {
|
|
65
|
+
* Some_Airtable_Field: 'abcd',
|
|
66
|
+
* Field1: 314,
|
|
67
|
+
* Field2: 159,
|
|
68
|
+
* another: true,
|
|
69
|
+
* }
|
|
70
|
+
*/
|
|
71
|
+
const mapRecordFieldNamesTsToAirtable = (table, item) => {
|
|
72
|
+
const schemaEntries = Object.entries(table.schema);
|
|
73
|
+
const tsRecord = Object.fromEntries(schemaEntries.map(([outputFieldName, tsType]) => {
|
|
74
|
+
const mappingToAirtable = table.mappings?.[outputFieldName];
|
|
75
|
+
if (!(outputFieldName in item)) {
|
|
76
|
+
// If we don't have the field, just skip: this allows us to support partial updates
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const value = item[outputFieldName];
|
|
80
|
+
if (!mappingToAirtable) {
|
|
81
|
+
return [[outputFieldName, value]];
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(mappingToAirtable)) {
|
|
84
|
+
if (value === null) {
|
|
85
|
+
if (tsType.endsWith('| null')) {
|
|
86
|
+
return mappingToAirtable.map((airtableFieldName) => [airtableFieldName, null]);
|
|
87
|
+
}
|
|
88
|
+
// This should be unreachable because of our types
|
|
89
|
+
throw new Error(`[airtable-ts] Expected field ${table.name}.${outputFieldName} to match type \`${tsType}\` but got null. This should never happen in normal operation as it should be caught before this point.`);
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(value)) {
|
|
92
|
+
throw new Error(`[airtable-ts] Got non-array type ${typeof value} for ${table.name}.${outputFieldName}, but expected ${table.schema[outputFieldName]}.`);
|
|
93
|
+
}
|
|
94
|
+
if (value.length !== mappingToAirtable.length) {
|
|
95
|
+
throw new Error(`[airtable-ts] Got ${value.length} values for ${table.name}.${outputFieldName}, but ${mappingToAirtable.length} mappings. Expected these to be the same.`);
|
|
96
|
+
}
|
|
97
|
+
return mappingToAirtable.map((airtableFieldName, index) => [airtableFieldName, value[index]]);
|
|
98
|
+
}
|
|
99
|
+
return [[mappingToAirtable, value]];
|
|
100
|
+
}).flat(1));
|
|
101
|
+
return Object.assign(tsRecord, { id: item.id });
|
|
102
|
+
};
|
|
103
|
+
exports.mapRecordFieldNamesTsToAirtable = mapRecordFieldNamesTsToAirtable;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FieldSet } from 'airtable';
|
|
2
|
+
import { AirtableRecord, AirtableTable } from '../types';
|
|
3
|
+
import { FromTsTypeString, Item, Table, TsTypeString } from './typeUtils';
|
|
4
|
+
export declare const mapRecordFromAirtable: <T extends Item>(table: Table<T>, record: AirtableRecord) => T;
|
|
5
|
+
export declare const mapRecordToAirtable: <T extends Item>(table: Table<T>, item: Partial<T>, airtableTable: AirtableTable) => FieldSet;
|
|
6
|
+
export declare const visibleForTesting: {
|
|
7
|
+
mapRecordTypeAirtableToTs: <T extends {
|
|
8
|
+
[fieldName: string]: TsTypeString;
|
|
9
|
+
}>(tsTypes: T, record: AirtableRecord) => { [F in keyof T]: FromTsTypeString<T[F]>; } & {
|
|
10
|
+
id: string;
|
|
11
|
+
};
|
|
12
|
+
mapRecordTypeTsToAirtable: <T_1 extends {
|
|
13
|
+
[fieldName: string]: TsTypeString;
|
|
14
|
+
}, R extends { [K in keyof T_1]?: FromTsTypeString<T_1[K]>; } & {
|
|
15
|
+
id?: string;
|
|
16
|
+
}>(tsTypes: T_1, tsRecord: R, airtableTable: AirtableTable) => FieldSet;
|
|
17
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.visibleForTesting = exports.mapRecordToAirtable = exports.mapRecordFromAirtable = void 0;
|
|
4
|
+
const fieldMappers_1 = require("./fieldMappers");
|
|
5
|
+
const nameMapper_1 = require("./nameMapper");
|
|
6
|
+
const typeUtils_1 = require("./typeUtils");
|
|
7
|
+
/**
|
|
8
|
+
* This function coerces an Airtable record to a TypeScript object, given an
|
|
9
|
+
* object type definition. It will do this using the field mappers on each
|
|
10
|
+
* field, based on the tsTypes and Airtable table schema (via the record).
|
|
11
|
+
* It does NOT change any property names.
|
|
12
|
+
*
|
|
13
|
+
* @param tsTypes TypeScript types for the record.
|
|
14
|
+
* @example { a: 'string', b: 'number', c: 'boolean', d: 'string' }
|
|
15
|
+
*
|
|
16
|
+
* @param record The Airtable record to convert.
|
|
17
|
+
* @example { id: 'rec012', a: 'Some text', b: 123, d: ['rec345'] } // (c is an un-ticked checkbox, d is a multipleRecordLinks)
|
|
18
|
+
*
|
|
19
|
+
* @returns An object matching the TypeScript type passed in, based on the Airtable record. Throws if cannot coerce to requested type.
|
|
20
|
+
* @example { id: 'rec012', a: 'Some text', b: 123, c: false, d: 'rec345' }
|
|
21
|
+
*/
|
|
22
|
+
const mapRecordTypeAirtableToTs = (tsTypes, record) => {
|
|
23
|
+
const item = {};
|
|
24
|
+
Object.entries(tsTypes).forEach(([fieldName, tsType]) => {
|
|
25
|
+
const value = record.fields[fieldName];
|
|
26
|
+
// 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}`);
|
|
30
|
+
}
|
|
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[airtableType]?.fromAirtable;
|
|
36
|
+
if (!specificMapper) {
|
|
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.`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
item[fieldName] = specificMapper(value);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error instanceof Error) {
|
|
46
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
47
|
+
error.message = `Failed to map field ${record._table.name}.${fieldName}: ${error.message}`;
|
|
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}`;
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return Object.assign(item, { id: record.id });
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* This function coerces a TypeScript object to a Airtable record, given an
|
|
58
|
+
* Airtable table. It will do this using the field mappers on each field, based
|
|
59
|
+
* on the tsTypes and Airtable table schema.
|
|
60
|
+
* It does NOT change any property names.
|
|
61
|
+
*
|
|
62
|
+
* @param tsTypes TypeScript types for the record (necessary to handle nullables).
|
|
63
|
+
* @example { a: 'string', b: 'number', c: 'boolean', d: 'string' }
|
|
64
|
+
*
|
|
65
|
+
* @param tsRecord TypeScript object to convert.
|
|
66
|
+
* @example { a: 'Some text', b: 123, c: false, d: 'rec123' }
|
|
67
|
+
*
|
|
68
|
+
* @param airtableTable An Airtable table.
|
|
69
|
+
* @example { fields: { a: 'singleLineText', b: 'number', c: 'checkbox', d: 'multipleRecordLinks' }, ... }
|
|
70
|
+
*
|
|
71
|
+
* @returns An Airtable FieldSet. Throws if cannot coerce to requested type.
|
|
72
|
+
* @example { a: 'Some text', b: 123, d: ['rec123'] } // (c is an un-ticked checkbox, d is a multipleRecordLinks)
|
|
73
|
+
*/
|
|
74
|
+
const mapRecordTypeTsToAirtable = (tsTypes, tsRecord, airtableTable) => {
|
|
75
|
+
const item = {};
|
|
76
|
+
Object.entries(tsTypes).forEach(([fieldName, tsType]) => {
|
|
77
|
+
const value = tsRecord[fieldName];
|
|
78
|
+
if (!(fieldName in tsRecord)) {
|
|
79
|
+
// If we don't have the field, just skip: this allows us to support partial updates
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!(0, typeUtils_1.matchesType)(value, tsType)) {
|
|
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.`);
|
|
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}`);
|
|
89
|
+
}
|
|
90
|
+
const tsMapper = fieldMappers_1.fieldMappers[tsType];
|
|
91
|
+
if (!tsMapper) {
|
|
92
|
+
throw new Error(`[airtable-ts] No mappers for ts type ${tsType}`);
|
|
93
|
+
}
|
|
94
|
+
const specificMapper = tsMapper[airtableType]?.toAirtable;
|
|
95
|
+
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
|
+
}
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
item[fieldName] = specificMapper(value);
|
|
100
|
+
});
|
|
101
|
+
return Object.assign(item, { id: tsRecord.id });
|
|
102
|
+
};
|
|
103
|
+
const mapRecordFromAirtable = (table, record) => {
|
|
104
|
+
const tsTypes = (0, typeUtils_1.airtableFieldNameTsTypes)(table);
|
|
105
|
+
const tsRecord = mapRecordTypeAirtableToTs(tsTypes, record);
|
|
106
|
+
const mappedRecord = (0, nameMapper_1.mapRecordFieldNamesAirtableToTs)(table, tsRecord);
|
|
107
|
+
return mappedRecord;
|
|
108
|
+
};
|
|
109
|
+
exports.mapRecordFromAirtable = mapRecordFromAirtable;
|
|
110
|
+
const mapRecordToAirtable = (table, item, airtableTable) => {
|
|
111
|
+
const mappedItem = (0, nameMapper_1.mapRecordFieldNamesTsToAirtable)(table, item);
|
|
112
|
+
const tsTypes = (0, typeUtils_1.airtableFieldNameTsTypes)(table);
|
|
113
|
+
const fieldSet = mapRecordTypeTsToAirtable(tsTypes, mappedItem, airtableTable);
|
|
114
|
+
return fieldSet;
|
|
115
|
+
};
|
|
116
|
+
exports.mapRecordToAirtable = mapRecordToAirtable;
|
|
117
|
+
exports.visibleForTesting = {
|
|
118
|
+
mapRecordTypeAirtableToTs,
|
|
119
|
+
mapRecordTypeTsToAirtable,
|
|
120
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type TsTypeString = NonNullToString<any> | ToTsTypeString<any>;
|
|
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
|
+
export type ToTsTypeString<T> = null extends T ? `${NonNullToString<T>} | null` : NonNullToString<T>;
|
|
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);
|
|
7
|
+
/**
|
|
8
|
+
* Verifies whether the given value is assignable to the given type
|
|
9
|
+
*
|
|
10
|
+
* @param value
|
|
11
|
+
* @example [1, 2, 3]
|
|
12
|
+
*
|
|
13
|
+
* @param tsType
|
|
14
|
+
* @example 'number[]'
|
|
15
|
+
*
|
|
16
|
+
* @returns
|
|
17
|
+
* @example true
|
|
18
|
+
*/
|
|
19
|
+
export declare const matchesType: (value: unknown, tsType: TsTypeString) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Constructs a TypeScript object type definition given a table definition
|
|
22
|
+
*
|
|
23
|
+
* @param table Table definition
|
|
24
|
+
* @example {
|
|
25
|
+
* schema: { someProp: 'string', otherProps: 'number[]', another: 'boolean' },
|
|
26
|
+
* mappings: { someProp: 'Some_Airtable_Field', otherProps: ['Field1', 'Field2'], another: 'another' },
|
|
27
|
+
* ...
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* @returns The TypeScript object type we expect the Airtable record to coerce to
|
|
31
|
+
* @example {
|
|
32
|
+
* Some_Airtable_Field: 'string',
|
|
33
|
+
* Field1: 'number',
|
|
34
|
+
* Field2: 'number',
|
|
35
|
+
* another: 'boolean',
|
|
36
|
+
* }
|
|
37
|
+
*/
|
|
38
|
+
export declare const airtableFieldNameTsTypes: <T extends Item>(table: Table<T>) => Record<string, TsTypeString>;
|
|
39
|
+
export type MappingValue<T> = T extends unknown[] ? string | string[] : string;
|
|
40
|
+
export interface Item {
|
|
41
|
+
/** Represents the Airtable record id, @example "rec1234" */
|
|
42
|
+
id: string;
|
|
43
|
+
}
|
|
44
|
+
export interface Table<T extends Item> {
|
|
45
|
+
/** A simple name for the entities in this table, to be used in error messages @example "person" */
|
|
46
|
+
name: string;
|
|
47
|
+
/** The base id for this table. You can get this from the URL when accessing the table in the web UI. @example "app1234" */
|
|
48
|
+
baseId: string;
|
|
49
|
+
/** The table id for this table. You can get this from the URL when accessing the table in the web UI. @example "tbl1234" */
|
|
50
|
+
tableId: string;
|
|
51
|
+
/** The schema. We need to define this as a real object (rather than a type) because we do checks at run-time. You should usually be able to just take the autocomplete suggestions (provided you gave a TypeScript type already). */
|
|
52
|
+
schema: {
|
|
53
|
+
[k in keyof Omit<T, 'id'>]: ToTsTypeString<T[k]>;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Optional name mappings. This allows you to detach the schema names from the names you want to use in your code.
|
|
57
|
+
* @example
|
|
58
|
+
* export const personTable: Table<{ id: string, firstName: string }> = {
|
|
59
|
+
* name: 'person', baseId: 'app1234', tableId: 'tbl1234',
|
|
60
|
+
* schema: { firstName: 'string' },
|
|
61
|
+
* // The field is named '[core] First Name' in the base. If this ever changes, we just need to update it here.
|
|
62
|
+
* mappings: { firstName: '[core] First Name' },
|
|
63
|
+
* };
|
|
64
|
+
* const people = await db.scan(studentTable);
|
|
65
|
+
* const firstPersonsFirstName = people[0].firstName;
|
|
66
|
+
* */
|
|
67
|
+
mappings?: {
|
|
68
|
+
[k in keyof Omit<T, 'id'>]: MappingValue<T[k]>;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.airtableFieldNameTsTypes = exports.matchesType = void 0;
|
|
4
|
+
const parseType = (t) => {
|
|
5
|
+
if (t.endsWith('[] | null')) {
|
|
6
|
+
return {
|
|
7
|
+
single: t.slice(0, -('[] | null'.length)),
|
|
8
|
+
array: true,
|
|
9
|
+
nullable: true,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (t.endsWith('[]')) {
|
|
13
|
+
return {
|
|
14
|
+
single: t.slice(0, -('[]'.length)),
|
|
15
|
+
array: true,
|
|
16
|
+
nullable: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (t.endsWith(' | null')) {
|
|
20
|
+
return {
|
|
21
|
+
single: t.slice(0, -(' | null'.length)),
|
|
22
|
+
array: false,
|
|
23
|
+
nullable: true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
single: t,
|
|
28
|
+
array: false,
|
|
29
|
+
nullable: false,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Verifies whether the given value is assignable to the given type
|
|
34
|
+
*
|
|
35
|
+
* @param value
|
|
36
|
+
* @example [1, 2, 3]
|
|
37
|
+
*
|
|
38
|
+
* @param tsType
|
|
39
|
+
* @example 'number[]'
|
|
40
|
+
*
|
|
41
|
+
* @returns
|
|
42
|
+
* @example true
|
|
43
|
+
*/
|
|
44
|
+
const matchesType = (value, tsType) => {
|
|
45
|
+
const expectedType = parseType(tsType);
|
|
46
|
+
if (expectedType.nullable && value === null) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (!expectedType.array && typeof value === expectedType.single) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return (expectedType.array
|
|
53
|
+
&& Array.isArray(value)
|
|
54
|
+
&& value.every((entry) => typeof entry === expectedType.single));
|
|
55
|
+
};
|
|
56
|
+
exports.matchesType = matchesType;
|
|
57
|
+
/**
|
|
58
|
+
* Returns a single type for an array type
|
|
59
|
+
*
|
|
60
|
+
* @param tsType
|
|
61
|
+
* @example 'string[]'
|
|
62
|
+
*
|
|
63
|
+
* @returns
|
|
64
|
+
* @example 'string'
|
|
65
|
+
*/
|
|
66
|
+
const arrayToSingleType = (tsType) => {
|
|
67
|
+
if (tsType.endsWith('[] | null')) {
|
|
68
|
+
// This results in:
|
|
69
|
+
// string[] | null -> string | null
|
|
70
|
+
// Going the other way might not work - e.g. we'd get (string | null)[]
|
|
71
|
+
return `${tsType.slice(0, -'[] | null'.length)} | null`;
|
|
72
|
+
}
|
|
73
|
+
if (tsType.endsWith('[]')) {
|
|
74
|
+
return tsType.slice(0, -'[]'.length);
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`[airtable-ts] Not an array type: ${tsType}`);
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Constructs a TypeScript object type definition given a table definition
|
|
80
|
+
*
|
|
81
|
+
* @param table Table definition
|
|
82
|
+
* @example {
|
|
83
|
+
* schema: { someProp: 'string', otherProps: 'number[]', another: 'boolean' },
|
|
84
|
+
* mappings: { someProp: 'Some_Airtable_Field', otherProps: ['Field1', 'Field2'], another: 'another' },
|
|
85
|
+
* ...
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* @returns The TypeScript object type we expect the Airtable record to coerce to
|
|
89
|
+
* @example {
|
|
90
|
+
* Some_Airtable_Field: 'string',
|
|
91
|
+
* Field1: 'number',
|
|
92
|
+
* Field2: 'number',
|
|
93
|
+
* another: 'boolean',
|
|
94
|
+
* }
|
|
95
|
+
*/
|
|
96
|
+
const airtableFieldNameTsTypes = (table) => {
|
|
97
|
+
const schemaEntries = Object.entries(table.schema);
|
|
98
|
+
return Object.fromEntries(schemaEntries.map(([outputFieldName, tsType]) => {
|
|
99
|
+
const mappingToAirtable = table.mappings?.[outputFieldName];
|
|
100
|
+
if (!mappingToAirtable) {
|
|
101
|
+
return [[outputFieldName, tsType]];
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(mappingToAirtable)) {
|
|
104
|
+
return mappingToAirtable.map((airtableFieldName) => [airtableFieldName, arrayToSingleType(tsType)]);
|
|
105
|
+
}
|
|
106
|
+
return [[mappingToAirtable, tsType]];
|
|
107
|
+
}).flat(1));
|
|
108
|
+
};
|
|
109
|
+
exports.airtableFieldNameTsTypes = airtableFieldNameTsTypes;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FieldSet, Table as AirtableSdkTable, Record as AirtableSdkRecord, AirtableOptions } from 'airtable';
|
|
2
|
+
export type AirtableRecord = Omit<AirtableSdkRecord<FieldSet>, '_table'> & {
|
|
3
|
+
_table: AirtableTable;
|
|
4
|
+
};
|
|
5
|
+
export type AirtableTable = AirtableSdkTable<FieldSet> & {
|
|
6
|
+
fields: {
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
}[];
|
|
10
|
+
};
|
|
11
|
+
interface AirtableTsSpecificOptions {
|
|
12
|
+
/** The Airtable base schema is used to determine the appropriate type mapper for the field type (for example converting a number to a string representing a date is different to converting a number to a singleLineText). For performance reasons, airtable-ts caches base schemas so we don't refetch it for every request. Note that we always still do validation against the expected type at runtime so the library is always type-safe. @default 120_000 */
|
|
13
|
+
baseSchemaCacheDurationMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export type AirtableTsOptions = AirtableOptions & AirtableTsSpecificOptions;
|
|
16
|
+
export type CompleteAirtableTsOptions = AirtableTsOptions & Required<AirtableTsSpecificOptions>;
|
|
17
|
+
export {};
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "airtable-ts",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A type-safe Airtable SDK",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Adam Jones (domdomegg)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/domdomegg/airtable-ts.git"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest --watch",
|
|
19
|
+
"lint": "eslint --ext .js,.jsx,.ts,.tsx .",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"build": "tsc --project tsconfig.build.json",
|
|
22
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@tsconfig/node-lts": "^20.1.0",
|
|
26
|
+
"@tsconfig/strictest": "^2.0.2",
|
|
27
|
+
"eslint": "^8.56.0",
|
|
28
|
+
"eslint-config-domdomegg": "^1.2.3",
|
|
29
|
+
"typescript": "^5.3.3",
|
|
30
|
+
"vitest": "^1.0.4"
|
|
31
|
+
},
|
|
32
|
+
"eslintConfig": {
|
|
33
|
+
"extends": [
|
|
34
|
+
"eslint-config-domdomegg"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"airtable": "^0.12.2",
|
|
39
|
+
"axios": "^1.6.8"
|
|
40
|
+
}
|
|
41
|
+
}
|