airtable-ts-codegen 2.0.0 → 2.1.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/README.md +10 -0
- package/dist/__fixture__/tablesMeta.json.d.ts +1 -0
- package/dist/__fixture__/tablesMeta.json.js +1 -0
- package/dist/cli.js +10 -2
- package/dist/escape/escapeIdentifier.d.ts +5 -0
- package/dist/escape/escapeIdentifier.js +20 -1
- package/dist/getBaseSchema.d.ts +7 -0
- package/dist/getBaseSchema.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +13 -7
- package/dist/view.d.ts +10 -0
- package/dist/view.js +73 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,16 @@ AIRTABLE_API_KEY=pat1234.abcd AIRTABLE_BASE_ID=app1234 npx airtable-ts-codegen
|
|
|
12
12
|
|
|
13
13
|
This will output a file `app1234.ts` that exports all the table definitions
|
|
14
14
|
|
|
15
|
+
### Generate from a specific view
|
|
16
|
+
|
|
17
|
+
You can also generate TypeScript definitions based on a specific view, which will only include the fields visible in those views:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
AIRTABLE_API_KEY=pat1234.abcd AIRTABLE_BASE_ID=app1234 AIRTABLE_VIEW_IDS=viw1234,viw5678 npx airtable-ts-codegen
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This will output a file `app1234.ts` that exports table definitions with only the fields visible in the specified views.
|
|
24
|
+
|
|
15
25
|
<details>
|
|
16
26
|
<summary>Example generated file</summary>
|
|
17
27
|
|
package/dist/cli.js
CHANGED
|
@@ -12,8 +12,16 @@ const baseId = process.env.AIRTABLE_BASE_ID;
|
|
|
12
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
|
-
|
|
16
|
-
|
|
15
|
+
const viewIds = process.env.AIRTABLE_VIEW_IDS;
|
|
16
|
+
const config = { apiKey, baseId, ...(viewIds && { viewIds: viewIds.split(',') }) };
|
|
17
|
+
const generateCode = async () => {
|
|
18
|
+
console.log(`Generating TypeScript definitions for base ${baseId}${viewIds ? ` with views ${viewIds}` : ''}...`);
|
|
19
|
+
return (0, _1.main)(config);
|
|
20
|
+
};
|
|
21
|
+
generateCode().then((result) => {
|
|
22
|
+
const filename = `${(0, escapeIdentifier_1.escapeIdentifier)(baseId)}.ts`;
|
|
23
|
+
(0, fs_1.writeFileSync)(filename, result);
|
|
24
|
+
console.log(`Generated ${filename}`);
|
|
17
25
|
}).catch((err) => {
|
|
18
26
|
console.error(err);
|
|
19
27
|
process.exit(1);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reset the internal state - useful for testing
|
|
3
|
+
*/
|
|
4
|
+
export declare function resetIdentifierState(): void;
|
|
1
5
|
/**
|
|
2
6
|
* Used for identifiers:
|
|
3
7
|
* - If the name is already a valid JS identifier, return it unmodified.
|
|
@@ -5,6 +9,7 @@
|
|
|
5
9
|
* - Remove invalid characters.
|
|
6
10
|
* - Convert to PascalCase.
|
|
7
11
|
* - If the result starts with a digit, prefix with `_`.
|
|
12
|
+
* - If duplicate, add a number suffix.
|
|
8
13
|
* - Returns a default identifier if the identifier cannot be salvaged.
|
|
9
14
|
*/
|
|
10
15
|
export declare function escapeIdentifier(name: string): string;
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resetIdentifierState = resetIdentifierState;
|
|
3
4
|
exports.escapeIdentifier = escapeIdentifier;
|
|
4
5
|
const diacritics_1 = require("diacritics");
|
|
5
6
|
let invalidIdentifierCount = 0;
|
|
6
7
|
const DEFAULT_IDENTIFIER = 'invalidIdentifier';
|
|
8
|
+
// Track used identifiers to avoid duplicates
|
|
9
|
+
const usedIdentifiers = new Set();
|
|
10
|
+
/**
|
|
11
|
+
* Reset the internal state - useful for testing
|
|
12
|
+
*/
|
|
13
|
+
function resetIdentifierState() {
|
|
14
|
+
usedIdentifiers.clear();
|
|
15
|
+
invalidIdentifierCount = 0;
|
|
16
|
+
}
|
|
7
17
|
/**
|
|
8
18
|
* Checks if 'str' is already a valid JavaScript identifier.
|
|
9
19
|
* If yes, returns true. Otherwise false.
|
|
@@ -46,6 +56,7 @@ function toPascalCase(str) {
|
|
|
46
56
|
* - Remove invalid characters.
|
|
47
57
|
* - Convert to PascalCase.
|
|
48
58
|
* - If the result starts with a digit, prefix with `_`.
|
|
59
|
+
* - If duplicate, add a number suffix.
|
|
49
60
|
* - Returns a default identifier if the identifier cannot be salvaged.
|
|
50
61
|
*/
|
|
51
62
|
function escapeIdentifier(name) {
|
|
@@ -92,5 +103,13 @@ function escapeIdentifier(name) {
|
|
|
92
103
|
return `${DEFAULT_IDENTIFIER}${invalidIdentifierCount}`;
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
|
-
|
|
106
|
+
// Handle duplicates by adding numbers
|
|
107
|
+
let finalIdentifier = pascal;
|
|
108
|
+
let counter = 2;
|
|
109
|
+
while (usedIdentifiers.has(finalIdentifier)) {
|
|
110
|
+
finalIdentifier = `${pascal}${counter}`;
|
|
111
|
+
counter += 1;
|
|
112
|
+
}
|
|
113
|
+
usedIdentifiers.add(finalIdentifier);
|
|
114
|
+
return finalIdentifier;
|
|
96
115
|
}
|
package/dist/getBaseSchema.d.ts
CHANGED
|
@@ -6,12 +6,19 @@ export type FieldSchema = {
|
|
|
6
6
|
description?: string;
|
|
7
7
|
options?: object;
|
|
8
8
|
};
|
|
9
|
+
export type ViewSchema = {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
type: 'grid' | 'form' | 'calendar' | 'gallery' | 'kanban' | 'timeline' | 'block';
|
|
13
|
+
visibleFieldIds?: string[];
|
|
14
|
+
};
|
|
9
15
|
export type BaseSchema = {
|
|
10
16
|
id: string;
|
|
11
17
|
name: string;
|
|
12
18
|
primaryFieldId?: string;
|
|
13
19
|
description?: string;
|
|
14
20
|
fields: FieldSchema[];
|
|
21
|
+
views: ViewSchema[];
|
|
15
22
|
}[];
|
|
16
23
|
/**
|
|
17
24
|
* Get the schemas from the cache or Airtable API for the tables in the given base.
|
package/dist/getBaseSchema.js
CHANGED
|
@@ -14,6 +14,9 @@ const getBaseSchema = async (baseId, options) => {
|
|
|
14
14
|
const res = await (0, axios_1.default)({
|
|
15
15
|
baseURL: options.endpointUrl ?? 'https://api.airtable.com',
|
|
16
16
|
url: `/v0/meta/bases/${baseId}/tables`,
|
|
17
|
+
params: {
|
|
18
|
+
include: ['visibleFieldIds'],
|
|
19
|
+
},
|
|
17
20
|
...(options.requestTimeout ? { timeout: options.requestTimeout } : {}),
|
|
18
21
|
headers: {
|
|
19
22
|
Authorization: `Bearer ${options.apiKey}`,
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -6,29 +6,33 @@ const getBaseSchema_1 = require("./getBaseSchema");
|
|
|
6
6
|
const escapeString_1 = require("./escape/escapeString");
|
|
7
7
|
const escapeIdentifier_1 = require("./escape/escapeIdentifier");
|
|
8
8
|
const jsTypeForAirtableType_1 = require("./jsTypeForAirtableType");
|
|
9
|
+
const view_1 = require("./view");
|
|
9
10
|
// This generates a single typescript file containing all table definitions for a given base.
|
|
10
11
|
const main = async (config) => {
|
|
11
12
|
const baseSchema = await (0, getBaseSchema_1.getBaseSchema)(config.baseId, config);
|
|
13
|
+
const filteredBaseSchema = await (0, view_1.filterBaseSchemaByView)(baseSchema, config);
|
|
12
14
|
return [
|
|
13
15
|
'/* DO NOT EDIT: this file was automatically generated by airtable-ts-codegen */',
|
|
14
16
|
'/* eslint-disable */',
|
|
15
17
|
'import type { Item, Table } from \'airtable-ts\';',
|
|
16
18
|
'',
|
|
17
|
-
|
|
19
|
+
filteredBaseSchema.map((tableSchema) => generateCode(config, tableSchema)).join('\n\n'),
|
|
18
20
|
].join('\n');
|
|
19
21
|
};
|
|
20
22
|
exports.main = main;
|
|
21
|
-
const generateInterfaceEntry = ({ jsName, jsType, name, type }) => {
|
|
23
|
+
const generateInterfaceEntry = ({ jsName, jsType, name, type, originalName }) => {
|
|
22
24
|
if (jsType === null) {
|
|
23
|
-
return `\n // Unsupported field ${name} of type ${type}`;
|
|
25
|
+
return `\n // Unsupported field "${name}" of type ${type}`;
|
|
24
26
|
}
|
|
25
|
-
|
|
27
|
+
const comment = originalName !== jsName ? ` // Original field: "${originalName}"` : '';
|
|
28
|
+
return `\n ${jsName}: ${jsType},${comment}`;
|
|
26
29
|
};
|
|
27
|
-
const generateMappingEntry = ({ jsName, id, jsType, name }) => {
|
|
30
|
+
const generateMappingEntry = ({ jsName, id, jsType, name, originalName }) => {
|
|
28
31
|
if (jsType === null) {
|
|
29
|
-
return `\n // Unsupported field ${name}: ${(0, escapeString_1.escapeString)(id)}`;
|
|
32
|
+
return `\n // Unsupported field "${name}": ${(0, escapeString_1.escapeString)(id)}`;
|
|
30
33
|
}
|
|
31
|
-
|
|
34
|
+
const comment = originalName !== jsName ? ` // Original field: "${originalName}"` : '';
|
|
35
|
+
return `\n ${jsName}: '${(0, escapeString_1.escapeString)(id)}',${comment}`;
|
|
32
36
|
};
|
|
33
37
|
const generateSchemaEntry = ({ jsName, jsType }) => {
|
|
34
38
|
if (jsType === null) {
|
|
@@ -37,11 +41,13 @@ const generateSchemaEntry = ({ jsName, jsType }) => {
|
|
|
37
41
|
return `\n ${jsName}: '${(0, escapeString_1.escapeString)(jsType)}',`;
|
|
38
42
|
};
|
|
39
43
|
const generateCode = (config, tableSchema) => {
|
|
44
|
+
(0, escapeIdentifier_1.resetIdentifierState)();
|
|
40
45
|
const itemNameRaw = (0, escapeIdentifier_1.escapeIdentifier)((0, recase_1.recase)(null, 'pascal', tableSchema.name));
|
|
41
46
|
const itemName = /.s$/.test(itemNameRaw) ? itemNameRaw.slice(0, itemNameRaw.length - 1) : itemNameRaw;
|
|
42
47
|
const tableName = (0, escapeIdentifier_1.escapeIdentifier)(`${(0, recase_1.recase)(null, 'camel', tableSchema.name)}Table`);
|
|
43
48
|
const fields = tableSchema.fields.map((f) => ({
|
|
44
49
|
...f,
|
|
50
|
+
originalName: f.name,
|
|
45
51
|
jsName: (0, escapeIdentifier_1.escapeIdentifier)((0, recase_1.recase)(null, 'camel', (0, escapeIdentifier_1.escapeIdentifier)(f.name))),
|
|
46
52
|
jsType: (0, jsTypeForAirtableType_1.jsTypeForAirtableType)(f),
|
|
47
53
|
}));
|
package/dist/view.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Config } from './index';
|
|
2
|
+
import { type BaseSchema } from './getBaseSchema';
|
|
3
|
+
/**
|
|
4
|
+
* Filter base schema based on view IDs.
|
|
5
|
+
* If viewIds are specified:
|
|
6
|
+
* - Only include tables that have at least one matching view
|
|
7
|
+
* - For grid views with visibleFieldIds, filter fields to only visible ones
|
|
8
|
+
* - Throw error if any view ID doesn't match any table
|
|
9
|
+
*/
|
|
10
|
+
export declare const filterBaseSchemaByView: (baseSchema: BaseSchema, config: Config) => Promise<BaseSchema>;
|
package/dist/view.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterBaseSchemaByView = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Find a view by ID in the base schema
|
|
6
|
+
*/
|
|
7
|
+
const findViewInBaseSchema = (baseSchema, viewId) => {
|
|
8
|
+
for (const table of baseSchema) {
|
|
9
|
+
const view = table.views.find((v) => v.id === viewId);
|
|
10
|
+
if (view) {
|
|
11
|
+
return { table, view };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Filter base schema based on view IDs.
|
|
18
|
+
* If viewIds are specified:
|
|
19
|
+
* - Only include tables that have at least one matching view
|
|
20
|
+
* - For grid views with visibleFieldIds, filter fields to only visible ones
|
|
21
|
+
* - Throw error if any view ID doesn't match any table
|
|
22
|
+
*/
|
|
23
|
+
const filterBaseSchemaByView = async (baseSchema, config) => {
|
|
24
|
+
// If no viewIds are provided, return the original schema
|
|
25
|
+
if (!config.viewIds) {
|
|
26
|
+
return baseSchema;
|
|
27
|
+
}
|
|
28
|
+
const matchedTableIds = new Set();
|
|
29
|
+
const viewToTableMap = new Map();
|
|
30
|
+
// First pass: find all views and their tables, validate all views exist
|
|
31
|
+
for (const viewId of config.viewIds) {
|
|
32
|
+
const result = findViewInBaseSchema(baseSchema, viewId);
|
|
33
|
+
if (!result) {
|
|
34
|
+
throw new Error(`View "${viewId}" not found in any table. Please check the view ID is correct.`);
|
|
35
|
+
}
|
|
36
|
+
viewToTableMap.set(viewId, result);
|
|
37
|
+
matchedTableIds.add(result.table.id);
|
|
38
|
+
}
|
|
39
|
+
// Second pass: filter tables and fields
|
|
40
|
+
const filteredTables = [];
|
|
41
|
+
for (const table of baseSchema) {
|
|
42
|
+
// Only include tables that have at least one matching view
|
|
43
|
+
if (!matchedTableIds.has(table.id)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Find all matching views for this table
|
|
47
|
+
const matchingViews = config.viewIds
|
|
48
|
+
.map((viewId) => viewToTableMap.get(viewId))
|
|
49
|
+
.filter((result) => result?.table.id === table.id)
|
|
50
|
+
.map((result) => result.view);
|
|
51
|
+
// Check if any of the matching views are grid views with visibleFieldIds
|
|
52
|
+
const gridViewsWithVisibleFields = matchingViews.filter((view) => view.type === 'grid' && view.visibleFieldIds && view.visibleFieldIds.length > 0);
|
|
53
|
+
let filteredTable = table;
|
|
54
|
+
// If we have grid views with visible field restrictions, apply field filtering
|
|
55
|
+
if (gridViewsWithVisibleFields.length > 0) {
|
|
56
|
+
// Collect all visible field IDs from all matching grid views
|
|
57
|
+
const allVisibleFieldIds = new Set();
|
|
58
|
+
for (const view of gridViewsWithVisibleFields) {
|
|
59
|
+
if (view.visibleFieldIds) {
|
|
60
|
+
view.visibleFieldIds.forEach((fieldId) => allVisibleFieldIds.add(fieldId));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Filter fields to only include those visible in at least one grid view
|
|
64
|
+
filteredTable = {
|
|
65
|
+
...table,
|
|
66
|
+
fields: table.fields.filter((field) => allVisibleFieldIds.has(field.id)),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
filteredTables.push(filteredTable);
|
|
70
|
+
}
|
|
71
|
+
return filteredTables;
|
|
72
|
+
};
|
|
73
|
+
exports.filterBaseSchemaByView = filterBaseSchemaByView;
|