diff --git a/README.md b/README.md index ba9f7fd..beaa404 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,32 @@ Alternatively, if you are using pure JavaScript, you can generate JSDoc instead: When you `harper dev`, it will generate types based on the schema that's actually in your Harper database. If you change the schema, we will automatically regenerate the types for you. +## Options + +| Option | Default | Description | +| ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `globalTypes` | — | Path to write the ambient module augmentation (`.d.ts`). | +| `schemaTypes` | — | Path to write the exported interfaces (`.ts`). | +| `jsdoc` | — | Path to write JSDoc types instead, for pure JavaScript projects. | +| `module` | `harper` | The runtime package to augment. Harper 5.x apps import from `harper`; set this to `harperdb` for Harper 4.x apps. | +| `includeDatabases` | (all) | List of database names to generate types for. When set, only matching databases are emitted. Supports `*` as a wildcard, e.g. `metrics-*`. | +| `excludeDatabases` | (none) | List of database names to skip. Useful on a shared instance to keep other applications' databases out of your generated types. Supports `*` as a wildcard. | + +`includeDatabases`/`excludeDatabases` scope generation to your application's own +databases. On a shared local instance, codegen otherwise sees every database on +the instance — including ones belonging to other projects — so scoping keeps +your generated types focused: + +```yaml +'@harperfast/schema-codegen': + package: '@harperfast/schema-codegen' + globalTypes: 'schemas/globalTypes.d.ts' + schemaTypes: 'schemas/types.ts' + includeDatabases: + - data + - 'metrics-*' +``` + ## Example For example, here's a tracks.graphql schema: @@ -70,10 +96,10 @@ An ambient declaration will also be generated in globalTypes.d.ts to enhance the Manual changes will be lost! > harper dev . */ -import type { Table } from 'harperdb'; +import type { Table } from 'harper'; import type { Track } from './types.ts'; -declare module 'harperdb' { +declare module 'harper' { export const tables: { Tracks: { new (...args: any[]): Table }; }; diff --git a/extensionModule.js b/extensionModule.js index e00762c..543e9ad 100644 --- a/extensionModule.js +++ b/extensionModule.js @@ -21,6 +21,14 @@ export async function handleApplication(scope) { const globalTypes = /** @type {string} */ (scope.options.get(['globalTypes'])); const schemaTypes = /** @type {string} */ (scope.options.get(['schemaTypes'])); const jsdoc = /** @type {string | undefined} */ (scope.options.get(['jsdoc'])); + const moduleName = /** @type {string | undefined} */ (scope.options.get(['module'])); + const includeDatabases = /** @type {string[] | undefined} */ ( + scope.options.get(['includeDatabases']) + ); + const excludeDatabases = /** @type {string[] | undefined} */ ( + scope.options.get(['excludeDatabases']) + ); + const options = { module: moduleName, includeDatabases, excludeDatabases }; if (shouldWatch) { scope.on('close', scopeClosed); @@ -29,7 +37,7 @@ export async function handleApplication(scope) { // Do not await this. delay(5000).then(() => { // Initial generation - regenerateAll(globalTypes, schemaTypes, jsdoc); + regenerateAll(globalTypes, schemaTypes, jsdoc, options); if (shouldWatch) { // Watch for schema/database changes via events @@ -40,15 +48,15 @@ export async function handleApplication(scope) { }); function updateTable() { - regenerateAll(globalTypes, schemaTypes, jsdoc); + regenerateAll(globalTypes, schemaTypes, jsdoc, options); } function dropTable() { - regenerateAll(globalTypes, schemaTypes, jsdoc); + regenerateAll(globalTypes, schemaTypes, jsdoc, options); } function dropDatabase() { - regenerateAll(globalTypes, schemaTypes, jsdoc); + regenerateAll(globalTypes, schemaTypes, jsdoc, options); } function scopeClosed() { diff --git a/utils/collectTables.js b/utils/collectTables.js index 2473b98..a5527ad 100644 --- a/utils/collectTables.js +++ b/utils/collectTables.js @@ -1,12 +1,21 @@ /** @typedef {import('harperdb').Table} Table */ +import { shouldIncludeDatabase } from './databaseFilter.js'; /** + * Collects every table from the live `databases` object, optionally scoped to a + * subset of databases. Without options, every database on the instance is + * included — on a shared instance that can pull in databases from other + * applications, so pass `include`/`exclude` to scope output to this app. + * @param {{ include?: string[], exclude?: string[] }} [options] * @returns {(Table & { databaseName: string })[]} */ -export function collectTables() { +export function collectTables(options = {}) { /** @type {(Table & { databaseName: string })[]} */ const tablesList = []; for (const databaseName of Object.keys(databases || {})) { + if (!shouldIncludeDatabase(databaseName, options)) { + continue; + } const tables = databases[databaseName]; for (const tableName of Object.keys(tables || {})) { const TableClass = tables[tableName]; diff --git a/utils/databaseFilter.js b/utils/databaseFilter.js new file mode 100644 index 0000000..d681b24 --- /dev/null +++ b/utils/databaseFilter.js @@ -0,0 +1,40 @@ +/** + * Matches a database name against a list of patterns. Each pattern is either an + * exact name or a glob using `*` as a wildcard for any run of characters, e.g. + * `metrics-*` matches both `metrics-github` and `metrics-jira`. Matching is + * case-sensitive and anchored (the whole name must match). + * @param {string} name + * @param {(string | number)[]} patterns Patterns come from YAML config, so a + * purely numeric, unquoted name (e.g. `101`) arrives as a number; each pattern + * is coerced to a string before matching. + * @returns {boolean} + */ +export function matchesDatabase(name, patterns) { + return patterns.some((pattern) => { + // escape regex metacharacters, then turn the wildcard back into `.*` + const source = String(pattern) + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/\\\*/g, '.*'); + return new RegExp(`^${source}$`).test(name); + }); +} + +/** + * Decides whether a database should be included in codegen given optional + * include/exclude lists. An empty or absent `include` list includes every + * database; a non-empty `include` list restricts output to matching databases. + * `exclude` then removes any matching database. Exclude wins over include. + * @param {string} name + * @param {{ include?: string[], exclude?: string[] }} [options] + * @returns {boolean} + */ +export function shouldIncludeDatabase(name, options = {}) { + const { include, exclude } = options; + if (Array.isArray(include) && include.length > 0 && !matchesDatabase(name, include)) { + return false; + } + if (Array.isArray(exclude) && exclude.length > 0 && matchesDatabase(name, exclude)) { + return false; + } + return true; +} diff --git a/utils/databaseFilter.test.js b/utils/databaseFilter.test.js new file mode 100644 index 0000000..c46c5f7 --- /dev/null +++ b/utils/databaseFilter.test.js @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest'; +import { matchesDatabase, shouldIncludeDatabase } from './databaseFilter.js'; + +describe('matchesDatabase', () => { + it('matches exact names', () => { + expect(matchesDatabase('data', ['data'])).toBe(true); + expect(matchesDatabase('data', ['blog'])).toBe(false); + }); + + it('is anchored (no partial matches)', () => { + expect(matchesDatabase('metrics-github', ['metrics'])).toBe(false); + expect(matchesDatabase('metrics-github', ['github'])).toBe(false); + }); + + it('supports the * wildcard', () => { + expect(matchesDatabase('metrics-github', ['metrics-*'])).toBe(true); + expect(matchesDatabase('metrics-jira', ['metrics-*'])).toBe(true); + expect(matchesDatabase('billing', ['metrics-*'])).toBe(false); + expect(matchesDatabase('anything', ['*'])).toBe(true); + }); + + it('does not treat other regex metacharacters specially', () => { + // the dot must be a literal, not "any character" + expect(matchesDatabase('myXdb', ['my.db'])).toBe(false); + expect(matchesDatabase('my.db', ['my.db'])).toBe(true); + }); + + it('coerces numeric patterns from unquoted YAML without crashing', () => { + // an unquoted numeric database name in YAML parses as a number + expect(matchesDatabase('101', [101])).toBe(true); + expect(matchesDatabase('202', [101])).toBe(false); + }); +}); + +describe('shouldIncludeDatabase', () => { + it('includes everything when no options are given', () => { + expect(shouldIncludeDatabase('anything')).toBe(true); + expect(shouldIncludeDatabase('anything', {})).toBe(true); + }); + + it('restricts to the include list when provided', () => { + expect(shouldIncludeDatabase('data', { include: ['data', 'blog'] })).toBe(true); + expect(shouldIncludeDatabase('other', { include: ['data', 'blog'] })).toBe(false); + }); + + it('treats an empty include list as "include everything"', () => { + expect(shouldIncludeDatabase('anything', { include: [] })).toBe(true); + }); + + it('removes databases in the exclude list', () => { + expect(shouldIncludeDatabase('system', { exclude: ['system'] })).toBe(false); + expect(shouldIncludeDatabase('data', { exclude: ['system'] })).toBe(true); + }); + + it('lets exclude win over include', () => { + expect( + shouldIncludeDatabase('metrics-internal', { + include: ['metrics-*'], + exclude: ['metrics-internal'], + }), + ).toBe(false); + }); +}); diff --git a/utils/dbTypePrefix.js b/utils/dbTypePrefix.js new file mode 100644 index 0000000..709d0a0 --- /dev/null +++ b/utils/dbTypePrefix.js @@ -0,0 +1,22 @@ +import { toIdentifier } from './toIdentifier.js'; + +/** + * Builds the prefix applied to generated *type names* for tables that live in a + * non-default database. Tables in the default 'data' database are not + * namespaced, so they receive no prefix. + * + * The database name is passed through {@link toIdentifier} so that databases + * whose names are not valid identifiers still produce valid TypeScript type + * names, e.g. a "metrics-github" database yields the prefix "metricsGithub_" + * (and thus the type "metricsGithub_Repository" rather than the invalid + * "metrics-github_Repository", which TypeScript parses as a subtraction). + * + * This is for type-name positions only. Runtime object keys (the actual + * database and table names on the `databases` object) must be preserved + * verbatim — quote those with {@link safeKey} instead. + * @param {string | undefined} databaseName + * @returns {string} + */ +export function dbTypePrefix(databaseName) { + return databaseName && databaseName !== 'data' ? `${toIdentifier(databaseName)}_` : ''; +} diff --git a/utils/dbTypePrefix.test.js b/utils/dbTypePrefix.test.js new file mode 100644 index 0000000..224e361 --- /dev/null +++ b/utils/dbTypePrefix.test.js @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { dbTypePrefix } from './dbTypePrefix.js'; + +describe('dbTypePrefix', () => { + it('returns an empty prefix for the default data database', () => { + expect(dbTypePrefix('data')).toBe(''); + }); + + it('returns an empty prefix when the database name is missing', () => { + expect(dbTypePrefix(undefined)).toBe(''); + expect(dbTypePrefix('')).toBe(''); + }); + + it('namespaces a valid database name unchanged', () => { + expect(dbTypePrefix('blog')).toBe('blog_'); + }); + + it('sanitizes a hyphenated database name into a valid identifier prefix', () => { + expect(dbTypePrefix('metrics-github')).toBe('metricsGithub_'); + }); + + it('sanitizes a database name that starts with a digit', () => { + expect(dbTypePrefix('9lives')).toBe('_9lives_'); + }); + + it('always produces an identifier-safe prefix', () => { + const names = ['metrics-github', 'my.db', 'weird name', '9lives']; + for (const name of names) { + expect(dbTypePrefix(name)).toMatch(/^[a-zA-Z_$][a-zA-Z0-9_$]*_$/); + } + }); +}); diff --git a/utils/generateInterface.js b/utils/generateInterface.js index d3c4241..6a6ea64 100644 --- a/utils/generateInterface.js +++ b/utils/generateInterface.js @@ -1,4 +1,5 @@ /** @typedef {import('harperdb').Table} Table */ +import { dbTypePrefix } from './dbTypePrefix.js'; import { escapeSingleQuoted } from './escapeSingleQuoted.js'; import { isNullable } from './isNullable.js'; import { mapType } from './mapType.js'; @@ -12,8 +13,7 @@ import { toIdentifier } from './toIdentifier.js'; export function generateInterface(table) { const pluralRaw = toIdentifier(table.tableName); const singularRaw = toIdentifier(singularize(table.tableName)); - const dbPrefix = - table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : ''; + const dbPrefix = dbTypePrefix(table.databaseName); const plural = `${dbPrefix}${pluralRaw}`; const singular = `${dbPrefix}${singularRaw}`; const isDifferent = plural !== singular; diff --git a/utils/generateInterface.test.js b/utils/generateInterface.test.js index ae0b074..0306150 100644 --- a/utils/generateInterface.test.js +++ b/utils/generateInterface.test.js @@ -163,4 +163,23 @@ describe('generateInterface', () => { expect(result).toContain('export type mydb_auditLogs = mydb_auditLog[];'); expect(result).not.toContain('audit-log'); }); + + it('should produce valid identifiers when the database name contains dashes', () => { + const table = { + tableName: 'Repository', + databaseName: 'metrics-github', + attributes: [{ name: 'id', type: 'ID', isPrimaryKey: true }], + }; + const result = generateInterface(table); + // the hyphenated database name must be sanitized in every type position; + // "metrics-github_Repository" would parse as a subtraction + expect(result).toContain('export interface metricsGithub_Repository {'); + expect(result).toContain( + "export type metricsGithub_NewRepository = Omit;", + ); + expect(result).toContain( + 'export type metricsGithub_RepositoryRecords = metricsGithub_Repository[];', + ); + expect(result).not.toContain('metrics-github_Repository'); + }); }); diff --git a/utils/generateJSDoc.js b/utils/generateJSDoc.js index e64d4bf..797ea82 100644 --- a/utils/generateJSDoc.js +++ b/utils/generateJSDoc.js @@ -1,4 +1,5 @@ /** @typedef {import('harperdb').Table} Table */ +import { dbTypePrefix } from './dbTypePrefix.js'; import { escapeSingleQuoted } from './escapeSingleQuoted.js'; import { isNullable } from './isNullable.js'; import { mapType } from './mapType.js'; @@ -21,8 +22,7 @@ import { toIdentifier } from './toIdentifier.js'; export function generateJSDoc(table) { const pluralRaw = toIdentifier(table.tableName); const singularRaw = toIdentifier(singularize(table.tableName)); - const dbPrefix = - table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : ''; + const dbPrefix = dbTypePrefix(table.databaseName); const plural = `${dbPrefix}${pluralRaw}`; const singular = `${dbPrefix}${singularRaw}`; const isDifferent = plural !== singular; diff --git a/utils/generateJSDoc.test.js b/utils/generateJSDoc.test.js index d1dac67..305c572 100644 --- a/utils/generateJSDoc.test.js +++ b/utils/generateJSDoc.test.js @@ -86,4 +86,18 @@ describe('generateJSDoc', () => { expect(code).toContain("@typedef {Omit} NewblogPost"); expect(code).not.toContain('blog-post'); }); + + it('should produce valid identifiers when the database name contains dashes', () => { + const table = { + tableName: 'Repository', + databaseName: 'metrics-github', + attributes: [{ name: 'id', type: 'ID', isPrimaryKey: true }], + }; + const code = generateJSDoc(table); + expect(code).toContain('@typedef {Object} metricsGithub_Repository'); + expect(code).toContain( + "@typedef {Omit} metricsGithub_NewRepository", + ); + expect(code).not.toContain('metrics-github_Repository'); + }); }); diff --git a/utils/generateJSDocFromTables.js b/utils/generateJSDocFromTables.js index 39ffc3f..0b6a006 100644 --- a/utils/generateJSDocFromTables.js +++ b/utils/generateJSDocFromTables.js @@ -1,7 +1,9 @@ /** @typedef {import('harperdb').Table} Table */ /** @import { TableMeta } from './tableMeta.js' */ +import { dbTypePrefix } from './dbTypePrefix.js'; import { generateJSDoc } from './generateJSDoc.js'; import { singularize } from './singularize.js'; +import { toIdentifier } from './toIdentifier.js'; /** * @param {(Table & { databaseName: string })[]} tablesInput @@ -18,11 +20,8 @@ export function generateJSDocFromTables(tablesInput, label = 'HarperDB schemas') for (const table of tablesInput) { jsCode += generateJSDoc(table); - const dbPrefix = - table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : ''; - const plural = `${dbPrefix}${table.tableName}`; - const singular = `${dbPrefix}${singularize(table.tableName)}`; - tables.push({ plural, singular, databaseName: table.databaseName }); + const singular = `${dbTypePrefix(table.databaseName)}${toIdentifier(singularize(table.tableName))}`; + tables.push({ tableName: table.tableName, singular, databaseName: table.databaseName }); } return { jsCode, tables }; diff --git a/utils/generateTS.js b/utils/generateTS.js index 5a3ea74..cc3e2ab 100644 --- a/utils/generateTS.js +++ b/utils/generateTS.js @@ -1,5 +1,6 @@ /** @typedef {import('harperdb').Table} Table */ /** @import { TableMeta } from './tableMeta.js' */ +import { dbTypePrefix } from './dbTypePrefix.js'; import { generateInterface } from './generateInterface.js'; import { singularize } from './singularize.js'; import { toIdentifier } from './toIdentifier.js'; @@ -20,11 +21,8 @@ export function generateTSFromTables(tablesInput, label = 'HarperDB schemas') { for (const table of tablesInput) { tsCode += generateInterface(table); - const dbPrefix = - table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : ''; - const plural = `${dbPrefix}${table.tableName}`; - const singular = `${dbPrefix}${toIdentifier(singularize(table.tableName))}`; - tables.push({ plural, singular, databaseName: table.databaseName }); + const singular = `${dbTypePrefix(table.databaseName)}${toIdentifier(singularize(table.tableName))}`; + tables.push({ tableName: table.tableName, singular, databaseName: table.databaseName }); } return { tsCode, tables }; diff --git a/utils/generateTS.test.js b/utils/generateTS.test.js index a528c4f..cbf384f 100644 --- a/utils/generateTS.test.js +++ b/utils/generateTS.test.js @@ -22,12 +22,12 @@ describe('generateTSFromTables', () => { expect(tablesMeta).toHaveLength(2); expect(tablesMeta[0]).toEqual({ - plural: 'Users', + tableName: 'Users', singular: 'User', databaseName: 'data', }); expect(tablesMeta[1]).toEqual({ - plural: 'blog_Posts', + tableName: 'Posts', singular: 'blog_Post', databaseName: 'blog', }); @@ -50,7 +50,7 @@ describe('generateTSFromTables', () => { expect(tsCode).toContain('export interface _123_New4 {'); // the runtime key is preserved verbatim, but the type name is a valid identifier expect(tablesMeta[0]).toEqual({ - plural: '123_New4', + tableName: '123_New4', singular: '_123_New4', databaseName: 'data', }); @@ -67,9 +67,30 @@ describe('generateTSFromTables', () => { const { tsCode, tables: tablesMeta } = generateTSFromTables(tables); expect(tsCode).toContain('export interface blogPost {'); expect(tablesMeta[0]).toEqual({ - plural: 'blog-posts', + tableName: 'blog-posts', singular: 'blogPost', databaseName: 'data', }); }); + + it('should produce valid identifiers when the database name contains dashes', () => { + const tables = [ + { + tableName: 'Repository', + databaseName: 'metrics-github', + attributes: [{ name: 'id', type: 'ID', isPrimaryKey: true }], + }, + ]; + const { tsCode, tables: tablesMeta } = generateTSFromTables(tables); + // the database prefix must be sanitized into a valid identifier; an + // unsanitized "metrics-github_Repository" parses as a subtraction + expect(tsCode).toContain('export interface metricsGithub_Repository {'); + expect(tsCode).not.toContain('metrics-github_Repository'); + // the runtime key (databaseName) is preserved verbatim for the .d.ts to quote + expect(tablesMeta[0]).toEqual({ + tableName: 'Repository', + singular: 'metricsGithub_Repository', + databaseName: 'metrics-github', + }); + }); }); diff --git a/utils/generateTablesDTS.js b/utils/generateTablesDTS.js index 1ca074f..c64966d 100644 --- a/utils/generateTablesDTS.js +++ b/utils/generateTablesDTS.js @@ -8,15 +8,18 @@ import { safeKey } from './safeKey.js'; * @param {string} globalTypesPath * @param {string} schemaTypesPath * @param {TableMeta[]} tables + * @param {string} [moduleName] The runtime package to augment. Defaults to + * 'harper' (Harper 5.x); Harper 4.x apps that import from 'harperdb' should + * override this via the `module` config option. */ -export function generateTablesDTS(globalTypesPath, schemaTypesPath, tables) { +export function generateTablesDTS(globalTypesPath, schemaTypesPath, tables, moduleName = 'harper') { let content = `/** Generated from your schema files Manual changes will be lost! > harper dev . */ `; - content += `import type { Table } from 'harperdb';\n`; + content += `import type { Table } from '${moduleName}';\n`; // Build a single import of all relevant types from schemaTypesPath /** @type {Set} */ const namesToImport = new Set(); @@ -40,13 +43,13 @@ export function generateTablesDTS(globalTypesPath, schemaTypesPath, tables) { } } - content += `declare module 'harperdb' {\n`; + content += `declare module '${moduleName}' {\n`; // Export top-level tables for 'data' database const dataTables = dbMap.get('data') || []; content += `\texport const tables: {\n`; for (const table of dataTables) { - content += `\t\t${safeKey(table.plural)}: { new(...args: any[]): Table<${table.singular}> };\n`; + content += `\t\t${safeKey(table.tableName)}: { new(...args: any[]): Table<${table.singular}> };\n`; } content += `\t};\n\n`; @@ -55,10 +58,7 @@ export function generateTablesDTS(globalTypesPath, schemaTypesPath, tables) { for (const [dbName, dbTables] of dbMap.entries()) { 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${safeKey(pluralRaw)}: { new(...args: any[]): Table<${table.singular}> };\n`; + content += `\t\t\t${safeKey(table.tableName)}: { new(...args: any[]): Table<${table.singular}> };\n`; } content += `\t\t};\n`; } diff --git a/utils/generateTablesDTS.test.js b/utils/generateTablesDTS.test.js index 1ef2722..4f35aea 100644 --- a/utils/generateTablesDTS.test.js +++ b/utils/generateTablesDTS.test.js @@ -22,20 +22,25 @@ describe('generateTablesDTS', () => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); - /** @param {import('./tableMeta.js').TableMeta[]} tables */ - function generate(tables) { - generateTablesDTS(globalTypesPath, schemaTypesPath, tables); + /** + * @param {import('./tableMeta.js').TableMeta[]} tables + * @param {string} [moduleName] + */ + function generate(tables, moduleName) { + generateTablesDTS(globalTypesPath, schemaTypesPath, tables, moduleName); 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' }]); + const content = generate([{ tableName: 'Users', singular: 'User', databaseName: 'data' }]); expect(content).toContain('import type { User } from'); expect(content).toContain('Users: { new(...args: any[]): Table };'); }); it('should quote a runtime key that starts with a digit', () => { - const content = generate([{ plural: '123_New4', singular: '_123_New4', databaseName: 'data' }]); + const content = generate([ + { tableName: '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'); @@ -45,8 +50,8 @@ describe('generateTablesDTS', () => { 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' }, + { tableName: 'Users', singular: 'User', databaseName: 'data' }, + { tableName: 'audit-logs', singular: 'auditLog', databaseName: 'data' }, ]); expect(content).toContain('Users: {'); expect(content).toContain("'audit-logs': {"); @@ -54,8 +59,41 @@ describe('generateTablesDTS', () => { it('should quote a database name that starts with a digit', () => { const content = generate([ - { plural: '9lives_Cats', singular: '_9lives_Cat', databaseName: '9lives' }, + { tableName: 'Cats', singular: '_9lives_Cat', databaseName: '9lives' }, ]); expect(content).toContain("'9lives': {"); }); + + it('should quote a hyphenated database key while referencing a sanitized type', () => { + const content = generate([ + { + tableName: 'Repository', + singular: 'metricsGithub_Repository', + databaseName: 'metrics-github', + }, + ]); + // runtime key is quoted verbatim; the type reference is a valid identifier + expect(content).toContain("'metrics-github': {"); + expect(content).toContain( + 'Repository: { new(...args: any[]): Table };', + ); + expect(content).toContain('import type { metricsGithub_Repository } from'); + expect(content).not.toContain('metrics-github_Repository'); + }); + + it('should augment the harper module by default', () => { + const content = generate([{ tableName: 'Users', singular: 'User', databaseName: 'data' }]); + expect(content).toContain("import type { Table } from 'harper';"); + expect(content).toContain("declare module 'harper' {"); + expect(content).not.toContain('harperdb'); + }); + + it('should augment a configurable module for Harper 4.x compatibility', () => { + const content = generate( + [{ tableName: 'Users', singular: 'User', databaseName: 'data' }], + 'harperdb', + ); + expect(content).toContain("import type { Table } from 'harperdb';"); + expect(content).toContain("declare module 'harperdb' {"); + }); }); diff --git a/utils/regenerateAll.js b/utils/regenerateAll.js index abb0c4f..0e56239 100644 --- a/utils/regenerateAll.js +++ b/utils/regenerateAll.js @@ -9,9 +9,11 @@ import { writeIfChanged } from './writeIfChanged.js'; * @param {string} globalTypes * @param {string} schemaTypes * @param {string} [jsdoc] + * @param {{ module?: string, includeDatabases?: string[], excludeDatabases?: string[] }} [options] */ -export function regenerateAll(globalTypes, schemaTypes, jsdoc) { - const list = collectTables(); +export function regenerateAll(globalTypes, schemaTypes, jsdoc, options = {}) { + const { module: moduleName, includeDatabases, excludeDatabases } = options; + const list = collectTables({ include: includeDatabases, exclude: excludeDatabases }); if (jsdoc) { const { jsCode } = generateJSDocFromTables(list, 'HarperDB schema'); @@ -23,6 +25,6 @@ export function regenerateAll(globalTypes, schemaTypes, jsdoc) { writeIfChanged(path.resolve(schemaTypes), tsCode); } if (globalTypes) { - generateTablesDTS(path.resolve(globalTypes), path.resolve(schemaTypes), tables); + generateTablesDTS(path.resolve(globalTypes), path.resolve(schemaTypes), tables, moduleName); } } diff --git a/utils/tableMeta.js b/utils/tableMeta.js index 288ab41..0ce7ac1 100644 --- a/utils/tableMeta.js +++ b/utils/tableMeta.js @@ -1,7 +1,7 @@ /** * @typedef {Object} TableMeta - * @property {string} singular - * @property {string} plural - * @property {string} databaseName + * @property {string} tableName The raw table name, used verbatim as a runtime object key. + * @property {string} singular The identifier-safe singular type name (namespaced by database). + * @property {string} databaseName The raw database name, used verbatim as a runtime object key. */ export {};