@triptease/sql-template-postgres 0.23.24 → 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 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 (the main SQL function is [3 lines of code](https://github.com/triptease/sql-template/blob/master/sql-template/src/SQL.ts#L5))
10
- * Full escaping of identifiers and values
11
- * Plugable to any DB (currently only Postgres @triptease\sql-template-postgres )
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
- ## Usage
21
-
22
- ```javascript
23
- import {SQL, id} from "@triptease/sql-template";
24
- import {statement} from "@triptease/sql-template-postgres";
25
-
26
- client.query(statement(SQL`select * from ${id(table)} where name = ${name}`));
27
- ```
28
-
29
- ## Cheatsheet
30
-
31
- ### Core (@triptease/sql-template)
32
-
33
- | function | Description |
34
- |------------------------------------------------|----------------------------------------------------------------------|
35
- | `SQL` | The main function to create tagged templates for SQL (*DB agnostic*) |
36
- | `text` (alias `raw`) | Input raw SQL without any escaping (*use with care*) |
37
- | `id` / `ids` | Input dynamic identifiers into SQL (*escaped as needed*) |
38
- | `value` (optional) / `values` (alias `spread`) | Input one or more values into SQL (*escaped as needed*) |
39
-
40
-
41
- ### Postgres (@triptease/sql-template-postgres)
42
-
43
- | function | Description |
44
- |------------------------------------------------|---------------------------------------------------------------------|
45
- | `statement` | Converts DB agnostic `SQL` template into postgres statement |
46
- | `prepareStatement` | Converts DB agnostic `SQL` template into postgres prepare statement |
47
- | `debugQuery` | Used to debug a query (*use with care*) |
48
-
49
- *use with care -> Used incorrectly you can open yourself up to SQL injection*
50
-
51
-
52
- ### Extending
53
-
54
- It is incredibly simple to extend to other DBs, have a look at the [postgres implementation](https://github.com/triptease/sql-template/blob/master/sql-template-postgres/src/index.ts#L17).
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":"AAAA,sFAAsF;AACtF,gGAAgG;AAChG,4EAA4E;AAE5E,SAAS,aAAa,CAAC,KAAc,EAAE,IAAY;IAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,SAAS,CAAC,GAAG,IAAI,6BAA6B,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;IACjI,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB,CAAC,GAAW;IACxC,OAAO,GAAG,GAAG,aAAa,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,GAAG,CAAC;AAC5E,CAAC;AAED,mHAAmH;AACnH,MAAM,UAAU,aAAa,CAAC,GAAW;IACrC,aAAa,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC9B,IAAI,YAAY,GAAG,KAAK,CAAC;IACzB,IAAI,OAAO,GAAG,GAAG,CAAC;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACZ,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;aAAM,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YACpB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;YACjB,YAAY,GAAG,IAAI,CAAC;QACxB,CAAC;aAAM,CAAC;YACJ,OAAO,IAAI,CAAC,CAAC;QACjB,CAAC;IACL,CAAC;IACD,OAAO,IAAI,GAAG,CAAC;IACf,IAAI,YAAY;QAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC;IAC3C,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,GAAG,CAAC,IAAqB;IACrC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC5E,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,MAAM,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACjE,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,+CAA+C;AAC/C,SAAS,aAAa,CAAC,OAAe;IAClC,OAAO,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,GAAG,CAAC;AAC3E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAyB;IAClD,OAAO,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QAC1B,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QACvD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,OAAO,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;QACzD,IAAI,IAAI,YAAY,IAAI;YAAE,OAAO,aAAa,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QACnE,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QACzE,OAAO,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;AACvB,CAAC","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
- import { QueryConfig } from "pg";
2
- import { Template } from '@triptease/sql-template';
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
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 : `urn:hash::sha256:${hashSHA256(text)}`,
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.23.24",
3
+ "version": "0.35.6",
4
+ "type": "module",
4
5
  "repository": {
5
6
  "type": "git",
6
- "url": "git+ssh://git@github.com/triptease/sql-template.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.23.24",
24
- "pg": "^8.7.1",
25
- "tslib": "^2.3.1"
17
+ "@triptease/sql-template": "0.35.6"
26
18
  },
27
- "devDependencies": {
28
- "@types/pg": "^8.6.4"
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
- "scripts": {},
31
- "readme": "# Sql Template\r\n\r\nThis is yet another SQL tagged template for Typescript/Javascript\r\n\r\n## Why another library?\r\n\r\n* Typescript first\r\n* Functional/Immutable\r\n* Super simple implementation (the main SQL function is [3 lines of code](https://github.com/triptease/sql-template/blob/master/sql-template/src/SQL.ts#L5))\r\n* Full escaping of identifiers and values\r\n* Plugable to any DB (currently only Postgres @triptease\\sql-template-postgres )\r\n* Automatic support for prepareStatement naming (Postgres)\r\n\r\n## Installation\r\n\r\n```shell\r\nnpm install @triptease/sql-template @triptease/sql-template-postgres\r\n```\r\n\r\n## Usage\r\n\r\n```javascript\r\nimport {SQL, id} from \"@triptease/sql-template\";\r\nimport {statement} from \"@triptease/sql-template-postgres\";\r\n\r\nclient.query(statement(SQL`select * from ${id(table)} where name = ${name}`));\r\n```\r\n\r\n## Cheatsheet\r\n\r\n### Core (@triptease/sql-template) \r\n\r\n| function | Description |\r\n|------------------------------------------------|----------------------------------------------------------------------|\r\n| `SQL` | The main function to create tagged templates for SQL (*DB agnostic*) |\r\n| `text` (alias `raw`) | Input raw SQL without any escaping (*use with care*) |\r\n | `id` / `ids` | Input dynamic identifiers into SQL (*escaped as needed*) |\r\n | `value` (optional) / `values` (alias `spread`) | Input one or more values into SQL (*escaped as needed*) |\r\n\r\n\r\n### Postgres (@triptease/sql-template-postgres)\r\n\r\n| function | Description |\r\n|------------------------------------------------|---------------------------------------------------------------------|\r\n| `statement` | Converts DB agnostic `SQL` template into postgres statement |\r\n| `prepareStatement` | Converts DB agnostic `SQL` template into postgres prepare statement |\r\n| `debugQuery` | Used to debug a query (*use with care*) |\r\n\r\n*use with care -> Used incorrectly you can open yourself up to SQL injection*\r\n\r\n\r\n### Extending\r\n\r\nIt is incredibly simple to extend to other DBs, have a look at the [postgres implementation](https://github.com/triptease/sql-template/blob/master/sql-template-postgres/src/index.ts#L17). \r\n\r\n\r\n"
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
@@ -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"]}