airtable-ts-codegen 1.2.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 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
- /** 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;
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 = void 0;
4
- const recase_1 = require("@kristiandupont/recase");
3
+ exports.escapeIdentifier = escapeIdentifier;
5
4
  const diacritics_1 = require("diacritics");
6
- const toPascalCase = (0, recase_1.recase)(null, 'pascal');
7
- /** Used for identifiers. If the name has symbols or is illegal Typescript, strip out symbols and make it PascalCase. */
8
- // 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.
9
- const escapeIdentifier = (name) => {
10
- const preprocessed = (0, diacritics_1.remove)(name).trim();
11
- let isLegalIdentifier = true;
12
- if (!/^[$A-Z_a-z][\w$]*$/.test(preprocessed)) {
13
- isLegalIdentifier = false;
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
- // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new
17
- new Function(`const ${preprocessed} = 1;`);
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
- isLegalIdentifier = false;
25
+ return false;
21
26
  }
22
- if (isLegalIdentifier) {
23
- return preprocessed;
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 all characters up to the first valid identifier start character (A-Z, a-z, $), then
26
- // replace all invalid characters with underscores, and finally collapse multiple underscores into one.
27
- const validIdentifierStartIndex = preprocessed.search(/[$A-Za-z]/);
28
- if (validIdentifierStartIndex === -1) {
29
- throw new Error(`Invalid and unsalvageable identifier: ${name}`);
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
- const snaked = preprocessed
32
- .slice(validIdentifierStartIndex)
33
- .replace(/[^$A-Z_a-z\d]/g, '_')
34
- .replace(/_+/g, '_');
35
- const result = toPascalCase(snaked);
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
- return result;
40
- };
41
- exports.escapeIdentifier = escapeIdentifier;
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, "\\'").replace(/\n/g, '\\n');
5
+ const escapeString = (str) => str.replace(/'/g, '\\\'').replace(/\n/g, '\\n');
6
6
  exports.escapeString = escapeString;
@@ -1,4 +1,4 @@
1
- import { Config } from '.';
1
+ import { type Config } from '.';
2
2
  export type FieldSchema = {
3
3
  id: string;
4
4
  type: string;
@@ -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 interface Config {
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
- "import type { Item, Table } from 'airtable-ts';",
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 == null) {
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 == null) {
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 == null) {
34
+ if (jsType === null) {
35
35
  return null;
36
36
  }
37
37
  return `\n ${jsName}: '${(0, escapeString_1.escapeString)(jsType)}',`;
@@ -1,4 +1,4 @@
1
- import { FieldSchema } from './getBaseSchema';
1
+ import { type FieldSchema } from './getBaseSchema';
2
2
  /**
3
3
  * Returns the corresponding Typescript type for the given Airtable field type.
4
4
  *
@@ -50,10 +50,11 @@ const jsTypeForAirtableType = (field) => {
50
50
  if (field.options
51
51
  && 'result' in field.options
52
52
  && typeof field.options.result === 'object'
53
- && field.options.result != null) {
53
+ && field.options.result !== null) {
54
54
  const innerType = (0, exports.jsTypeForAirtableType)(field.options.result);
55
- if (innerType == null)
55
+ if (innerType === null) {
56
56
  return null;
57
+ }
57
58
  return `${innerType} | null`;
58
59
  }
59
60
  throw new Error(`Invalid ${field.type} field (no options.result): ${field.id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airtable-ts-codegen",
3
- "version": "1.2.0",
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 --ext .js,.jsx,.ts,.tsx .",
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"
@@ -35,24 +35,10 @@
35
35
  "@types/diacritics": "^1.3.3",
36
36
  "@types/node": "^20.12.8",
37
37
  "airtable-ts": "^1.4.0",
38
- "eslint": "^8.57.0",
39
- "eslint-config-domdomegg": "^1.2.3",
38
+ "eslint": "^9.19.0",
39
+ "eslint-config-domdomegg": "^2.0.8",
40
40
  "tsconfig-domdomegg": "^1.0.0",
41
- "typescript": "^5.4.4",
42
- "vitest": "^1.4.0"
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
  }