@vanit-co/sql-ts 0.0.1 → 0.2.0
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 +90 -43
- package/dist/index.d.ts +13 -12
- package/dist/index.js +27 -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
|
---
|
|
@@ -53,6 +105,34 @@ const u = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
|
|
|
53
105
|
|
|
54
106
|
After calling `schema`, the returned object has a typed property for each column (`users.id`, `users.email`, etc.).
|
|
55
107
|
|
|
108
|
+
### `as(column)`
|
|
109
|
+
|
|
110
|
+
Returns the aliased column name as a plain string — the same `prefix_name` string that `selectAs` writes into the SQL `AS` clause. Use this to read a column out of query results by its aliased key without repeating the string manually.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { schema, as, selectAs, all } from '@vanit-co/sql-ts'
|
|
114
|
+
|
|
115
|
+
const users = schema({ table: 'users', columns: ['id', 'email'] })
|
|
116
|
+
const u = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
|
|
117
|
+
|
|
118
|
+
as(users.id) // 'users_id'
|
|
119
|
+
as(users.email) // 'users_email'
|
|
120
|
+
|
|
121
|
+
as(u.id) // 'u_id'
|
|
122
|
+
as(u.email) // 'u_email'
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This is particularly useful when mapping over result rows:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const rows = await client.query(selectAs`SELECT ${all(users)} FROM ${users}`)
|
|
129
|
+
|
|
130
|
+
rows.map(row => ({
|
|
131
|
+
id: row[as(users.id)], // row['users_id']
|
|
132
|
+
email: row[as(users.email)], // row['users_email']
|
|
133
|
+
}))
|
|
134
|
+
```
|
|
135
|
+
|
|
56
136
|
---
|
|
57
137
|
|
|
58
138
|
### Result object
|
|
@@ -335,7 +415,7 @@ An empty fragment. Acts as the identity element for `concat` — useful as the s
|
|
|
335
415
|
|
|
336
416
|
```ts
|
|
337
417
|
import { pipe, reduce } from 'ramda'
|
|
338
|
-
import { schema, select, join, where, concat, empty, all
|
|
418
|
+
import { schema, select, join, where, concat, empty, all } from '@vanit-co/sql-ts'
|
|
339
419
|
|
|
340
420
|
const users = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
|
|
341
421
|
const posts = schema({ table: 'posts', columns: ['id', 'user_id', 'title'], alias: 'p' })
|
|
@@ -343,7 +423,7 @@ const posts = schema({ table: 'posts', columns: ['id', 'user_id', 'title'], alia
|
|
|
343
423
|
type Filters = { userId?: number; titleLike?: string }
|
|
344
424
|
|
|
345
425
|
const buildPostsQuery = ({ userId, titleLike }: Filters) => {
|
|
346
|
-
const clauses
|
|
426
|
+
const clauses = [
|
|
347
427
|
select`SELECT ${all(users, posts)} FROM ${posts}`,
|
|
348
428
|
join` JOIN ${users} ON ${posts.user_id} = ${users.id}`,
|
|
349
429
|
userId ? where` WHERE ${users.id} = ${userId}` : empty,
|
|
@@ -351,7 +431,7 @@ const buildPostsQuery = ({ userId, titleLike }: Filters) => {
|
|
|
351
431
|
]
|
|
352
432
|
|
|
353
433
|
return reduce(
|
|
354
|
-
(acc
|
|
434
|
+
(acc, clause) => concat(clause)(acc),
|
|
355
435
|
empty,
|
|
356
436
|
clauses
|
|
357
437
|
)
|
|
@@ -407,7 +487,7 @@ Attach a name to a `Result` for use with named prepared statements (e.g., `pg`'s
|
|
|
407
487
|
```ts
|
|
408
488
|
import { sql, preparedStatementName } from '@vanit-co/sql-ts'
|
|
409
489
|
|
|
410
|
-
const q = sql`SELECT * FROM users WHERE id = ${1}`
|
|
490
|
+
const q = sql`SELECT * FROM ${users} WHERE id = ${1}`
|
|
411
491
|
const named = preparedStatementName(q, 'get-user-by-id')
|
|
412
492
|
|
|
413
493
|
named.name // 'get-user-by-id'
|
|
@@ -415,46 +495,13 @@ named.text // SELECT * FROM users WHERE id = $1
|
|
|
415
495
|
named.values // [1]
|
|
416
496
|
|
|
417
497
|
// Pass to pg:
|
|
418
|
-
await client.query(
|
|
498
|
+
await client.query(named)
|
|
419
499
|
```
|
|
420
500
|
|
|
421
501
|
`preparedStatementName` does not mutate the original result — it returns a new object.
|
|
422
502
|
|
|
423
503
|
---
|
|
424
504
|
|
|
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
505
|
## License
|
|
459
506
|
|
|
460
507
|
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;
|
|
@@ -47,12 +46,14 @@ type Params<T extends string> = {
|
|
|
47
46
|
readonly alias?: string;
|
|
48
47
|
};
|
|
49
48
|
declare const schema: <T extends string>({ table, columns, alias }: Params<T>) => SchemaTable<T>;
|
|
49
|
+
declare const as: (col: { [K in symbol]: Column; }) => string;
|
|
50
50
|
|
|
51
51
|
declare const sql: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
|
|
52
52
|
declare const select: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
|
|
53
53
|
declare const selectAs: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
|
|
54
54
|
declare const join: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
|
|
55
55
|
declare const where: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
|
|
56
|
+
declare const empty: Result;
|
|
56
57
|
declare const insert: <T extends string>(table: SchemaTable<T>, ...colsVals: Array<{ [K in T]?: any; }>) => Result;
|
|
57
58
|
declare const update: <T extends string>(table: SchemaTable<T>, colsVals: { [K in T]?: any; }) => Result;
|
|
58
59
|
|
|
@@ -68,4 +69,4 @@ declare const pick: (...columns: Array<{
|
|
|
68
69
|
}>) => Value;
|
|
69
70
|
declare const raw: (content: string) => Value;
|
|
70
71
|
|
|
71
|
-
export { all, concat, empty, insert, join as j, join, pick, preparedStatementName, raw, select as s, selectAs as sa, schema, select, selectAs, sql, toMysql, toPostgres, update, where as w, where };
|
|
72
|
+
export { all, as, concat, empty, insert, join as j, join, pick, preparedStatementName, raw, select as s, selectAs as sa, schema, select, selectAs, sql, toMysql, toPostgres, update, where as w, where };
|
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;
|
|
@@ -80,6 +79,10 @@ const schema = ({ table, columns, alias }) => {
|
|
|
80
79
|
...makeColumns(resolvedAlias, columns)
|
|
81
80
|
};
|
|
82
81
|
};
|
|
82
|
+
const as = (col) => {
|
|
83
|
+
const c = col[SYM_COLUMN];
|
|
84
|
+
return c.prefix + '_' + c.name;
|
|
85
|
+
};
|
|
83
86
|
|
|
84
87
|
const identifier = (content) => ({ [SYM_IDENTIFIER]: true, content });
|
|
85
88
|
|
|
@@ -118,6 +121,7 @@ const select = buildTag(prefix);
|
|
|
118
121
|
const selectAs = buildTag(alias);
|
|
119
122
|
const join = select;
|
|
120
123
|
const where = select;
|
|
124
|
+
const empty = sql ``;
|
|
121
125
|
const insert = (table, ...colsVals) => {
|
|
122
126
|
const tableName = table[SYM_TABLE].name;
|
|
123
127
|
const columns = Object.keys(colsVals[0]);
|
|
@@ -150,6 +154,7 @@ const raw = (content) => ({
|
|
|
150
154
|
});
|
|
151
155
|
|
|
152
156
|
exports.all = all;
|
|
157
|
+
exports.as = as;
|
|
153
158
|
exports.concat = concat;
|
|
154
159
|
exports.empty = empty;
|
|
155
160
|
exports.insert = insert;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanit-co/sql-ts",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|