From 2ef5f0db8ec69b2bfe429201767ab2d3f5926023 Mon Sep 17 00:00:00 2001 From: Chris Barber Date: Mon, 18 May 2026 11:54:00 -0500 Subject: [PATCH 1/2] feat: use quotes for db and table names that contain non-alphanumeric chars --- utils/generateTablesDTS.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/utils/generateTablesDTS.js b/utils/generateTablesDTS.js index ec39430..0726079 100644 --- a/utils/generateTablesDTS.js +++ b/utils/generateTablesDTS.js @@ -8,6 +8,15 @@ import { getLogger } from './logger.js'; * @param {string} schemaTypesPath * @param {TableMeta[]} tables */ +/** + * Wraps a property name in quotes if it contains characters that are not + * valid in an unquoted TypeScript identifier (anything other than word chars). + * @param {string} name + */ +function safeKey(name) { + return /[^\w]/.test(name) ? `'${name}'` : name; +} + export function generateTablesDTS(globalTypesPath, schemaTypesPath, tables) { let content = `/** Generated from your schema files @@ -45,19 +54,19 @@ export function generateTablesDTS(globalTypesPath, schemaTypesPath, tables) { const dataTables = dbMap.get('data') || []; content += `\texport const tables: {\n`; for (const table of dataTables) { - content += `\t\t${table.plural}: { new(...args: any[]): Table<${table.singular}> };\n`; + content += `\t\t${safeKey(table.plural)}: { new(...args: any[]): Table<${table.singular}> };\n`; } content += `\t};\n\n`; // Export namespaced databases content += `\texport const databases: {\n`; for (const [dbName, dbTables] of dbMap.entries()) { - content += `\t\t${dbName}: {\n`; + content += `\t\t${safeKey(dbName)}: {\n`; for (const table of dbTables) { const pluralRaw = table.plural.startsWith(`${dbName}_`) ? table.plural.slice(dbName.length + 1) : table.plural; - content += `\t\t\t${pluralRaw}: { new(...args: any[]): Table<${table.singular}> };\n`; + content += `\t\t\t${safeKey(pluralRaw)}: { new(...args: any[]): Table<${table.singular}> };\n`; } content += `\t\t};\n`; } From a0e477465a7785e692106ace3ed26c4673bf5b23 Mon Sep 17 00:00:00 2001 From: Chris Barber Date: Mon, 18 May 2026 14:11:53 -0500 Subject: [PATCH 2/2] feat: support databases and tables with dashes --- utils/generateInterface.js | 5 +++-- utils/generateInterface.test.js | 29 +++++++++++++++++++++++++++++ utils/generateJSDoc.js | 5 +++-- utils/generateJSDoc.test.js | 16 ++++++++++++++++ utils/generateTS.js | 3 ++- utils/generateTS.test.js | 17 +++++++++++++++++ utils/toIdentifier.js | 11 +++++++++++ 7 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 utils/toIdentifier.js diff --git a/utils/generateInterface.js b/utils/generateInterface.js index db51f99..6f9f768 100644 --- a/utils/generateInterface.js +++ b/utils/generateInterface.js @@ -2,13 +2,14 @@ import { isNullable } from './isNullable.js'; import { mapType } from './mapType.js'; import { singularize } from './singularize.js'; +import { toIdentifier } from './toIdentifier.js'; /** * @param {Table & { databaseName?: string }} table */ export function generateInterface(table) { - const pluralRaw = table.tableName; - const singularRaw = singularize(pluralRaw); + const pluralRaw = toIdentifier(table.tableName); + const singularRaw = toIdentifier(singularize(table.tableName)); const dbPrefix = table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : ''; const plural = `${dbPrefix}${pluralRaw}`; diff --git a/utils/generateInterface.test.js b/utils/generateInterface.test.js index 1bf4b91..5e3653e 100644 --- a/utils/generateInterface.test.js +++ b/utils/generateInterface.test.js @@ -75,4 +75,33 @@ describe('generateInterface', () => { const result = generateInterface(table); expect(result).not.toContain('export type NewLog ='); }); + + it('should produce valid identifiers for table names containing dashes', () => { + const table = { + tableName: 'blog-posts', + attributes: [ + { name: 'id', type: 'ID', isPrimaryKey: true }, + { name: 'title', type: 'String' }, + ], + }; + const result = generateInterface(table); + expect(result).toContain('export interface blogPost {'); + expect(result).toContain('export type blogPosts = blogPost[];'); + expect(result).toContain('export type { blogPost as blogPostRecord };'); + expect(result).toContain('export type blogPostRecords = blogPost[];'); + expect(result).toContain("export type NewblogPost = Omit;"); + expect(result).not.toContain('blog-post'); + }); + + it('should handle databaseName prefix with dashed table name', () => { + const table = { + tableName: 'audit-logs', + databaseName: 'mydb', + attributes: [{ name: 'id', type: 'ID', isPrimaryKey: true }], + }; + const result = generateInterface(table); + expect(result).toContain('export interface mydb_auditLog {'); + expect(result).toContain('export type mydb_auditLogs = mydb_auditLog[];'); + expect(result).not.toContain('audit-log'); + }); }); diff --git a/utils/generateJSDoc.js b/utils/generateJSDoc.js index afcbc08..fbdaeb9 100644 --- a/utils/generateJSDoc.js +++ b/utils/generateJSDoc.js @@ -2,6 +2,7 @@ import { isNullable } from './isNullable.js'; import { mapType } from './mapType.js'; import { singularize } from './singularize.js'; +import { toIdentifier } from './toIdentifier.js'; /** * Generates JSDoc types for a given HarperDB table. @@ -17,8 +18,8 @@ import { singularize } from './singularize.js'; * @param {Table & { databaseName?: string }} table */ export function generateJSDoc(table) { - const pluralRaw = table.tableName; - const singularRaw = singularize(pluralRaw); + const pluralRaw = toIdentifier(table.tableName); + const singularRaw = toIdentifier(singularize(table.tableName)); const dbPrefix = table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : ''; const plural = `${dbPrefix}${pluralRaw}`; diff --git a/utils/generateJSDoc.test.js b/utils/generateJSDoc.test.js index 8b9831d..eed5a68 100644 --- a/utils/generateJSDoc.test.js +++ b/utils/generateJSDoc.test.js @@ -58,4 +58,20 @@ describe('generateJSDoc', () => { const code = generateJSDoc(table); expect(code).toContain("@typedef {Omit} NewUserRole"); }); + + it('should produce valid identifiers for table names containing dashes', () => { + const table = { + tableName: 'blog-posts', + databaseName: 'data', + attributes: [ + { name: 'id', type: 'ID', isPrimaryKey: true }, + { name: 'title', type: 'String' }, + ], + }; + const code = generateJSDoc(table); + expect(code).toContain('@typedef {Object} blogPost'); + expect(code).toContain('@typedef {blogPost[]} blogPosts'); + expect(code).toContain("@typedef {Omit} NewblogPost"); + expect(code).not.toContain('blog-post'); + }); }); diff --git a/utils/generateTS.js b/utils/generateTS.js index 08f534e..5a3ea74 100644 --- a/utils/generateTS.js +++ b/utils/generateTS.js @@ -2,6 +2,7 @@ /** @import { TableMeta } from './tableMeta.js' */ import { generateInterface } from './generateInterface.js'; import { singularize } from './singularize.js'; +import { toIdentifier } from './toIdentifier.js'; /** * @param {(Table & { databaseName: string })[]} tablesInput @@ -22,7 +23,7 @@ export function generateTSFromTables(tablesInput, label = 'HarperDB schemas') { const dbPrefix = table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : ''; const plural = `${dbPrefix}${table.tableName}`; - const singular = `${dbPrefix}${singularize(table.tableName)}`; + const singular = `${dbPrefix}${toIdentifier(singularize(table.tableName))}`; tables.push({ plural, singular, databaseName: table.databaseName }); } diff --git a/utils/generateTS.test.js b/utils/generateTS.test.js index 59bf3f4..d9577c3 100644 --- a/utils/generateTS.test.js +++ b/utils/generateTS.test.js @@ -37,4 +37,21 @@ describe('generateTSFromTables', () => { const { tsCode } = generateTSFromTables([], undefined); expect(tsCode).toContain('Generated from HarperDB schemas'); }); + + it('should produce valid identifiers when table name contains dashes', () => { + const tables = [ + { + tableName: 'blog-posts', + databaseName: 'data', + attributes: [{ name: 'id', type: 'ID', isPrimaryKey: true }], + }, + ]; + const { tsCode, tables: tablesMeta } = generateTSFromTables(tables); + expect(tsCode).toContain('export interface blogPost {'); + expect(tablesMeta[0]).toEqual({ + plural: 'blog-posts', + singular: 'blogPost', + databaseName: 'data', + }); + }); }); diff --git a/utils/toIdentifier.js b/utils/toIdentifier.js new file mode 100644 index 0000000..81f4498 --- /dev/null +++ b/utils/toIdentifier.js @@ -0,0 +1,11 @@ +/** + * 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. + * @param {string} name + * @returns {string} + */ +export function toIdentifier(name) { + return name.replace(/-([a-zA-Z0-9])/g, (_, c) => c.toUpperCase()); +}