From 45974741c9cd23fbf3d38e5d8ee7adeae2c2f9da Mon Sep 17 00:00:00 2001 From: "dustin.silk" Date: Mon, 29 Jun 2026 15:28:40 -0700 Subject: [PATCH] Support glob patterns in srcDir --- README.md | 2 +- src/cli.ts | 24 ++++++++++++------------ src/load-config.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7dc3e00b..e7eb6605 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Options: | :--------------------- | :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------ | | **client** | Database client driver to use. | | | **databaseUri** | Connection string for the database. Supports environment variables (`${VAR_NAME}`). | | -| **sqlDir** | Directory where SQL queries are stored. Will search recursively by appending the `**/*.sql` glob pattern. | `./src` | +| **sqlDir** | Where SQL queries are located. A directory (or directory glob) searches recursively — `**/*.sql` is appended automatically. A pattern already ending in `.sql` is used as-is for full control over which files match. | | | **authToken** | Authentication token. Required **only for the `libsql` client**. Supports environment variables (`${VAR_NAME}`). | | | **includeCrudTables** | Generates `select`, `insert`, `update`, and `delete` queries for specified tables. | `['users', 'permissions', 'tags']` | | **generateIndexFiles** | Whether to recursively generate index.ts barrel files in each output folder. Defaults to true if not specified. | true | diff --git a/src/cli.ts b/src/cli.ts index 24006cb2..f93977e8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,7 +14,7 @@ import { closeClient, createClient, loadSchemaInfo, loadTableSchema, PostgresSch import { generateCrud } from './codegen/sqlite'; import { generateCrud as generatePgCrud } from './codegen/pg'; import uniqBy from 'lodash.uniqby'; -import { buildExportList, buildExportMap, loadConfig, resolveTsFilePath } from './load-config'; +import { buildExportList, buildExportMap, loadConfig, resolveSqlDir, resolveTsFilePath } from './load-config'; import { PostgresColumnSchema } from './drivers/types'; import { createCodeBlockWriter } from './codegen/shared/codegen-util'; @@ -107,17 +107,15 @@ function validateDirectories(dir: string) { } } -function watchDirectories(client: DatabaseClient, sqlDir: string, outDir: string, dbSchema: SchemaInfo | PostgresSchemaInfo, config: TypeSqlConfig) { - const dirGlob = `${sqlDir}/**/*.sql`; - +function watchDirectories(client: DatabaseClient, dirGlob: string, baseDir: string, outDir: string, dbSchema: SchemaInfo | PostgresSchemaInfo, config: TypeSqlConfig) { chokidar .watch(dirGlob, { awaitWriteFinish: { stabilityThreshold: 100 } }) - .on('add', (path) => rewiteFiles(client, path, sqlDir, outDir, dbSchema, isCrudFile(sqlDir, path), config)) - .on('change', (path) => rewiteFiles(client, path, sqlDir, outDir, dbSchema, isCrudFile(sqlDir, path), config)); + .on('add', (path) => rewiteFiles(client, path, baseDir, outDir, dbSchema, isCrudFile(baseDir, path), config)) + .on('change', (path) => rewiteFiles(client, path, baseDir, outDir, dbSchema, isCrudFile(baseDir, path), config)); } async function rewiteFiles(client: DatabaseClient, sqlPath: string, sqlDir: string, outDir: string, schemaInfo: SchemaInfo | PostgresSchemaInfo, isCrudFile: boolean, config: TypeSqlConfig) { @@ -134,8 +132,10 @@ async function main() { } async function compile(watch: boolean, config: TypeSqlConfig) { - const { sqlDir, outDir = sqlDir, databaseUri, client: dialect, attach, loadExtensions, authToken } = config; - validateDirectories(sqlDir); + const { sqlDir, databaseUri, client: dialect, attach, loadExtensions, authToken } = config; + const { glob: dirGlob, baseDir } = resolveSqlDir(sqlDir); + const outDir = config.outDir ?? baseDir; + validateDirectories(baseDir); const databaseClientResult = await createClient(databaseUri, dialect, attach, loadExtensions, authToken); if (databaseClientResult.isErr()) { @@ -153,11 +153,10 @@ async function compile(watch: boolean, config: TypeSqlConfig) { } await generateCrudTables(outDir, dbSchema.value, includeCrudTables); - const dirGlob = `${sqlDir}/**/*.sql`; const sqlFiles = globSync(dirGlob); - const filesGeneration = sqlFiles.map((sqlPath) => generateTsFile(databaseClient, sqlPath, resolveTsFilePath(sqlPath, sqlDir, outDir), dbSchema.value, isCrudFile(sqlDir, sqlPath))); + const filesGeneration = sqlFiles.map((sqlPath) => generateTsFile(databaseClient, sqlPath, resolveTsFilePath(sqlPath, baseDir, outDir), dbSchema.value, isCrudFile(baseDir, sqlPath))); await Promise.all(filesGeneration); if (config.generateIndexFiles !== false) { @@ -166,7 +165,7 @@ async function compile(watch: boolean, config: TypeSqlConfig) { if (watch) { console.log('watching mode!'); - watchDirectories(databaseClient, sqlDir, outDir, dbSchema.value, config); + watchDirectories(databaseClient, dirGlob, baseDir, outDir, dbSchema.value, config); } else { closeClient(databaseClient); } @@ -218,7 +217,8 @@ async function writeSql(stmtType: SqlGenOption, tableName: string, queryName: st } const columns = columnsOption.value; - const filePath = `${sqlDir}/${queryName}`; + const { baseDir } = resolveSqlDir(sqlDir); + const filePath = `${baseDir}/${queryName}`; const generatedOk = checkAndGenerateSql(client.type, filePath, stmtType, tableName, columns); return generatedOk; diff --git a/src/load-config.ts b/src/load-config.ts index 2991ba7e..94285955 100644 --- a/src/load-config.ts +++ b/src/load-config.ts @@ -1,5 +1,6 @@ import path from 'node:path'; import fs from 'node:fs'; +import { hasMagic } from 'glob'; import { TypeSqlConfig } from './types'; export function loadConfig(configPath: string): TypeSqlConfig { @@ -53,6 +54,34 @@ function isRelativeFilePath(uri: string): boolean { return typeof uri === 'string' && !uri.includes('://') && !path.isAbsolute(uri); } +/** + * Derives the file-discovery glob and the static base directory from `sqlDir`. + * + * - When `sqlDir` already targets `.sql` files (e.g. `src/queries/**\/*.sql`), it is used + * verbatim as the glob. + * - Otherwise `**\/*.sql` is appended, so both a plain directory (`./src`) and a directory + * glob (`src/**\/queries`) recursively match every `.sql` file beneath what they describe. + * + * The base directory is the glob's leading magic-free prefix; it anchors output-path + * resolution (`resolveTsFilePath`) and crud-file detection. + */ +export function resolveSqlDir(sqlDir: string): { glob: string; baseDir: string } { + const normalized = sqlDir.split(path.sep).join('/'); // glob requires forward slashes + const glob = /\.sql$/i.test(normalized) ? normalized : path.posix.join(normalized, '**/*.sql'); + const baseDir = hasMagic(glob, { magicalBraces: true }) ? globBaseDir(glob) : path.posix.dirname(glob); + return { glob, baseDir }; +} + +// Longest leading run of path segments that contain no glob magic. +function globBaseDir(glob: string): string { + const baseSegments: string[] = []; + for (const segment of glob.split('/')) { + if (hasMagic(segment, { magicalBraces: true })) break; + baseSegments.push(segment); + } + return baseSegments.join('/') || '.'; +} + export function resolveTsFilePath(sqlPath: string, sqlDir: string, outDir?: string) { const outputBase = outDir || sqlDir; const relativeDir = path.relative(sqlDir, path.dirname(sqlPath));