@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 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
  ---
@@ -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, Fragment } from '@vanit-co/sql-ts'
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: Fragment[] = [
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: Fragment, clause: Fragment) => concat(clause)(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({ name: named.name, text: named.text, values: named.values })
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 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;
@@ -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.1",
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",