@zvndev/powdb-client 0.8.1 → 0.10.0
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 +124 -2
- package/dist/cjs/errors.d.ts +70 -0
- package/dist/cjs/errors.js +68 -0
- package/dist/cjs/escape.d.ts +54 -0
- package/dist/cjs/escape.js +131 -0
- package/dist/cjs/index.d.ts +393 -0
- package/dist/cjs/index.js +1046 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pool.d.ts +140 -0
- package/dist/cjs/pool.js +335 -0
- package/dist/cjs/protocol.d.ts +185 -0
- package/dist/cjs/protocol.js +645 -0
- package/dist/cjs/script.d.ts +25 -0
- package/dist/cjs/script.js +69 -0
- package/dist/cjs/typed.d.ts +49 -0
- package/dist/cjs/typed.js +105 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +30 -0
- package/dist/index.d.ts +137 -4
- package/dist/index.js +255 -77
- package/dist/pool.d.ts +18 -1
- package/dist/pool.js +18 -1
- package/dist/script.d.ts +25 -0
- package/dist/script.js +66 -0
- package/package.json +18 -6
package/README.md
CHANGED
|
@@ -81,6 +81,101 @@ await client.query("insert User { name := $1, age := $2 }", ["Dana", null]);
|
|
|
81
81
|
|
|
82
82
|
`QueryParam` is `string | number | bigint | boolean | null`. The params form sends the `QueryWithParams` (0x04) wire message and **requires powdb-server >= 0.4.7**. The plain `query(q)` and `query(q, { signal })` forms are unchanged.
|
|
83
83
|
|
|
84
|
+
## Multi-statement scripts (`execScript`)
|
|
85
|
+
|
|
86
|
+
`execScript` runs a whole `;`-separated PowQL script down one connection,
|
|
87
|
+
**pipelined**: every statement is written back-to-back without waiting for
|
|
88
|
+
the previous reply, so an N-statement script costs one round trip instead of
|
|
89
|
+
N. Splitting is statement-aware with the exact semantics of the CLI's script
|
|
90
|
+
path — `;` inside `"..."` string literals or `#` comments never splits, and
|
|
91
|
+
empty statements are dropped. (The splitter is exported as
|
|
92
|
+
`splitStatements(script)` if you need it standalone.)
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const results = await client.execScript(`
|
|
96
|
+
type User { required name: str, age: int };
|
|
97
|
+
insert User { name := "Alice", age := 30 }; # seed; more to come
|
|
98
|
+
insert User { name := "Bob;Bobby", age := 25 };
|
|
99
|
+
count(User)
|
|
100
|
+
`);
|
|
101
|
+
// results: QueryResult[] in statement order — results[3].value === "2"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
By default execution is **fail-fast**: the first failed statement rejects the
|
|
105
|
+
promise with a `PowDBScriptError` carrying `statementIndex`, the failing
|
|
106
|
+
`statement` text, and the successful `results` so far. Because dispatch is
|
|
107
|
+
pipelined, statements already on the wire when the error reply arrives
|
|
108
|
+
(typically the whole script) still execute server-side — use
|
|
109
|
+
`transactional: true` if you need all-or-nothing behavior. Do **not** embed
|
|
110
|
+
`begin`/`commit` in the script yourself: the trailing `commit` is already on
|
|
111
|
+
the wire when an error reply arrives, so it commits the partial work.
|
|
112
|
+
|
|
113
|
+
With `transactional: true`, `execScript` opens the transaction itself, waits
|
|
114
|
+
for every statement's reply, and only then sends `commit` — or `rollback` if
|
|
115
|
+
any statement failed — so no statement's effect survives a failure.
|
|
116
|
+
Transactional scripts may not contain their own `begin`/`commit`/`rollback`,
|
|
117
|
+
and the option is mutually exclusive with `continueOnError`. On failure the
|
|
118
|
+
`PowDBScriptError`'s `results` are the replies received before rollback —
|
|
119
|
+
their effects are not persisted.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// Either every statement commits, or none do.
|
|
123
|
+
await client.execScript(seedScript, { transactional: true });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { isPowDBScriptError } from "@zvndev/powdb-client";
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await client.execScript(script);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (isPowDBScriptError(err)) {
|
|
133
|
+
console.error(`statement ${err.statementIndex} failed: ${err.statement}`);
|
|
134
|
+
console.error(`${err.results.length} statements succeeded before it`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
With `continueOnError: true`, every statement runs regardless of failures and
|
|
140
|
+
you get a dense per-statement outcome array instead of a throw:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const outcomes = await client.execScript(script, { continueOnError: true });
|
|
144
|
+
for (const o of outcomes) {
|
|
145
|
+
if (o.ok) console.log(o.result.kind);
|
|
146
|
+
else console.warn(`failed: ${o.statement} — ${o.error.message}`);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`Pool.execScript(script, opts?)` does the same through a pool, checking out a
|
|
151
|
+
single connection for the whole script (so statement order — and
|
|
152
|
+
`transactional: true` — holds).
|
|
153
|
+
|
|
154
|
+
## Eager (pipelined) connect
|
|
155
|
+
|
|
156
|
+
`Client.connect` normally performs a blocking Connect→ConnectOk handshake, so
|
|
157
|
+
every fresh connection pays a protocol round trip before its first query.
|
|
158
|
+
With `eager: true` the promise resolves as soon as the socket is open and the
|
|
159
|
+
Connect frame is written; queries issued immediately are queued right behind
|
|
160
|
+
the Connect frame on the wire, and the server answers everything in order.
|
|
161
|
+
For connection-per-request callers this removes the handshake round trip
|
|
162
|
+
entirely:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const client = await Client.connect({ host, port, eager: true });
|
|
166
|
+
// No extra round trip — this query rides directly behind the Connect frame.
|
|
167
|
+
const result = await client.query("count(User)");
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
If the handshake then fails (bad password, protocol mismatch), every queued
|
|
171
|
+
query rejects with the handshake error (`code: "auth_failed"`, etc.) and the
|
|
172
|
+
client closes. Two things to know:
|
|
173
|
+
|
|
174
|
+
- `client.serverVersion` is `""` until the ConnectOk arrives.
|
|
175
|
+
- `await client.ready()` waits for the handshake outcome explicitly — it
|
|
176
|
+
resolves on ConnectOk and rejects with the handshake error. (For non-eager
|
|
177
|
+
clients it has already settled by the time `connect` returns.)
|
|
178
|
+
|
|
84
179
|
## Experimental embedded sync protocol helpers
|
|
85
180
|
|
|
86
181
|
The client exposes experimental low-level helpers for the private authenticated
|
|
@@ -255,6 +350,11 @@ The full taxonomy: `connect_failed`, `auth_failed`, `query_failed`,
|
|
|
255
350
|
`aborted`, `size_exceeded`, `protocol_error`, `closed`, `timeout`,
|
|
256
351
|
`type_coercion_failed`.
|
|
257
352
|
|
|
353
|
+
`execScript` failures throw `PowDBScriptError`, a `PowDBError` subclass whose
|
|
354
|
+
`code` mirrors the failing statement's error and which adds
|
|
355
|
+
`statementIndex`, `statement`, and `results` (see Multi-statement scripts
|
|
356
|
+
above). Narrow with `isPowDBScriptError(err)`.
|
|
357
|
+
|
|
258
358
|
## Polling watch
|
|
259
359
|
|
|
260
360
|
For simple change-polling (the server doesn't ship a subscription
|
|
@@ -344,6 +444,7 @@ Returns a `Promise<Client>`. Options:
|
|
|
344
444
|
| `user` | `string` | *(omitted)* | User name for multi-user servers (see Authentication above). Omit for shared-password / no-auth servers |
|
|
345
445
|
| `connectTimeoutMs` | `number` | `5000` | Connection timeout in milliseconds |
|
|
346
446
|
| `tls` | `boolean \| tls.ConnectionOptions` | `false` | Enable TLS; `true` uses system defaults, or pass a `tls.connect` options object |
|
|
447
|
+
| `eager` | `boolean` | `false` | Resolve as soon as the Connect frame is written instead of waiting for ConnectOk; queries pipeline behind the handshake (see Eager connect above) |
|
|
347
448
|
|
|
348
449
|
> **Multi-user servers:** requires client ≥0.4.0 (`user` option) and server
|
|
349
450
|
> ≥0.4.6 (enforced roles). See the version matrix under Authentication.
|
|
@@ -380,6 +481,26 @@ const result = await client.querySql("SELECT name, age FROM User WHERE age > 27"
|
|
|
380
481
|
Like `query()`, but coerces each row's string values to JS types using the
|
|
381
482
|
supplied schema and returns `Promise<TypedRow[]>`. See Typed rows above.
|
|
382
483
|
|
|
484
|
+
### `client.execScript(script, opts?)`
|
|
485
|
+
|
|
486
|
+
Splits `script` into statements (statement-aware — see Multi-statement
|
|
487
|
+
scripts above) and executes them all down this connection, pipelined.
|
|
488
|
+
Returns `Promise<QueryResult[]>` in statement order; rejects with a
|
|
489
|
+
`PowDBScriptError` on the first failed statement. With
|
|
490
|
+
`opts.transactional: true`, runs the whole script atomically (commit only
|
|
491
|
+
after every reply; rollback on any failure; embedded
|
|
492
|
+
`begin`/`commit`/`rollback` rejected; mutually exclusive with
|
|
493
|
+
`continueOnError`). With `opts.continueOnError: true`, returns
|
|
494
|
+
`Promise<ScriptStatementOutcome[]>` (`{ statement, ok: true, result }` or
|
|
495
|
+
`{ statement, ok: false, error }`) and never rejects for statement failures.
|
|
496
|
+
`opts.signal?: AbortSignal` aborts the remaining statements.
|
|
497
|
+
|
|
498
|
+
### `client.ready()`
|
|
499
|
+
|
|
500
|
+
Resolves once the Connect→ConnectOk handshake has completed; rejects with
|
|
501
|
+
the handshake error if it failed. Only interesting for eager connects — for
|
|
502
|
+
default connects it has settled before `Client.connect` returns.
|
|
503
|
+
|
|
383
504
|
### `client.watch(query, options)`
|
|
384
505
|
|
|
385
506
|
Re-runs `query` every `intervalMs` and passes each `QueryResult` to
|
|
@@ -395,7 +516,7 @@ Sends a disconnect message and closes the TCP socket.
|
|
|
395
516
|
|
|
396
517
|
### `client.serverVersion`
|
|
397
518
|
|
|
398
|
-
The PowDB server version string (e.g., `"0.4.4"`). On connect, the client warns once per `host:port` if the server's major version differs from the client's.
|
|
519
|
+
The PowDB server version string (e.g., `"0.4.4"`). On connect, the client warns once per `host:port` if the server's major version differs from the client's. For an eager connect this is `""` until the ConnectOk arrives (await `client.ready()` to guarantee it is populated).
|
|
399
520
|
|
|
400
521
|
### `Pool` (class)
|
|
401
522
|
|
|
@@ -409,7 +530,7 @@ Constructor options extend `ClientOptions` with:
|
|
|
409
530
|
| `connectBackoffMs` | `number` | `100` | Initial delay between connect retries; doubles each attempt |
|
|
410
531
|
| `connectMaxBackoffMs` | `number` | `2000` | Cap on the exponential backoff |
|
|
411
532
|
|
|
412
|
-
Methods: `acquire()`, `release(client)`, `destroy(client)`, `withClient(fn)`, `close()`.
|
|
533
|
+
Methods: `acquire()`, `release(client)`, `destroy(client)`, `withClient(fn)`, `execScript(script, opts?)`, `close()`.
|
|
413
534
|
Getters: `size`, `idle`, `closed`.
|
|
414
535
|
|
|
415
536
|
### Safety helpers
|
|
@@ -418,6 +539,7 @@ Getters: `size`, `idle`, `closed`.
|
|
|
418
539
|
- `ident(name)` — wrap a string so `powql` treats it as an identifier
|
|
419
540
|
- `escapeLiteral(value)` — render a JS value as a PowQL literal
|
|
420
541
|
- `escapeIdent(name)` — validate an identifier (throws `TypeError` on invalid)
|
|
542
|
+
- `splitStatements(script)` — the statement-aware script splitter used by `execScript`
|
|
421
543
|
|
|
422
544
|
## Limits
|
|
423
545
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured error taxonomy for the PowDB client.
|
|
3
|
+
*
|
|
4
|
+
* Every error thrown by the client is a `PowDBError` with a stable `.code`
|
|
5
|
+
* so callers can branch without string-matching the message. The taxonomy
|
|
6
|
+
* is intentionally small — new codes are added only when a caller has a
|
|
7
|
+
* legitimate reason to handle them differently.
|
|
8
|
+
*/
|
|
9
|
+
import type { QueryResult } from "./index.js";
|
|
10
|
+
export type PowDBErrorCode =
|
|
11
|
+
/** TCP/TLS connect, DNS, connect timeout. Transient — safe to retry. */
|
|
12
|
+
"connect_failed"
|
|
13
|
+
/** Server rejected the Connect handshake (bad password, unknown db). Not transient. */
|
|
14
|
+
| "auth_failed"
|
|
15
|
+
/** Server returned an `Error` frame in response to a query. Not transient. */
|
|
16
|
+
| "query_failed"
|
|
17
|
+
/** Caller's AbortSignal fired. Never retry — the caller asked to stop. */
|
|
18
|
+
| "aborted"
|
|
19
|
+
/** Peer sent a frame that exceeds one of the configured caps. Likely a bug/attack — do not retry. */
|
|
20
|
+
| "size_exceeded"
|
|
21
|
+
/** Wire protocol violation (bad framing, unknown message type, truncated payload). */
|
|
22
|
+
| "protocol_error"
|
|
23
|
+
/** `close()` has been called on the client or pool. */
|
|
24
|
+
| "closed"
|
|
25
|
+
/** An operation exceeded its configured time budget. */
|
|
26
|
+
| "timeout"
|
|
27
|
+
/** Type coercion on a row failed (queryTyped). */
|
|
28
|
+
| "type_coercion_failed";
|
|
29
|
+
/**
|
|
30
|
+
* All errors thrown by `@zvndev/powdb-client` are instances of this class.
|
|
31
|
+
* Use `err.code` to branch; `err.cause` optionally carries the underlying
|
|
32
|
+
* cause (e.g. the Node socket error).
|
|
33
|
+
*/
|
|
34
|
+
export declare class PowDBError extends Error {
|
|
35
|
+
readonly code: PowDBErrorCode;
|
|
36
|
+
constructor(message: string, code: PowDBErrorCode, options?: {
|
|
37
|
+
cause?: unknown;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Narrow `unknown` to a PowDBError. Useful in catch blocks where you want
|
|
42
|
+
* to branch on `.code` but TypeScript sees `unknown`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isPowDBError(err: unknown): err is PowDBError;
|
|
45
|
+
/**
|
|
46
|
+
* Failure of one statement inside `client.execScript(...)` (fail-fast mode).
|
|
47
|
+
*
|
|
48
|
+
* `code` mirrors the failing statement's error code (`"query_failed"` for a
|
|
49
|
+
* server Error frame, `"aborted"` for a fired AbortSignal, ...), so the usual
|
|
50
|
+
* `.code` branching keeps working; `cause` carries the underlying error.
|
|
51
|
+
* `statementIndex`/`statement` identify the failing statement within the
|
|
52
|
+
* split script, and `results` holds the successful results of every
|
|
53
|
+
* statement before it, in order.
|
|
54
|
+
*/
|
|
55
|
+
export declare class PowDBScriptError extends PowDBError {
|
|
56
|
+
/** Zero-based index of the failing statement within the split script. */
|
|
57
|
+
readonly statementIndex: number;
|
|
58
|
+
/** Text of the failing statement (as split, trimmed). */
|
|
59
|
+
readonly statement: string;
|
|
60
|
+
/** Results of the statements before the failing one, in order. */
|
|
61
|
+
readonly results: QueryResult[];
|
|
62
|
+
constructor(message: string, code: PowDBErrorCode, details: {
|
|
63
|
+
statementIndex: number;
|
|
64
|
+
statement: string;
|
|
65
|
+
results: QueryResult[];
|
|
66
|
+
cause?: unknown;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Narrow `unknown` to a PowDBScriptError. */
|
|
70
|
+
export declare function isPowDBScriptError(err: unknown): err is PowDBScriptError;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Structured error taxonomy for the PowDB client.
|
|
4
|
+
*
|
|
5
|
+
* Every error thrown by the client is a `PowDBError` with a stable `.code`
|
|
6
|
+
* so callers can branch without string-matching the message. The taxonomy
|
|
7
|
+
* is intentionally small — new codes are added only when a caller has a
|
|
8
|
+
* legitimate reason to handle them differently.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.PowDBScriptError = exports.PowDBError = void 0;
|
|
12
|
+
exports.isPowDBError = isPowDBError;
|
|
13
|
+
exports.isPowDBScriptError = isPowDBScriptError;
|
|
14
|
+
/**
|
|
15
|
+
* All errors thrown by `@zvndev/powdb-client` are instances of this class.
|
|
16
|
+
* Use `err.code` to branch; `err.cause` optionally carries the underlying
|
|
17
|
+
* cause (e.g. the Node socket error).
|
|
18
|
+
*/
|
|
19
|
+
class PowDBError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
constructor(message, code, options) {
|
|
22
|
+
// ErrorOptions.cause is supported in Node 16.9+; we require Node 18+.
|
|
23
|
+
super(message, options);
|
|
24
|
+
this.name = "PowDBError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
// Preserve the prototype chain across `target: es2020` and friends.
|
|
27
|
+
Object.setPrototypeOf(this, PowDBError.prototype);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.PowDBError = PowDBError;
|
|
31
|
+
/**
|
|
32
|
+
* Narrow `unknown` to a PowDBError. Useful in catch blocks where you want
|
|
33
|
+
* to branch on `.code` but TypeScript sees `unknown`.
|
|
34
|
+
*/
|
|
35
|
+
function isPowDBError(err) {
|
|
36
|
+
return err instanceof PowDBError;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Failure of one statement inside `client.execScript(...)` (fail-fast mode).
|
|
40
|
+
*
|
|
41
|
+
* `code` mirrors the failing statement's error code (`"query_failed"` for a
|
|
42
|
+
* server Error frame, `"aborted"` for a fired AbortSignal, ...), so the usual
|
|
43
|
+
* `.code` branching keeps working; `cause` carries the underlying error.
|
|
44
|
+
* `statementIndex`/`statement` identify the failing statement within the
|
|
45
|
+
* split script, and `results` holds the successful results of every
|
|
46
|
+
* statement before it, in order.
|
|
47
|
+
*/
|
|
48
|
+
class PowDBScriptError extends PowDBError {
|
|
49
|
+
/** Zero-based index of the failing statement within the split script. */
|
|
50
|
+
statementIndex;
|
|
51
|
+
/** Text of the failing statement (as split, trimmed). */
|
|
52
|
+
statement;
|
|
53
|
+
/** Results of the statements before the failing one, in order. */
|
|
54
|
+
results;
|
|
55
|
+
constructor(message, code, details) {
|
|
56
|
+
super(message, code, { cause: details.cause });
|
|
57
|
+
this.name = "PowDBScriptError";
|
|
58
|
+
this.statementIndex = details.statementIndex;
|
|
59
|
+
this.statement = details.statement;
|
|
60
|
+
this.results = details.results;
|
|
61
|
+
Object.setPrototypeOf(this, PowDBScriptError.prototype);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.PowDBScriptError = PowDBScriptError;
|
|
65
|
+
/** Narrow `unknown` to a PowDBScriptError. */
|
|
66
|
+
function isPowDBScriptError(err) {
|
|
67
|
+
return err instanceof PowDBScriptError;
|
|
68
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe PowQL composition helpers: literal and identifier escaping, plus a
|
|
3
|
+
* tagged-template convenience for building queries without string splicing.
|
|
4
|
+
*
|
|
5
|
+
* const q = powql`insert ${ident("User")} { name := ${userName}, age := ${age} }`;
|
|
6
|
+
* await client.query(q);
|
|
7
|
+
*
|
|
8
|
+
* Security model:
|
|
9
|
+
* - Literals are rendered with quoting/escaping so interpolated values
|
|
10
|
+
* cannot break out of the surrounding literal.
|
|
11
|
+
* - Identifiers are validated against `^[A-Za-z_][A-Za-z0-9_]*$` and passed
|
|
12
|
+
* through unchanged. Anything else throws a `TypeError`.
|
|
13
|
+
*
|
|
14
|
+
* PowQL string-escape rules (verified against `crates/query/src/lexer.rs`):
|
|
15
|
+
* - Strings are delimited by `"`.
|
|
16
|
+
* - `\\` → `\`, `\"` → `"`, `\n` → newline, `\t` → tab.
|
|
17
|
+
* - For any other `\X`, the backslash is dropped and `X` is kept literally.
|
|
18
|
+
* - A bare `"` terminates the string, so `"` inside must be `\"` and any
|
|
19
|
+
* literal backslash must be `\\` (otherwise it would swallow the next char).
|
|
20
|
+
*/
|
|
21
|
+
/** Wrapper marking a string as an identifier (vs a literal) for `powql` tagged templates. */
|
|
22
|
+
export declare class PowqlIdent {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
constructor(name: string);
|
|
25
|
+
}
|
|
26
|
+
/** Factory for {@link PowqlIdent}. Prefer this over `new PowqlIdent(...)` at call sites. */
|
|
27
|
+
export declare function ident(name: string): PowqlIdent;
|
|
28
|
+
/**
|
|
29
|
+
* Validate a PowQL identifier (table name, field name, alias). Returns the
|
|
30
|
+
* identifier unchanged on success; throws `TypeError` on any invalid input
|
|
31
|
+
* (non-string, empty, or containing characters outside `[A-Za-z_][A-Za-z0-9_]*`).
|
|
32
|
+
*/
|
|
33
|
+
export declare function escapeIdent(name: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Render a JS value as a PowQL literal. Supports `string`, `number`, `bigint`,
|
|
36
|
+
* `boolean`, and `null`. Rejects `NaN`/`±Infinity`, `undefined`, symbols,
|
|
37
|
+
* objects, and arrays with `TypeError`.
|
|
38
|
+
*
|
|
39
|
+
* - string → `"..."` with `\` and `"` backslash-escaped (C-style, per the
|
|
40
|
+
* PowQL lexer). Backslash must be escaped first to avoid double-processing.
|
|
41
|
+
* - number → decimal; rejects non-finite
|
|
42
|
+
* - bigint → decimal digits
|
|
43
|
+
* - boolean → `true` / `false`
|
|
44
|
+
* - null → `null`
|
|
45
|
+
*/
|
|
46
|
+
export declare function escapeLiteral(value: string | number | bigint | boolean | null): string;
|
|
47
|
+
/**
|
|
48
|
+
* Tagged template for safe PowQL composition. Each interpolated value is
|
|
49
|
+
* escaped as a literal by default; wrap it in `ident(...)` to interpolate as
|
|
50
|
+
* an identifier.
|
|
51
|
+
*
|
|
52
|
+
* const q = powql`insert ${ident(table)} { name := ${userName} }`;
|
|
53
|
+
*/
|
|
54
|
+
export declare function powql(strings: TemplateStringsArray, ...values: unknown[]): string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Safe PowQL composition helpers: literal and identifier escaping, plus a
|
|
4
|
+
* tagged-template convenience for building queries without string splicing.
|
|
5
|
+
*
|
|
6
|
+
* const q = powql`insert ${ident("User")} { name := ${userName}, age := ${age} }`;
|
|
7
|
+
* await client.query(q);
|
|
8
|
+
*
|
|
9
|
+
* Security model:
|
|
10
|
+
* - Literals are rendered with quoting/escaping so interpolated values
|
|
11
|
+
* cannot break out of the surrounding literal.
|
|
12
|
+
* - Identifiers are validated against `^[A-Za-z_][A-Za-z0-9_]*$` and passed
|
|
13
|
+
* through unchanged. Anything else throws a `TypeError`.
|
|
14
|
+
*
|
|
15
|
+
* PowQL string-escape rules (verified against `crates/query/src/lexer.rs`):
|
|
16
|
+
* - Strings are delimited by `"`.
|
|
17
|
+
* - `\\` → `\`, `\"` → `"`, `\n` → newline, `\t` → tab.
|
|
18
|
+
* - For any other `\X`, the backslash is dropped and `X` is kept literally.
|
|
19
|
+
* - A bare `"` terminates the string, so `"` inside must be `\"` and any
|
|
20
|
+
* literal backslash must be `\\` (otherwise it would swallow the next char).
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.PowqlIdent = void 0;
|
|
24
|
+
exports.ident = ident;
|
|
25
|
+
exports.escapeIdent = escapeIdent;
|
|
26
|
+
exports.escapeLiteral = escapeLiteral;
|
|
27
|
+
exports.powql = powql;
|
|
28
|
+
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
29
|
+
/** Wrapper marking a string as an identifier (vs a literal) for `powql` tagged templates. */
|
|
30
|
+
class PowqlIdent {
|
|
31
|
+
name;
|
|
32
|
+
constructor(name) {
|
|
33
|
+
this.name = name;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.PowqlIdent = PowqlIdent;
|
|
37
|
+
/** Factory for {@link PowqlIdent}. Prefer this over `new PowqlIdent(...)` at call sites. */
|
|
38
|
+
function ident(name) {
|
|
39
|
+
return new PowqlIdent(name);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validate a PowQL identifier (table name, field name, alias). Returns the
|
|
43
|
+
* identifier unchanged on success; throws `TypeError` on any invalid input
|
|
44
|
+
* (non-string, empty, or containing characters outside `[A-Za-z_][A-Za-z0-9_]*`).
|
|
45
|
+
*/
|
|
46
|
+
function escapeIdent(name) {
|
|
47
|
+
if (typeof name !== "string") {
|
|
48
|
+
throw new TypeError(`escapeIdent: expected string, got ${typeof name}`);
|
|
49
|
+
}
|
|
50
|
+
if (name.length === 0) {
|
|
51
|
+
throw new TypeError("escapeIdent: identifier must not be empty");
|
|
52
|
+
}
|
|
53
|
+
if (!IDENT_RE.test(name)) {
|
|
54
|
+
throw new TypeError(`escapeIdent: invalid identifier ${JSON.stringify(name)} (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`);
|
|
55
|
+
}
|
|
56
|
+
return name;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Render a JS value as a PowQL literal. Supports `string`, `number`, `bigint`,
|
|
60
|
+
* `boolean`, and `null`. Rejects `NaN`/`±Infinity`, `undefined`, symbols,
|
|
61
|
+
* objects, and arrays with `TypeError`.
|
|
62
|
+
*
|
|
63
|
+
* - string → `"..."` with `\` and `"` backslash-escaped (C-style, per the
|
|
64
|
+
* PowQL lexer). Backslash must be escaped first to avoid double-processing.
|
|
65
|
+
* - number → decimal; rejects non-finite
|
|
66
|
+
* - bigint → decimal digits
|
|
67
|
+
* - boolean → `true` / `false`
|
|
68
|
+
* - null → `null`
|
|
69
|
+
*/
|
|
70
|
+
function escapeLiteral(value) {
|
|
71
|
+
if (value === null)
|
|
72
|
+
return "null";
|
|
73
|
+
const t = typeof value;
|
|
74
|
+
if (t === "string") {
|
|
75
|
+
const escaped = value
|
|
76
|
+
.replace(/\\/g, "\\\\")
|
|
77
|
+
.replace(/"/g, '\\"');
|
|
78
|
+
return `"${escaped}"`;
|
|
79
|
+
}
|
|
80
|
+
if (t === "number") {
|
|
81
|
+
const n = value;
|
|
82
|
+
if (!Number.isFinite(n)) {
|
|
83
|
+
throw new TypeError(`escapeLiteral: non-finite number ${String(n)} cannot be represented as a PowQL literal`);
|
|
84
|
+
}
|
|
85
|
+
return String(n);
|
|
86
|
+
}
|
|
87
|
+
if (t === "bigint") {
|
|
88
|
+
return value.toString(10);
|
|
89
|
+
}
|
|
90
|
+
if (t === "boolean") {
|
|
91
|
+
return value ? "true" : "false";
|
|
92
|
+
}
|
|
93
|
+
throw new TypeError(`escapeLiteral: unsupported type ${describe(value)}`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Tagged template for safe PowQL composition. Each interpolated value is
|
|
97
|
+
* escaped as a literal by default; wrap it in `ident(...)` to interpolate as
|
|
98
|
+
* an identifier.
|
|
99
|
+
*
|
|
100
|
+
* const q = powql`insert ${ident(table)} { name := ${userName} }`;
|
|
101
|
+
*/
|
|
102
|
+
function powql(strings, ...values) {
|
|
103
|
+
let out = strings[0] ?? "";
|
|
104
|
+
for (let i = 0; i < values.length; i++) {
|
|
105
|
+
out += renderInterpolation(values[i]);
|
|
106
|
+
out += strings[i + 1] ?? "";
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
// ──────────────────────────────────────────────────────────
|
|
111
|
+
// internals
|
|
112
|
+
// ──────────────────────────────────────────────────────────
|
|
113
|
+
function renderInterpolation(value) {
|
|
114
|
+
if (value instanceof PowqlIdent) {
|
|
115
|
+
return escapeIdent(value.name);
|
|
116
|
+
}
|
|
117
|
+
// `escapeLiteral` enforces the allowed set — for anything else it throws.
|
|
118
|
+
return escapeLiteral(value);
|
|
119
|
+
}
|
|
120
|
+
function describe(value) {
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
return "undefined";
|
|
123
|
+
if (value === null)
|
|
124
|
+
return "null";
|
|
125
|
+
if (Array.isArray(value))
|
|
126
|
+
return "array";
|
|
127
|
+
const t = typeof value;
|
|
128
|
+
if (t === "object")
|
|
129
|
+
return "object";
|
|
130
|
+
return t;
|
|
131
|
+
}
|