Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions utils/generateInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down
29 changes: 29 additions & 0 deletions utils/generateInterface.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<blogPost, 'id'>;");
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');
});
});
5 changes: 3 additions & 2 deletions utils/generateJSDoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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}`;
Expand Down
16 changes: 16 additions & 0 deletions utils/generateJSDoc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,20 @@ describe('generateJSDoc', () => {
const code = generateJSDoc(table);
expect(code).toContain("@typedef {Omit<UserRole, 'userId' | 'roleId'>} 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<blogPost, 'id'>} NewblogPost");
expect(code).not.toContain('blog-post');
});
});
3 changes: 2 additions & 1 deletion utils/generateTS.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 });
}

Expand Down
17 changes: 17 additions & 0 deletions utils/generateTS.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});
});
15 changes: 12 additions & 3 deletions utils/generateTablesDTS.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`;
}
Expand Down
11 changes: 11 additions & 0 deletions utils/toIdentifier.js
Original file line number Diff line number Diff line change
@@ -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());
}
Loading