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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,2 @@
1
+ /** Used for identifiers. If the name has symbols or is illegal Typescript, strip out symbols and make it PascalCase. */
2
+ export declare const escapeIdentifier: (name: string) => string;
@@ -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,2 @@
1
+ /** Used for single-quoted strings. */
2
+ export declare const escapeString: (str: string) => string;
@@ -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;
@@ -0,0 +1,8 @@
1
+ export interface Config {
2
+ apiKey: string;
3
+ baseId: string;
4
+ endpointUrl?: string;
5
+ requestTimeout?: number;
6
+ customHeaders?: Record<string, string | number | boolean>;
7
+ }
8
+ export declare const main: (config: Config) => Promise<string>;
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,2 @@
1
+ import { FieldSchema } from './getBaseSchema';
2
+ export declare const jsTypeForAirtableType: (field: FieldSchema) => string;
@@ -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
+ }