@triptease/sql-template 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/Expression.d.ts +23 -1
- package/Expression.js +25 -5
- package/Expression.js.map +1 -0
- package/Identifier.d.ts +12 -3
- package/Identifier.js +22 -13
- package/Identifier.js.map +1 -0
- package/README.md +200 -56
- package/SQL.d.ts +2 -2
- package/SQL.js +10 -11
- package/SQL.js.map +1 -0
- package/Template.d.ts +10 -1
- package/Template.js +32 -17
- package/Template.js.map +1 -0
- package/Text.d.ts +7 -1
- package/Text.js +15 -9
- package/Text.js.map +1 -0
- package/Value.d.ts +19 -6
- package/Value.js +29 -15
- package/Value.js.map +1 -0
- package/index.d.ts +6 -6
- package/index.js +6 -9
- package/index.js.map +1 -0
- package/invalid.d.ts +6 -0
- package/invalid.js +14 -0
- package/invalid.js.map +1 -0
- package/package.json +18 -14
- package/separated.d.ts +3 -0
- package/separated.js +9 -0
- package/separated.js.map +1 -0
package/Expression.d.ts
CHANGED
|
@@ -1,2 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Brand used to identify expressions at runtime.
|
|
3
|
+
*
|
|
4
|
+
* It is registered with `Symbol.for` so that expressions created by a different copy of this package
|
|
5
|
+
* (e.g. a duplicated install, or a different version pulled in by an adapter) are still recognised,
|
|
6
|
+
* which `instanceof` cannot do. It is a symbol rather than a string field so that untrusted data
|
|
7
|
+
* (e.g. the output of `JSON.parse`) can never masquerade as an expression.
|
|
8
|
+
*/
|
|
9
|
+
export declare const kind: unique symbol;
|
|
10
|
+
/** The kinds of expression understood by adapters. Adapters must throw on anything else. */
|
|
11
|
+
export type Kind = 'text' | 'identifier' | 'value' | 'template';
|
|
12
|
+
export interface Expression {
|
|
13
|
+
readonly [kind]: Kind;
|
|
2
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Base class of all expressions. Every concrete expression defines its `kind` brand on its prototype.
|
|
17
|
+
* Subclassing it outside this package is not supported: such instances have no brand and are rejected
|
|
18
|
+
* by `SQL`, `value` and `template` (compose the built-in expressions instead).
|
|
19
|
+
*/
|
|
20
|
+
export declare abstract class Expression {
|
|
21
|
+
}
|
|
22
|
+
/** The kind of `value` if it is an expression (from any copy of this package), otherwise `undefined`. */
|
|
23
|
+
export declare function kindOf(value: unknown): string | undefined;
|
|
24
|
+
export declare function isExpression(value: unknown): value is Expression;
|
package/Expression.js
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Brand used to identify expressions at runtime.
|
|
3
|
+
*
|
|
4
|
+
* It is registered with `Symbol.for` so that expressions created by a different copy of this package
|
|
5
|
+
* (e.g. a duplicated install, or a different version pulled in by an adapter) are still recognised,
|
|
6
|
+
* which `instanceof` cannot do. It is a symbol rather than a string field so that untrusted data
|
|
7
|
+
* (e.g. the output of `JSON.parse`) can never masquerade as an expression.
|
|
8
|
+
*/
|
|
9
|
+
export const kind = Symbol.for('@triptease/sql-template/kind');
|
|
10
|
+
/**
|
|
11
|
+
* Base class of all expressions. Every concrete expression defines its `kind` brand on its prototype.
|
|
12
|
+
* Subclassing it outside this package is not supported: such instances have no brand and are rejected
|
|
13
|
+
* by `SQL`, `value` and `template` (compose the built-in expressions instead).
|
|
14
|
+
*/
|
|
15
|
+
export class Expression {
|
|
16
|
+
}
|
|
17
|
+
/** The kind of `value` if it is an expression (from any copy of this package), otherwise `undefined`. */
|
|
18
|
+
export function kindOf(value) {
|
|
19
|
+
if (typeof value !== 'object' || value === null)
|
|
20
|
+
return undefined;
|
|
21
|
+
const k = value[kind];
|
|
22
|
+
return typeof k === 'string' ? k : undefined;
|
|
23
|
+
}
|
|
24
|
+
export function isExpression(value) {
|
|
25
|
+
return kindOf(value) !== undefined;
|
|
5
26
|
}
|
|
6
|
-
exports.Expression = Expression;
|
|
7
27
|
//# sourceMappingURL=Expression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Expression.js","sourceRoot":"","sources":["../src/Expression.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,IAAI,GAAkB,MAAM,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;AAS9E;;;;GAIG;AACH,MAAM,OAAgB,UAAU;CAC/B;AAED,yGAAyG;AACzG,MAAM,UAAU,MAAM,CAAC,KAAc;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAClE,MAAM,CAAC,GAAa,KAA8B,CAAC,IAAI,CAAC,CAAC;IACzD,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IACvC,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,SAAS,CAAC;AACvC,CAAC","sourcesContent":["/**\n * Brand used to identify expressions at runtime.\n *\n * It is registered with `Symbol.for` so that expressions created by a different copy of this package\n * (e.g. a duplicated install, or a different version pulled in by an adapter) are still recognised,\n * which `instanceof` cannot do. It is a symbol rather than a string field so that untrusted data\n * (e.g. the output of `JSON.parse`) can never masquerade as an expression.\n */\nexport const kind: unique symbol = Symbol.for('@triptease/sql-template/kind');\n\n/** The kinds of expression understood by adapters. Adapters must throw on anything else. */\nexport type Kind = 'text' | 'identifier' | 'value' | 'template';\n\nexport interface Expression {\n readonly [kind]: Kind;\n}\n\n/**\n * Base class of all expressions. Every concrete expression defines its `kind` brand on its prototype.\n * Subclassing it outside this package is not supported: such instances have no brand and are rejected\n * by `SQL`, `value` and `template` (compose the built-in expressions instead).\n */\nexport abstract class Expression {\n}\n\n/** The kind of `value` if it is an expression (from any copy of this package), otherwise `undefined`. */\nexport function kindOf(value: unknown): string | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const k: unknown = (value as { [kind]?: unknown })[kind];\n return typeof k === 'string' ? k : undefined;\n}\n\nexport function isExpression(value: unknown): value is Expression {\n return kindOf(value) !== undefined;\n}\n"]}
|
package/Identifier.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import { Expression } from "./Expression";
|
|
2
|
-
import { Template } from "./Template";
|
|
1
|
+
import { Expression, kind } from "./Expression.js";
|
|
2
|
+
import { Template } from "./Template.js";
|
|
3
|
+
export interface Identifier {
|
|
4
|
+
readonly [kind]: 'identifier';
|
|
5
|
+
}
|
|
6
|
+
/** A dynamic identifier (table, column, ...) that adapters escape. */
|
|
3
7
|
export declare class Identifier extends Expression {
|
|
4
8
|
readonly identifier: string;
|
|
5
9
|
constructor(identifier: string);
|
|
6
10
|
}
|
|
11
|
+
export declare function isIdentifier(value: unknown): value is Identifier;
|
|
7
12
|
export declare function id(identifier: string): Identifier;
|
|
8
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Multiple identifiers separated by `separator` (default `text(', ')`).
|
|
15
|
+
* The separator is an Expression so any raw SQL is explicit at the call site.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ids(identifiers: readonly string[], separator?: Expression): Template;
|
package/Identifier.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { Expression, kind, kindOf } from "./Expression.js";
|
|
2
|
+
import { template, Template } from "./Template.js";
|
|
3
|
+
import { text } from "./Text.js";
|
|
4
|
+
import { separated } from "./separated.js";
|
|
5
|
+
/** A dynamic identifier (table, column, ...) that adapters escape. */
|
|
6
|
+
export class Identifier extends Expression {
|
|
7
|
+
identifier;
|
|
8
8
|
constructor(identifier) {
|
|
9
9
|
super();
|
|
10
|
+
if (typeof identifier !== 'string')
|
|
11
|
+
throw new TypeError(`Identifier must be a string but was ${typeof identifier}`);
|
|
10
12
|
this.identifier = identifier;
|
|
13
|
+
if (new.target === Identifier)
|
|
14
|
+
Object.freeze(this);
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
|
-
|
|
14
|
-
function
|
|
17
|
+
Object.defineProperty(Identifier.prototype, kind, { value: 'identifier' });
|
|
18
|
+
export function isIdentifier(value) {
|
|
19
|
+
return kindOf(value) === 'identifier';
|
|
20
|
+
}
|
|
21
|
+
export function id(identifier) {
|
|
15
22
|
return new Identifier(identifier);
|
|
16
23
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Multiple identifiers separated by `separator` (default `text(', ')`).
|
|
26
|
+
* The separator is an Expression so any raw SQL is explicit at the call site.
|
|
27
|
+
*/
|
|
28
|
+
export function ids(identifiers, separator = text(', ')) {
|
|
29
|
+
return template(...separated(identifiers.map(id), separator));
|
|
20
30
|
}
|
|
21
|
-
exports.ids = ids;
|
|
22
31
|
//# sourceMappingURL=Identifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Identifier.js","sourceRoot":"","sources":["../src/Identifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,IAAI,EAAE,MAAM,EAAC,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAC,QAAQ,EAAE,QAAQ,EAAC,MAAM,eAAe,CAAC;AACjD,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAC;AAC/B,OAAO,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAMzC,sEAAsE;AACtE,MAAM,OAAO,UAAW,SAAQ,UAAU;IAC7B,UAAU,CAAS;IAE5B,YAAY,UAAkB;QAC1B,KAAK,EAAE,CAAC;QACR,IAAI,OAAO,UAAU,KAAK,QAAQ;YAAE,MAAM,IAAI,SAAS,CAAC,uCAAuC,OAAO,UAAU,EAAE,CAAC,CAAC;QACpH,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,IAAI,MAAM,KAAK,UAAU;YAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC;CACJ;AAED,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,SAAS,EAAE,IAAI,EAAE,EAAC,KAAK,EAAE,YAAY,EAAC,CAAC,CAAC;AAEzE,MAAM,UAAU,YAAY,CAAC,KAAc;IACvC,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,YAAY,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,EAAE,CAAC,UAAkB;IACjC,OAAO,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,GAAG,CAAC,WAA8B,EAAE,SAAS,GAAe,IAAI,CAAC,IAAI,CAAC;IAClF,OAAO,QAAQ,CAAC,GAAG,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;AAClE,CAAC","sourcesContent":["import {Expression, kind, kindOf} from \"./Expression.js\";\nimport {template, Template} from \"./Template.js\";\nimport {text} from \"./Text.js\";\nimport {separated} from \"./separated.js\";\n\nexport interface Identifier {\n readonly [kind]: 'identifier';\n}\n\n/** A dynamic identifier (table, column, ...) that adapters escape. */\nexport class Identifier extends Expression {\n readonly identifier: string;\n\n constructor(identifier: string) {\n super();\n if (typeof identifier !== 'string') throw new TypeError(`Identifier must be a string but was ${typeof identifier}`);\n this.identifier = identifier;\n if (new.target === Identifier) Object.freeze(this);\n }\n}\n\nObject.defineProperty(Identifier.prototype, kind, {value: 'identifier'});\n\nexport function isIdentifier(value: unknown): value is Identifier {\n return kindOf(value) === 'identifier';\n}\n\nexport function id(identifier: string): Identifier {\n return new Identifier(identifier);\n}\n\n/**\n * Multiple identifiers separated by `separator` (default `text(', ')`).\n * The separator is an Expression so any raw SQL is explicit at the call site.\n */\nexport function ids(identifiers: readonly string[], separator: Expression = text(', ')): Template {\n return template(...separated(identifiers.map(id), separator));\n}\n"]}
|
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/SQL.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Template } from "./Template";
|
|
2
|
-
export declare function SQL(chunks: TemplateStringsArray, ...values:
|
|
1
|
+
import { Template } from "./Template.js";
|
|
2
|
+
export declare function SQL(chunks: TemplateStringsArray, ...values: unknown[]): Template;
|
package/SQL.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { template, Template } from "./Template.js";
|
|
2
|
+
import { text } from "./Text.js";
|
|
3
|
+
import { value } from "./Value.js";
|
|
4
|
+
export function SQL(chunks, ...values) {
|
|
5
|
+
return template(...chunks.flatMap((chunk, index) => {
|
|
6
|
+
if (typeof chunk !== 'string') {
|
|
7
|
+
throw new SyntaxError(`Invalid escape sequence in SQL template: ${JSON.stringify(chunks.raw?.[index])}`);
|
|
8
|
+
}
|
|
9
9
|
if (index > (values.length - 1))
|
|
10
|
-
return [
|
|
11
|
-
return [
|
|
10
|
+
return [text(chunk)];
|
|
11
|
+
return [text(chunk), value(values[index])];
|
|
12
12
|
}));
|
|
13
13
|
}
|
|
14
|
-
exports.SQL = SQL;
|
|
15
14
|
//# sourceMappingURL=SQL.js.map
|
package/SQL.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SQL.js","sourceRoot":"","sources":["../src/SQL.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAE,QAAQ,EAAC,MAAM,eAAe,CAAC;AACjD,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAC;AAC/B,OAAO,EAAC,KAAK,EAAC,MAAM,YAAY,CAAC;AAEjC,MAAM,UAAU,GAAG,CAAC,MAA4B,EAAE,GAAG,MAAiB;IAClE,OAAO,QAAQ,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5B,MAAM,IAAI,WAAW,CAAC,4CAA4C,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;QAC7G,CAAC;QACD,IAAI,KAAK,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC,CAAC;AACR,CAAC","sourcesContent":["import {template, Template} from \"./Template.js\";\nimport {text} from \"./Text.js\";\nimport {value} from \"./Value.js\";\n\nexport function SQL(chunks: TemplateStringsArray, ...values: unknown[]): Template {\n return template(...chunks.flatMap((chunk, index) => {\n if (typeof chunk !== 'string') {\n throw new SyntaxError(`Invalid escape sequence in SQL template: ${JSON.stringify(chunks.raw?.[index])}`);\n }\n if (index > (values.length - 1)) return [text(chunk)];\n return [text(chunk), value(values[index])];\n }));\n}\n"]}
|
package/Template.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { Expression } from "./Expression";
|
|
1
|
+
import { Expression, kind } from "./Expression.js";
|
|
2
|
+
export interface Template {
|
|
3
|
+
readonly [kind]: 'template';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* A sequence of expressions. Construction always normalises: nested templates are flattened
|
|
7
|
+
* (recursively) and empty text is removed, so adapters only ever see Text, Identifier and Value.
|
|
8
|
+
* Templates are frozen.
|
|
9
|
+
*/
|
|
2
10
|
export declare class Template extends Expression {
|
|
3
11
|
readonly expressions: ReadonlyArray<Expression>;
|
|
4
12
|
constructor(expressions: ReadonlyArray<Expression>);
|
|
5
13
|
}
|
|
14
|
+
export declare function isTemplate(value: unknown): value is Template;
|
|
6
15
|
export declare function template(...expressions: Expression[]): Template;
|
package/Template.js
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { Expression, isExpression, kind, kindOf } from "./Expression.js";
|
|
2
|
+
import { invalidExpression } from "./invalid.js";
|
|
3
|
+
function flatten(expressions, result) {
|
|
4
|
+
for (const e of expressions) {
|
|
5
|
+
if (!isExpression(e))
|
|
6
|
+
throw invalidExpression(e, 'Template');
|
|
7
|
+
const k = kindOf(e);
|
|
8
|
+
if (k === 'template')
|
|
9
|
+
flatten(e.expressions, result);
|
|
10
|
+
else if (k === 'text' && e.text === '')
|
|
11
|
+
continue;
|
|
12
|
+
else
|
|
13
|
+
result.push(e);
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A sequence of expressions. Construction always normalises: nested templates are flattened
|
|
19
|
+
* (recursively) and empty text is removed, so adapters only ever see Text, Identifier and Value.
|
|
20
|
+
* Templates are frozen.
|
|
21
|
+
*/
|
|
22
|
+
export class Template extends Expression {
|
|
23
|
+
expressions;
|
|
7
24
|
constructor(expressions) {
|
|
8
25
|
super();
|
|
9
|
-
this.expressions = expressions;
|
|
26
|
+
this.expressions = Object.freeze(flatten(expressions, []));
|
|
27
|
+
if (new.target === Template)
|
|
28
|
+
Object.freeze(this);
|
|
10
29
|
}
|
|
11
30
|
}
|
|
12
|
-
|
|
13
|
-
function
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return e.expressions;
|
|
19
|
-
return [e];
|
|
20
|
-
}));
|
|
31
|
+
Object.defineProperty(Template.prototype, kind, { value: 'template' });
|
|
32
|
+
export function isTemplate(value) {
|
|
33
|
+
return kindOf(value) === 'template';
|
|
34
|
+
}
|
|
35
|
+
export function template(...expressions) {
|
|
36
|
+
return new Template(expressions);
|
|
21
37
|
}
|
|
22
|
-
exports.template = template;
|
|
23
38
|
//# sourceMappingURL=Template.js.map
|
package/Template.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Template.js","sourceRoot":"","sources":["../src/Template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAC,MAAM,iBAAiB,CAAC;AACvE,OAAO,EAAC,iBAAiB,EAAC,MAAM,cAAc,CAAC;AAE/C,SAAS,OAAO,CAAC,WAA+B,EAAE,MAAoB;IAClE,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;QAC1B,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;YAAE,MAAM,iBAAiB,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QAC7D,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,CAAC,KAAK,UAAU;YAAE,OAAO,CAAE,CAAc,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;aAC9D,IAAI,CAAC,KAAK,MAAM,IAAK,CAAwB,CAAC,IAAI,KAAK,EAAE;YAAE,SAAS;;YACpE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAMD;;;;GAIG;AACH,MAAM,OAAO,QAAS,SAAQ,UAAU;IAC3B,WAAW,CAA4B;IAEhD,YAAY,WAAsC;QAC9C,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC;QAC3D,IAAI,IAAI,MAAM,KAAK,QAAQ;YAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACrD,CAAC;CACJ;AAED,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,SAAS,EAAE,IAAI,EAAE,EAAC,KAAK,EAAE,UAAU,EAAC,CAAC,CAAC;AAErE,MAAM,UAAU,UAAU,CAAC,KAAc;IACrC,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,UAAU,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAG,WAAyB;IACjD,OAAO,IAAI,QAAQ,CAAC,WAAW,CAAC,CAAC;AACrC,CAAC","sourcesContent":["import {Expression, isExpression, kind, kindOf} from \"./Expression.js\";\nimport {invalidExpression} from \"./invalid.js\";\n\nfunction flatten(expressions: readonly unknown[], result: Expression[]): Expression[] {\n for (const e of expressions) {\n if (!isExpression(e)) throw invalidExpression(e, 'Template');\n const k = kindOf(e);\n if (k === 'template') flatten((e as Template).expressions, result);\n else if (k === 'text' && (e as { text?: unknown }).text === '') continue;\n else result.push(e);\n }\n return result;\n}\n\nexport interface Template {\n readonly [kind]: 'template';\n}\n\n/**\n * A sequence of expressions. Construction always normalises: nested templates are flattened\n * (recursively) and empty text is removed, so adapters only ever see Text, Identifier and Value.\n * Templates are frozen.\n */\nexport class Template extends Expression {\n readonly expressions: ReadonlyArray<Expression>;\n\n constructor(expressions: ReadonlyArray<Expression>) {\n super();\n this.expressions = Object.freeze(flatten(expressions, []));\n if (new.target === Template) Object.freeze(this);\n }\n}\n\nObject.defineProperty(Template.prototype, kind, {value: 'template'});\n\nexport function isTemplate(value: unknown): value is Template {\n return kindOf(value) === 'template';\n}\n\nexport function template(...expressions: Expression[]): Template {\n return new Template(expressions);\n}\n"]}
|
package/Text.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { Expression } from "./Expression";
|
|
1
|
+
import { Expression, kind } from "./Expression.js";
|
|
2
|
+
export interface Text {
|
|
3
|
+
readonly [kind]: 'text';
|
|
4
|
+
}
|
|
5
|
+
/** Raw SQL, inserted verbatim without any escaping. */
|
|
2
6
|
export declare class Text extends Expression {
|
|
3
7
|
readonly text: string;
|
|
4
8
|
constructor(text: string);
|
|
5
9
|
}
|
|
10
|
+
export declare function isText(value: unknown): value is Text;
|
|
11
|
+
/** Raw SQL, inserted verbatim without any escaping (*use with care*). */
|
|
6
12
|
export declare function text(text: string): Text;
|
|
7
13
|
export declare const raw: typeof text;
|
package/Text.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Text extends Expression_1.Expression {
|
|
1
|
+
import { Expression, kind, kindOf } from "./Expression.js";
|
|
2
|
+
/** Raw SQL, inserted verbatim without any escaping. */
|
|
3
|
+
export class Text extends Expression {
|
|
4
|
+
text;
|
|
6
5
|
constructor(text) {
|
|
7
6
|
super();
|
|
7
|
+
if (typeof text !== 'string')
|
|
8
|
+
throw new TypeError(`Text must be a string but was ${typeof text}`);
|
|
8
9
|
this.text = text;
|
|
10
|
+
if (new.target === Text)
|
|
11
|
+
Object.freeze(this);
|
|
9
12
|
}
|
|
10
13
|
}
|
|
11
|
-
|
|
12
|
-
function
|
|
14
|
+
Object.defineProperty(Text.prototype, kind, { value: 'text' });
|
|
15
|
+
export function isText(value) {
|
|
16
|
+
return kindOf(value) === 'text';
|
|
17
|
+
}
|
|
18
|
+
/** Raw SQL, inserted verbatim without any escaping (*use with care*). */
|
|
19
|
+
export function text(text) {
|
|
13
20
|
return new Text(text);
|
|
14
21
|
}
|
|
15
|
-
|
|
16
|
-
exports.raw = text;
|
|
22
|
+
export const raw = text;
|
|
17
23
|
//# sourceMappingURL=Text.js.map
|
package/Text.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Text.js","sourceRoot":"","sources":["../src/Text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,IAAI,EAAE,MAAM,EAAC,MAAM,iBAAiB,CAAC;AAMzD,uDAAuD;AACvD,MAAM,OAAO,IAAK,SAAQ,UAAU;IACvB,IAAI,CAAS;IAEtB,YAAY,IAAY;QACpB,KAAK,EAAE,CAAC;QACR,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,MAAM,IAAI,SAAS,CAAC,iCAAiC,OAAO,IAAI,EAAE,CAAC,CAAC;QAClG,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,IAAI,MAAM,KAAK,IAAI;YAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACjD,CAAC;CACJ;AAED,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,EAAC,KAAK,EAAE,MAAM,EAAC,CAAC,CAAC;AAE7D,MAAM,UAAU,MAAM,CAAC,KAAc;IACjC,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC;AACpC,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,IAAI,CAAC,IAAY;IAC7B,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAgB,IAAI,CAAC","sourcesContent":["import {Expression, kind, kindOf} from \"./Expression.js\";\n\nexport interface Text {\n readonly [kind]: 'text';\n}\n\n/** Raw SQL, inserted verbatim without any escaping. */\nexport class Text extends Expression {\n readonly text: string;\n\n constructor(text: string) {\n super();\n if (typeof text !== 'string') throw new TypeError(`Text must be a string but was ${typeof text}`);\n this.text = text;\n if (new.target === Text) Object.freeze(this);\n }\n}\n\nObject.defineProperty(Text.prototype, kind, {value: 'text'});\n\nexport function isText(value: unknown): value is Text {\n return kindOf(value) === 'text';\n}\n\n/** Raw SQL, inserted verbatim without any escaping (*use with care*). */\nexport function text(text: string): Text {\n return new Text(text);\n}\n\nexport const raw: typeof text = text;\n"]}
|
package/Value.d.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import { Expression } from "./Expression";
|
|
2
|
-
import { Template } from "./Template";
|
|
1
|
+
import { Expression, kind } from "./Expression.js";
|
|
2
|
+
import { Template } from "./Template.js";
|
|
3
|
+
export interface Value {
|
|
4
|
+
readonly [kind]: 'value';
|
|
5
|
+
}
|
|
6
|
+
/** A value that adapters pass as a bound parameter. The wrapper is frozen; the wrapped value is not copied. */
|
|
3
7
|
export declare class Value extends Expression {
|
|
4
|
-
readonly value:
|
|
5
|
-
constructor(value:
|
|
8
|
+
readonly value: unknown;
|
|
9
|
+
constructor(value: unknown);
|
|
6
10
|
}
|
|
7
|
-
export declare function
|
|
8
|
-
|
|
11
|
+
export declare function isValue(value: unknown): value is Value;
|
|
12
|
+
/**
|
|
13
|
+
* Wraps `value` as a bound Value. Expressions are passed through unchanged and `undefined` becomes `null`.
|
|
14
|
+
* Throws for an instance of a user subclass of Expression (subclassing Expression is not supported).
|
|
15
|
+
*/
|
|
16
|
+
export declare function value(value: unknown): Expression;
|
|
17
|
+
/**
|
|
18
|
+
* Multiple values separated by `separator` (default `text(', ')`).
|
|
19
|
+
* The separator is an Expression so any raw SQL is explicit at the call site.
|
|
20
|
+
*/
|
|
21
|
+
export declare function values(values: readonly unknown[], separator?: Expression): Template;
|
|
9
22
|
export declare const spread: typeof values;
|
package/Value.js
CHANGED
|
@@ -1,27 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Value extends
|
|
1
|
+
import { Expression, isExpression, kind, kindOf } from "./Expression.js";
|
|
2
|
+
import { template, Template } from "./Template.js";
|
|
3
|
+
import { text } from "./Text.js";
|
|
4
|
+
import { invalidExpression } from "./invalid.js";
|
|
5
|
+
import { separated } from "./separated.js";
|
|
6
|
+
/** A value that adapters pass as a bound parameter. The wrapper is frozen; the wrapped value is not copied. */
|
|
7
|
+
export class Value extends Expression {
|
|
8
|
+
value;
|
|
8
9
|
constructor(value) {
|
|
9
10
|
super();
|
|
10
11
|
this.value = value;
|
|
12
|
+
if (new.target === Value)
|
|
13
|
+
Object.freeze(this);
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
|
-
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
+
Object.defineProperty(Value.prototype, kind, { value: 'value' });
|
|
17
|
+
export function isValue(value) {
|
|
18
|
+
return kindOf(value) === 'value';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Wraps `value` as a bound Value. Expressions are passed through unchanged and `undefined` becomes `null`.
|
|
22
|
+
* Throws for an instance of a user subclass of Expression (subclassing Expression is not supported).
|
|
23
|
+
*/
|
|
24
|
+
export function value(value) {
|
|
25
|
+
if (isExpression(value))
|
|
16
26
|
return value;
|
|
27
|
+
if (value instanceof Expression)
|
|
28
|
+
throw invalidExpression(value, 'SQL');
|
|
17
29
|
if (value === undefined)
|
|
18
30
|
return new Value(null);
|
|
19
31
|
return new Value(value);
|
|
20
32
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Multiple values separated by `separator` (default `text(', ')`).
|
|
35
|
+
* The separator is an Expression so any raw SQL is explicit at the call site.
|
|
36
|
+
*/
|
|
37
|
+
export function values(values, separator = text(', ')) {
|
|
38
|
+
return template(...separated(values.map(value), separator));
|
|
24
39
|
}
|
|
25
|
-
|
|
26
|
-
exports.spread = values;
|
|
40
|
+
export const spread = values;
|
|
27
41
|
//# sourceMappingURL=Value.js.map
|
package/Value.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Value.js","sourceRoot":"","sources":["../src/Value.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAC,MAAM,iBAAiB,CAAC;AACvE,OAAO,EAAC,QAAQ,EAAE,QAAQ,EAAC,MAAM,eAAe,CAAC;AACjD,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAC;AAC/B,OAAO,EAAC,iBAAiB,EAAC,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAMzC,+GAA+G;AAC/G,MAAM,OAAO,KAAM,SAAQ,UAAU;IACxB,KAAK,CAAU;IAExB,YAAY,KAAc;QACtB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,IAAI,MAAM,KAAK,KAAK;YAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;CACJ;AAED,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,EAAC,KAAK,EAAE,OAAO,EAAC,CAAC,CAAC;AAE/D,MAAM,UAAU,OAAO,CAAC,KAAc;IAClC,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,OAAO,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,KAAc;IAChC,IAAI,YAAY,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,KAAK,YAAY,UAAU;QAAE,MAAM,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACvE,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC;IAChD,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,MAA0B,EAAE,SAAS,GAAe,IAAI,CAAC,IAAI,CAAC;IACjF,OAAO,QAAQ,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAkB,MAAM,CAAC","sourcesContent":["import {Expression, isExpression, kind, kindOf} from \"./Expression.js\";\nimport {template, Template} from \"./Template.js\";\nimport {text} from \"./Text.js\";\nimport {invalidExpression} from \"./invalid.js\";\nimport {separated} from \"./separated.js\";\n\nexport interface Value {\n readonly [kind]: 'value';\n}\n\n/** A value that adapters pass as a bound parameter. The wrapper is frozen; the wrapped value is not copied. */\nexport class Value extends Expression {\n readonly value: unknown;\n\n constructor(value: unknown) {\n super();\n this.value = value;\n if (new.target === Value) Object.freeze(this);\n }\n}\n\nObject.defineProperty(Value.prototype, kind, {value: 'value'});\n\nexport function isValue(value: unknown): value is Value {\n return kindOf(value) === 'value';\n}\n\n/**\n * Wraps `value` as a bound Value. Expressions are passed through unchanged and `undefined` becomes `null`.\n * Throws for an instance of a user subclass of Expression (subclassing Expression is not supported).\n */\nexport function value(value: unknown): Expression {\n if (isExpression(value)) return value;\n if (value instanceof Expression) throw invalidExpression(value, 'SQL');\n if (value === undefined) return new Value(null);\n return new Value(value);\n}\n\n/**\n * Multiple values separated by `separator` (default `text(', ')`).\n * The separator is an Expression so any raw SQL is explicit at the call site.\n */\nexport function values(values: readonly unknown[], separator: Expression = text(', ')): Template {\n return template(...separated(values.map(value), separator));\n}\n\nexport const spread: typeof values = values;\n"]}
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export * from "./Expression";
|
|
2
|
-
export * from "./Identifier";
|
|
3
|
-
export * from "./SQL";
|
|
4
|
-
export * from "./Template";
|
|
5
|
-
export * from "./Text";
|
|
6
|
-
export * from "./Value";
|
|
1
|
+
export * from "./Expression.js";
|
|
2
|
+
export * from "./Identifier.js";
|
|
3
|
+
export * from "./SQL.js";
|
|
4
|
+
export * from "./Template.js";
|
|
5
|
+
export * from "./Text.js";
|
|
6
|
+
export * from "./Value.js";
|
package/index.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
(0, tslib_1.__exportStar)(require("./Template"), exports);
|
|
8
|
-
(0, tslib_1.__exportStar)(require("./Text"), exports);
|
|
9
|
-
(0, tslib_1.__exportStar)(require("./Value"), exports);
|
|
1
|
+
export * from "./Expression.js";
|
|
2
|
+
export * from "./Identifier.js";
|
|
3
|
+
export * from "./SQL.js";
|
|
4
|
+
export * from "./Template.js";
|
|
5
|
+
export * from "./Text.js";
|
|
6
|
+
export * from "./Value.js";
|
|
10
7
|
//# 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,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA;AAC7B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA","sourcesContent":["export * from \"./Expression.js\"\nexport * from \"./Identifier.js\"\nexport * from \"./SQL.js\"\nexport * from \"./Template.js\"\nexport * from \"./Text.js\"\nexport * from \"./Value.js\"\n\n\n"]}
|
package/invalid.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error for something that is not a usable expression where one is required.
|
|
3
|
+
* An instance of a user subclass of Expression has no `kind` brand, so no adapter could render it:
|
|
4
|
+
* it is rejected rather than being bound as a value or silently dropped.
|
|
5
|
+
*/
|
|
6
|
+
export declare function invalidExpression(value: unknown, where: string): TypeError;
|
package/invalid.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Expression } from "./Expression.js";
|
|
2
|
+
/**
|
|
3
|
+
* The error for something that is not a usable expression where one is required.
|
|
4
|
+
* An instance of a user subclass of Expression has no `kind` brand, so no adapter could render it:
|
|
5
|
+
* it is rejected rather than being bound as a value or silently dropped.
|
|
6
|
+
*/
|
|
7
|
+
export function invalidExpression(value, where) {
|
|
8
|
+
if (value instanceof Expression) {
|
|
9
|
+
return new TypeError(`${where}: ${value.constructor.name} extends Expression but is not a supported kind of expression. ` +
|
|
10
|
+
`Subclassing Expression is not supported: build expressions with SQL, text, id, value or template instead`);
|
|
11
|
+
}
|
|
12
|
+
return new TypeError(`${where} can only contain Expressions but got ${value === null ? 'null' : typeof value}`);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=invalid.js.map
|
package/invalid.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalid.js","sourceRoot":"","sources":["../src/invalid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc,EAAE,KAAa;IAC3D,IAAI,KAAK,YAAY,UAAU,EAAE,CAAC;QAC9B,OAAO,IAAI,SAAS,CAAC,GAAG,KAAK,KAAK,KAAK,CAAC,WAAW,CAAC,IAAI,iEAAiE;YACrH,0GAA0G,CAAC,CAAC;IACpH,CAAC;IACD,OAAO,IAAI,SAAS,CAAC,GAAG,KAAK,yCAAyC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;AACpH,CAAC","sourcesContent":["import {Expression} from \"./Expression.js\";\n\n/**\n * The error for something that is not a usable expression where one is required.\n * An instance of a user subclass of Expression has no `kind` brand, so no adapter could render it:\n * it is rejected rather than being bound as a value or silently dropped.\n */\nexport function invalidExpression(value: unknown, where: string): TypeError {\n if (value instanceof Expression) {\n return new TypeError(`${where}: ${value.constructor.name} extends Expression but is not a supported kind of expression. ` +\n `Subclassing Expression is not supported: build expressions with SQL, text, id, value or template instead`);\n }\n return new TypeError(`${where} can only contain Expressions but got ${value === null ? 'null' : typeof value}`);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@triptease/sql-template",
|
|
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/src"
|
|
7
9
|
},
|
|
8
10
|
"author": "Triptease",
|
|
9
11
|
"license": "MIT",
|
|
@@ -11,17 +13,19 @@
|
|
|
11
13
|
"url": "https://github.com/triptease/sql-template/issues"
|
|
12
14
|
},
|
|
13
15
|
"homepage": "https://github.com/triptease/sql-template/",
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
"main": "./index.js",
|
|
17
|
+
"types": "./index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./index.d.ts",
|
|
21
|
+
"default": "./index.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
17
24
|
},
|
|
18
25
|
"files": [
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"scripts": {},
|
|
26
|
-
"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"
|
|
27
|
-
}
|
|
26
|
+
"**/*.js",
|
|
27
|
+
"**/*.js.map",
|
|
28
|
+
"**/*.d.ts",
|
|
29
|
+
"README.md"
|
|
30
|
+
]
|
|
31
|
+
}
|
package/separated.d.ts
ADDED
package/separated.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isExpression } from "./Expression.js";
|
|
2
|
+
/** @internal Interleaves `separator` between `expressions`. */
|
|
3
|
+
export function separated(expressions, separator) {
|
|
4
|
+
if (!isExpression(separator)) {
|
|
5
|
+
throw new TypeError('separator must be an Expression, e.g. text(\', \') (raw strings are no longer accepted)');
|
|
6
|
+
}
|
|
7
|
+
return expressions.flatMap((e, i) => i > 0 ? [separator, e] : [e]);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=separated.js.map
|
package/separated.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"separated.js","sourceRoot":"","sources":["../src/separated.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,YAAY,EAAC,MAAM,iBAAiB,CAAC;AAE9D,+DAA+D;AAC/D,MAAM,UAAU,SAAS,CAAC,WAAkC,EAAE,SAAqB;IAC/E,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,SAAS,CAAC,yFAAyF,CAAC,CAAC;IACnH,CAAC;IACD,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,CAAC","sourcesContent":["import {type Expression, isExpression} from \"./Expression.js\";\n\n/** @internal Interleaves `separator` between `expressions`. */\nexport function separated(expressions: readonly Expression[], separator: Expression): Expression[] {\n if (!isExpression(separator)) {\n throw new TypeError('separator must be an Expression, e.g. text(\\', \\') (raw strings are no longer accepted)');\n }\n return expressions.flatMap((e, i) => i > 0 ? [separator, e] : [e]);\n}\n"]}
|