Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Options:
| :--------------------- | :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------ |
| **client** | Database client driver to use. | <ul><li>pg</li><li>mysql2</li><li>better-sqlite3</li><li>libsql</li><li>bun:sqlite</li><li>d1</li></ul> |
| **databaseUri** | Connection string for the database. Supports environment variables (`${VAR_NAME}`). | <ul><li>mysql://root:password@localhost/mydb</li><li>./database.sqlite</li></ul> |
| **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. | <ul><li>`./src`</li><li>`./src/**/queries`</li><li>`./src/**/*.query.sql`</li></ul> |
| **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 |
Expand Down
24 changes: 12 additions & 12 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand All @@ -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()) {
Expand All @@ -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) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
29 changes: 29 additions & 0 deletions src/load-config.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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));
Expand Down