@vanit-co/sql-ts 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -43
- package/dist/index.d.ts +11 -11
- package/dist/index.js +22 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,20 @@ A TypeScript SQL query builder using tagged template literals. Write plain SQL w
|
|
|
4
4
|
|
|
5
5
|
Supports **MySQL** (`?` placeholders, `` ` `` backtick identifiers) and **PostgreSQL** (`$n` placeholders, `"` double-quote identifiers) out of the box.
|
|
6
6
|
|
|
7
|
+
## Why this library?
|
|
8
|
+
|
|
9
|
+
Writing raw SQL in TypeScript runs into a set of recurring friction points that this library addresses directly:
|
|
10
|
+
|
|
11
|
+
- **Automatic identifier quoting** — table and column names are interpolated as properly quoted identifiers (`` `name` `` for MySQL, `"name"` for PostgreSQL), never as bind parameters. No manual quoting, no dialect-specific escaping scattered across your codebase.
|
|
12
|
+
|
|
13
|
+
- **Table alias and qualified column references** — the `select` tag (and its aliases `join`, `where`) automatically expands schema tables as `"table" "alias"` and columns as `"alias"."column"`, so JOIN-heavy queries stay unambiguous without hand-writing every qualified reference.
|
|
14
|
+
|
|
15
|
+
- **Column alias expansion** — `selectAs` goes further, rendering each column as `"alias"."column" as "alias_column"`. When querying multiple joined tables, result-set keys no longer collide.
|
|
16
|
+
|
|
17
|
+
- **`concat` and `empty` as a monoid** — every tagged template and statement builder returns a `Fragment`. `concat` joins two fragments into one, and `empty` is the identity element. This lets you accumulate query fragments conditionally with `reduce`, compose them with `pipe`, or chain them with `.append` — treating query construction as plain data transformation.
|
|
18
|
+
|
|
19
|
+
- **`all` and `pick` helpers** — expand an entire table's columns or a chosen subset into a comma-separated list inside any tag, so `SELECT ${all(users, posts)}` replaces repetitive column lists without losing type safety.
|
|
20
|
+
|
|
7
21
|
## Installation
|
|
8
22
|
|
|
9
23
|
```sh
|
|
@@ -12,19 +26,57 @@ npm install @vanit-co/sql-ts
|
|
|
12
26
|
|
|
13
27
|
## Quick example
|
|
14
28
|
|
|
29
|
+
**sql** tagged template literal takes care of quoting the identifiers (tables and columns) with the proper quotes `` ` `` for MySQL and `"` for PostgreSQL.
|
|
30
|
+
|
|
15
31
|
```ts
|
|
16
|
-
import { schema,
|
|
32
|
+
import { schema, sql, all } from '@vanit-co/sql-ts'
|
|
17
33
|
|
|
18
34
|
const users = schema({ table: 'users', columns: ['id', 'email'] })
|
|
19
35
|
|
|
20
|
-
const query =
|
|
36
|
+
const query = sql`SELECT ${all(users)} FROM ${users} WHERE ${users.id} = ${42}`
|
|
21
37
|
|
|
22
38
|
// PostgreSQL
|
|
23
|
-
console.log(query.text) // SELECT "
|
|
24
|
-
console.log(query.values) // [42]
|
|
39
|
+
console.log(query.text) // SELECT "id" ,"email" FROM "users" WHERE "id" = $1
|
|
25
40
|
|
|
26
41
|
// MySQL
|
|
27
|
-
console.log(query.sql) // SELECT `
|
|
42
|
+
console.log(query.sql) // SELECT `id` ,`email` FROM `users` WHERE `id` = ?
|
|
43
|
+
|
|
44
|
+
console.log(query.values) // [42]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Full query example
|
|
48
|
+
|
|
49
|
+
**select** tagged template literal works like **sql** but automatically adds table aliases and column prefixes. The other tagged template literals **join** and **where** are just aliases to **select** with the aim of maintaining semantics. The **selectAs** tagged template literal besides the column prefixes will also add the column alias using the format `$prefix_$columnName`.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { schema, select, selectAs, join, where, all, insert, update, empty } from '@vanit-co/sql-ts'
|
|
53
|
+
|
|
54
|
+
const users = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
|
|
55
|
+
const posts = schema({ table: 'posts', columns: ['id', 'user_id', 'title'], alias: 'p' })
|
|
56
|
+
|
|
57
|
+
const id = 42
|
|
58
|
+
|
|
59
|
+
// SELECT with JOIN
|
|
60
|
+
const query = selectAs`SELECT ${all(users, posts)} FROM ${posts}`
|
|
61
|
+
.append(join`JOIN ${users} ON ${posts.user_id} = ${users.id}`)
|
|
62
|
+
.append(id ? where`WHERE ${users.id} = ${42}` : empty)
|
|
63
|
+
|
|
64
|
+
query.text
|
|
65
|
+
// SELECT "u"."id" as "u_id" ,"u"."email" as "u_email" ,"p"."id" as "p_id" ,"p"."user_id" as "p_user_id" ,"p"."title" as "p_title"
|
|
66
|
+
// FROM "posts" "p"
|
|
67
|
+
// JOIN "users" "u" ON "p"."user_id" = "u"."id"
|
|
68
|
+
// WHERE "u"."id" = $1
|
|
69
|
+
query.values // [42]
|
|
70
|
+
|
|
71
|
+
// INSERT
|
|
72
|
+
const ins = insert(users, { id: 1, email: 'alice@example.com' })
|
|
73
|
+
ins.text // insert into "users" ("id" ,"email") values ($1 ,$2)
|
|
74
|
+
ins.values // [1, 'alice@example.com']
|
|
75
|
+
|
|
76
|
+
// UPDATE
|
|
77
|
+
const upd = update(users, { email: 'new@example.com' })
|
|
78
|
+
upd.text // update "users" set "email" = $1
|
|
79
|
+
upd.values // ['new@example.com']
|
|
28
80
|
```
|
|
29
81
|
|
|
30
82
|
---
|
|
@@ -335,7 +387,7 @@ An empty fragment. Acts as the identity element for `concat` — useful as the s
|
|
|
335
387
|
|
|
336
388
|
```ts
|
|
337
389
|
import { pipe, reduce } from 'ramda'
|
|
338
|
-
import { schema, select, join, where, concat, empty, all
|
|
390
|
+
import { schema, select, join, where, concat, empty, all } from '@vanit-co/sql-ts'
|
|
339
391
|
|
|
340
392
|
const users = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
|
|
341
393
|
const posts = schema({ table: 'posts', columns: ['id', 'user_id', 'title'], alias: 'p' })
|
|
@@ -343,7 +395,7 @@ const posts = schema({ table: 'posts', columns: ['id', 'user_id', 'title'], alia
|
|
|
343
395
|
type Filters = { userId?: number; titleLike?: string }
|
|
344
396
|
|
|
345
397
|
const buildPostsQuery = ({ userId, titleLike }: Filters) => {
|
|
346
|
-
const clauses
|
|
398
|
+
const clauses = [
|
|
347
399
|
select`SELECT ${all(users, posts)} FROM ${posts}`,
|
|
348
400
|
join` JOIN ${users} ON ${posts.user_id} = ${users.id}`,
|
|
349
401
|
userId ? where` WHERE ${users.id} = ${userId}` : empty,
|
|
@@ -351,7 +403,7 @@ const buildPostsQuery = ({ userId, titleLike }: Filters) => {
|
|
|
351
403
|
]
|
|
352
404
|
|
|
353
405
|
return reduce(
|
|
354
|
-
(acc
|
|
406
|
+
(acc, clause) => concat(clause)(acc),
|
|
355
407
|
empty,
|
|
356
408
|
clauses
|
|
357
409
|
)
|
|
@@ -407,7 +459,7 @@ Attach a name to a `Result` for use with named prepared statements (e.g., `pg`'s
|
|
|
407
459
|
```ts
|
|
408
460
|
import { sql, preparedStatementName } from '@vanit-co/sql-ts'
|
|
409
461
|
|
|
410
|
-
const q = sql`SELECT * FROM users WHERE id = ${1}`
|
|
462
|
+
const q = sql`SELECT * FROM ${users} WHERE id = ${1}`
|
|
411
463
|
const named = preparedStatementName(q, 'get-user-by-id')
|
|
412
464
|
|
|
413
465
|
named.name // 'get-user-by-id'
|
|
@@ -415,46 +467,13 @@ named.text // SELECT * FROM users WHERE id = $1
|
|
|
415
467
|
named.values // [1]
|
|
416
468
|
|
|
417
469
|
// Pass to pg:
|
|
418
|
-
await client.query(
|
|
470
|
+
await client.query(named)
|
|
419
471
|
```
|
|
420
472
|
|
|
421
473
|
`preparedStatementName` does not mutate the original result — it returns a new object.
|
|
422
474
|
|
|
423
475
|
---
|
|
424
476
|
|
|
425
|
-
## Full query example
|
|
426
|
-
|
|
427
|
-
```ts
|
|
428
|
-
import { schema, select, selectAs, join, where, all, insert, update, concat } from '@vanit-co/sql-ts'
|
|
429
|
-
|
|
430
|
-
const users = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
|
|
431
|
-
const posts = schema({ table: 'posts', columns: ['id', 'user_id', 'title'], alias: 'p' })
|
|
432
|
-
|
|
433
|
-
// SELECT with JOIN
|
|
434
|
-
const query = selectAs`SELECT ${all(users, posts)} FROM ${posts}`
|
|
435
|
-
.append(join`JOIN ${users} ON ${posts.user_id} = ${users.id}`)
|
|
436
|
-
.append(where`WHERE ${users.id} = ${42}`)
|
|
437
|
-
|
|
438
|
-
query.text
|
|
439
|
-
// SELECT "u"."id" as "u_id" ,"u"."email" as "u_email" ,"p"."id" as "p_id" ,"p"."user_id" as "p_user_id" ,"p"."title" as "p_title"
|
|
440
|
-
// FROM "posts" "p"
|
|
441
|
-
// JOIN "users" "u" ON "p"."user_id" = "u"."id"
|
|
442
|
-
// WHERE "u"."id" = $1
|
|
443
|
-
query.values // [42]
|
|
444
|
-
|
|
445
|
-
// INSERT
|
|
446
|
-
const ins = insert(users, { id: 1, email: 'alice@example.com' })
|
|
447
|
-
ins.text // insert into "users" ("id" ,"email") values ($1 ,$2)
|
|
448
|
-
ins.values // [1, 'alice@example.com']
|
|
449
|
-
|
|
450
|
-
// UPDATE
|
|
451
|
-
const upd = update(users, { email: 'new@example.com' })
|
|
452
|
-
upd.text // update "users" set "email" = $1
|
|
453
|
-
upd.values // ['new@example.com']
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
---
|
|
457
|
-
|
|
458
477
|
## License
|
|
459
478
|
|
|
460
479
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
type Result = {
|
|
2
|
-
append: (s: Fragment) => Fragment;
|
|
3
|
-
name?: string;
|
|
4
|
-
sql: string;
|
|
5
|
-
text: string;
|
|
6
|
-
values: Array<any>;
|
|
7
|
-
} & Fragment;
|
|
8
|
-
declare const preparedStatementName: (r: Result, n: string) => Result;
|
|
9
|
-
|
|
10
1
|
type Fragment = {
|
|
11
2
|
readonly strings: ReadonlyArray<string>;
|
|
12
3
|
readonly binds: ReadonlyArray<unknown>;
|
|
13
4
|
};
|
|
14
|
-
declare const concat: (right: Fragment) => (left: Fragment) => Result;
|
|
15
|
-
declare const empty: Fragment;
|
|
16
5
|
|
|
17
6
|
type MysqlResult = {
|
|
18
7
|
readonly sql: string;
|
|
@@ -25,6 +14,16 @@ type PostgresResult = {
|
|
|
25
14
|
declare const toMysql: (s: Fragment) => MysqlResult;
|
|
26
15
|
declare const toPostgres: (s: Fragment) => PostgresResult;
|
|
27
16
|
|
|
17
|
+
type Result = {
|
|
18
|
+
append: (s: Fragment) => Result;
|
|
19
|
+
name?: string;
|
|
20
|
+
sql: string;
|
|
21
|
+
text: string;
|
|
22
|
+
values: Array<any>;
|
|
23
|
+
} & Fragment;
|
|
24
|
+
declare const preparedStatementName: (r: Result, n: string) => Result;
|
|
25
|
+
declare const concat: (right: Fragment) => (left: Fragment) => Result;
|
|
26
|
+
|
|
28
27
|
type Table = {
|
|
29
28
|
readonly name: string;
|
|
30
29
|
readonly alias: string;
|
|
@@ -53,6 +52,7 @@ declare const select: (strings: TemplateStringsArray, ...binds: Array<any>) => R
|
|
|
53
52
|
declare const selectAs: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
|
|
54
53
|
declare const join: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
|
|
55
54
|
declare const where: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
|
|
55
|
+
declare const empty: Result;
|
|
56
56
|
declare const insert: <T extends string>(table: SchemaTable<T>, ...colsVals: Array<{ [K in T]?: any; }>) => Result;
|
|
57
57
|
declare const update: <T extends string>(table: SchemaTable<T>, colsVals: { [K in T]?: any; }) => Result;
|
|
58
58
|
|
package/dist/index.js
CHANGED
|
@@ -15,32 +15,11 @@ const zipLongest = (l1, l2) => {
|
|
|
15
15
|
return Array.from({ length: maxLength }, (_, i) => [l1[i], l2[i]]);
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
const result = (s) => ({
|
|
20
|
-
...s,
|
|
21
|
-
append: (x) => concat(x)(s),
|
|
22
|
-
get sql() {
|
|
23
|
-
return toMysql(s).sql;
|
|
24
|
-
},
|
|
25
|
-
get text() {
|
|
26
|
-
return toPostgres(s).text;
|
|
27
|
-
},
|
|
28
|
-
get values() {
|
|
29
|
-
return s.binds.filter((x) => !(x[SYM_RAW] || x[SYM_IDENTIFIER]));
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const concat = (right) => (left) => {
|
|
34
|
-
const lastLeft = left.strings[left.strings.length - 1] ?? '';
|
|
35
|
-
const firstRight = right.strings[0] ?? '';
|
|
36
|
-
return result(fragment([...left.strings.slice(0, -1), lastLeft + firstRight, ...right.strings.slice(1)], [...left.binds, ...right.binds]));
|
|
37
|
-
};
|
|
38
|
-
const fragment = (strings, binds) => {
|
|
18
|
+
const fragment = (strings, binds = []) => {
|
|
39
19
|
if (strings.length !== binds.length + 1)
|
|
40
20
|
throw new Error(`Malformed fragment: ${strings.length} strings for ${binds.length} values`);
|
|
41
21
|
return Object.freeze({ strings, binds });
|
|
42
22
|
};
|
|
43
|
-
const empty = fragment([''], []);
|
|
44
23
|
const stringfyIdentifierAndRaw = (quote = '') => (s) => {
|
|
45
24
|
const fn = ([strings, binds, merge], [ss, bs]) => {
|
|
46
25
|
if (bs && bs[SYM_RAW]) {
|
|
@@ -72,6 +51,26 @@ const toPostgres = (s) => {
|
|
|
72
51
|
};
|
|
73
52
|
};
|
|
74
53
|
|
|
54
|
+
const preparedStatementName = (r, n) => ({ ...r, name: n });
|
|
55
|
+
const concat = (right) => (left) => {
|
|
56
|
+
const lastLeft = left.strings[left.strings.length - 1] ?? '';
|
|
57
|
+
const firstRight = right.strings[0] ?? '';
|
|
58
|
+
return result(fragment([...left.strings.slice(0, -1), lastLeft + firstRight, ...right.strings.slice(1)], [...left.binds, ...right.binds]));
|
|
59
|
+
};
|
|
60
|
+
const result = (s) => ({
|
|
61
|
+
...s,
|
|
62
|
+
append: (x) => concat(x)(s),
|
|
63
|
+
get sql() {
|
|
64
|
+
return toMysql(s).sql;
|
|
65
|
+
},
|
|
66
|
+
get text() {
|
|
67
|
+
return toPostgres(s).text;
|
|
68
|
+
},
|
|
69
|
+
get values() {
|
|
70
|
+
return s.binds.filter((x) => !(x[SYM_RAW] || x[SYM_IDENTIFIER]));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
75
74
|
const makeColumns = (prefix, cols) => cols.reduce((a, key) => ({ ...a, [key]: { [SYM_COLUMN]: { name: key, prefix } } }), {});
|
|
76
75
|
const schema = ({ table, columns, alias }) => {
|
|
77
76
|
const resolvedAlias = alias ?? table;
|
|
@@ -118,6 +117,7 @@ const select = buildTag(prefix);
|
|
|
118
117
|
const selectAs = buildTag(alias);
|
|
119
118
|
const join = select;
|
|
120
119
|
const where = select;
|
|
120
|
+
const empty = sql ``;
|
|
121
121
|
const insert = (table, ...colsVals) => {
|
|
122
122
|
const tableName = table[SYM_TABLE].name;
|
|
123
123
|
const columns = Object.keys(colsVals[0]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanit-co/sql-ts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "A TypeScript SQL query builder using tagged template literals. Write plain SQL with safe, automatic parameter binding and properly quoted identifiers. No DSL to learn, no magic, no ORM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|