|
| 1 | +# sql-render |
| 2 | + |
| 3 | +> Type-safe `{{variable}}` renderer for .sql files |
| 4 | + |
| 5 | +sql-render is a TypeScript library for rendering SQL template files with variable substitution, schema validation, and SQL injection protection. It is designed for SQL engines that lack parameterized query support, such as AWS Athena and Trino DDL. |
| 6 | + |
| 7 | +## Install |
| 8 | + |
| 9 | +```bash |
| 10 | +npm install sql-render |
| 11 | +``` |
| 12 | + |
| 13 | +## API |
| 14 | + |
| 15 | +### defineQuery (generic mode) |
| 16 | + |
| 17 | +```typescript |
| 18 | +import { defineQuery } from 'sql-render'; |
| 19 | + |
| 20 | +const query = defineQuery<{ table: string; id: number }>('./query.sql'); |
| 21 | +const { sql } = query({ table: 'users', id: 42 }); |
| 22 | +``` |
| 23 | + |
| 24 | +Accepts string, number, and boolean parameters. Validates types at runtime, escapes strings, and checks for SQL injection patterns. |
| 25 | + |
| 26 | +### defineQuery (schema mode) |
| 27 | + |
| 28 | +```typescript |
| 29 | +import { defineQuery, schema } from 'sql-render'; |
| 30 | + |
| 31 | +const getEvents = defineQuery('./queries/getEvents.sql', { |
| 32 | + tableName: schema.identifier, |
| 33 | + status: schema.enum('active', 'pending', 'done'), |
| 34 | + startDate: schema.isoDate, |
| 35 | + orderBy: schema.identifier, |
| 36 | + limit: schema.positiveInt, |
| 37 | +}); |
| 38 | + |
| 39 | +const { sql } = getEvents({ |
| 40 | + tableName: 'prod_events', |
| 41 | + status: 'active', |
| 42 | + startDate: '2024-01-01', |
| 43 | + orderBy: 'created_at', |
| 44 | + limit: 100, |
| 45 | +}); |
| 46 | +``` |
| 47 | + |
| 48 | +Schema mode validates at define-time (schema keys must match template tokens) and at runtime (each value is validated by its type descriptor). Types are inferred automatically from the schema via `InferParams<S>`. |
| 49 | + |
| 50 | +### Schema types |
| 51 | + |
| 52 | +- `schema.string` - any string, checked against SQL injection denylist |
| 53 | +- `schema.number` - finite number (rejects NaN, Infinity) |
| 54 | +- `schema.boolean` - true or false |
| 55 | +- `schema.isoDate` - YYYY-MM-DD with calendar validation |
| 56 | +- `schema.isoTimestamp` - ISO 8601 with timezone (e.g. 2024-01-01T00:00:00Z) |
| 57 | +- `schema.identifier` - SQL identifier, up to 3 dot-separated parts (db.schema.table) |
| 58 | +- `schema.uuid` - RFC 4122 UUID |
| 59 | +- `schema.positiveInt` - integer greater than 0 |
| 60 | +- `schema.enum(...values)` - whitelist of allowed string values |
| 61 | +- `schema.s3Path` - S3 URI (s3://bucket/path) |
| 62 | + |
| 63 | +### Custom schema types |
| 64 | + |
| 65 | +Any object with a `validate(val: unknown) => boolean` method: |
| 66 | + |
| 67 | +```typescript |
| 68 | +const prodTable = { |
| 69 | + validate: (val: unknown) => typeof val === 'string' && val.startsWith('prod_'), |
| 70 | +}; |
| 71 | + |
| 72 | +const query = defineQuery('./query.sql', { |
| 73 | + table: prodTable, |
| 74 | + id: schema.positiveInt, |
| 75 | +}); |
| 76 | +``` |
| 77 | + |
| 78 | +### SQL file format |
| 79 | + |
| 80 | +SQL files use `{{variableName}}` placeholders: |
| 81 | + |
| 82 | +```sql |
| 83 | +SELECT event_id, event_name |
| 84 | +FROM {{tableName}} |
| 85 | +WHERE status = '{{status}}' |
| 86 | + AND created_at >= '{{startDate}}' |
| 87 | +ORDER BY {{orderBy}} |
| 88 | +LIMIT {{limit}} |
| 89 | +``` |
| 90 | + |
| 91 | +### Exports |
| 92 | + |
| 93 | +- `defineQuery` - main function (overloaded for generic and schema modes) |
| 94 | +- `schema` - built-in schema type descriptors |
| 95 | +- `SQL_INJECTION_PATTERNS` - array of { name, regex } patterns used for injection detection |
| 96 | +- Types: `TypeDescriptor`, `SchemaDefinition`, `InferParams`, `QueryResult`, `GenericQueryFn`, `SchemaQueryFn` |
| 97 | + |
| 98 | +## Key details |
| 99 | + |
| 100 | +- Zero runtime dependencies |
| 101 | +- Returns `{ sql: string }` - does not execute queries |
| 102 | +- Uses denylist + escape strategy, not parameterized queries |
| 103 | +- Single quotes are escaped: `'` becomes `''` |
| 104 | +- Template tokens must match `[a-zA-Z_][a-zA-Z0-9_]*` |
| 105 | +- Duplicate tokens in a template are deduplicated but all occurrences are replaced |
| 106 | +- Schema keys must exactly match template tokens (no missing, no extra) |
| 107 | +- Node.js >= 20 required |
0 commit comments