airtable-ts-codegen 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 +82 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +16 -0
- package/dist/escape/escapeIdentifier.d.ts +2 -0
- package/dist/escape/escapeIdentifier.js +31 -0
- package/dist/escape/escapeString.d.ts +2 -0
- package/dist/escape/escapeString.js +6 -0
- package/dist/getBaseSchema.d.ts +20 -0
- package/dist/getBaseSchema.js +26 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +39 -0
- package/dist/jsTypeForAirtableType.d.ts +2 -0
- package/dist/jsTypeForAirtableType.js +63 -0
- package/package.json +46 -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,82 @@
|
|
|
1
|
+
# airtable-ts-codegen
|
|
2
|
+
|
|
3
|
+
Autogenerate TypeScript definitions for your Airtable base
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Run with:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
AIRTABLE_API_KEY=pat1234.abcd AIRTABLE_BASE_ID=app1234 npx airtable-ts-codegen
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This will output a file `app1234.ts` that exports all the table definitions
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary>Example generated file</summary>
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
/* DO NOT EDIT: this file was automatically generated by airtable-ts-codegen */
|
|
20
|
+
/* eslint-disable */
|
|
21
|
+
import { Item, Table } from 'airtable-ts';
|
|
22
|
+
|
|
23
|
+
export interface Task extends Item {
|
|
24
|
+
id: string,
|
|
25
|
+
name: string,
|
|
26
|
+
status: string,
|
|
27
|
+
dueAt: number,
|
|
28
|
+
isOptional: boolean,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const tasksTable: Table<Task> = {
|
|
32
|
+
name: 'Tasks',
|
|
33
|
+
baseId: 'app1234',
|
|
34
|
+
tableId: 'tbl5678',
|
|
35
|
+
mappings: {
|
|
36
|
+
name: 'fld9012',
|
|
37
|
+
status: 'fld3456',
|
|
38
|
+
dueAt: 'fld7890',
|
|
39
|
+
isOptional: 'fld1234',
|
|
40
|
+
},
|
|
41
|
+
schema: {
|
|
42
|
+
name: 'string',
|
|
43
|
+
status: 'string',
|
|
44
|
+
dueAt: 'number',
|
|
45
|
+
isOptional: 'boolean',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
</details>
|
|
51
|
+
|
|
52
|
+
You can then easily use this with [airtable-ts](https://github.com/domdomegg/airtable-ts), for example:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { AirtableTs } from 'airtable-ts';
|
|
56
|
+
import { tasksTable } from './generated/app1234';
|
|
57
|
+
|
|
58
|
+
const db = new AirtableTs({ apiKey: 'pat1234.abcdef' });
|
|
59
|
+
const allTasks = await db.scan(tasksTable);
|
|
60
|
+
|
|
61
|
+
// You now have all the benefits of airtable-ts, without having to define schemas manually 🎉
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Contributing
|
|
65
|
+
|
|
66
|
+
Pull requests are welcomed on GitHub! To get started:
|
|
67
|
+
|
|
68
|
+
1. Install Git and Node.js
|
|
69
|
+
2. Clone the repository
|
|
70
|
+
3. Install dependencies with `npm install`
|
|
71
|
+
4. Run `npm run test` to run tests
|
|
72
|
+
5. Build with `npm run build`
|
|
73
|
+
|
|
74
|
+
## Releases
|
|
75
|
+
|
|
76
|
+
Versions follow the [semantic versioning spec](https://semver.org/).
|
|
77
|
+
|
|
78
|
+
To release:
|
|
79
|
+
|
|
80
|
+
1. Use `npm version <major | minor | patch>` to bump the version
|
|
81
|
+
2. Run `git push --follow-tags` to push with tags
|
|
82
|
+
3. Wait for GitHub Actions to publish to the NPM registry.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const _1 = require(".");
|
|
6
|
+
const escapeIdentifier_1 = require("./escape/escapeIdentifier");
|
|
7
|
+
/* eslint-disable no-console */
|
|
8
|
+
const apiKey = process.env.AIRTABLE_API_KEY;
|
|
9
|
+
if (!apiKey)
|
|
10
|
+
throw new Error('No Airtable API key set. Make sure the AIRTABLE_API_KEY environment variable is set.');
|
|
11
|
+
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
12
|
+
if (!baseId)
|
|
13
|
+
throw new Error('No Airtable base id set. Make sure the AIRTABLE_BASE_ID environment variable is set.');
|
|
14
|
+
(0, _1.main)({ apiKey, baseId }).then((result) => {
|
|
15
|
+
(0, fs_1.writeFileSync)(`${(0, escapeIdentifier_1.escapeIdentifier)(baseId)}.ts`, result);
|
|
16
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.escapeIdentifier = void 0;
|
|
4
|
+
const recase_1 = require("@kristiandupont/recase");
|
|
5
|
+
const toPascalCase = (0, recase_1.recase)(null, 'pascal');
|
|
6
|
+
/** Used for identifiers. If the name has symbols or is illegal Typescript, strip out symbols and make it PascalCase. */
|
|
7
|
+
// NB: A wider set of things than are accepted by this function are valid identifiers in TypeScript (see https://stackoverflow.com/a/9337047). However, this works well for our purposes.
|
|
8
|
+
const escapeIdentifier = (name) => {
|
|
9
|
+
const trimmed = name.trim();
|
|
10
|
+
let isLegalIdentifier = true;
|
|
11
|
+
if (!/^[$A-Z_a-z][\w$]*$/.test(trimmed)) {
|
|
12
|
+
isLegalIdentifier = false;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new
|
|
16
|
+
new Function(`const ${trimmed} = 1;`);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
isLegalIdentifier = false;
|
|
20
|
+
}
|
|
21
|
+
if (isLegalIdentifier) {
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
const snaked = trimmed.replace(/[^$A-Z_a-z]/g, '_').replace(/_+/g, '_');
|
|
25
|
+
const result = toPascalCase(snaked);
|
|
26
|
+
if (result.length === 0) {
|
|
27
|
+
throw new Error(`Invalid and unsalvageable identifier: ${name}`);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
exports.escapeIdentifier = escapeIdentifier;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.escapeString = void 0;
|
|
4
|
+
/** Used for single-quoted strings. */
|
|
5
|
+
const escapeString = (str) => str.replace(/'/g, "\\'").replace(/\n/g, '\\n');
|
|
6
|
+
exports.escapeString = escapeString;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Config } from '.';
|
|
2
|
+
export type FieldSchema = {
|
|
3
|
+
id: string;
|
|
4
|
+
type: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
options?: object;
|
|
8
|
+
};
|
|
9
|
+
export type BaseSchema = {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
fields: FieldSchema[];
|
|
14
|
+
}[];
|
|
15
|
+
/**
|
|
16
|
+
* Get the schemas from the cache or Airtable API for the tables in the given base.
|
|
17
|
+
* @see https://airtable.com/developers/web/api/get-base-schema
|
|
18
|
+
* @param baseId The base id to get the schemas for
|
|
19
|
+
*/
|
|
20
|
+
export declare const getBaseSchema: (baseId: string, options: Config) => Promise<BaseSchema>;
|
|
@@ -0,0 +1,26 @@
|
|
|
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.getBaseSchema = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
/**
|
|
9
|
+
* Get the schemas from the cache or Airtable API for the tables in the given base.
|
|
10
|
+
* @see https://airtable.com/developers/web/api/get-base-schema
|
|
11
|
+
* @param baseId The base id to get the schemas for
|
|
12
|
+
*/
|
|
13
|
+
const getBaseSchema = async (baseId, options) => {
|
|
14
|
+
const res = await (0, axios_1.default)({
|
|
15
|
+
baseURL: options.endpointUrl ?? 'https://api.airtable.com',
|
|
16
|
+
url: `/v0/meta/bases/${baseId}/tables`,
|
|
17
|
+
...(options.requestTimeout ? { timeout: options.requestTimeout } : {}),
|
|
18
|
+
headers: {
|
|
19
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
20
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
21
|
+
...options.customHeaders,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
return res.data.tables;
|
|
25
|
+
};
|
|
26
|
+
exports.getBaseSchema = getBaseSchema;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.main = void 0;
|
|
4
|
+
const recase_1 = require("@kristiandupont/recase");
|
|
5
|
+
const getBaseSchema_1 = require("./getBaseSchema");
|
|
6
|
+
const escapeString_1 = require("./escape/escapeString");
|
|
7
|
+
const escapeIdentifier_1 = require("./escape/escapeIdentifier");
|
|
8
|
+
const jsTypeForAirtableType_1 = require("./jsTypeForAirtableType");
|
|
9
|
+
// This will output a folder named `app1234` with:
|
|
10
|
+
// - an `index.ts` that exports all the tables
|
|
11
|
+
// - (under the hood?) various `tbl1234.ts` files that contain table definitions
|
|
12
|
+
const main = async (config) => {
|
|
13
|
+
const baseSchema = await (0, getBaseSchema_1.getBaseSchema)(config.baseId, config);
|
|
14
|
+
return `/* DO NOT EDIT: this file was automatically generated by airtable-ts-codegen */\n/* eslint-disable */\nimport { Item, Table } from 'airtable-ts';\n\n${baseSchema.map((tableSchema) => generateCode(config, tableSchema)).join('\n\n')}`;
|
|
15
|
+
};
|
|
16
|
+
exports.main = main;
|
|
17
|
+
const generateCode = (config, tableSchema) => {
|
|
18
|
+
const itemNameRaw = (0, escapeIdentifier_1.escapeIdentifier)((0, recase_1.recase)(null, 'pascal', tableSchema.name));
|
|
19
|
+
const itemName = /.s$/.test(itemNameRaw) ? itemNameRaw.slice(0, itemNameRaw.length - 1) : itemNameRaw;
|
|
20
|
+
const tableName = (0, escapeIdentifier_1.escapeIdentifier)(`${(0, recase_1.recase)(null, 'camel', tableSchema.name)}Table`);
|
|
21
|
+
const fields = tableSchema.fields.map((f) => ({
|
|
22
|
+
...f,
|
|
23
|
+
jsName: (0, escapeIdentifier_1.escapeIdentifier)((0, recase_1.recase)(null, 'camel', (0, escapeIdentifier_1.escapeIdentifier)(f.name))),
|
|
24
|
+
jsType: (0, jsTypeForAirtableType_1.jsTypeForAirtableType)(f),
|
|
25
|
+
}));
|
|
26
|
+
return `export interface ${itemName} extends Item {
|
|
27
|
+
id: string,${fields.map((f) => `\n ${f.jsName}: ${f.jsType},`).join('')}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ${tableName}: Table<${itemName}> = {
|
|
31
|
+
name: '${(0, escapeString_1.escapeString)(tableSchema.name)}',
|
|
32
|
+
baseId: '${(0, escapeString_1.escapeString)(config.baseId)}',
|
|
33
|
+
tableId: '${(0, escapeString_1.escapeString)(tableSchema.id)}',
|
|
34
|
+
mappings: {${fields.map((f) => `\n ${f.jsName}: '${(0, escapeString_1.escapeString)(f.id)}',`).join('')}
|
|
35
|
+
},
|
|
36
|
+
schema: {${fields.map((f) => `\n ${f.jsName}: '${(0, escapeString_1.escapeString)(f.jsType)}',`).join('')}
|
|
37
|
+
},
|
|
38
|
+
};`;
|
|
39
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.jsTypeForAirtableType = void 0;
|
|
4
|
+
const jsTypeForAirtableType = (field) => {
|
|
5
|
+
switch (field.type) {
|
|
6
|
+
case 'url':
|
|
7
|
+
case 'email':
|
|
8
|
+
case 'phoneNumber':
|
|
9
|
+
case 'singleLineText':
|
|
10
|
+
case 'multilineText':
|
|
11
|
+
case 'richText':
|
|
12
|
+
case 'singleSelect':
|
|
13
|
+
case 'externalSyncSource':
|
|
14
|
+
return 'string';
|
|
15
|
+
case 'multipleRecordLinks':
|
|
16
|
+
case 'multipleSelects':
|
|
17
|
+
return 'string[]';
|
|
18
|
+
case 'number':
|
|
19
|
+
case 'rating':
|
|
20
|
+
case 'duration':
|
|
21
|
+
case 'currency':
|
|
22
|
+
case 'percent':
|
|
23
|
+
case 'count':
|
|
24
|
+
case 'autoNumber':
|
|
25
|
+
return 'number';
|
|
26
|
+
case 'date':
|
|
27
|
+
case 'dateTime':
|
|
28
|
+
case 'createdTime':
|
|
29
|
+
case 'lastModifiedTime':
|
|
30
|
+
return 'number'; // Unix timestamp in seconds
|
|
31
|
+
case 'checkbox':
|
|
32
|
+
return 'boolean';
|
|
33
|
+
case 'lookup':
|
|
34
|
+
case 'multipleLookupValues':
|
|
35
|
+
case 'rollup':
|
|
36
|
+
case 'formula':
|
|
37
|
+
if (field.options
|
|
38
|
+
&& 'result' in field.options
|
|
39
|
+
&& typeof field.options.result === 'object'
|
|
40
|
+
&& field.options.result != null) {
|
|
41
|
+
return `${(0, exports.jsTypeForAirtableType)(field.options.result)} | null`;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Invalid ${field.type} field (no options.result): ${field.id}`);
|
|
44
|
+
// Special cases we don't yet support
|
|
45
|
+
case 'aiText':
|
|
46
|
+
return 'AiTextObject';
|
|
47
|
+
case 'singleCollaborator':
|
|
48
|
+
case 'createdBy':
|
|
49
|
+
case 'lastModifiedBy':
|
|
50
|
+
return 'CollaboratorObject';
|
|
51
|
+
case 'multipleCollaborators':
|
|
52
|
+
return 'CollaboratorObject[]';
|
|
53
|
+
case 'multipleAttachments':
|
|
54
|
+
return 'AttachmentObject[]';
|
|
55
|
+
case 'barcode':
|
|
56
|
+
return 'BarcodeObject';
|
|
57
|
+
case 'button':
|
|
58
|
+
return 'ButtonObject';
|
|
59
|
+
default:
|
|
60
|
+
throw new Error(`Could not convert Airtable type '${field.type}' to a TypeScript type for field ${field.id}`);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
exports.jsTypeForAirtableType = jsTypeForAirtableType;
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "airtable-ts-codegen",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Autogenerate TypeScript definitions for your Airtable base",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Adam Jones (domdomegg)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/domdomegg/airtable-ts-codegen.git"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"bin": {
|
|
14
|
+
"airtable-ts-codegen": "dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest --watch",
|
|
22
|
+
"lint": "eslint --ext .js,.jsx,.ts,.tsx .",
|
|
23
|
+
"clean": "rm -rf dist",
|
|
24
|
+
"build": "tsc --project tsconfig.build.json",
|
|
25
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@kristiandupont/recase": "^1.2.1",
|
|
29
|
+
"axios": "^1.6.8"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@tsconfig/node-lts": "^20.1.3",
|
|
33
|
+
"@types/node": "^20.12.8",
|
|
34
|
+
"airtable-ts": "^1.1.0",
|
|
35
|
+
"eslint": "^8.57.0",
|
|
36
|
+
"eslint-config-domdomegg": "^1.2.3",
|
|
37
|
+
"tsconfig-domdomegg": "^1.0.0",
|
|
38
|
+
"typescript": "^5.4.4",
|
|
39
|
+
"vitest": "^1.4.0"
|
|
40
|
+
},
|
|
41
|
+
"eslintConfig": {
|
|
42
|
+
"extends": [
|
|
43
|
+
"eslint-config-domdomegg"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|