@filtron/sql
Advanced tools
+2
-2
@@ -1,4 +0,4 @@ | ||
| function F(q,E={}){let R={params:[],parameterStyle:E.parameterStyle??"numbered",fieldMapper:E.fieldMapper??((H)=>H),valueMapper:E.valueMapper??((H)=>H),paramIndex:E.startIndex??1};return{sql:$(q,R),params:R.params}}function $(q,E){switch(q.type){case"or":return T(q,E);case"and":return b(q,E);case"not":return z(q,E);case"comparison":return A(q,E);case"oneOf":return Q(q,E);case"notOneOf":return U(q,E);case"exists":return C(q,E);case"booleanField":return G(q,E);case"range":return J(q,E);default:let R=q;throw Error(`Unknown node type: ${q.type}`)}}function T(q,E){let R=$(q.left,E),S=$(q.right,E);return`(${R} OR ${S})`}function b(q,E){let R=$(q.left,E),S=$(q.right,E);return`(${R} AND ${S})`}function z(q,E){return`NOT (${$(q.expression,E)})`}function A(q,E){let R=E.fieldMapper(q.field),S=M(q.operator),H=D(q.value);if(q.operator==="~")H=E.valueMapper(H);let W=j(H,E);return`${R} ${S} ${W}`}function Q(q,E){let R=E.fieldMapper(q.field),S=q.values.map((W)=>D(W));if(S.length===0)return"1 = 0";let H=S.map((W)=>j(W,E));return`${R} IN (${H.join(", ")})`}function U(q,E){let R=E.fieldMapper(q.field),S=q.values.map((W)=>D(W));if(S.length===0)return"1 = 1";let H=S.map((W)=>j(W,E));return`${R} NOT IN (${H.join(", ")})`}function C(q,E){return`${E.fieldMapper(q.field)} IS NOT NULL`}function G(q,E){let R=E.fieldMapper(q.field),S=j(!0,E);return`${R} = ${S}`}function J(q,E){let R=E.fieldMapper(q.field),S=j(q.min,E),H=j(q.max,E);return`${R} BETWEEN ${S} AND ${H}`}function M(q){switch(q){case"=":case":":return"=";case"!=":return"!=";case"~":return"LIKE";case">":return">";case">=":return">=";case"<":return"<";case"<=":return"<=";default:let E=q;throw Error(`Unknown operator: ${q}`)}}function D(q){switch(q.type){case"string":return q.value;case"number":return q.value;case"boolean":return q.value;case"identifier":return q.value;default:let E=q;throw Error(`Unknown value type: ${q.type}`)}}function j(q,E){if(E.params.push(q),E.parameterStyle==="numbered"){let R=`$${E.paramIndex}`;return E.paramIndex++,R}return"?"}function y(q){return q.replace(/\\/g,"\\\\").replace(/%/g,"\\%").replace(/_/g,"\\_")}function N(q){return`%${y(String(q))}%`}function X(q){return`${y(String(q))}%`}function Y(q){return`%${y(String(q))}`}export{F as toSQL,Y as suffix,X as prefix,y as escapeLike,N as contains}; | ||
| function b(q,E={}){let R={params:[],parameterStyle:E.parameterStyle??"numbered",fieldMapper:E.fieldMapper??((H)=>H),valueMapper:E.valueMapper??((H)=>H),paramIndex:E.startIndex??1};return{sql:W(q,R),params:R.params}}function W(q,E){switch(q.type){case"or":return z(q,E);case"and":return A(q,E);case"not":return N(q,E);case"comparison":return Q(q,E);case"oneOf":return F(E.fieldMapper(q.field),q.values,E,!1);case"notOneOf":return F(E.fieldMapper(q.field),q.values,E,!0);case"exists":return U(q,E);case"booleanField":return G(q,E);case"range":return J(q,E);default:let R=q;throw Error(`Unknown node type: ${q.type}`)}}function z(q,E){let R=W(q.left,E),S=W(q.right,E);return`(${R} OR ${S})`}function A(q,E){let R=W(q.left,E),S=W(q.right,E);return`(${R} AND ${S})`}function N(q,E){return`NOT (${W(q.expression,E)})`}function Q(q,E){let R=E.fieldMapper(q.field),S=M(q.operator),H=T(q.value);if(q.operator==="~")H=E.valueMapper(H);let j=$(H,E);return`${R} ${S} ${j}`}function F(q,E,R,S){let H=E.length;if(H===0)return S?"1 = 1":"1 = 0";let j=[];for(let D=0;D<H;D++)j.push($(T(E[D]),R));return`${q} ${S?"NOT IN":"IN"} (${j.join(", ")})`}function U(q,E){return`${E.fieldMapper(q.field)} IS NOT NULL`}function G(q,E){let R=E.fieldMapper(q.field),S=$(!0,E);return`${R} = ${S}`}function J(q,E){let R=E.fieldMapper(q.field),S=$(q.min,E),H=$(q.max,E);return`${R} BETWEEN ${S} AND ${H}`}function M(q){switch(q){case"=":case":":return"=";case"!=":return"!=";case"~":return"LIKE";case">":return">";case">=":return">=";case"<":return"<";case"<=":return"<=";default:let E=q;throw Error(`Unknown operator: ${q}`)}}function T(q){switch(q.type){case"string":return q.value;case"number":return q.value;case"boolean":return q.value;case"identifier":return q.value;default:let E=q;throw Error(`Unknown value type: ${q.type}`)}}function $(q,E){if(E.params.push(q),E.parameterStyle==="numbered")return`$${E.paramIndex++}`;return"?"}function y(q){if(!/[\\%_]/.test(q))return q;return q.replace(/[\\%_]/g,(E)=>"\\"+E)}function O(q){return`%${y(String(q))}%`}function X(q){return`${y(String(q))}%`}function Y(q){return`%${y(String(q))}`}export{b as toSQL,Y as suffix,X as prefix,y as escapeLike,O as contains}; | ||
| //# debugId=7C5E6C130D5FBAFC64756E2164756E21 | ||
| //# debugId=0FE3C3872A388FA764756E2164756E21 | ||
| //# sourceMappingURL=index.js.map |
@@ -21,3 +21,3 @@ /** | ||
| * Parameter placeholder style | ||
| * - 'numbered': PostgreSQL/DuckDB style ($1, $2, $3) | ||
| * - 'numbered': PostgreSQL/CockroachDB style ($1, $2, $3) | ||
| * - 'question': MySQL/SQLite/DuckDB style (?, ?, ?) | ||
@@ -24,0 +24,0 @@ * @default 'numbered' |
+71
-71
| { | ||
| "$schema": "https://json.schemastore.org/package.json", | ||
| "name": "@filtron/sql", | ||
| "version": "1.1.0", | ||
| "description": "SQL WHERE clause generator for Filtron AST with parameterized queries", | ||
| "keywords": [ | ||
| "filtron", | ||
| "sql", | ||
| "where-clause", | ||
| "query-builder", | ||
| "ast", | ||
| "typescript", | ||
| "duckdb", | ||
| "postgresql", | ||
| "mysql", | ||
| "sqlite" | ||
| ], | ||
| "homepage": "https://github.com/jbergstroem/filtron#readme", | ||
| "bugs": { | ||
| "url": "https://github.com/jbergstroem/filtron/issues" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/jbergstroem/filtron.git", | ||
| "directory": "packages/sql" | ||
| }, | ||
| "license": "MIT", | ||
| "author": "Johan Bergström <bugs@bergstroem.nu>", | ||
| "type": "module", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.js", | ||
| "default": "./dist/index.js" | ||
| } | ||
| }, | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "files": [ | ||
| "dist", | ||
| "README.md", | ||
| "LICENSE" | ||
| ], | ||
| "scripts": { | ||
| "bench": "bun --expose-gc run benchmark.ts", | ||
| "build": "bun build --minify --splitting --sourcemap=linked --outdir=dist --external @filtron/core index.ts && tsc", | ||
| "prepublishOnly": "bun run build && bun test", | ||
| "test": "bun test", | ||
| "typecheck": "tsc --noemit" | ||
| }, | ||
| "dependencies": { | ||
| "@filtron/core": "1.0.0" | ||
| }, | ||
| "peerDependencies": { | ||
| "typescript": "5.9.3" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
| "typescript": { | ||
| "optional": true | ||
| } | ||
| }, | ||
| "devDependencies": { | ||
| "@types/bun": "1.3.3", | ||
| "mitata": "1.0.34", | ||
| "typescript": "5.9.3" | ||
| }, | ||
| "engines": { | ||
| "node": ">=20.0.0", | ||
| "bun": ">=1.1.0" | ||
| }, | ||
| "packageManager": "bun@1.3.3" | ||
| "$schema": "https://json.schemastore.org/package.json", | ||
| "name": "@filtron/sql", | ||
| "version": "1.2.0", | ||
| "description": "Filtron helper: generate safe, parameterized SQL WHERE clauses from filter expressions", | ||
| "keywords": [ | ||
| "ast", | ||
| "duckdb", | ||
| "filtron", | ||
| "mysql", | ||
| "postgresql", | ||
| "query-builder", | ||
| "sql", | ||
| "sqlite", | ||
| "typescript", | ||
| "where-clause" | ||
| ], | ||
| "homepage": "https://github.com/jbergstroem/filtron#readme", | ||
| "bugs": { | ||
| "url": "https://github.com/jbergstroem/filtron/issues" | ||
| }, | ||
| "license": "MIT", | ||
| "author": "Johan Bergström <bugs@bergstroem.nu>", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/jbergstroem/filtron.git", | ||
| "directory": "packages/sql" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "LICENSE", | ||
| "README.md" | ||
| ], | ||
| "type": "module", | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.js", | ||
| "default": "./dist/index.js" | ||
| } | ||
| }, | ||
| "scripts": { | ||
| "bench": "bun --expose-gc run benchmark.ts", | ||
| "build": "bun build --production --splitting --sourcemap=linked --outdir=dist --external @filtron/core index.ts && tsc", | ||
| "prepublishOnly": "bun run build && bun test", | ||
| "test": "bun test", | ||
| "typecheck": "tsc --noemit" | ||
| }, | ||
| "dependencies": { | ||
| "@filtron/core": "workspace:*" | ||
| }, | ||
| "devDependencies": { | ||
| "@filtron/benchmark": "workspace:*", | ||
| "@types/bun": "catalog:", | ||
| "typescript": "catalog:" | ||
| }, | ||
| "peerDependencies": { | ||
| "typescript": "catalog:" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
| "typescript": { | ||
| "optional": true | ||
| } | ||
| }, | ||
| "engines": { | ||
| "bun": ">=1.1.0", | ||
| "node": ">=20.0.0" | ||
| }, | ||
| "packageManager": "bun@1.3.3" | ||
| } |
+132
-24
| # @filtron/sql | ||
| SQL WHERE clause generator for [Filtron](https://github.com/jbergstroem/filtron) AST with parameterized queries. | ||
| Convert Filtron AST to parameterized SQL WHERE clauses. | ||
| [](https://www.npmjs.com/package/@filtron/sql) | ||
| [](https://bundlephobia.com/package/@filtron/sql) | ||
| [](https://codecov.io/gh/jbergstroem/filtron) | ||
| ## Installation | ||
| ```bash | ||
| bun add @filtron/sql | ||
| npm install @filtron/sql | ||
| ``` | ||
| ## Quick Start | ||
| ## Usage | ||
@@ -24,3 +28,3 @@ ```typescript | ||
| const users = await db.query(`SELECT * FROM users WHERE ${sql}`, params); | ||
| await db.query(`SELECT * FROM users WHERE ${sql}`, params); | ||
| } | ||
@@ -31,45 +35,149 @@ ``` | ||
| ### `toSQL(ast, options?)` | ||
| ### `toSQL(ast, options?): SQLResult` | ||
| Converts a Filtron AST to a parameterized SQL WHERE clause. | ||
| **Options:** | ||
| **Returns:** | ||
| | Option | Type | Description | | ||
| |--------|------|-------------| | ||
| | `parameterStyle` | `'numbered'` \| `'question'` | Placeholder style: `$1` (default) or `?` | | ||
| | `fieldMapper` | `(field) => string` | Transform field names to column names | | ||
| | `valueMapper` | `(value) => value` | Transform values (useful for LIKE wildcards) | | ||
| | `startIndex` | `number` | Starting parameter index (default: `1`) | | ||
| ```typescript | ||
| interface SQLResult { | ||
| sql: string; // The WHERE clause (without "WHERE" keyword) | ||
| params: unknown[]; // Parameter values in order | ||
| } | ||
| ``` | ||
| #### Options | ||
| | Option | Type | Default | Description | | ||
| | ---------------- | ----------------------------- | ------------ | ---------------------------------------- | | ||
| | `parameterStyle` | `"numbered"` \| `"question"` | `"numbered"` | Placeholder format | | ||
| | `fieldMapper` | `(field: string) => string` | `undefined` | Transform field names to column names | | ||
| | `valueMapper` | `(value: unknown) => unknown` | `undefined` | Transform values before parameterization | | ||
| | `startIndex` | `number` | `1` | Starting index for numbered placeholders | | ||
| #### Parameter styles | ||
| **Numbered (`$1`, `$2`, ...)** — PostgreSQL, CockroachDB: | ||
| ```typescript | ||
| const { sql, params } = toSQL(ast); | ||
| // sql: "(age > $1 AND status = $2)" | ||
| ``` | ||
| **Question marks (`?`, `?`, ...)** — MariaDB, SQLite, DuckDB: | ||
| ```typescript | ||
| const { sql, params } = toSQL(ast, { | ||
| parameterStyle: "question", // for MySQL/SQLite | ||
| parameterStyle: "question", | ||
| }); | ||
| // sql: "(age > ? AND status = ?)" | ||
| ``` | ||
| #### Examples | ||
| **Custom field mapping:** | ||
| ```typescript | ||
| const { sql, params } = toSQL(ast, { | ||
| fieldMapper: (field) => `users.${field}`, | ||
| }); | ||
| // "age > 18" becomes "users.age > $1" | ||
| ``` | ||
| ### Helper Functions | ||
| **Table-qualified columns:** | ||
| Helpers for use with `valueMapper` when using the LIKE operator (`~`): | ||
| ```typescript | ||
| const { sql, params } = toSQL(ast, { | ||
| fieldMapper: (field) => `"${field}"`, // Quote column names | ||
| }); | ||
| ``` | ||
| **Start index (for combining queries):** | ||
| ```typescript | ||
| const { sql, params } = toSQL(ast, { | ||
| startIndex: 3, | ||
| }); | ||
| // Placeholders start at $3 | ||
| ``` | ||
| ### LIKE helpers | ||
| Helper functions for the contains operator (`~`): | ||
| ```typescript | ||
| import { toSQL, contains, prefix, suffix, escapeLike } from "@filtron/sql"; | ||
| ``` | ||
| // contains("foo") → "%foo%" | ||
| // prefix("foo") → "foo%" | ||
| // suffix("foo") → "%foo" | ||
| // escapeLike("foo%bar") → "foo\\%bar" | ||
| | Function | Input | Output | Use case | | ||
| | ------------ | ------- | --------- | -------------------- | | ||
| | `contains` | `"foo"` | `"%foo%"` | Substring match | | ||
| | `prefix` | `"foo"` | `"foo%"` | Starts with | | ||
| | `suffix` | `"foo"` | `"%foo"` | Ends with | | ||
| | `escapeLike` | `"a%b"` | `"a\\%b"` | Escape special chars | | ||
| const { sql, params } = toSQL(ast, { valueMapper: contains }); | ||
| **Usage with valueMapper:** | ||
| ```typescript | ||
| const { sql, params } = toSQL(ast, { | ||
| valueMapper: contains, | ||
| }); | ||
| // Query "name ~ 'john'" produces params: ["%john%"] | ||
| ``` | ||
| ## Performance | ||
| For APIs with repeated filter queries, cache parsed results to avoid redundant parsing: | ||
| ```typescript | ||
| const cache = new Map<string, SQLResult>(); | ||
| function getFilterSQL(filter: string): SQLResult | null { | ||
| const cached = cache.get(filter); | ||
| if (cached) return cached; | ||
| const result = parse(filter); | ||
| if (!result.success) return null; | ||
| const sql = toSQL(result.ast, { parameterStyle: "question" }); | ||
| cache.set(filter, sql); | ||
| return sql; | ||
| } | ||
| ``` | ||
| Consider using an LRU cache with a size limit for production: | ||
| ```typescript | ||
| // https://github.com/isaacs/node-lru-cache | ||
| import { LRUCache } from "lru-cache"; | ||
| const cache = new LRUCache<string, SQLResult>({ max: 1000 }); | ||
| ``` | ||
| Caching is effective when: | ||
| - Users frequently repeat the same filter queries | ||
| - Filter expressions are complex (nested `AND`/`OR` conditions) | ||
| For simple queries or unique filters, caching overhead is not worthwhile. | ||
| Caching at the HTTP level (database query results) is often more effective than caching a parsed Filtron SQLResult. Database queries are typically orders of magnitude slower than processing the Filtron query. | ||
| ## Security | ||
| This library generates parameterized queries to prevent SQL injection: | ||
| All queries are parameterized to prevent SQL injection: | ||
| ```typescript | ||
| const { sql, params } = toSQL(ast); | ||
| db.query(`SELECT * FROM users WHERE ${sql}`, params); | ||
| // Params: ["admin' OR '1'='1"] - safely escaped | ||
| // User input with SQL injection attempt | ||
| const result = parse('name = "admin\' OR \'1\'=\'1"'); | ||
| const { sql, params } = toSQL(result.ast); | ||
| // sql: "(name = $1)" | ||
| // params: ["admin' OR '1'='1"] — treated as literal string value | ||
| ``` | ||
| Never interpolate user input directly into SQL. Always use the `params` array with your database driver's parameterized query support. | ||
| ## License | ||
| MIT |
Sorry, the diff of this file is not supported yet
27439
9.8%182
145.95%- Removed
- Removed
- Removed
Updated