@vanit-co/sql-ts 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vanit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,460 @@
1
+ # @vanit-co/sql-ts
2
+
3
+ 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.
4
+
5
+ Supports **MySQL** (`?` placeholders, `` ` `` backtick identifiers) and **PostgreSQL** (`$n` placeholders, `"` double-quote identifiers) out of the box.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ npm install @vanit-co/sql-ts
11
+ ```
12
+
13
+ ## Quick example
14
+
15
+ ```ts
16
+ import { schema, select, where, all } from '@vanit-co/sql-ts'
17
+
18
+ const users = schema({ table: 'users', columns: ['id', 'email'] })
19
+
20
+ const query = select`SELECT ${all(users)} FROM ${users} WHERE ${users.id} = ${42}`
21
+
22
+ // PostgreSQL
23
+ console.log(query.text) // SELECT "users"."id" ,"users"."email" FROM "users" "users" WHERE "users"."id" = $1
24
+ console.log(query.values) // [42]
25
+
26
+ // MySQL
27
+ console.log(query.sql) // SELECT `users`.`id` ,`users`.`email` FROM `users` `users` WHERE `users`.`id` = ?
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Core concepts
33
+
34
+ ### Schema
35
+
36
+ Define your tables once. The schema carries table and column metadata used by the template tags to produce properly quoted, prefixed identifiers.
37
+
38
+ ```ts
39
+ import { schema } from '@vanit-co/sql-ts'
40
+
41
+ const users = schema({ table: 'users', columns: ['id', 'email'] })
42
+
43
+ // With a table alias (useful for self-joins or verbose table names)
44
+ const u = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
45
+ ```
46
+
47
+ `schema` takes:
48
+ | field | type | description |
49
+ |-------|------|-------------|
50
+ | `table` | `string` | The actual table name in the database |
51
+ | `columns` | `string[]` | Column names to expose as typed properties |
52
+ | `alias` | `string` (optional) | Alias used for prefixing columns; defaults to `table` |
53
+
54
+ After calling `schema`, the returned object has a typed property for each column (`users.id`, `users.email`, etc.).
55
+
56
+ ---
57
+
58
+ ### Result object
59
+
60
+ Every template tag and statement builder returns a `Result` object with three properties:
61
+
62
+ | property | description |
63
+ |----------|-------------|
64
+ | `.sql` | Query string in MySQL format (`?` placeholders, backtick-quoted identifiers) |
65
+ | `.text` | Query string in PostgreSQL format (`$n` placeholders, double-quote identifiers) |
66
+ | `.values` | Array of bind values in order, ready to pass to your database driver |
67
+
68
+ Pass these directly to your driver:
69
+
70
+ ```ts
71
+ // node-postgres (pg)
72
+ await client.query(query)
73
+
74
+ // mysql2
75
+ await connection.query(query)
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Template tag functions
81
+
82
+ ### `sql`
83
+
84
+ The base tag. Interpolated plain values become bind parameters. Interpolated schema tables become their bare name identifier; columns become their bare column name identifier (no table prefix).
85
+
86
+ ```ts
87
+ import { sql } from '@vanit-co/sql-ts'
88
+
89
+ const users = schema({ table: 'users', columns: ['id', 'email'] })
90
+
91
+ // Plain values → bind parameters
92
+ const q1 = sql`SELECT * FROM users WHERE id = ${42}`
93
+ q1.text // SELECT * FROM users WHERE id = $1
94
+ q1.values // [42]
95
+
96
+ // Schema table → quoted identifier (name only)
97
+ const q2 = sql`FROM ${users}`
98
+ q2.text // FROM "users"
99
+
100
+ // Schema column → quoted identifier (name only, no prefix)
101
+ const q3 = sql`SELECT ${users.id}`
102
+ q3.text // SELECT "id"
103
+ ```
104
+
105
+ ### `select` (alias: `s`)
106
+
107
+ Like `sql`, but schema tables are rendered as `"name" "alias"` and columns are rendered as `"alias"."column"`. Use this for `SELECT`, `FROM`, `JOIN`, and `WHERE` clauses when you want fully qualified column references.
108
+
109
+ ```ts
110
+ import { select, s, schema } from '@vanit-co/sql-ts'
111
+
112
+ const users = schema({ table: 'users', columns: ['id', 'email'] })
113
+ const u = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
114
+
115
+ // Table → name + alias
116
+ select`FROM ${users}`.text // FROM "users" "users"
117
+ select`FROM ${u}`.text // FROM "users" "u"
118
+
119
+ // Column → alias.column
120
+ select`SELECT ${users.email}`.text // SELECT "users"."email"
121
+ select`SELECT ${u.email}`.text // SELECT "u"."email"
122
+
123
+ // Mix with values
124
+ select`SELECT ${users.id} WHERE id = ${7}`.text // SELECT "users"."id" WHERE id = $1
125
+ select`SELECT ${users.id} WHERE id = ${7}`.values // [7]
126
+
127
+ // Short alias
128
+ s`SELECT ${users.id}`.text // SELECT "users"."id"
129
+ ```
130
+
131
+ ### `selectAs` (alias: `sa`)
132
+
133
+ Same as `select` for tables, but columns are rendered as `"alias"."column" as "alias_column"`. Use this when fetching from multiple joined tables and you want unambiguous aliased column names in the result set.
134
+
135
+ ```ts
136
+ import { selectAs, sa, schema } from '@vanit-co/sql-ts'
137
+
138
+ const users = schema({ table: 'users', columns: ['id', 'email'] })
139
+ const u = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
140
+
141
+ selectAs`SELECT ${users.id}`.text // SELECT "users"."id" as "users_id"
142
+ selectAs`SELECT ${users.email}`.text // SELECT "users"."email" as "users_email"
143
+
144
+ // With alias
145
+ sa`SELECT ${u.email}`.text // SELECT "u"."email" as "u_email"
146
+ ```
147
+
148
+ ### `join` (alias: `j`) and `where` (alias: `w`)
149
+
150
+ These are identical to `select`. They exist as semantic aliases so your query construction reads naturally.
151
+
152
+ ```ts
153
+ import { select, join, j, where, w, schema } from '@vanit-co/sql-ts'
154
+
155
+ const posts = schema({ table: 'posts', columns: ['id', 'user_id', 'title'] })
156
+ const users = schema({ table: 'users', columns: ['id', 'email'] })
157
+
158
+ const q = select`SELECT ${posts.title}, ${users.email}`
159
+ const fromClause = join`FROM ${posts}`
160
+ const joinClause = join`JOIN ${users} ON ${posts.user_id} = ${users.id}`
161
+ const whereClause = where`WHERE ${users.id} = ${99}`
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Column helpers: `all` and `pick`
167
+
168
+ Use these inside any template tag to expand multiple columns at once.
169
+
170
+ ### `all(...tables)`
171
+
172
+ Expands every column from one or more schema tables.
173
+
174
+ ```ts
175
+ import { select, selectAs, schema, all } from '@vanit-co/sql-ts'
176
+
177
+ const users = schema({ table: 'users', columns: ['id', 'email'] })
178
+ const posts = schema({ table: 'posts', columns: ['title', 'body'] })
179
+
180
+ // Single table
181
+ select`SELECT ${all(users)}`.text
182
+ // SELECT "users"."id" ,"users"."email"
183
+
184
+ // Multiple tables
185
+ select`SELECT ${all(users, posts)}`.text
186
+ // SELECT "users"."id" ,"users"."email" ,"posts"."title" ,"posts"."body"
187
+
188
+ // With selectAs for aliased columns
189
+ selectAs`SELECT ${all(users)}`.text
190
+ // SELECT "users"."id" as "users_id" ,"users"."email" as "users_email"
191
+ ```
192
+
193
+ ### `pick(...columns)`
194
+
195
+ Expands a specific subset of columns from any tables.
196
+
197
+ ```ts
198
+ import { select, selectAs, schema, pick } from '@vanit-co/sql-ts'
199
+
200
+ const users = schema({ table: 'users', columns: ['id', 'email', 'name'] })
201
+ const posts = schema({ table: 'posts', columns: ['title', 'body'] })
202
+
203
+ // Pick from one table
204
+ select`SELECT ${pick(users.id, users.email)}`.text
205
+ // SELECT "users"."id" ,"users"."email"
206
+
207
+ // Pick from different tables
208
+ select`SELECT ${pick(users.id, posts.title)}`.text
209
+ // SELECT "users"."id" ,"posts"."title"
210
+
211
+ // With selectAs
212
+ selectAs`SELECT ${pick(users.id, users.email)}`.text
213
+ // SELECT "users"."id" as "users_id" ,"users"."email" as "users_email"
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Statement builders: `insert` and `update`
219
+
220
+ ### `insert(table, ...rows)`
221
+
222
+ Builds a parameterised `INSERT` statement. Accepts one or more row objects. Column names from the first row are used; table and column names are properly quoted identifiers (never bind params).
223
+
224
+ ```ts
225
+ import { schema, insert } from '@vanit-co/sql-ts'
226
+
227
+ const users = schema({ table: 'users', columns: ['id', 'email'] })
228
+
229
+ // Single row
230
+ const q1 = insert(users, { id: 1, email: 'alice@example.com' })
231
+ q1.text // insert into "users" ("id" ,"email") values ($1 ,$2)
232
+ q1.values // [1, 'alice@example.com']
233
+
234
+ // Multiple rows
235
+ const q2 = insert(users,
236
+ { id: 1, email: 'alice@example.com' },
237
+ { id: 2, email: 'bob@example.com' }
238
+ )
239
+ q2.text // insert into "users" ("id" ,"email") values ($1 ,$2) ,($3 ,$4)
240
+ q2.values // [1, 'alice@example.com', 2, 'bob@example.com']
241
+ ```
242
+
243
+ ### `update(table, colsVals)`
244
+
245
+ Builds a parameterised `UPDATE ... SET ...` statement (without a `WHERE` clause — compose that separately using `concat`).
246
+
247
+ ```ts
248
+ import { schema, update } from '@vanit-co/sql-ts'
249
+
250
+ const users = schema({ table: 'users', columns: ['id', 'email'] })
251
+
252
+ const q = update(users, { email: 'new@example.com' })
253
+ q.text // update "users" set "email" = $1
254
+ q.values // ['new@example.com']
255
+
256
+ // Multiple columns
257
+ const q2 = update(users, { id: 99, email: 'updated@example.com' })
258
+ q2.text // update "users" set "id" = $1 ,"email" = $2
259
+ q2.values // [99, 'updated@example.com']
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Fragment utilities: `concat` and `empty`
265
+
266
+ ### `concat`
267
+
268
+ Joins two `Fragment` objects into one. `concat` is curried — `concat(right)(left)` appends `right` after `left`.
269
+
270
+ ```ts
271
+ import { select, where, concat, schema } from '@vanit-co/sql-ts'
272
+
273
+ const users = schema({ table: 'users', columns: ['id', 'email'] })
274
+
275
+ const base = select`SELECT ${users.id} FROM ${users}`
276
+ const clause = where`WHERE ${users.id} = ${5}`
277
+
278
+ const full = concat(clause)(base)
279
+ full.text // SELECT "users"."id" FROM "users" "users" WHERE "users"."id" = $1
280
+ full.values // [5]
281
+ ```
282
+
283
+ You can also use the `.append` method on a `Result`:
284
+
285
+ ```ts
286
+ const full = base.append(where`WHERE ${users.id} = ${5}`)
287
+ ```
288
+
289
+
290
+ **Composing queries with `pipe`**
291
+
292
+ Because `concat` is curried, it fits naturally into a `pipe`. Each `concat(fragment)` call becomes one step in the pipeline, appending a fragment to the result of the previous step.
293
+
294
+ Using **ramda** (already a dependency of this package):
295
+
296
+ ```ts
297
+ import { pipe } from 'ramda'
298
+ import { schema, selectAs, join, where, concat, all } from '@vanit-co/sql-ts'
299
+
300
+ const users = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
301
+ const posts = schema({ table: 'posts', columns: ['id', 'user_id', 'title'], alias: 'p' })
302
+
303
+ const buildQuery = (userId: number) =>
304
+ pipe(
305
+ concat(join` JOIN ${users} ON ${posts.user_id} = ${users.id}`),
306
+ concat(where` WHERE ${users.id} = ${userId}`)
307
+ )(selectAs`SELECT ${all(users, posts)} FROM ${posts}`)
308
+
309
+ const q = buildQuery(42)
310
+ q.text
311
+ // SELECT "u"."id" as "u_id" ,"u"."email" as "u_email"
312
+ // ,"p"."id" as "p_id" ,"p"."user_id" as "p_user_id" ,"p"."title" as "p_title"
313
+ // FROM "posts" "p"
314
+ // JOIN "users" "u" ON "p"."user_id" = "u"."id"
315
+ // WHERE "u"."id" = $1
316
+ q.values // [42]
317
+ ```
318
+
319
+ Using **Effect** (`pipe` is data-first, which many find more readable):
320
+
321
+ ```ts
322
+ import { pipe } from 'effect'
323
+ import { schema, selectAs, join, where, concat, all } from '@vanit-co/sql-ts'
324
+
325
+ const q = pipe(
326
+ selectAs`SELECT ${all(users, posts)} FROM ${posts}`,
327
+ concat(join` JOIN ${users} ON ${posts.user_id} = ${users.id}`),
328
+ concat(where` WHERE ${users.id} = ${42}`)
329
+ )
330
+ ```
331
+
332
+ ### `empty`
333
+
334
+ An empty fragment. Acts as the identity element for `concat` — useful as the starting value when accumulating fragments conditionally.
335
+
336
+ ```ts
337
+ import { pipe, reduce } from 'ramda'
338
+ import { schema, select, join, where, concat, empty, all, Fragment } from '@vanit-co/sql-ts'
339
+
340
+ const users = schema({ table: 'users', columns: ['id', 'email'], alias: 'u' })
341
+ const posts = schema({ table: 'posts', columns: ['id', 'user_id', 'title'], alias: 'p' })
342
+
343
+ type Filters = { userId?: number; titleLike?: string }
344
+
345
+ const buildPostsQuery = ({ userId, titleLike }: Filters) => {
346
+ const clauses: Fragment[] = [
347
+ select`SELECT ${all(users, posts)} FROM ${posts}`,
348
+ join` JOIN ${users} ON ${posts.user_id} = ${users.id}`,
349
+ userId ? where` WHERE ${users.id} = ${userId}` : empty,
350
+ titleLike ? where` AND ${posts.title} LIKE ${titleLike}` : empty,
351
+ ]
352
+
353
+ return reduce(
354
+ (acc: Fragment, clause: Fragment) => concat(clause)(acc),
355
+ empty,
356
+ clauses
357
+ )
358
+ }
359
+
360
+ const q = buildPostsQuery({ userId: 7, titleLike: '%TypeScript%' })
361
+ q.text
362
+ // SELECT "u"."id" as "u_id" ,"u"."email" as "u_email"
363
+ // ,"p"."id" as "p_id" ,"p"."user_id" as "p_user_id" ,"p"."title" as "p_title"
364
+ // FROM "posts" "p"
365
+ // JOIN "users" "u" ON "p"."user_id" = "u"."id"
366
+ // WHERE "u"."id" = $1
367
+ // AND "p"."title" LIKE $2
368
+ q.values // [7, '%TypeScript%']
369
+ ```
370
+
371
+ ---
372
+
373
+ ## Dialect functions: `toMysql` and `toPostgres`
374
+
375
+ Convert any `Fragment` directly to a dialect-specific result. The template tags already expose `.sql` and `.text` on their return value, but you can call these directly when working with raw fragments.
376
+
377
+ ```ts
378
+ import { toMysql, toPostgres } from '@vanit-co/sql-ts'
379
+
380
+ const result = toPostgres(someFragment)
381
+ // { text: 'SELECT ... WHERE id = $1', values: [42] }
382
+
383
+ const mysqlResult = toMysql(someFragment)
384
+ // { sql: 'SELECT ... WHERE id = ?', values: [42] }
385
+ ```
386
+
387
+ ---
388
+
389
+ ## Escaping and raw SQL: `raw`
390
+
391
+ By default, every interpolated value is treated as a bind parameter. Use `raw` to inject an unescaped SQL snippet directly into the query string. **Only use `raw` with trusted, static strings.**
392
+
393
+ ```ts
394
+ import { sql, raw } from '@vanit-co/sql-ts'
395
+
396
+ const q = sql`SELECT ${raw('COUNT(*)')} AS total FROM users`
397
+ q.text // SELECT COUNT(*) AS total FROM users
398
+ q.values // []
399
+ ```
400
+
401
+ ---
402
+
403
+ ## Prepared statements: `preparedStatementName`
404
+
405
+ Attach a name to a `Result` for use with named prepared statements (e.g., `pg`'s `{ name, text, values }` query format).
406
+
407
+ ```ts
408
+ import { sql, preparedStatementName } from '@vanit-co/sql-ts'
409
+
410
+ const q = sql`SELECT * FROM users WHERE id = ${1}`
411
+ const named = preparedStatementName(q, 'get-user-by-id')
412
+
413
+ named.name // 'get-user-by-id'
414
+ named.text // SELECT * FROM users WHERE id = $1
415
+ named.values // [1]
416
+
417
+ // Pass to pg:
418
+ await client.query({ name: named.name, text: named.text, values: named.values })
419
+ ```
420
+
421
+ `preparedStatementName` does not mutate the original result — it returns a new object.
422
+
423
+ ---
424
+
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
+ ## License
459
+
460
+ MIT
@@ -0,0 +1,71 @@
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
+ type Fragment = {
11
+ readonly strings: ReadonlyArray<string>;
12
+ readonly binds: ReadonlyArray<unknown>;
13
+ };
14
+ declare const concat: (right: Fragment) => (left: Fragment) => Result;
15
+ declare const empty: Fragment;
16
+
17
+ type MysqlResult = {
18
+ readonly sql: string;
19
+ readonly values: ReadonlyArray<unknown>;
20
+ };
21
+ type PostgresResult = {
22
+ readonly text: string;
23
+ readonly values: ReadonlyArray<unknown>;
24
+ };
25
+ declare const toMysql: (s: Fragment) => MysqlResult;
26
+ declare const toPostgres: (s: Fragment) => PostgresResult;
27
+
28
+ type Table = {
29
+ readonly name: string;
30
+ readonly alias: string;
31
+ };
32
+ type Column = {
33
+ readonly name: string;
34
+ readonly prefix: string;
35
+ };
36
+ type SchemaColumn<T extends string> = {
37
+ [K in T]: {
38
+ [KC in symbol]: Column;
39
+ };
40
+ };
41
+ type SchemaTable<T extends string> = {
42
+ readonly [KT in symbol]: Table;
43
+ } & SchemaColumn<T>;
44
+ type Params<T extends string> = {
45
+ readonly table: string;
46
+ readonly columns: ReadonlyArray<T>;
47
+ readonly alias?: string;
48
+ };
49
+ declare const schema: <T extends string>({ table, columns, alias }: Params<T>) => SchemaTable<T>;
50
+
51
+ declare const sql: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
52
+ declare const select: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
53
+ declare const selectAs: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
54
+ declare const join: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
55
+ declare const where: (strings: TemplateStringsArray, ...binds: Array<any>) => Result;
56
+ declare const insert: <T extends string>(table: SchemaTable<T>, ...colsVals: Array<{ [K in T]?: any; }>) => Result;
57
+ declare const update: <T extends string>(table: SchemaTable<T>, colsVals: { [K in T]?: any; }) => Result;
58
+
59
+ type Value = {
60
+ readonly [K in symbol]: boolean;
61
+ } & {
62
+ content: any;
63
+ };
64
+
65
+ declare const all: (...tables: Array<SchemaTable<string>>) => Value;
66
+ declare const pick: (...columns: Array<{
67
+ [key: symbol]: Column;
68
+ }>) => Value;
69
+ declare const raw: (content: string) => Value;
70
+
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 };
package/dist/index.js ADDED
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ var ramda = require('ramda');
4
+
5
+ // for schema
6
+ const SYM_COLUMN = Symbol('sql-ts/column');
7
+ const SYM_MULTIPLE_COLUMNS = Symbol('sql-ts/multipleColumns');
8
+ const SYM_TABLE = Symbol('sql-ts/table');
9
+ // for fragment
10
+ const SYM_RAW = Symbol('sql-ts/raw');
11
+ const SYM_IDENTIFIER = Symbol('sql-ts/identifier');
12
+
13
+ const zipLongest = (l1, l2) => {
14
+ const maxLength = Math.max(l1.length, l2.length);
15
+ return Array.from({ length: maxLength }, (_, i) => [l1[i], l2[i]]);
16
+ };
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) => {
39
+ if (strings.length !== binds.length + 1)
40
+ throw new Error(`Malformed fragment: ${strings.length} strings for ${binds.length} values`);
41
+ return Object.freeze({ strings, binds });
42
+ };
43
+ const empty = fragment([''], []);
44
+ const stringfyIdentifierAndRaw = (quote = '') => (s) => {
45
+ const fn = ([strings, binds, merge], [ss, bs]) => {
46
+ if (bs && bs[SYM_RAW]) {
47
+ return [strings, binds, [...merge, ss, bs.content]];
48
+ }
49
+ else if (bs && bs[SYM_IDENTIFIER]) {
50
+ return [strings, binds, [...merge, ss, (quote + bs.content + quote)]];
51
+ }
52
+ else if (bs) {
53
+ return [[...strings, merge.join('') + ss], [...binds, bs], []];
54
+ }
55
+ else {
56
+ return [[...strings, merge.join('') + ss], binds, []];
57
+ }
58
+ };
59
+ const [ns, nb] = zipLongest(s.strings, s.binds).reduce(fn, [[], [], []]);
60
+ return fragment(ns, nb);
61
+ };
62
+
63
+ const toMysql = (s) => {
64
+ const ss = stringfyIdentifierAndRaw('`')(s);
65
+ return { sql: [...ss.strings].join('?'), values: ss.binds };
66
+ };
67
+ const toPostgres = (s) => {
68
+ const ss = stringfyIdentifierAndRaw('"')(s);
69
+ return {
70
+ text: ss.strings[0] + ramda.zipWith((i, str) => `$${i}${str}`, ramda.range(1, ss.binds.length + 1), ss.strings.slice(1)).join(''),
71
+ values: ss.binds
72
+ };
73
+ };
74
+
75
+ const makeColumns = (prefix, cols) => cols.reduce((a, key) => ({ ...a, [key]: { [SYM_COLUMN]: { name: key, prefix } } }), {});
76
+ const schema = ({ table, columns, alias }) => {
77
+ const resolvedAlias = alias ?? table;
78
+ return {
79
+ [SYM_TABLE]: { name: table, alias: resolvedAlias },
80
+ ...makeColumns(resolvedAlias, columns)
81
+ };
82
+ };
83
+
84
+ const identifier = (content) => ({ [SYM_IDENTIFIER]: true, content });
85
+
86
+ const transformer = (table, column) => ([ss, bs]) => {
87
+ if (bs === undefined)
88
+ return [[ss]];
89
+ if (bs[SYM_TABLE])
90
+ return table(ss, bs[SYM_TABLE]);
91
+ if (bs[SYM_COLUMN])
92
+ return column(ss, bs[SYM_COLUMN]);
93
+ if (bs[SYM_MULTIPLE_COLUMNS])
94
+ return bs.content
95
+ .flatMap((c, i) => i === 0 ? column(ss, c) : column(' ,', c));
96
+ return [[ss, bs]];
97
+ };
98
+ const tableWithAlias = (s, t) => [
99
+ [s, identifier(t.name)],
100
+ [' ', identifier(t.alias)]
101
+ ];
102
+ const prefixedColumn = (s, c) => [
103
+ [s, identifier(c.prefix)],
104
+ ['.', identifier(c.name)]
105
+ ];
106
+ const pure = transformer((s, t) => [[s, identifier(t.name)]], (s, c) => [[s, identifier(c.name)]]);
107
+ const prefix = transformer(tableWithAlias, prefixedColumn);
108
+ const alias = transformer(tableWithAlias, (s, c) => [
109
+ ...prefixedColumn(s, c),
110
+ [' as ', identifier(`${c.prefix}_${c.name}`)]
111
+ ]);
112
+ const buildTag = (fn) => (strings, ...binds) => {
113
+ const [s, v] = ramda.transpose(zipLongest(strings, binds).flatMap(fn));
114
+ return result(fragment(s, v ?? []));
115
+ };
116
+ const sql = buildTag(pure);
117
+ const select = buildTag(prefix);
118
+ const selectAs = buildTag(alias);
119
+ const join = select;
120
+ const where = select;
121
+ const insert = (table, ...colsVals) => {
122
+ const tableName = table[SYM_TABLE].name;
123
+ const columns = Object.keys(colsVals[0]);
124
+ const colStrings = columns.map((_, i) => i === 0 ? ' (' : ' ,');
125
+ const colBinds = columns.map(c => identifier(c));
126
+ const [valStrings, valBinds] = colsVals.reduce(([ss, bs], row, rowIdx) => {
127
+ const rowStrings = columns.map((_, colIdx) => colIdx === 0 ? (rowIdx === 0 ? ') values (' : ') ,(') : ' ,');
128
+ return [[...ss, ...rowStrings], [...bs, ...columns.map(c => row[c])]];
129
+ }, [[], []]);
130
+ return result(fragment(['insert into ', ...colStrings, ...valStrings, ')'], [identifier(tableName), ...colBinds, ...valBinds]));
131
+ };
132
+ const update = (table, colsVals) => {
133
+ const tableName = table[SYM_TABLE].name;
134
+ const entries = Object.entries(colsVals);
135
+ const [strings, binds] = entries.reduce(([ss, bs], [colName, value], i) => [[...ss, i === 0 ? ' set ' : ' ,', ' = '], [...bs, identifier(colName), value]], [['update '], [identifier(tableName)]]);
136
+ return result(fragment([...strings, ''], binds));
137
+ };
138
+
139
+ const all = (...tables) => ({
140
+ [SYM_MULTIPLE_COLUMNS]: true,
141
+ content: tables.flatMap(t => Object.values(t).map(x => x[SYM_COLUMN]))
142
+ });
143
+ const pick = (...columns) => ({
144
+ [SYM_MULTIPLE_COLUMNS]: true,
145
+ content: columns.map(x => x[SYM_COLUMN])
146
+ });
147
+ const raw = (content) => ({
148
+ [SYM_RAW]: true,
149
+ content
150
+ });
151
+
152
+ exports.all = all;
153
+ exports.concat = concat;
154
+ exports.empty = empty;
155
+ exports.insert = insert;
156
+ exports.j = join;
157
+ exports.join = join;
158
+ exports.pick = pick;
159
+ exports.preparedStatementName = preparedStatementName;
160
+ exports.raw = raw;
161
+ exports.s = select;
162
+ exports.sa = selectAs;
163
+ exports.schema = schema;
164
+ exports.select = select;
165
+ exports.selectAs = selectAs;
166
+ exports.sql = sql;
167
+ exports.toMysql = toMysql;
168
+ exports.toPostgres = toPostgres;
169
+ exports.update = update;
170
+ exports.w = where;
171
+ exports.where = where;
172
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@vanit-co/sql-ts",
3
+ "version": "0.0.1",
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
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "./dist/index.js",
10
+ "./dist/index.d.ts",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "test": "vitest run",
16
+ "build": "npx rollup --config --bundleConfigAsCjs"
17
+ },
18
+ "devDependencies": {
19
+ "@rollup/plugin-typescript": "^12.3.0",
20
+ "@types/ramda": "^0.31.1",
21
+ "rollup": "^4.60.0",
22
+ "rollup-plugin-dts": "^6.4.1",
23
+ "tslib": "^2.8.1",
24
+ "typescript": "^5.9.0",
25
+ "vitest": "^4.1.0"
26
+ },
27
+ "dependencies": {
28
+ "ramda": "^0.32.0"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/vanit-co/sql-ts.git"
33
+ },
34
+ "keywords": [
35
+ "sql",
36
+ "query",
37
+ "postgres",
38
+ "pg",
39
+ "mysql",
40
+ "mysql2"
41
+ ],
42
+ "author": "Geovanny Silva",
43
+ "license": "MIT",
44
+ "bugs": {
45
+ "url": "https://github.com/vanit-co/sql-ts/issues"
46
+ },
47
+ "homepage": "https://github.com/vanit-co/sql-ts#readme"
48
+ }