@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 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, select, where, all } from '@vanit-co/sql-ts'
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 = select`SELECT ${all(users)} FROM ${users} WHERE ${users.id} = ${42}`
36
+ const query = sql`SELECT ${all(users)} FROM ${users} WHERE ${users.id} = ${42}`
21
37
 
22
38
  // PostgreSQL
23
- console.log(query.text) // SELECT "users"."id" ,"users"."email" FROM "users" "users" WHERE "users"."id" = $1
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 `users`.`id` ,`users`.`email` FROM `users` `users` WHERE `users`.`id` = ?
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, Fragment } from '@vanit-co/sql-ts'
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: Fragment[] = [
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: Fragment, clause: Fragment) => concat(clause)(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({ name: named.name, text: named.text, values: named.values })
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 preparedStatementName = (r, n) => ({ ...r, name: n });
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.1",
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",