airtable-ts-codegen 1.1.0 → 1.3.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/dist/cli.js +7 -3
- package/dist/escape/escapeIdentifier.d.ts +10 -2
- package/dist/escape/escapeIdentifier.js +85 -30
- package/dist/escape/escapeString.js +1 -1
- package/dist/getBaseSchema.d.ts +1 -1
- package/dist/getBaseSchema.js +0 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/jsTypeForAirtableType.d.ts +1 -1
- package/dist/jsTypeForAirtableType.js +13 -27
- package/package.json +7 -21
package/dist/cli.js
CHANGED
|
@@ -4,13 +4,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const _1 = require(".");
|
|
6
6
|
const escapeIdentifier_1 = require("./escape/escapeIdentifier");
|
|
7
|
-
/* eslint-disable no-console */
|
|
8
7
|
const apiKey = process.env.AIRTABLE_API_KEY;
|
|
9
|
-
if (!apiKey)
|
|
8
|
+
if (!apiKey) {
|
|
10
9
|
throw new Error('No Airtable API key set. Make sure the AIRTABLE_API_KEY environment variable is set.');
|
|
10
|
+
}
|
|
11
11
|
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
12
|
-
if (!baseId)
|
|
12
|
+
if (!baseId) {
|
|
13
13
|
throw new Error('No Airtable base id set. Make sure the AIRTABLE_BASE_ID environment variable is set.');
|
|
14
|
+
}
|
|
14
15
|
(0, _1.main)({ apiKey, baseId }).then((result) => {
|
|
15
16
|
(0, fs_1.writeFileSync)(`${(0, escapeIdentifier_1.escapeIdentifier)(baseId)}.ts`, result);
|
|
17
|
+
}).catch((err) => {
|
|
18
|
+
console.error(err);
|
|
19
|
+
process.exit(1);
|
|
16
20
|
});
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Used for identifiers:
|
|
3
|
+
* - If the name is already a valid JS identifier, return it unmodified.
|
|
4
|
+
* - Otherwise:
|
|
5
|
+
* - Remove invalid characters.
|
|
6
|
+
* - Convert to PascalCase.
|
|
7
|
+
* - If the result starts with a digit, prefix with `_`.
|
|
8
|
+
* - Returns a default identifier if the identifier cannot be salvaged.
|
|
9
|
+
*/
|
|
10
|
+
export declare function escapeIdentifier(name: string): string;
|
|
@@ -1,41 +1,96 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.escapeIdentifier =
|
|
4
|
-
const recase_1 = require("@kristiandupont/recase");
|
|
3
|
+
exports.escapeIdentifier = escapeIdentifier;
|
|
5
4
|
const diacritics_1 = require("diacritics");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
let invalidIdentifierCount = 0;
|
|
6
|
+
const DEFAULT_IDENTIFIER = 'invalidIdentifier';
|
|
7
|
+
/**
|
|
8
|
+
* Checks if 'str' is already a valid JavaScript identifier.
|
|
9
|
+
* If yes, returns true. Otherwise false.
|
|
10
|
+
*
|
|
11
|
+
* - Must conform to JavaScript identifier rules (e.g., no spaces, cannot start with a digit).
|
|
12
|
+
* - Also dynamically tests against reserved words and invalid usages by attempting declaration.
|
|
13
|
+
*/
|
|
14
|
+
function isValidJsIdentifier(str) {
|
|
15
|
+
if (!/^[$A-Z_a-z][\w$]*$/.test(str)) {
|
|
16
|
+
return false;
|
|
14
17
|
}
|
|
15
18
|
try {
|
|
16
|
-
//
|
|
17
|
-
|
|
19
|
+
// Test against reserved words and invalid declarations
|
|
20
|
+
// eslint-disable-next-line no-new, no-new-func
|
|
21
|
+
new Function(`const ${str} = 1;`);
|
|
22
|
+
return true;
|
|
18
23
|
}
|
|
19
24
|
catch {
|
|
20
|
-
|
|
25
|
+
return false;
|
|
21
26
|
}
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Converts a string to PascalCase:
|
|
30
|
+
* - Trims and splits by whitespace.
|
|
31
|
+
* - Capitalizes the first letter of each token.
|
|
32
|
+
* Example:
|
|
33
|
+
* "hello world" -> "HelloWorld"
|
|
34
|
+
* "123 abc" -> "123Abc"
|
|
35
|
+
*/
|
|
36
|
+
function toPascalCase(str) {
|
|
37
|
+
return str
|
|
38
|
+
.split(/\s+/)
|
|
39
|
+
.map((token) => (token ? token[0].toUpperCase() + token.slice(1).toLowerCase() : ''))
|
|
40
|
+
.join('');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Used for identifiers:
|
|
44
|
+
* - If the name is already a valid JS identifier, return it unmodified.
|
|
45
|
+
* - Otherwise:
|
|
46
|
+
* - Remove invalid characters.
|
|
47
|
+
* - Convert to PascalCase.
|
|
48
|
+
* - If the result starts with a digit, prefix with `_`.
|
|
49
|
+
* - Returns a default identifier if the identifier cannot be salvaged.
|
|
50
|
+
*/
|
|
51
|
+
function escapeIdentifier(name) {
|
|
52
|
+
// Normalize string by removing diacritics and trimming whitespace
|
|
53
|
+
const trimmed = (0, diacritics_1.remove)(name).trim();
|
|
54
|
+
// If already a valid identifier, return unchanged
|
|
55
|
+
if (isValidJsIdentifier(trimmed)) {
|
|
56
|
+
return trimmed;
|
|
24
57
|
}
|
|
25
|
-
// Remove
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
58
|
+
// Sanitize: Remove invalid characters, preserving letters, numbers, underscores, and spaces for token splitting
|
|
59
|
+
const sanitized = trimmed
|
|
60
|
+
.replace(/[^\p{L}\p{N}_\s]+/gu, ' ') // Replace special characters with spaces
|
|
61
|
+
.replace(/\s+/g, ' ') // Collapse multiple spaces
|
|
62
|
+
.trim();
|
|
63
|
+
// Return a default identifier if identifier is purely numeric after sanitization
|
|
64
|
+
if (/^\d+$/.test(sanitized)) {
|
|
65
|
+
invalidIdentifierCount += 1;
|
|
66
|
+
console.warn(`Invalid identifier "${name}" became purely numeric after sanitization ("${sanitized}"). Using default identifier "${DEFAULT_IDENTIFIER}${invalidIdentifierCount}".`);
|
|
67
|
+
return `${DEFAULT_IDENTIFIER}${invalidIdentifierCount}`;
|
|
30
68
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (result.length === 0) {
|
|
37
|
-
throw new Error(`Invalid and unsalvageable identifier: ${name}`);
|
|
69
|
+
// Convert sanitized string to PascalCase
|
|
70
|
+
let pascal = toPascalCase(sanitized);
|
|
71
|
+
// If it starts with a digit after conversion, prefix with an underscore
|
|
72
|
+
if (/^\d/.test(pascal)) {
|
|
73
|
+
pascal = `_${pascal}`;
|
|
38
74
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
75
|
+
// Final validation to ensure it conforms to JS identifier rules
|
|
76
|
+
if (!isValidJsIdentifier(pascal)) {
|
|
77
|
+
// Fallback: Strip all invalid characters, replace with underscores, and re-collapse
|
|
78
|
+
const validStartIndex = pascal.search(/[$A-Za-z]/);
|
|
79
|
+
if (validStartIndex === -1) {
|
|
80
|
+
invalidIdentifierCount += 1;
|
|
81
|
+
console.warn(`Invalid identifier "${name}" contains no valid starting character after sanitization. Using default identifier "${DEFAULT_IDENTIFIER}${invalidIdentifierCount}".`);
|
|
82
|
+
return `${DEFAULT_IDENTIFIER}${invalidIdentifierCount}`;
|
|
83
|
+
}
|
|
84
|
+
pascal = pascal
|
|
85
|
+
.slice(validStartIndex)
|
|
86
|
+
.replace(/[^A-Za-z0-9_$]/g, '_')
|
|
87
|
+
.replace(/_+/g, '_');
|
|
88
|
+
pascal = toPascalCase(pascal);
|
|
89
|
+
if (!isValidJsIdentifier(pascal) || pascal.length === 0) {
|
|
90
|
+
invalidIdentifierCount += 1;
|
|
91
|
+
console.warn(`Invalid identifier "${name}" could not be salvaged. Using default identifier "${DEFAULT_IDENTIFIER}${invalidIdentifierCount}".`);
|
|
92
|
+
return `${DEFAULT_IDENTIFIER}${invalidIdentifierCount}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return pascal;
|
|
96
|
+
}
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.escapeString = void 0;
|
|
4
4
|
/** Used for single-quoted strings. */
|
|
5
|
-
const escapeString = (str) => str.replace(/'/g,
|
|
5
|
+
const escapeString = (str) => str.replace(/'/g, '\\\'').replace(/\n/g, '\\n');
|
|
6
6
|
exports.escapeString = escapeString;
|
package/dist/getBaseSchema.d.ts
CHANGED
package/dist/getBaseSchema.js
CHANGED
|
@@ -16,7 +16,6 @@ const getBaseSchema = async (baseId, options) => {
|
|
|
16
16
|
url: `/v0/meta/bases/${baseId}/tables`,
|
|
17
17
|
...(options.requestTimeout ? { timeout: options.requestTimeout } : {}),
|
|
18
18
|
headers: {
|
|
19
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
20
19
|
Authorization: `Bearer ${options.apiKey}`,
|
|
21
20
|
...options.customHeaders,
|
|
22
21
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type Config = {
|
|
2
2
|
apiKey: string;
|
|
3
3
|
baseId: string;
|
|
4
4
|
endpointUrl?: string;
|
|
5
5
|
requestTimeout?: number;
|
|
6
6
|
customHeaders?: Record<string, string | number | boolean>;
|
|
7
|
-
}
|
|
7
|
+
};
|
|
8
8
|
export declare const main: (config: Config) => Promise<string>;
|
package/dist/index.js
CHANGED
|
@@ -12,26 +12,26 @@ const main = async (config) => {
|
|
|
12
12
|
return [
|
|
13
13
|
'/* DO NOT EDIT: this file was automatically generated by airtable-ts-codegen */',
|
|
14
14
|
'/* eslint-disable */',
|
|
15
|
-
|
|
15
|
+
'import type { Item, Table } from \'airtable-ts\';',
|
|
16
16
|
'',
|
|
17
17
|
baseSchema.map((tableSchema) => generateCode(config, tableSchema)).join('\n\n'),
|
|
18
18
|
].join('\n');
|
|
19
19
|
};
|
|
20
20
|
exports.main = main;
|
|
21
21
|
const generateInterfaceEntry = ({ jsName, jsType, name, type }) => {
|
|
22
|
-
if (jsType
|
|
22
|
+
if (jsType === null) {
|
|
23
23
|
return `\n // Unsupported field ${name} of type ${type}`;
|
|
24
24
|
}
|
|
25
25
|
return `\n ${jsName}: ${jsType},`;
|
|
26
26
|
};
|
|
27
27
|
const generateMappingEntry = ({ jsName, id, jsType, name }) => {
|
|
28
|
-
if (jsType
|
|
28
|
+
if (jsType === null) {
|
|
29
29
|
return `\n // Unsupported field ${name}: ${(0, escapeString_1.escapeString)(id)}`;
|
|
30
30
|
}
|
|
31
31
|
return `\n ${jsName}: '${(0, escapeString_1.escapeString)(id)}',`;
|
|
32
32
|
};
|
|
33
33
|
const generateSchemaEntry = ({ jsName, jsType }) => {
|
|
34
|
-
if (jsType
|
|
34
|
+
if (jsType === null) {
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
37
|
return `\n ${jsName}: '${(0, escapeString_1.escapeString)(jsType)}',`;
|
|
@@ -16,7 +16,15 @@ const jsTypeForAirtableType = (field) => {
|
|
|
16
16
|
case 'richText':
|
|
17
17
|
case 'singleSelect':
|
|
18
18
|
case 'externalSyncSource':
|
|
19
|
+
case 'aiText':
|
|
20
|
+
case 'singleCollaborator':
|
|
21
|
+
case 'createdBy':
|
|
22
|
+
case 'lastModifiedBy':
|
|
23
|
+
case 'barcode':
|
|
24
|
+
case 'button':
|
|
19
25
|
return 'string';
|
|
26
|
+
case 'multipleAttachments':
|
|
27
|
+
case 'multipleCollaborators':
|
|
20
28
|
case 'multipleRecordLinks':
|
|
21
29
|
case 'multipleSelects':
|
|
22
30
|
return 'string[]';
|
|
@@ -42,39 +50,17 @@ const jsTypeForAirtableType = (field) => {
|
|
|
42
50
|
if (field.options
|
|
43
51
|
&& 'result' in field.options
|
|
44
52
|
&& typeof field.options.result === 'object'
|
|
45
|
-
&& field.options.result
|
|
53
|
+
&& field.options.result !== null) {
|
|
46
54
|
const innerType = (0, exports.jsTypeForAirtableType)(field.options.result);
|
|
47
|
-
if (innerType
|
|
55
|
+
if (innerType === null) {
|
|
48
56
|
return null;
|
|
57
|
+
}
|
|
49
58
|
return `${innerType} | null`;
|
|
50
59
|
}
|
|
51
60
|
throw new Error(`Invalid ${field.type} field (no options.result): ${field.id}`);
|
|
52
|
-
// Special cases we don't yet support; for now, skip these fields
|
|
53
|
-
// case 'aiText':
|
|
54
|
-
// return 'AiTextObject';
|
|
55
|
-
// case 'singleCollaborator':
|
|
56
|
-
// case 'createdBy':
|
|
57
|
-
// case 'lastModifiedBy':
|
|
58
|
-
// return 'CollaboratorObject';
|
|
59
|
-
// case 'multipleCollaborators':
|
|
60
|
-
// return 'CollaboratorObject[]';
|
|
61
|
-
// case 'multipleAttachments':
|
|
62
|
-
// return 'AttachmentObject[]';
|
|
63
|
-
// case 'barcode':
|
|
64
|
-
// return 'BarcodeObject';
|
|
65
|
-
// case 'button':
|
|
66
|
-
// return 'ButtonObject';
|
|
67
|
-
case 'aiText':
|
|
68
|
-
case 'singleCollaborator':
|
|
69
|
-
case 'createdBy':
|
|
70
|
-
case 'lastModifiedBy':
|
|
71
|
-
case 'multipleCollaborators':
|
|
72
|
-
case 'multipleAttachments':
|
|
73
|
-
case 'barcode':
|
|
74
|
-
case 'button':
|
|
75
|
-
return null;
|
|
76
61
|
default:
|
|
77
|
-
|
|
62
|
+
console.warn(`Could not convert Airtable type '${field.type}' to a TypeScript type for field ${field.id}`);
|
|
63
|
+
return null;
|
|
78
64
|
}
|
|
79
65
|
};
|
|
80
66
|
exports.jsTypeForAirtableType = jsTypeForAirtableType;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "airtable-ts-codegen",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Autogenerate TypeScript definitions for your Airtable base",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Adam Jones (domdomegg)",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"start": "npm run build && node dist/cli.js",
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"test:watch": "vitest --watch",
|
|
23
|
-
"lint": "eslint
|
|
23
|
+
"lint": "eslint",
|
|
24
24
|
"clean": "rm -rf dist",
|
|
25
25
|
"build": "tsc --project tsconfig.build.json",
|
|
26
26
|
"prepublishOnly": "npm run clean && npm run build"
|
|
@@ -34,25 +34,11 @@
|
|
|
34
34
|
"@tsconfig/node-lts": "^20.1.3",
|
|
35
35
|
"@types/diacritics": "^1.3.3",
|
|
36
36
|
"@types/node": "^20.12.8",
|
|
37
|
-
"airtable-ts": "^1.
|
|
38
|
-
"eslint": "^
|
|
39
|
-
"eslint-config-domdomegg": "^
|
|
37
|
+
"airtable-ts": "^1.4.0",
|
|
38
|
+
"eslint": "^9.19.0",
|
|
39
|
+
"eslint-config-domdomegg": "^2.0.8",
|
|
40
40
|
"tsconfig-domdomegg": "^1.0.0",
|
|
41
|
-
"typescript": "^5.
|
|
42
|
-
"vitest": "^
|
|
43
|
-
},
|
|
44
|
-
"eslintConfig": {
|
|
45
|
-
"extends": [
|
|
46
|
-
"eslint-config-domdomegg"
|
|
47
|
-
],
|
|
48
|
-
"rules": {
|
|
49
|
-
"object-curly-newline": [
|
|
50
|
-
"error",
|
|
51
|
-
{
|
|
52
|
-
"multiline": true,
|
|
53
|
-
"consistent": true
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
}
|
|
41
|
+
"typescript": "^5.7.3",
|
|
42
|
+
"vitest": "^3.0.7"
|
|
57
43
|
}
|
|
58
44
|
}
|