4747 schema.ts
4848 types.ts
4949 validator.ts
50+ writer.ts
5051tests/
5152 fixtures/
5253 duplicate-vars.sql
@@ -166,16 +167,17 @@ jobs:
166167import { loadTemplate } from './loader';
167168import { validateAndConvert, escapeValue } from './validator';
168169import { render } from './renderer';
170+ import { writeRendered } from './writer';
169171import type { SchemaDefinition } from './schema';
170172import type {
171- QueryResult, GenericQueryFn, SchemaQueryFn,
173+ QueryResult, QueryOptions, GenericQueryFn, SchemaQueryFn,
172174} from './types';
173175
174176export { SQL_INJECTION_PATTERNS } from './validator';
175177export { schema } from './schema';
176178export type { TypeDescriptor, SchemaDefinition, InferParams } from './schema';
177179export type {
178- QueryResult, GenericQueryFn, SchemaQueryFn,
180+ QueryResult, QueryOptions, GenericQueryFn, SchemaQueryFn,
179181} from './types';
180182
181183export function defineQuery<S extends SchemaDefinition>(
@@ -188,7 +190,7 @@ export function defineQuery<T extends Record<string, string | number | boolean>>
188190export function defineQuery(
189191 filePath: string,
190192 schemaDef?: SchemaDefinition,
191- ): (params: Record<string, unknown>) => QueryResult {
193+ ): (params: Record<string, unknown>, options?: QueryOptions ) => QueryResult {
192194 const { template, tokens } = loadTemplate(filePath);
193195
194196 if (schemaDef) {
@@ -218,7 +220,7 @@ export function defineQuery(
218220 }
219221 }
220222
221- return (params) => {
223+ return (params, options ) => {
222224 const paramKeys = Object.keys(params);
223225
224226 const missing = tokens.filter((tok) => !paramKeys.includes(tok));
@@ -252,7 +254,13 @@ export function defineQuery(
252254 }
253255 }
254256
255- return { sql: render(template, values) };
257+ const sql = render(template, values);
258+
259+ if (options?.exportTo) {
260+ writeRendered(options.exportTo, sql);
261+ }
262+
263+ return { sql };
256264 };
257265}
258266````
@@ -383,9 +391,16 @@ export interface QueryResult {
383391 sql: string;
384392}
385393
386- export type GenericQueryFn<T> = (params: T) => QueryResult;
394+ export interface QueryOptions {
395+ exportTo?: string;
396+ }
387397
388- export type SchemaQueryFn<S extends SchemaDefinition> = (params: InferParams<S>) => QueryResult;
398+ export type GenericQueryFn<T> = (params: T, options?: QueryOptions) => QueryResult;
399+
400+ export type SchemaQueryFn<S extends SchemaDefinition> = (
401+ params: InferParams<S>,
402+ options?: QueryOptions,
403+ ) => QueryResult;
389404````
390405
391406## File: src/validator.ts
@@ -453,6 +468,18 @@ export function validateAndConvert(key: string, value: unknown): string {
453468}
454469````
455470
471+ ## File: src/writer.ts
472+ ````typescript
473+ import fs from 'node:fs';
474+ import path from 'node:path';
475+
476+ export function writeRendered(filePath: string, sql: string): void {
477+ const resolved = path.resolve(filePath);
478+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
479+ fs.writeFileSync(resolved, sql, 'utf-8');
480+ }
481+ ````
482+
456483## File: tests/fixtures/duplicate-vars.sql
457484````sql
458485SELECT
@@ -498,8 +525,10 @@ WHERE
498525
499526## File: tests/define-query.test.ts
500527````typescript
528+ import fs from 'node:fs';
529+ import os from 'node:os';
501530import path from 'node:path';
502- import { describe, it, expect } from 'vitest';
531+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
503532import { defineQuery, schema } from '../src/index';
504533
505534const fixture = (name: string) => path.join(__dirname, 'fixtures', name);
@@ -740,6 +769,54 @@ describe('defineQuery — schema mode', () => {
740769 });
741770 });
742771
772+ describe('exportTo option', () => {
773+ let tmpDir: string;
774+
775+ beforeEach(() => {
776+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sql-render-'));
777+ });
778+
779+ afterEach(() => {
780+ fs.rmSync(tmpDir, { recursive: true, force: true });
781+ });
782+
783+ it('writes rendered sql to the given file', () => {
784+ const query = defineQuery(fixture('simple.sql'), {
785+ table: schema.identifier,
786+ id: schema.number,
787+ });
788+ const outPath = path.join(tmpDir, 'rendered.sql');
789+ const { sql } = query({ table: 'users', id: 33 }, { exportTo: outPath });
790+ expect(fs.readFileSync(outPath, 'utf-8')).toBe(sql);
791+ });
792+
793+ it('creates parent directories recursively', () => {
794+ const query = defineQuery(fixture('simple.sql'), {
795+ table: schema.identifier,
796+ id: schema.number,
797+ });
798+ const outPath = path.join(tmpDir, 'nested', 'deep', 'out.sql');
799+ query({ table: 'users', id: 1 }, { exportTo: outPath });
800+ expect(fs.existsSync(outPath)).toBe(true);
801+ });
802+
803+ it('does not write a file when exportTo is omitted', () => {
804+ const query = defineQuery(fixture('simple.sql'), {
805+ table: schema.identifier,
806+ id: schema.number,
807+ });
808+ query({ table: 'users', id: 1 });
809+ expect(fs.readdirSync(tmpDir)).toHaveLength(0);
810+ });
811+
812+ it('works in generic mode', () => {
813+ const query = defineQuery<{ table: string; id: number }>(fixture('simple.sql'));
814+ const outPath = path.join(tmpDir, 'generic.sql');
815+ const { sql } = query({ table: 'users', id: 7 }, { exportTo: outPath });
816+ expect(fs.readFileSync(outPath, 'utf-8')).toBe(sql);
817+ });
818+ });
819+
743820 describe('custom schema types', () => {
744821 it('accepts a custom type descriptor', () => {
745822 const prodTable = {
@@ -1337,12 +1414,20 @@ ORDER BY {{orderBy}}
13371414LIMIT {{limit}}
13381415```
13391416
1417+ ### Exporting rendered SQL
1418+
1419+ Pass `{ exportTo: string }` as the second argument to write the rendered SQL to disk. Missing parent directories are created recursively.
1420+
1421+ ```typescript
1422+ const { sql } = getEvents(params, { exportTo: './debug/getEvents.sql' });
1423+ ```
1424+
13401425### Exports
13411426
13421427- `defineQuery` - main function (overloaded for generic and schema modes)
13431428- `schema` - built-in schema type descriptors
13441429- `SQL_INJECTION_PATTERNS` - array of { name, regex } patterns used for injection detection
1345- - Types: `TypeDescriptor`, `SchemaDefinition`, `InferParams`, `QueryResult`, `GenericQueryFn`, `SchemaQueryFn`
1430+ - Types: `TypeDescriptor`, `SchemaDefinition`, `InferParams`, `QueryResult`, `QueryOptions`, ` GenericQueryFn`, `SchemaQueryFn`
13461431
13471432## Key details
13481433
@@ -1605,6 +1690,17 @@ const query = defineQuery('./query.sql', {
16051690
16061691A type descriptor is any object with a `validate(val: unknown) => boolean` method.
16071692
1693+ ## Exporting Rendered SQL
1694+
1695+ Pass an `exportTo` path to write the rendered SQL to disk for debugging or audit purposes. Missing parent directories are created automatically.
1696+
1697+ ```typescript
1698+ const { sql } = getEvents(
1699+ { tableName: 'prod_events', status: 'active', startDate: '2022-02-22', orderBy: 'created_at', limit: 99 },
1700+ { exportTo: './debug/getEvents.sql' },
1701+ );
1702+ ```
1703+
16081704## SQL Injection Protection
16091705
16101706`schema.string` and the generic `string` type check values against built-in patterns:
@@ -1699,7 +1795,7 @@ Two machine-readable summaries are maintained for AI consumption, following the
16991795## v0.2
17001796
17011797- [ ] **Inline template support** - Allow passing SQL strings directly without requiring a `.sql` file, reducing boilerplate for short one-off queries
1702- - [ ] **Export rendered SQL to file** - Add an option to write the rendered SQL to a file for debugging and audit purposes
1798+ - [x ] **Export rendered SQL to file** - Add an option to write the rendered SQL to a file for debugging and audit purposes
17031799- [ ] **TypeDescriptor `description` field** - Add an optional `description` property to `TypeDescriptor` so schema validation errors can display the expected format (e.g. `Expected: ISO date YYYY-MM-DD`)
17041800
17051801## Maybe
0 commit comments