https://github.com/kikuchan/query-weaver
Query Weaver - SQL query builder using template string literal
https://github.com/kikuchan/query-weaver
builder helper literal node postgresql query sql string tagged template
Last synced: about 1 month ago
JSON representation
Query Weaver - SQL query builder using template string literal
- Host: GitHub
- URL: https://github.com/kikuchan/query-weaver
- Owner: kikuchan
- Created: 2023-02-16T09:41:18.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2025-03-19T10:17:49.000Z (about 2 months ago)
- Last Synced: 2025-03-21T13:23:02.117Z (about 2 months ago)
- Topics: builder, helper, literal, node, postgresql, query, sql, string, tagged, template
- Language: TypeScript
- Homepage:
- Size: 419 KB
- Stars: 3
- Watchers: 2
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Query Weaver
Compose SQL statements safely by leveraging template string literals.
## Install
```sh
$ npm install query-weaver
```## Basic Usage
### As an SQL Builder
```ts
import { sql } from 'query-weaver';const foo = 1, bar = 'Bar';
const query = sql`SELECT * FROM foobar WHERE foo = ${foo} AND bar = ${bar}`;
console.log(query);
// QueryFragments { text: [Getter], values: [Getter], embed: [Getter] }console.log(JSON.stringify(query, null, 2));
// {
// "text": "SELECT * FROM foobar WHERE foo = $1 AND bar = $2",
// "values": [
// 1,
// "Bar"
// ],
// "embed": "SELECT * FROM foobar WHERE foo = '1' AND bar = 'Bar'"
// }
```The query is executed using **placeholders** in the database, which makes string-value concatenation safe. You can also obtain an embedded string version of the query so that you can easily debug it by copying and pasting.
### Inject a Query Helper
```ts
import { withQueryHelper } from 'query-weaver';
import pg from 'pg';const db = withQueryHelper(new pg.Pool());
const foo = 1, bar = 'Bar';
const { rows } =
await db.query`SELECT * FROM foobar WHERE foo = ${foo} AND bar = ${bar}`;console.log(rows);
// [ { foo: 1, bar: 'Bar' } ]db.end();
```The `withQueryHelper` utility wraps the original object with a Query Helper, providing utility functions as if they were built into the original object. Essentially, these are shorthand functions that bridge the original `query` and the Query Weaver functions, with the most notable features being debugging and transaction support.
## Utilities
### WHERE Builder
`WHERE_AND` / `WHERE_OR` / `AND` / `OR` / `WHERE` (`WHERE_AND` alias)
```ts
import { sql, WHERE, OR } from 'query-weaver';const a = 1, b = 'string', c = null, d = 5, e = false, f = [1, 2, 3, 4, 5];
console.log(String(sql`SELECT * FROM foobar ${WHERE({ a, b, c }, OR({ d, e }))}`));
// SELECT * FROM foobar WHERE ((a = '1') AND (b = 'string') AND (c IS NULL) AND (((d = '5') OR (e = false))))const q = sql`SELECT * FROM foobar ${WHERE(
{
a: 10,
b: 'string',
c: sql`IS UNKNOWN`,
d: sql`BETWEEN ${a} AND ${d}`,
},
'e IS NULL',
sql`f = ANY (${f})`
)}`;
console.log(q.text);
// SELECT * FROM foobar WHERE ((a = $1) AND (b = $2) AND (c IS UNKNOWN) AND (d BETWEEN $3 AND $4) AND (e IS NULL) AND (f = ANY ($5)))console.log(q.embed);
// SELECT * FROM foobar WHERE ((a = '10') AND (b = 'string') AND (c IS UNKNOWN) AND (d BETWEEN '1' AND '5') AND (e IS NULL) AND (f = ANY (ARRAY['1','2','3','4','5'])))
```### JSON Builder
`json`
```js
import pg from 'pg';
import { withQueryHelper, json } from 'query-weaver';const db = withQueryHelper(new pg.Pool());
const id = 10;
const obj = { b: 'string', c: [1, 2, 'X'], d: { e: null, f: undefined } };const row =
await db.getRow`SELECT * FROM jsonb_to_record(${json`{"a": ${obj}, "b": ${id}}`}) AS (a jsonb, b int);`;console.log(row);
// {
// a: { b: 'string', c: [ 1, 2, 'X' ], d: { e: null } },
// b: 10,
// }db.end();
```### VALUES Builder
`buildValues` / `sql.values`
```js
sql.values([[1, 2, 3], ...]); // => VALUES (1, 2, 3), (...), ...
sql.values([{ a: 1, b: 2, c: 3 }, ...]); // => VALUES (1, 2, 3), (...), ...
```### Key Builder
`buildKeys` / `sql.keys`
```js
sql.keys({ a: 1, b: 2, c: 3 }); // => (a, b, c)
sql.keys([{ a: 1, b: 2, c: 3 }, ...]); // => (a, b, c)
```### Raw Builder
`raw`
```ts
import { sql, raw } from 'query-weaver';console.log(JSON.stringify(sql`SELECT * FROM foobar WHERE ${raw("bar LIKE '%something%'")}`));
// {"text":"SELECT * FROM foobar WHERE bar LIKE '%something%'","values":[],"embed":"SELECT * FROM foobar WHERE bar LIKE '%something%'"}
```### INSERT Builder and Helper
`buildInsert` / `sql.insert` builder, and `insert` helper
```js
sql.insert(tableName, { ...fieldValuePairs }); // => sql`INSERT INTO ...`
db.insert(tableName, { ...fieldValuePairs }); // => db.query`INSERT INTO ...`// Bulk insert
sql.insert(tableName, [{ ...fieldValuePairs }, ...]); // => sql`INSERT INTO ... VALUES (...), (...), ...`
db.insert(tableName, [{ ...fieldValuePairs }, ...]); // => db.query`INSERT INTO ... VALUES (...), (...), ...`
```### UPDATE Builder and Helper
`buildUpdate` / `sql.update` builder, and `update` helper
```js
sql.update(tableName, { ...fieldValuePairs }, { ...whereCondition }); // => sql`UPDATE ...`
db.update(tableName, { ...fieldValuePairs }, { ...whereCondition }); // => db.query`UPDATE ...`
```### DELETE Builder and Helper
`buildDelete` / `sql.delete` builder, and `delete` helper
```js
sql.delete(tableName, { ...whereCondition }); // => sql`DELETE FROM ...`
db.delete(tableName, { ...whereCondition }); // => db.query`DELETE FROM ...`
```### Transaction Helper
`begin` helper
```js
db.begin(() => {
db.delete(...);
db.insert(...);
});
```If an error occurs, the transaction is safely rolled back.
**NOTE:** Transactions can be nested, but only the outermost transaction is effective.## Low-level APIs
An object created by the `sql` keyword behaves like a simple object with the following properties: `text`, `values`, and `embed`.
In addition, the object provides the following APIs:
- `append(...)` / `push(...)`
- Appends a **raw SQL string** or an object created by `sql` to the query.
- `join(glue = ', ')` / `prefix(prefix)` / `suffix(suffix)` / `empty(empty)`
- Sets the glue, prefix, suffix, or empty string for `toString()`, respectively.
- `setSewingPattern(prefix, glue, suffix, empty)`
- Sets the sewing pattern for `toString()` all at once.
- `toString(opts?)`
- Constructs the SQL string using the sewing pattern and any provided `opts` settings.To create a query object, you can use:
- `` sql`template string with ${value}` `` / `` json`{"a": "b", "c": ${value}}` ``
- Creates a query object with values that are automatically escaped.
- `sql(value, ...)` / `json(value)`
- Creates a query object from values that are automatically escaped.These APIs can be used, for example, to construct an `IN` clause as follows:
```ts
import { sql } from 'query-weaver';console.log(sql`SELECT * FROM foobar WHERE foo IN (${sql(1, 2, 3).join()})`.embed);
// SELECT * FROM foobar WHERE foo IN ('1', '2', '3')const a = [1, 2, 3];
console.log(sql`SELECT * FROM foobar WHERE foo IN (${sql(...a).join()})`.text);
// SELECT * FROM foobar WHERE foo IN ($1, $2, $3)
```### Caveats
- Only `sql` and `json` accept a template string literal.
- The actual SQL statement executed on the database may sometimes differ between `[.text, .values]` and `.embed` due to differences in serialization functions.### DEBUG
You can easily access the statements and results when using the Query Helper. For example, the following code records the session:
```js
import zlib from 'node:zlib'const db = withQueryHelper(new pg.Pool(), {
onError: (ctx, error) => console.error(JSON.stringify({ error: zlib.gzipSync(JSON.stringify({ ...ctx, error })).toString('base64') })),
beforeQuery: (ctx) => console.log(JSON.stringify({ query: zlib.gzipSync(JSON.stringify(ctx)).toString('base64') })),
afterQuery: (ctx, result) => console.log(JSON.stringify({ result: zlib.gzipSync(JSON.stringify({ ...ctx, result })).toString('base64') })),
});
```After running this, you will see output similar to the following in the console:
```json
{"query":"H4sIAAAAAAACA6tWKkmtKFGyUgp29XF1DlHQUnAL8vdVSMvPT0osUgj3cA1yBXEUbBVUDJV0lMoSc0pTi5Wsog1jdZRSc5NSU4jRqm6oDtRblFpcmgO0qlopOT83NzEPoRUkmV/unF+aB5Q2BHNAdlQrAbWDBYAGAhU7Acna2NpaABybVha0AAAA"}
```You can extract the contents by executing the following command:
```sh
% echo '... base64part' | base64 -d | zcat | jq -r .# Or, if you're using X11, select the base64 part and then:
% xclip -o | base64 -d | zcat | jq -r .
``````json
{
"text": "SELECT * FROM foobar WHERE foo = $1",
"values": [
1
],
"embed": "SELECT * FROM foobar WHERE foo = '1'",
"result": {
"command": "SELECT",
"rowCount": 1,
"rows": [
{
"foo": 1,
"bar": "Bar"
}
]
}
}
```It is very handy to replay the session as follows:
```sh
% xclip -o | base64 -d | zcat | jq -r .embed | psql
foo | bar
-----+-----
1 | Bar
(1 row)# Or, you can create a shell script named `xclip-query` and then run:
% xclip-query .embed | psql
```### Query Helper with a Custom `query` Function
The underlying `query` function, which is used to perform queries, can be replaced by using the `query` option. This allows you to use Prisma, TypeORM, or even raw SQLite object with Query Helper. For example:
```js
const db = withQueryHelper(new PrismaClient(), {
query: QueryHelper.prisma,
});console.log(await db.query`...`);
``````js
const db = withQueryHelper(new DataSource({ ... }), {
query: QueryHelper.typeorm,
});console.log(await db.query`...`);
``````js
import { DatabaseSync } from 'node:sqlite';const db = withQueryHelper(new DatabaseSync(), {
query: QueryHelper.sqlite,
});console.log(await db.query`...`);
``````ts
import { withQueryHelper } from 'query-weaver';const queryable = async function (this: object, { text, values }: QueryConfig) {
return {
rows: [this], // `this` will be the object you passed to the constructor
rowCount: 1,
}
}const db = withQueryHelper({ some: 'Hello, object' }, {
query: queryable,
});console.log(await db.getRow`HELLO QUERY`);
// { some: 'Hello, object' }
```That's it!
Now you can use Query Weaver interfaces on the objects.