-
Notifications
You must be signed in to change notification settings - Fork 0
fix: Handle various table and attribute names better #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dawsontoth
wants to merge
2
commits into
main
Choose a base branch
from
number-name-handling
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /** | ||
| * Escapes a string so it can be safely embedded inside a single-quoted | ||
| * TypeScript/JavaScript string literal. Backslashes are escaped before quotes | ||
| * so that the backslashes added when escaping quotes are not re-escaped. | ||
| * e.g. o'connor → o\'connor, a\b → a\\b | ||
| * @param {string} value | ||
| * @returns {string} | ||
| */ | ||
| export function escapeSingleQuoted(value) { | ||
| return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { escapeSingleQuoted } from './escapeSingleQuoted.js'; | ||
|
|
||
| describe('escapeSingleQuoted', () => { | ||
| it('should leave a plain string unchanged', () => { | ||
| expect(escapeSingleQuoted('connor')).toBe('connor'); | ||
| }); | ||
|
|
||
| it('should escape single quotes', () => { | ||
| expect(escapeSingleQuoted("o'connor")).toBe("o\\'connor"); | ||
| }); | ||
|
|
||
| it('should escape backslashes', () => { | ||
| expect(escapeSingleQuoted('a\\b')).toBe('a\\\\b'); | ||
| }); | ||
|
|
||
| it('should escape backslashes before quotes so escapes are not doubled', () => { | ||
| // input: a ' b \ c -> a \' b \\ c | ||
| expect(escapeSingleQuoted("a'b\\c")).toBe("a\\'b\\\\c"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import fs from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { afterEach, beforeEach, describe, expect, it } from 'vitest'; | ||
| import { generateTablesDTS } from './generateTablesDTS.js'; | ||
|
|
||
| describe('generateTablesDTS', () => { | ||
| /** @type {string} */ | ||
| let tmpDir; | ||
| /** @type {string} */ | ||
| let globalTypesPath; | ||
| /** @type {string} */ | ||
| let schemaTypesPath; | ||
|
|
||
| beforeEach(() => { | ||
| tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegen-dts-')); | ||
| globalTypesPath = path.join(tmpDir, 'global.d.ts'); | ||
| schemaTypesPath = path.join(tmpDir, 'schemas', 'types.ts'); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| fs.rmSync(tmpDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| /** @param {import('./tableMeta.js').TableMeta[]} tables */ | ||
| function generate(tables) { | ||
| generateTablesDTS(globalTypesPath, schemaTypesPath, tables); | ||
| return fs.readFileSync(globalTypesPath, 'utf8'); | ||
| } | ||
|
|
||
| it('should import and reference the singular type for each table', () => { | ||
| const content = generate([{ plural: 'Users', singular: 'User', databaseName: 'data' }]); | ||
| expect(content).toContain('import type { User } from'); | ||
| expect(content).toContain('Users: { new(...args: any[]): Table<User> };'); | ||
| }); | ||
|
|
||
| it('should quote a runtime key that starts with a digit', () => { | ||
| const content = generate([{ plural: '123_New4', singular: '_123_New4', databaseName: 'data' }]); | ||
| // the runtime key must be quoted, and the type reference must be a valid identifier | ||
| expect(content).toContain("'123_New4': { new(...args: any[]): Table<_123_New4> };"); | ||
| expect(content).toContain('import type { _123_New4 } from'); | ||
| // an unquoted leading-digit key would be a TypeScript syntax error | ||
| expect(content).not.toMatch(/\n\t+123_New4:/); | ||
| }); | ||
|
|
||
| it('should quote keys with non-word characters but leave valid ones unquoted', () => { | ||
| const content = generate([ | ||
| { plural: 'Users', singular: 'User', databaseName: 'data' }, | ||
| { plural: 'audit-logs', singular: 'auditLog', databaseName: 'data' }, | ||
| ]); | ||
| expect(content).toContain('Users: {'); | ||
| expect(content).toContain("'audit-logs': {"); | ||
| }); | ||
|
|
||
| it('should quote a database name that starts with a digit', () => { | ||
| const content = generate([ | ||
| { plural: '9lives_Cats', singular: '_9lives_Cat', databaseName: '9lives' }, | ||
| ]); | ||
| expect(content).toContain("'9lives': {"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { escapeSingleQuoted } from './escapeSingleQuoted.js'; | ||
|
|
||
| /** | ||
| * Wraps a property name in quotes unless it is already a valid unquoted | ||
| * TypeScript identifier. This covers names containing characters that are not | ||
| * valid in an identifier (e.g. dashes or dots) as well as names that start with | ||
| * a digit (e.g. "123_New4"). Valid identifiers are returned unchanged. Quotes | ||
| * and backslashes within the name are escaped so the result is always valid. | ||
| * | ||
| * Use this for object keys and interface property names, which may be quoted. | ||
| * It is not suitable for type names (interfaces, type aliases), which must be | ||
| * real identifiers — use {@link toIdentifier} for those. | ||
| * @param {string} name | ||
| * @returns {string} | ||
| */ | ||
| export function safeKey(name) { | ||
| return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${escapeSingleQuoted(name)}'`; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { safeKey } from './safeKey.js'; | ||
|
|
||
| describe('safeKey', () => { | ||
| it('should leave valid identifiers unquoted', () => { | ||
| expect(safeKey('id')).toBe('id'); | ||
| expect(safeKey('userName')).toBe('userName'); | ||
| expect(safeKey('__createdtime__')).toBe('__createdtime__'); | ||
| expect(safeKey('$ref')).toBe('$ref'); | ||
| }); | ||
|
|
||
| it('should quote names containing characters invalid in an identifier', () => { | ||
| expect(safeKey('first-name')).toBe("'first-name'"); | ||
| expect(safeKey('with space')).toBe("'with space'"); | ||
| expect(safeKey('a.b')).toBe("'a.b'"); | ||
| }); | ||
|
|
||
| it('should quote names that start with a digit', () => { | ||
| expect(safeKey('123field')).toBe("'123field'"); | ||
| expect(safeKey('123_New4')).toBe("'123_New4'"); | ||
| }); | ||
|
|
||
| it('should escape single quotes and backslashes within a quoted name', () => { | ||
| expect(safeKey("o'connor")).toBe("'o\\'connor'"); | ||
| expect(safeKey('a\\b')).toBe("'a\\\\b'"); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,25 @@ | ||
| /** | ||
| * Converts a name that may contain dashes into a valid TypeScript identifier | ||
| * by converting kebab-case to camelCase. | ||
| * e.g. "blog-posts" → "blogPosts", "my-table-name" → "myTableName" | ||
| * Names without dashes are returned unchanged. | ||
| * Converts an arbitrary table name into a valid TypeScript identifier. | ||
| * | ||
| * - kebab-case is converted to camelCase: "blog-posts" → "blogPosts" | ||
| * - any remaining characters that are not valid in an identifier are replaced | ||
| * with underscores: "my.table name" → "my_table_name" | ||
| * - a leading digit is prefixed with an underscore, since an identifier cannot | ||
| * start with a number: "123_New4" → "_123_New4" | ||
| * | ||
| * Names that are already valid identifiers are returned unchanged. | ||
| * @param {string} name | ||
| * @returns {string} | ||
| */ | ||
| export function toIdentifier(name) { | ||
| return name.replace(/-([a-zA-Z0-9])/g, (_, c) => c.toUpperCase()); | ||
| let identifier = name | ||
| // kebab-case → camelCase, e.g. "blog-posts" → "blogPosts" | ||
| .replace(/-([a-zA-Z0-9])/g, (_, c) => c.toUpperCase()) | ||
| // replace any remaining characters that are invalid in an identifier | ||
| .replace(/[^a-zA-Z0-9_$]/g, '_'); | ||
| // an identifier cannot start with a digit | ||
| if (/^[0-9]/.test(identifier)) { | ||
| identifier = `_${identifier}`; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That'll work! |
||
| } | ||
| return identifier; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { toIdentifier } from './toIdentifier.js'; | ||
|
|
||
| describe('toIdentifier', () => { | ||
| it('should leave a valid identifier unchanged', () => { | ||
| expect(toIdentifier('Users')).toBe('Users'); | ||
| expect(toIdentifier('blog_Posts')).toBe('blog_Posts'); | ||
| expect(toIdentifier('$special')).toBe('$special'); | ||
| }); | ||
|
|
||
| it('should convert kebab-case to camelCase', () => { | ||
| expect(toIdentifier('blog-posts')).toBe('blogPosts'); | ||
| expect(toIdentifier('my-table-name')).toBe('myTableName'); | ||
| }); | ||
|
|
||
| it('should prefix a leading digit with an underscore', () => { | ||
| expect(toIdentifier('123_New4')).toBe('_123_New4'); | ||
| expect(toIdentifier('4chan')).toBe('_4chan'); | ||
| }); | ||
|
|
||
| it('should replace characters that are invalid in an identifier', () => { | ||
| expect(toIdentifier('my.table')).toBe('my_table'); | ||
| expect(toIdentifier('table name')).toBe('table_name'); | ||
| expect(toIdentifier('weird@name!')).toBe('weird_name_'); | ||
| }); | ||
|
|
||
| it('should always produce a valid identifier', () => { | ||
| const names = ['123_New4', 'blog-posts', 'my.table', 'table name', '99 bottles']; | ||
| for (const name of names) { | ||
| expect(toIdentifier(name)).toMatch(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/); | ||
| } | ||
| }); | ||
| }); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.