@triptease/sql-template-postgres 0.24.25 → 0.35.6
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 +200 -56
- package/escape.d.ts +11 -0
- package/escape.js +67 -0
- package/escape.js.map +1 -0
- package/index.d.ts +2 -5
- package/index.js +2 -51
- package/index.js.map +1 -0
- package/package.json +20 -18
- package/statement.d.ts +17 -0
- package/statement.js +93 -0
- package/statement.js.map +1 -0
package/README.md
CHANGED
|
@@ -1,56 +1,200 @@
|
|
|
1
|
-
# Sql Template
|
|
2
|
-
|
|
3
|
-
This is yet another SQL tagged template for Typescript/Javascript
|
|
4
|
-
|
|
5
|
-
## Why another library?
|
|
6
|
-
|
|
7
|
-
* Typescript first
|
|
8
|
-
* Functional/Immutable
|
|
9
|
-
* Super simple implementation (
|
|
10
|
-
* Full escaping of identifiers and values
|
|
11
|
-
* Plugable to any DB (currently
|
|
12
|
-
* Automatic support for prepareStatement naming (Postgres)
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
```shell
|
|
17
|
-
npm install @triptease/sql-template @triptease/sql-template-postgres
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
# Sql Template
|
|
2
|
+
|
|
3
|
+
This is yet another SQL tagged template for Typescript/Javascript
|
|
4
|
+
|
|
5
|
+
## Why another library?
|
|
6
|
+
|
|
7
|
+
* Typescript first
|
|
8
|
+
* Functional/Immutable
|
|
9
|
+
* Super simple implementation (see [SQL.ts](https://github.com/triptease/sql-template/blob/master/sql-template/src/SQL.ts))
|
|
10
|
+
* Full escaping of identifiers and values
|
|
11
|
+
* Plugable to any DB (currently Postgres via `@triptease/sql-template-postgres` with `pg`)
|
|
12
|
+
* Automatic support for prepareStatement naming (Postgres)
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```shell
|
|
17
|
+
npm install @triptease/sql-template @triptease/sql-template-postgres
|
|
18
|
+
# or
|
|
19
|
+
bun add @triptease/sql-template @triptease/sql-template-postgres
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Both packages are plain ESM JavaScript with type declarations, so they work in Node and Bun.
|
|
23
|
+
`@triptease/sql-template` has no dependencies and `@triptease/sql-template-postgres` depends only on it
|
|
24
|
+
(it does not depend on `pg`; bring your own driver). In Node they can also be loaded with `require()`
|
|
25
|
+
(Node 20.19+/22.12+, which can `require()` ES modules).
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Node with [pg](https://node-postgres.com/)
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import pg from "pg";
|
|
33
|
+
import {SQL, id, values} from "@triptease/sql-template";
|
|
34
|
+
import {statement, prepareStatement} from "@triptease/sql-template-postgres";
|
|
35
|
+
|
|
36
|
+
const pool = new pg.Pool();
|
|
37
|
+
const {rows} = await pool.query(statement(SQL`select * from ${id(table)} where name = ${name}`));
|
|
38
|
+
// SQL: select * from "users" where name = $1 values: ['Dan']
|
|
39
|
+
|
|
40
|
+
// named prepared statement (name defaults to a hash of the SQL)
|
|
41
|
+
await pool.query(prepareStatement(SQL`select * from users where id in (${values(userIds)})`));
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Bun
|
|
45
|
+
|
|
46
|
+
If you only target Bun, its built-in [`sql`](https://bun.com/docs/api/sql) tagged template already binds values
|
|
47
|
+
and escapes identifiers, so you probably don't need this library. Use it in Bun when your query code also has to
|
|
48
|
+
run on Node with `pg`, or when you want queries as plain immutable values you can build, test and log
|
|
49
|
+
(`debugQuery`) without a connection.
|
|
50
|
+
|
|
51
|
+
### Composing
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import {SQL, id, ids, text, values} from "@triptease/sql-template";
|
|
55
|
+
|
|
56
|
+
const where = SQL`where ${id('name')} = ${name}`;
|
|
57
|
+
const query = SQL`select ${ids(['id', 'name'])} from users ${where}`; // templates nest
|
|
58
|
+
const either = SQL`select * from users where ${values([a, b], text(' or '))}`; // explicit raw separator
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Templates are immutable (frozen) and flat. Expressions are identified by a `Symbol.for` brand rather than
|
|
62
|
+
`instanceof`, so templates built by a different copy of `@triptease/sql-template` (e.g. two installed
|
|
63
|
+
versions) still work, while plain data (such as parsed JSON) can never be mistaken for SQL.
|
|
64
|
+
|
|
65
|
+
## Cheatsheet
|
|
66
|
+
|
|
67
|
+
### Core (@triptease/sql-template)
|
|
68
|
+
|
|
69
|
+
| function | Description |
|
|
70
|
+
|------------------------------------------------|---------------------------------------------------------------------------------------------|
|
|
71
|
+
| `SQL` | The main function to create tagged templates for SQL (*DB agnostic*) |
|
|
72
|
+
| `text` (alias `raw`) | Input raw SQL without any escaping (*use with care*) |
|
|
73
|
+
| `id` / `ids(names, separator?)` | Input dynamic identifiers into SQL (*escaped as needed*) |
|
|
74
|
+
| `value` (optional) / `values(list, separator?)` (alias `spread`) | Input one or more values into SQL (*bound as parameters*) |
|
|
75
|
+
| `template` | Combine expressions into a (flattened, frozen) template |
|
|
76
|
+
| `isExpression` / `isText` / `isIdentifier` / `isValue` / `isTemplate` / `kindOf` | Type guards based on the expression brand (for adapters) |
|
|
77
|
+
|
|
78
|
+
`separator` is an Expression and defaults to `text(', ')`.
|
|
79
|
+
|
|
80
|
+
### Postgres (@triptease/sql-template-postgres)
|
|
81
|
+
|
|
82
|
+
| function | Description |
|
|
83
|
+
|------------------------------------------------|----------------------------------------------------------------------|
|
|
84
|
+
| `statement` | Converts DB agnostic `SQL` template into a postgres statement (`pg`) |
|
|
85
|
+
| `prepareStatement` | Converts DB agnostic `SQL` template into a named prepared statement (`pg`) |
|
|
86
|
+
| `escapeIdentifier` / `escapeLiteral` | Postgres identifier and string literal escaping (ported from `pg`) |
|
|
87
|
+
| `debugQuery` | Renders a template with values inlined, for debugging (*see below*) |
|
|
88
|
+
|
|
89
|
+
*use with care -> Used incorrectly you can open yourself up to SQL injection*
|
|
90
|
+
|
|
91
|
+
`debugQuery` escapes every value (strings, numbers, booleans, null, Dates, Buffers, arrays and objects), but
|
|
92
|
+
it is meant for logging: execute queries with `statement`/`prepareStatement` so values are bound as
|
|
93
|
+
parameters. Inlined literals are not always typed the way parameters are (e.g. arrays become array literal
|
|
94
|
+
strings and objects JSON strings, which need a cast in some contexts). Dates are inlined as UTC ISO strings, so for
|
|
95
|
+
`timestamp` (without time zone) or text targets the output only matches what `pg` executes when the process runs
|
|
96
|
+
with `TZ=UTC`.
|
|
97
|
+
|
|
98
|
+
## Breaking changes (since the pg-only releases)
|
|
99
|
+
|
|
100
|
+
* The `separator` of `values`/`spread`/`ids` is now an Expression: replace `values(xs, ' or ')` with
|
|
101
|
+
`values(xs, text(' or '))`. Passing a string throws a `TypeError` (it used to be inserted as raw SQL).
|
|
102
|
+
* `@triptease/sql-template-postgres` no longer depends on `pg`; its `QueryConfig` is a local type
|
|
103
|
+
that is structurally compatible with pg's.
|
|
104
|
+
* Adapters throw on expressions they do not understand instead of silently dropping them, and SQL with an
|
|
105
|
+
invalid escape sequence (e.g. ``SQL`\u` ``) throws instead of producing the text `undefined`.
|
|
106
|
+
* Public types use `unknown` instead of `any`; `ids` takes `string[]`.
|
|
107
|
+
* The packages are ESM only (they used to be CommonJS):
|
|
108
|
+
* `require()` needs Node 20.19+/22.12+ (older Node versions can only `import` them).
|
|
109
|
+
* TypeScript consumers need `"module"`/`"moduleResolution"` set to `nodenext` (or `node20`) or `bundler`.
|
|
110
|
+
A CommonJS project using `node16` (or `nodenext` before TypeScript 5.8) gets error TS1479; switch to one of
|
|
111
|
+
those settings or load the packages with `import()`.
|
|
112
|
+
* Only the package roots are exported: deep imports such as `@triptease/sql-template/Text` fail with
|
|
113
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`. Import everything from `@triptease/sql-template` /
|
|
114
|
+
`@triptease/sql-template-postgres`.
|
|
115
|
+
* `Expression` is abstract and subclassing it is not supported: an instance of your own subclass is rejected with
|
|
116
|
+
a `TypeError` (compose `SQL`/`text`/`id`/`value`/`template` instead).
|
|
117
|
+
* `debugQuery` output changed: `null`/`undefined` render as `NULL`, booleans as `TRUE`/`FALSE`, negative numbers
|
|
118
|
+
are parenthesised, `NaN`/`Infinity` are quoted, and Dates, Buffers, arrays and objects are rendered as escaped
|
|
119
|
+
literals. Do not parse or compare its output.
|
|
120
|
+
* A `Template` built directly with `new Template([...])` now keeps the values of nested templates (they used to be
|
|
121
|
+
dropped).
|
|
122
|
+
|
|
123
|
+
## Extending
|
|
124
|
+
|
|
125
|
+
It is simple to extend to other DBs: switch on `kindOf(expression)` (`'text'`, `'identifier'`, `'value'`,
|
|
126
|
+
`'template'`) and throw on anything else. Have a look at the
|
|
127
|
+
[postgres implementation](https://github.com/triptease/sql-template/blob/master/sql-template-postgres/src/statement.ts).
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
Tool versions (node, bun) are pinned in `mise.toml`. Install [mise](https://mise.jdx.dev/getting-started.html), then:
|
|
132
|
+
|
|
133
|
+
```shell
|
|
134
|
+
mise install # installs the pinned node and bun
|
|
135
|
+
./run # install deps, clean, build (tsc --build), typecheck and test (bun test)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The repo is a bun workspace: each package has a published `src/package.json` (runtime dependencies only)
|
|
139
|
+
and a private `test/package.json` (test dependencies). Tests import the packages by name.
|
|
140
|
+
|
|
141
|
+
The postgres integration tests (`sql-template-postgres/test/integration.test.ts`) run real queries through
|
|
142
|
+
`pg`. They need a database:
|
|
143
|
+
|
|
144
|
+
```shell
|
|
145
|
+
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres ./run
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Without `DATABASE_URL` they are skipped locally (with a warning) but fail when `CI=true`; the GitHub Actions
|
|
149
|
+
test job provides a `postgres:18` service.
|
|
150
|
+
|
|
151
|
+
## Releasing
|
|
152
|
+
|
|
153
|
+
Every push to `triptease/sql-template` that passes the `test` job runs the `publish` job in
|
|
154
|
+
[`.github/workflows/build.yml`](.github/workflows/build.yml), which runs `scripts/release.ts`:
|
|
155
|
+
|
|
156
|
+
* Version: `0.<git rev-list --count HEAD>.<GITHUB_RUN_NUMBER>`; dist-tag `latest` on `master`, `dev` on other branches.
|
|
157
|
+
* It does a clean `tsc --build` (without declaration maps, as the `.ts` sources are not published), then
|
|
158
|
+
writes `<pkg>/dist/package.json` from `<pkg>/src/package.json` (version
|
|
159
|
+
stamped, `workspace:*` replaced by that version, `main`/`types`/`exports` pointing at the `.js`/`.d.ts`
|
|
160
|
+
files, explicit `files`) and copies this README next to it. The `dist` directory is what gets published, so
|
|
161
|
+
import paths never contain `src/` or `dist/`. It fails if any `workspace:` range is left.
|
|
162
|
+
* It publishes core first, then postgres, with `npm publish <pkg>/dist`:
|
|
163
|
+
* to **GitHub Packages** (`npm.pkg.github.com`) on every run, using the workflow's `GITHUB_TOKEN`;
|
|
164
|
+
* to **npmjs** only when the repo variable `NPM_PUBLISH` is `true`, with `--access public --provenance`,
|
|
165
|
+
authenticated by npm Trusted Publishing (OIDC), or by an `NPM_TOKEN` secret if one exists.
|
|
166
|
+
* Versions that are already on a registry are skipped, so a failed run can simply be re-run.
|
|
167
|
+
* The staged `dist` directories are kept as the `dist` artifact of the workflow run.
|
|
168
|
+
|
|
169
|
+
Check what would be published without uploading anything (`npm publish --dry-run`; add `NPM_PUBLISH=true`
|
|
170
|
+
to include npmjs):
|
|
171
|
+
|
|
172
|
+
```shell
|
|
173
|
+
./run release --dry-run
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`./run ci` runs the full build and tests, then the release script.
|
|
177
|
+
|
|
178
|
+
### One-off setup (needs an npm org admin and a GitHub repo admin)
|
|
179
|
+
|
|
180
|
+
1. On npmjs.com, for **each** of `@triptease/sql-template` and `@triptease/sql-template-postgres`:
|
|
181
|
+
Settings → Trusted Publisher → GitHub Actions, organization `triptease`, repository `sql-template`,
|
|
182
|
+
workflow filename `build.yml`, environment empty. The workflow filename is part of the trust
|
|
183
|
+
relationship, so do not rename `build.yml`. Once it works, npm recommends disallowing token publishing
|
|
184
|
+
for the packages (Settings → Publishing access).
|
|
185
|
+
2. In the GitHub repo, set the Actions variable `NPM_PUBLISH` to `true` (Settings → Secrets and variables →
|
|
186
|
+
Actions → Variables). Until then npmjs publishing is skipped and only GitHub Packages is published.
|
|
187
|
+
(Alternative to step 1: add an `NPM_TOKEN` repo secret with publish rights; the publish step uses it if set.)
|
|
188
|
+
3. GitHub Packages: after the first publish, check both packages under the org's Packages page, make sure
|
|
189
|
+
they are linked to this repository and set their visibility to public if they should be visible
|
|
190
|
+
outside the org.
|
|
191
|
+
|
|
192
|
+
### Installing from GitHub Packages
|
|
193
|
+
|
|
194
|
+
npmjs is the main channel. Installing from GitHub Packages needs authentication even for public packages:
|
|
195
|
+
a token with `read:packages` and an `.npmrc` like
|
|
196
|
+
|
|
197
|
+
```ini
|
|
198
|
+
@triptease:registry=https://npm.pkg.github.com
|
|
199
|
+
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
|
|
200
|
+
```
|
package/escape.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Quotes an identifier for Postgres, e.g. `user's "x"` -> `"user's ""x"""`. */
|
|
2
|
+
export declare function escapeIdentifier(str: string): string;
|
|
3
|
+
/** Quotes a string literal for Postgres, e.g. `it's` -> `'it''s'` (uses `E'...'` when it contains backslashes). */
|
|
4
|
+
export declare function escapeLiteral(str: string): string;
|
|
5
|
+
/** @internal Lower-case hex of the bytes of a typed array / DataView / Buffer. */
|
|
6
|
+
export declare function hex(view: ArrayBufferView): string;
|
|
7
|
+
/**
|
|
8
|
+
* @internal Converts a JS array to a Postgres array literal the same way pg does
|
|
9
|
+
* (ported from pg/lib/utils.js arrayString, MIT), except Dates are always sent as UTC ISO strings.
|
|
10
|
+
*/
|
|
11
|
+
export declare function arrayLiteral(array: readonly unknown[]): string;
|
package/escape.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// escapeIdentifier and escapeLiteral are ported from node-postgres (pg/lib/utils.js),
|
|
2
|
+
// Copyright (c) 2010 - 2021 Brian Carlson, MIT License: https://github.com/brianc/node-postgres
|
|
3
|
+
// The only change is that non-string input throws instead of being coerced.
|
|
4
|
+
function requireString(value, what) {
|
|
5
|
+
if (typeof value !== 'string')
|
|
6
|
+
throw new TypeError(`${what} must be a string but was ${value === null ? 'null' : typeof value}`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
/** Quotes an identifier for Postgres, e.g. `user's "x"` -> `"user's ""x"""`. */
|
|
10
|
+
export function escapeIdentifier(str) {
|
|
11
|
+
return '"' + requireString(str, 'Identifier').replace(/"/g, '""') + '"';
|
|
12
|
+
}
|
|
13
|
+
/** Quotes a string literal for Postgres, e.g. `it's` -> `'it''s'` (uses `E'...'` when it contains backslashes). */
|
|
14
|
+
export function escapeLiteral(str) {
|
|
15
|
+
requireString(str, 'Literal');
|
|
16
|
+
let hasBackslash = false;
|
|
17
|
+
let escaped = "'";
|
|
18
|
+
for (let i = 0; i < str.length; i++) {
|
|
19
|
+
const c = str[i];
|
|
20
|
+
if (c === "'") {
|
|
21
|
+
escaped += c + c;
|
|
22
|
+
}
|
|
23
|
+
else if (c === '\\') {
|
|
24
|
+
escaped += c + c;
|
|
25
|
+
hasBackslash = true;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
escaped += c;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
escaped += "'";
|
|
32
|
+
if (hasBackslash)
|
|
33
|
+
escaped = ' E' + escaped;
|
|
34
|
+
return escaped;
|
|
35
|
+
}
|
|
36
|
+
/** @internal Lower-case hex of the bytes of a typed array / DataView / Buffer. */
|
|
37
|
+
export function hex(view) {
|
|
38
|
+
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
39
|
+
let result = '';
|
|
40
|
+
for (const b of bytes)
|
|
41
|
+
result += b.toString(16).padStart(2, '0');
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
// Ported from pg/lib/utils.js (MIT, see above)
|
|
45
|
+
function escapeElement(element) {
|
|
46
|
+
return '"' + element.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* @internal Converts a JS array to a Postgres array literal the same way pg does
|
|
50
|
+
* (ported from pg/lib/utils.js arrayString, MIT), except Dates are always sent as UTC ISO strings.
|
|
51
|
+
*/
|
|
52
|
+
export function arrayLiteral(array) {
|
|
53
|
+
return '{' + array.map(item => {
|
|
54
|
+
if (item === null || item === undefined)
|
|
55
|
+
return 'NULL';
|
|
56
|
+
if (Array.isArray(item))
|
|
57
|
+
return arrayLiteral(item);
|
|
58
|
+
if (ArrayBuffer.isView(item))
|
|
59
|
+
return '\\\\x' + hex(item);
|
|
60
|
+
if (item instanceof Date)
|
|
61
|
+
return escapeElement(item.toISOString());
|
|
62
|
+
if (typeof item === 'object')
|
|
63
|
+
return escapeElement(JSON.stringify(item));
|
|
64
|
+
return escapeElement(String(item));
|
|
65
|
+
}).join(',') + '}';
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=escape.js.map
|
package/escape.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape.js","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"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","sourcesContent":["// escapeIdentifier and escapeLiteral are ported from node-postgres (pg/lib/utils.js),\n// Copyright (c) 2010 - 2021 Brian Carlson, MIT License: https://github.com/brianc/node-postgres\n// The only change is that non-string input throws instead of being coerced.\n\nfunction requireString(value: unknown, what: string): string {\n if (typeof value !== 'string') throw new TypeError(`${what} must be a string but was ${value === null ? 'null' : typeof value}`);\n return value;\n}\n\n/** Quotes an identifier for Postgres, e.g. `user's \"x\"` -> `\"user's \"\"x\"\"\"`. */\nexport function escapeIdentifier(str: string): string {\n return '\"' + requireString(str, 'Identifier').replace(/\"/g, '\"\"') + '\"';\n}\n\n/** Quotes a string literal for Postgres, e.g. `it's` -> `'it''s'` (uses `E'...'` when it contains backslashes). */\nexport function escapeLiteral(str: string): string {\n requireString(str, 'Literal');\n let hasBackslash = false;\n let escaped = \"'\";\n for (let i = 0; i < str.length; i++) {\n const c = str[i];\n if (c === \"'\") {\n escaped += c + c;\n } else if (c === '\\\\') {\n escaped += c + c;\n hasBackslash = true;\n } else {\n escaped += c;\n }\n }\n escaped += \"'\";\n if (hasBackslash) escaped = ' E' + escaped;\n return escaped;\n}\n\n/** @internal Lower-case hex of the bytes of a typed array / DataView / Buffer. */\nexport function hex(view: ArrayBufferView): string {\n const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);\n let result = '';\n for (const b of bytes) result += b.toString(16).padStart(2, '0');\n return result;\n}\n\n// Ported from pg/lib/utils.js (MIT, see above)\nfunction escapeElement(element: string): string {\n return '\"' + element.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"') + '\"';\n}\n\n/**\n * @internal Converts a JS array to a Postgres array literal the same way pg does\n * (ported from pg/lib/utils.js arrayString, MIT), except Dates are always sent as UTC ISO strings.\n */\nexport function arrayLiteral(array: readonly unknown[]): string {\n return '{' + array.map(item => {\n if (item === null || item === undefined) return 'NULL';\n if (Array.isArray(item)) return arrayLiteral(item);\n if (ArrayBuffer.isView(item)) return '\\\\\\\\x' + hex(item);\n if (item instanceof Date) return escapeElement(item.toISOString());\n if (typeof item === 'object') return escapeElement(JSON.stringify(item));\n return escapeElement(String(item));\n }).join(',') + '}';\n}\n"]}
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export declare function debugQuery(sql: Template): string;
|
|
4
|
-
export declare function statement(template: Template): QueryConfig;
|
|
5
|
-
export declare function prepareStatement(template: Template, name?: string): QueryConfig;
|
|
1
|
+
export { type QueryConfig, statement, prepareStatement, debugQuery } from './statement.js';
|
|
2
|
+
export { escapeIdentifier, escapeLiteral } from './escape.js';
|
package/index.js
CHANGED
|
@@ -1,52 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.prepareStatement = exports.statement = exports.debugQuery = void 0;
|
|
4
|
-
const pg_1 = require("pg");
|
|
5
|
-
const crypto_1 = require("crypto");
|
|
6
|
-
const sql_template_1 = require("@triptease/sql-template");
|
|
7
|
-
const escapeIdentifier = pg_1.Client.prototype.escapeIdentifier;
|
|
8
|
-
const escapeLiteral = pg_1.Client.prototype.escapeLiteral;
|
|
9
|
-
function debugQuery(sql) {
|
|
10
|
-
return sql.expressions.reduce((a, e) => {
|
|
11
|
-
if (e instanceof sql_template_1.Text)
|
|
12
|
-
return a + e.text;
|
|
13
|
-
if (e instanceof sql_template_1.Identifier)
|
|
14
|
-
return a + escapeIdentifier(e.identifier);
|
|
15
|
-
if (e instanceof sql_template_1.Value)
|
|
16
|
-
return a + (typeof e.value === 'string' ? escapeLiteral(e.value) : e.value);
|
|
17
|
-
return a;
|
|
18
|
-
}, '');
|
|
19
|
-
}
|
|
20
|
-
exports.debugQuery = debugQuery;
|
|
21
|
-
function toSql(sql) {
|
|
22
|
-
let count = 1;
|
|
23
|
-
return sql.expressions.reduce((a, e) => {
|
|
24
|
-
if (e instanceof sql_template_1.Text)
|
|
25
|
-
return a + e.text;
|
|
26
|
-
if (e instanceof sql_template_1.Identifier)
|
|
27
|
-
return a + escapeIdentifier(e.identifier);
|
|
28
|
-
if (e instanceof sql_template_1.Value)
|
|
29
|
-
return a + '$' + count++;
|
|
30
|
-
return a;
|
|
31
|
-
}, '');
|
|
32
|
-
}
|
|
33
|
-
function statement(template) {
|
|
34
|
-
return {
|
|
35
|
-
text: toSql(template),
|
|
36
|
-
values: template.expressions.flatMap(e => e instanceof sql_template_1.Value ? [e.value] : [])
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
exports.statement = statement;
|
|
40
|
-
function hashSHA256(value) {
|
|
41
|
-
return (0, crypto_1.createHash)('sha256').update(value).digest('hex');
|
|
42
|
-
}
|
|
43
|
-
function prepareStatement(template, name) {
|
|
44
|
-
const { text, values } = statement(template);
|
|
45
|
-
return {
|
|
46
|
-
name: name !== null && name !== void 0 ? name : hashSHA256(text).slice(0, 63),
|
|
47
|
-
text,
|
|
48
|
-
values
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
exports.prepareStatement = prepareStatement;
|
|
1
|
+
export { statement, prepareStatement, debugQuery } from './statement.js';
|
|
2
|
+
export { escapeIdentifier, escapeLiteral } from './escape.js';
|
|
52
3
|
//# sourceMappingURL=index.js.map
|
package/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,SAAS,EAAE,gBAAgB,EAAE,UAAU,EAAC,MAAM,gBAAgB,CAAC;AACzF,OAAO,EAAC,gBAAgB,EAAE,aAAa,EAAC,MAAM,aAAa,CAAC","sourcesContent":["export {type QueryConfig, statement, prepareStatement, debugQuery} from './statement.js';\nexport {escapeIdentifier, escapeLiteral} from './escape.js';\n"]}
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@triptease/sql-template-postgres",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.6",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"repository": {
|
|
5
6
|
"type": "git",
|
|
6
|
-
"url": "git+
|
|
7
|
+
"url": "git+https://github.com/triptease/sql-template.git",
|
|
8
|
+
"directory": "sql-template-postgres/src"
|
|
7
9
|
},
|
|
8
10
|
"author": "Triptease",
|
|
9
11
|
"license": "MIT",
|
|
@@ -11,22 +13,22 @@
|
|
|
11
13
|
"url": "https://github.com/triptease/sql-template/issues"
|
|
12
14
|
},
|
|
13
15
|
"homepage": "https://github.com/triptease/sql-template/",
|
|
14
|
-
"publishConfig": {
|
|
15
|
-
"directory": "../dist",
|
|
16
|
-
"access": "public"
|
|
17
|
-
},
|
|
18
|
-
"files": [
|
|
19
|
-
"*.js",
|
|
20
|
-
"*.d.ts"
|
|
21
|
-
],
|
|
22
16
|
"dependencies": {
|
|
23
|
-
"@triptease/sql-template": "0.
|
|
24
|
-
"pg": "^8.7.1",
|
|
25
|
-
"tslib": "^2.3.1"
|
|
17
|
+
"@triptease/sql-template": "0.35.6"
|
|
26
18
|
},
|
|
27
|
-
"
|
|
28
|
-
|
|
19
|
+
"main": "./index.js",
|
|
20
|
+
"types": "./index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./index.d.ts",
|
|
24
|
+
"default": "./index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
29
27
|
},
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
"files": [
|
|
29
|
+
"**/*.js",
|
|
30
|
+
"**/*.js.map",
|
|
31
|
+
"**/*.d.ts",
|
|
32
|
+
"README.md"
|
|
33
|
+
]
|
|
34
|
+
}
|
package/statement.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Template } from '@triptease/sql-template';
|
|
2
|
+
/** Structurally compatible with pg's `QueryConfig`, so pg is not a dependency. */
|
|
3
|
+
export interface QueryConfig {
|
|
4
|
+
text: string;
|
|
5
|
+
values: unknown[];
|
|
6
|
+
name?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Converts a DB agnostic template into a postgres statement (`$1`, `$2`, ... placeholders plus values). */
|
|
9
|
+
export declare function statement(template: Template): QueryConfig;
|
|
10
|
+
/** Like `statement` but named, so postgres prepares it. The default name is derived from the SQL text. */
|
|
11
|
+
export declare function prepareStatement(template: Template, name?: string): Required<QueryConfig>;
|
|
12
|
+
/**
|
|
13
|
+
* Renders the template as a single SQL string with every value inlined as an escaped literal.
|
|
14
|
+
* Intended for logging/debugging: prefer `statement`/`prepareStatement` (bound parameters) for execution,
|
|
15
|
+
* as inlined literals can be typed differently by postgres (e.g. arrays become text array literals).
|
|
16
|
+
*/
|
|
17
|
+
export declare function debugQuery(template: Template): string;
|
package/statement.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { kindOf } from '@triptease/sql-template';
|
|
3
|
+
import { arrayLiteral, escapeIdentifier, escapeLiteral, hex } from './escape.js';
|
|
4
|
+
/** @internal Renders a template, escaping identifiers and delegating values. Throws on anything it does not understand. */
|
|
5
|
+
function render(template, renderValue) {
|
|
6
|
+
let sql = '';
|
|
7
|
+
const visit = (e) => {
|
|
8
|
+
const kind = kindOf(e);
|
|
9
|
+
switch (kind) {
|
|
10
|
+
case 'text': {
|
|
11
|
+
const text = e.text;
|
|
12
|
+
if (typeof text !== 'string')
|
|
13
|
+
throw new TypeError('Text expression without a string');
|
|
14
|
+
sql += text;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
case 'identifier':
|
|
18
|
+
sql += escapeIdentifier(e.identifier);
|
|
19
|
+
return;
|
|
20
|
+
case 'value':
|
|
21
|
+
sql += renderValue(e.value);
|
|
22
|
+
return;
|
|
23
|
+
case 'template':
|
|
24
|
+
for (const child of e.expressions)
|
|
25
|
+
visit(child);
|
|
26
|
+
return;
|
|
27
|
+
default:
|
|
28
|
+
throw new TypeError(kind === undefined
|
|
29
|
+
? `Not an Expression: ${e === null ? 'null' : typeof e}`
|
|
30
|
+
: `Unsupported expression kind: ${kind}`);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
visit(template);
|
|
34
|
+
return sql;
|
|
35
|
+
}
|
|
36
|
+
/** Converts a DB agnostic template into a postgres statement (`$1`, `$2`, ... placeholders plus values). */
|
|
37
|
+
export function statement(template) {
|
|
38
|
+
const values = [];
|
|
39
|
+
const text = render(template, value => '$' + values.push(value));
|
|
40
|
+
return { text, values };
|
|
41
|
+
}
|
|
42
|
+
function hashSHA256(value) {
|
|
43
|
+
return createHash('sha256').update(value).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
/** Like `statement` but named, so postgres prepares it. The default name is derived from the SQL text. */
|
|
46
|
+
export function prepareStatement(template, name) {
|
|
47
|
+
const { text, values } = statement(template);
|
|
48
|
+
return {
|
|
49
|
+
name: name ?? hashSHA256(text).slice(0, 63),
|
|
50
|
+
text,
|
|
51
|
+
values
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function numberLiteral(value) {
|
|
55
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
56
|
+
return `'${value}'`; // 'NaN', 'Infinity', '-Infinity'
|
|
57
|
+
const text = String(value);
|
|
58
|
+
// parenthesise negatives so that e.g. `1 -${-1}` cannot become a `--` comment
|
|
59
|
+
return text.startsWith('-') ? `(${text})` : text;
|
|
60
|
+
}
|
|
61
|
+
/** @internal Renders a value as an escaped postgres literal. */
|
|
62
|
+
function literal(value) {
|
|
63
|
+
if (value === null || value === undefined)
|
|
64
|
+
return 'NULL';
|
|
65
|
+
switch (typeof value) {
|
|
66
|
+
case 'string':
|
|
67
|
+
return escapeLiteral(value);
|
|
68
|
+
case 'number':
|
|
69
|
+
case 'bigint':
|
|
70
|
+
return numberLiteral(value);
|
|
71
|
+
case 'boolean':
|
|
72
|
+
return value ? 'TRUE' : 'FALSE';
|
|
73
|
+
case 'object':
|
|
74
|
+
if (value instanceof Date)
|
|
75
|
+
return escapeLiteral(value.toISOString());
|
|
76
|
+
if (ArrayBuffer.isView(value))
|
|
77
|
+
return `'\\x${hex(value)}'::bytea`;
|
|
78
|
+
if (Array.isArray(value))
|
|
79
|
+
return escapeLiteral(arrayLiteral(value));
|
|
80
|
+
return escapeLiteral(JSON.stringify(value));
|
|
81
|
+
default:
|
|
82
|
+
throw new TypeError(`Cannot render a ${typeof value} as SQL`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Renders the template as a single SQL string with every value inlined as an escaped literal.
|
|
87
|
+
* Intended for logging/debugging: prefer `statement`/`prepareStatement` (bound parameters) for execution,
|
|
88
|
+
* as inlined literals can be typed differently by postgres (e.g. arrays become text array literals).
|
|
89
|
+
*/
|
|
90
|
+
export function debugQuery(template) {
|
|
91
|
+
return render(template, literal);
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=statement.js.map
|
package/statement.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statement.js","sourceRoot":"","sources":["../src/statement.ts"],"names":[],"mappings":"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","sourcesContent":["import {createHash} from 'node:crypto';\nimport {type Expression, type Identifier, kindOf, type Template, type Text, type Value} from '@triptease/sql-template';\nimport {arrayLiteral, escapeIdentifier, escapeLiteral, hex} from './escape.js';\n\n/** Structurally compatible with pg's `QueryConfig`, so pg is not a dependency. */\nexport interface QueryConfig {\n text: string;\n values: unknown[];\n name?: string;\n}\n\n/** @internal Renders a template, escaping identifiers and delegating values. Throws on anything it does not understand. */\nfunction render(template: Template, renderValue: (value: unknown) => string): string {\n let sql = '';\n const visit = (e: Expression): void => {\n const kind = kindOf(e);\n switch (kind) {\n case 'text': {\n const text = (e as Text).text;\n if (typeof text !== 'string') throw new TypeError('Text expression without a string');\n sql += text;\n return;\n }\n case 'identifier':\n sql += escapeIdentifier((e as Identifier).identifier);\n return;\n case 'value':\n sql += renderValue((e as Value).value);\n return;\n case 'template':\n for (const child of (e as Template).expressions) visit(child);\n return;\n default:\n throw new TypeError(kind === undefined\n ? `Not an Expression: ${e === null ? 'null' : typeof e}`\n : `Unsupported expression kind: ${kind}`);\n }\n };\n visit(template);\n return sql;\n}\n\n/** Converts a DB agnostic template into a postgres statement (`$1`, `$2`, ... placeholders plus values). */\nexport function statement(template: Template): QueryConfig {\n const values: unknown[] = [];\n const text = render(template, value => '$' + values.push(value));\n return {text, values};\n}\n\nfunction hashSHA256(value: string): string {\n return createHash('sha256').update(value).digest('hex');\n}\n\n/** Like `statement` but named, so postgres prepares it. The default name is derived from the SQL text. */\nexport function prepareStatement(template: Template, name?: string): Required<QueryConfig> {\n const {text, values} = statement(template);\n return {\n name: name ?? hashSHA256(text).slice(0, 63),\n text,\n values\n };\n}\n\nfunction numberLiteral(value: number | bigint): string {\n if (typeof value === 'number' && !Number.isFinite(value)) return `'${value}'`; // 'NaN', 'Infinity', '-Infinity'\n const text = String(value);\n // parenthesise negatives so that e.g. `1 -${-1}` cannot become a `--` comment\n return text.startsWith('-') ? `(${text})` : text;\n}\n\n/** @internal Renders a value as an escaped postgres literal. */\nfunction literal(value: unknown): string {\n if (value === null || value === undefined) return 'NULL';\n switch (typeof value) {\n case 'string':\n return escapeLiteral(value);\n case 'number':\n case 'bigint':\n return numberLiteral(value);\n case 'boolean':\n return value ? 'TRUE' : 'FALSE';\n case 'object':\n if (value instanceof Date) return escapeLiteral(value.toISOString());\n if (ArrayBuffer.isView(value)) return `'\\\\x${hex(value)}'::bytea`;\n if (Array.isArray(value)) return escapeLiteral(arrayLiteral(value));\n return escapeLiteral(JSON.stringify(value));\n default:\n throw new TypeError(`Cannot render a ${typeof value} as SQL`);\n }\n}\n\n/**\n * Renders the template as a single SQL string with every value inlined as an escaped literal.\n * Intended for logging/debugging: prefer `statement`/`prepareStatement` (bound parameters) for execution,\n * as inlined literals can be typed differently by postgres (e.g. arrays become text array literals).\n */\nexport function debugQuery(template: Template): string {\n return render(template, literal);\n}\n"]}
|