@zvndev/powdb-client 0.3.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 +309 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -0
- package/dist/escape.d.ts +55 -0
- package/dist/escape.d.ts.map +1 -0
- package/dist/escape.js +124 -0
- package/dist/escape.js.map +1 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +515 -0
- package/dist/index.js.map +1 -0
- package/dist/pool.d.ts +124 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +315 -0
- package/dist/pool.js.map +1 -0
- package/dist/protocol.d.ts +59 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +188 -0
- package/dist/protocol.js.map +1 -0
- package/dist/typed.d.ts +50 -0
- package/dist/typed.d.ts.map +1 -0
- package/dist/typed.js +101 -0
- package/dist/typed.js.map +1 -0
- package/package.json +42 -0
- package/src/errors.ts +54 -0
- package/src/escape.ts +141 -0
- package/src/index.ts +652 -0
- package/src/pool.ts +375 -0
- package/src/protocol.ts +218 -0
- package/src/typed.ts +148 -0
package/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# @zvndev/powdb-client
|
|
2
|
+
|
|
3
|
+
TypeScript client for [PowDB](https://github.com/zvndev/powdb) — speaks the native binary wire protocol over TCP (or TLS).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @zvndev/powdb-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Client, powql, ident } from "@zvndev/powdb-client";
|
|
15
|
+
|
|
16
|
+
const client = await Client.connect({
|
|
17
|
+
host: "localhost",
|
|
18
|
+
port: 5433,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Create a table
|
|
22
|
+
await client.query("type User { required name: str, required email: str, age: int }");
|
|
23
|
+
|
|
24
|
+
// Insert data — use `powql` to interpolate values safely
|
|
25
|
+
const name = 'O"Brien';
|
|
26
|
+
const age = 30;
|
|
27
|
+
await client.query(powql`insert User { name := ${name}, email := ${"alice@example.com"}, age := ${age} }`);
|
|
28
|
+
|
|
29
|
+
// Query
|
|
30
|
+
const result = await client.query(powql`User filter .age > ${25} { .name, .age }`);
|
|
31
|
+
if (result.kind === "rows") {
|
|
32
|
+
console.table(result.rows);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Aggregates
|
|
36
|
+
const count = await client.query(powql`count(${ident("User")})`);
|
|
37
|
+
if (count.kind === "scalar") {
|
|
38
|
+
console.log(`Total users: ${count.value}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await client.close();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Safe query composition
|
|
45
|
+
|
|
46
|
+
**Never build PowQL with template literals or string concatenation of untrusted values.** PowQL has its own injection class — the same risk as SQL injection.
|
|
47
|
+
|
|
48
|
+
Use the `powql` tagged template. Values are escaped as literals by default; wrap identifiers (table/column names) in `ident(...)`.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { powql, ident, escapeLiteral, escapeIdent } from "@zvndev/powdb-client";
|
|
52
|
+
|
|
53
|
+
// powql — recommended. Interpolations are escaped automatically.
|
|
54
|
+
const q = powql`${ident("User")} filter .city = ${city} and .age > ${age} { .name }`;
|
|
55
|
+
|
|
56
|
+
// Manual escaping — for when you need raw strings.
|
|
57
|
+
escapeLiteral("O\"Brien"); // → "\"O\\\"Brien\""
|
|
58
|
+
escapeIdent("User"); // → "User" (throws on invalid)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`escapeLiteral` accepts `string | number | bigint | boolean | null`. It rejects `NaN`/`Infinity`, `undefined`, objects, arrays, symbols, and `Date` — convert those yourself before passing them in.
|
|
62
|
+
|
|
63
|
+
## Connection pooling
|
|
64
|
+
|
|
65
|
+
For multi-query workloads (web servers, batch jobs), use `Pool`:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { Pool } from "@zvndev/powdb-client";
|
|
69
|
+
|
|
70
|
+
const pool = new Pool({
|
|
71
|
+
host: "localhost",
|
|
72
|
+
port: 5433,
|
|
73
|
+
max: 10,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Acquire, use, release — or let `withClient` handle it.
|
|
77
|
+
const rows = await pool.withClient(async (client) => {
|
|
78
|
+
const r = await client.query("User { .name }");
|
|
79
|
+
return r.kind === "rows" ? r.rows : [];
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await pool.close();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## TLS
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const client = await Client.connect({
|
|
89
|
+
host: "db.example.com",
|
|
90
|
+
port: 5433,
|
|
91
|
+
tls: true, // system defaults
|
|
92
|
+
// or: tls: { ca: fs.readFileSync("ca.pem") }
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Typed rows
|
|
97
|
+
|
|
98
|
+
The wire protocol serialises every value as a string. If you want JS types
|
|
99
|
+
back (numbers, `Date`, booleans), call `queryTyped` with a schema:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { Client } from "@zvndev/powdb-client";
|
|
103
|
+
|
|
104
|
+
const client = await Client.connect({ host: "localhost", port: 5433 });
|
|
105
|
+
|
|
106
|
+
const rows = await client.queryTyped(
|
|
107
|
+
"User { .id, .name, .age, .active, .created_at }",
|
|
108
|
+
{
|
|
109
|
+
id: "int", // number — or bigint if > Number.MAX_SAFE_INTEGER
|
|
110
|
+
age: "int",
|
|
111
|
+
active: "bool",
|
|
112
|
+
created_at: "datetime",
|
|
113
|
+
// columns not in the schema (like `name`) pass through as strings
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
rows[0].age; // typeof number
|
|
118
|
+
rows[0].created_at; // instanceof Date
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Supported column types: `int` | `float` | `bool` | `str` | `datetime` | `uuid`.
|
|
122
|
+
Bytes columns are intentionally unsupported (the wire format is lossy —
|
|
123
|
+
it renders `<N bytes>`) and throw on coercion. Declare `str` if you just
|
|
124
|
+
want the placeholder.
|
|
125
|
+
|
|
126
|
+
## Structured errors
|
|
127
|
+
|
|
128
|
+
Every error thrown by the client is a `PowDBError` with a stable `.code`:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { Client, PowDBError, isPowDBError } from "@zvndev/powdb-client";
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await client.query("bogus");
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (isPowDBError(err)) {
|
|
137
|
+
switch (err.code) {
|
|
138
|
+
case "connect_failed":
|
|
139
|
+
case "timeout":
|
|
140
|
+
// transient — safe to retry
|
|
141
|
+
break;
|
|
142
|
+
case "auth_failed":
|
|
143
|
+
case "query_failed":
|
|
144
|
+
case "protocol_error":
|
|
145
|
+
// not transient — surface to the caller
|
|
146
|
+
break;
|
|
147
|
+
case "aborted":
|
|
148
|
+
// caller asked to stop — never retry
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The full taxonomy: `connect_failed`, `auth_failed`, `query_failed`,
|
|
156
|
+
`aborted`, `size_exceeded`, `protocol_error`, `closed`, `timeout`,
|
|
157
|
+
`type_coercion_failed`.
|
|
158
|
+
|
|
159
|
+
## Polling watch
|
|
160
|
+
|
|
161
|
+
For simple change-polling (the server doesn't ship a subscription
|
|
162
|
+
protocol yet), `watch` re-runs a query on an interval and invokes a
|
|
163
|
+
callback with the latest rows:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const handle = client.watch("User filter .active = true { .id, .name }", {
|
|
167
|
+
intervalMs: 1000,
|
|
168
|
+
onRows: (rows, columns) => {
|
|
169
|
+
console.log(`${rows.length} active users`);
|
|
170
|
+
},
|
|
171
|
+
onError: (err) => {
|
|
172
|
+
console.error("watch error:", err);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ...later
|
|
177
|
+
handle.stop();
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If a query takes longer than `intervalMs` the next tick is skipped rather
|
|
181
|
+
than piling up. The watcher does not keep the event loop alive on its own
|
|
182
|
+
(it uses `timer.unref()`).
|
|
183
|
+
|
|
184
|
+
## Observability
|
|
185
|
+
|
|
186
|
+
`Client` is an `EventEmitter`. Wire it into your logger or metrics
|
|
187
|
+
pipeline:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
client.on("query", ({ query, durationMs, ok, kind, error }) => {
|
|
191
|
+
metrics.histogram("powdb_query_ms", durationMs, { ok: String(ok) });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
client.on("close", ({ error }) => {
|
|
195
|
+
if (error) logger.warn({ err: error }, "powdb connection lost");
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Events:
|
|
200
|
+
|
|
201
|
+
| Event | Payload | Fires when |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `query` | `{ query, durationMs, ok, kind?, error? }` | After every query completes (success or failure) |
|
|
204
|
+
| `close` | `{ error: Error \| null }` | Exactly once per socket, on normal or error close |
|
|
205
|
+
|
|
206
|
+
## Cancellation
|
|
207
|
+
|
|
208
|
+
Pass an `AbortSignal` to cancel a query:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const ctrl = new AbortController();
|
|
212
|
+
setTimeout(() => ctrl.abort(), 1000);
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await client.query("slow_query(...)", { signal: ctrl.signal });
|
|
216
|
+
} catch (err) {
|
|
217
|
+
if (err.name === "AbortError") { /* cancelled */ }
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The socket stays open — the server's reply is silently discarded so other in-flight queries keep working.
|
|
222
|
+
|
|
223
|
+
## API
|
|
224
|
+
|
|
225
|
+
### `Client.connect(options)`
|
|
226
|
+
|
|
227
|
+
Returns a `Promise<Client>`. Options:
|
|
228
|
+
|
|
229
|
+
| Option | Type | Default | Description |
|
|
230
|
+
|---|---|---|---|
|
|
231
|
+
| `host` | `string` | *(required)* | Server hostname or IP |
|
|
232
|
+
| `port` | `number` | *(required)* | Server port |
|
|
233
|
+
| `dbName` | `string` | `"default"` | Database name |
|
|
234
|
+
| `password` | `string \| null` | `null` | Server password (if auth is enabled) |
|
|
235
|
+
| `connectTimeoutMs` | `number` | `5000` | Connection timeout in milliseconds |
|
|
236
|
+
| `tls` | `boolean \| tls.ConnectionOptions` | `false` | Enable TLS; `true` uses system defaults, or pass a `tls.connect` options object |
|
|
237
|
+
|
|
238
|
+
### `client.query(query, opts?)`
|
|
239
|
+
|
|
240
|
+
Sends a PowQL query and returns a `Promise<QueryResult>`:
|
|
241
|
+
|
|
242
|
+
- `{ kind: "rows", columns: string[], rows: string[][] }` — for SELECT-like queries
|
|
243
|
+
- `{ kind: "scalar", value: string }` — for aggregates (`count`, `sum`, `avg`, etc.)
|
|
244
|
+
- `{ kind: "ok", affected: bigint }` — for mutations (`insert`, `update`, `delete`)
|
|
245
|
+
|
|
246
|
+
`opts.signal?: AbortSignal` — aborts the returned promise (see Cancellation above).
|
|
247
|
+
|
|
248
|
+
Throws a `PowDBError` (see Structured errors above) on any failure.
|
|
249
|
+
|
|
250
|
+
### `client.queryTyped(query, schema, opts?)`
|
|
251
|
+
|
|
252
|
+
Like `query()`, but coerces each row's string values to JS types using the
|
|
253
|
+
supplied schema and returns `Promise<TypedRow[]>`. See Typed rows above.
|
|
254
|
+
|
|
255
|
+
### `client.watch(query, options)`
|
|
256
|
+
|
|
257
|
+
Re-runs `query` every `intervalMs` and pushes rows to `onRows`. Returns
|
|
258
|
+
`{ stop(): void }`. See Polling watch above.
|
|
259
|
+
|
|
260
|
+
### `client.on("query", handler)` / `client.on("close", handler)`
|
|
261
|
+
|
|
262
|
+
`Client` extends `EventEmitter`. See Observability above.
|
|
263
|
+
|
|
264
|
+
### `client.close()`
|
|
265
|
+
|
|
266
|
+
Sends a disconnect message and closes the TCP socket.
|
|
267
|
+
|
|
268
|
+
### `client.serverVersion`
|
|
269
|
+
|
|
270
|
+
The PowDB server version string (e.g., `"0.2.0"`). On connect, the client warns once per `host:port` if the server's major version differs from the client's.
|
|
271
|
+
|
|
272
|
+
### `Pool` (class)
|
|
273
|
+
|
|
274
|
+
Constructor options extend `ClientOptions` with:
|
|
275
|
+
|
|
276
|
+
| Option | Type | Default | Description |
|
|
277
|
+
|---|---|---|---|
|
|
278
|
+
| `max` | `number` | `10` | Maximum concurrent connections |
|
|
279
|
+
| `acquireTimeoutMs` | `number` | `30000` | How long `acquire()` waits before rejecting (pass `0` to disable) |
|
|
280
|
+
| `connectRetries` | `number` | `3` | How many times to retry transient connect failures before giving up (set `0` to disable) |
|
|
281
|
+
| `connectBackoffMs` | `number` | `100` | Initial delay between connect retries; doubles each attempt |
|
|
282
|
+
| `connectMaxBackoffMs` | `number` | `2000` | Cap on the exponential backoff |
|
|
283
|
+
|
|
284
|
+
Methods: `acquire()`, `release(client)`, `destroy(client)`, `withClient(fn)`, `close()`.
|
|
285
|
+
Getters: `size`, `idle`, `closed`.
|
|
286
|
+
|
|
287
|
+
### Safety helpers
|
|
288
|
+
|
|
289
|
+
- `powql` — tagged template; escapes literals, validates identifiers
|
|
290
|
+
- `ident(name)` — wrap a string so `powql` treats it as an identifier
|
|
291
|
+
- `escapeLiteral(value)` — render a JS value as a PowQL literal
|
|
292
|
+
- `escapeIdent(name)` — validate an identifier (throws `TypeError` on invalid)
|
|
293
|
+
|
|
294
|
+
## Limits
|
|
295
|
+
|
|
296
|
+
The client enforces the same frame limits as the server and throws on violation:
|
|
297
|
+
|
|
298
|
+
- `MAX_PAYLOAD_SIZE` — 64 MiB per frame
|
|
299
|
+
- `MAX_ROWS` — 10,000,000 rows per result
|
|
300
|
+
- `MAX_COLUMNS` — 4,096 columns per result
|
|
301
|
+
|
|
302
|
+
## Requirements
|
|
303
|
+
|
|
304
|
+
- Node.js 18+ (uses `node:net`, `node:tls`)
|
|
305
|
+
- A running PowDB server (`cargo run --release -p powdb-server`)
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
MIT
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
export type PowDBErrorCode =
|
|
10
|
+
/** TCP/TLS connect, DNS, connect timeout. Transient — safe to retry. */
|
|
11
|
+
"connect_failed"
|
|
12
|
+
/** Server rejected the Connect handshake (bad password, unknown db). Not transient. */
|
|
13
|
+
| "auth_failed"
|
|
14
|
+
/** Server returned an `Error` frame in response to a query. Not transient. */
|
|
15
|
+
| "query_failed"
|
|
16
|
+
/** Caller's AbortSignal fired. Never retry — the caller asked to stop. */
|
|
17
|
+
| "aborted"
|
|
18
|
+
/** Peer sent a frame that exceeds one of the configured caps. Likely a bug/attack — do not retry. */
|
|
19
|
+
| "size_exceeded"
|
|
20
|
+
/** Wire protocol violation (bad framing, unknown message type, truncated payload). */
|
|
21
|
+
| "protocol_error"
|
|
22
|
+
/** `close()` has been called on the client or pool. */
|
|
23
|
+
| "closed"
|
|
24
|
+
/** An operation exceeded its configured time budget. */
|
|
25
|
+
| "timeout"
|
|
26
|
+
/** Type coercion on a row failed (queryTyped). */
|
|
27
|
+
| "type_coercion_failed";
|
|
28
|
+
/**
|
|
29
|
+
* All errors thrown by `@zvndev/powdb-client` are instances of this class.
|
|
30
|
+
* Use `err.code` to branch; `err.cause` optionally carries the underlying
|
|
31
|
+
* cause (e.g. the Node socket error).
|
|
32
|
+
*/
|
|
33
|
+
export declare class PowDBError extends Error {
|
|
34
|
+
readonly code: PowDBErrorCode;
|
|
35
|
+
constructor(message: string, code: PowDBErrorCode, options?: {
|
|
36
|
+
cause?: unknown;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Narrow `unknown` to a PowDBError. Useful in catch blocks where you want
|
|
41
|
+
* to branch on `.code` but TypeScript sees `unknown`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isPowDBError(err: unknown): err is PowDBError;
|
|
44
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,cAAc;AACxB,wEAAwE;AACtE,gBAAgB;AAClB,uFAAuF;GACrF,aAAa;AACf,8EAA8E;GAC5E,cAAc;AAChB,0EAA0E;GACxE,SAAS;AACX,qGAAqG;GACnG,eAAe;AACjB,sFAAsF;GACpF,gBAAgB;AAClB,uDAAuD;GACrD,QAAQ;AACV,wDAAwD;GACtD,SAAS;AACX,kDAAkD;GAChD,sBAAsB,CAAC;AAE3B;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;gBAElB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAQjF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,UAAU,CAE5D"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* All errors thrown by `@zvndev/powdb-client` are instances of this class.
|
|
11
|
+
* Use `err.code` to branch; `err.cause` optionally carries the underlying
|
|
12
|
+
* cause (e.g. the Node socket error).
|
|
13
|
+
*/
|
|
14
|
+
export class PowDBError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
constructor(message, code, options) {
|
|
17
|
+
// ErrorOptions.cause is supported in Node 16.9+; we require Node 18+.
|
|
18
|
+
super(message, options);
|
|
19
|
+
this.name = "PowDBError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
// Preserve the prototype chain across `target: es2020` and friends.
|
|
22
|
+
Object.setPrototypeOf(this, PowDBError.prototype);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Narrow `unknown` to a PowDBError. Useful in catch blocks where you want
|
|
27
|
+
* to branch on `.code` but TypeScript sees `unknown`.
|
|
28
|
+
*/
|
|
29
|
+
export function isPowDBError(err) {
|
|
30
|
+
return err instanceof PowDBError;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAsBH;;;;GAIG;AACH,MAAM,OAAO,UAAW,SAAQ,KAAK;IAC1B,IAAI,CAAiB;IAE9B,YAAY,OAAe,EAAE,IAAoB,EAAE,OAA6B;QAC9E,sEAAsE;QACtE,KAAK,CAAC,OAAO,EAAE,OAAuB,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,oEAAoE;QACpE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,OAAO,GAAG,YAAY,UAAU,CAAC;AACnC,CAAC"}
|
package/dist/escape.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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;
|
|
55
|
+
//# sourceMappingURL=escape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,6FAA6F;AAC7F,qBAAa,UAAU;aACO,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CACzC;AAED,4FAA4F;AAC5F,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAE9C;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAehD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAC/C,MAAM,CA+BR;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,CACnB,OAAO,EAAE,oBAAoB,EAC7B,GAAG,MAAM,EAAE,OAAO,EAAE,GACnB,MAAM,CAOR"}
|
package/dist/escape.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
22
|
+
/** Wrapper marking a string as an identifier (vs a literal) for `powql` tagged templates. */
|
|
23
|
+
export class PowqlIdent {
|
|
24
|
+
name;
|
|
25
|
+
constructor(name) {
|
|
26
|
+
this.name = name;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Factory for {@link PowqlIdent}. Prefer this over `new PowqlIdent(...)` at call sites. */
|
|
30
|
+
export function ident(name) {
|
|
31
|
+
return new PowqlIdent(name);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate a PowQL identifier (table name, field name, alias). Returns the
|
|
35
|
+
* identifier unchanged on success; throws `TypeError` on any invalid input
|
|
36
|
+
* (non-string, empty, or containing characters outside `[A-Za-z_][A-Za-z0-9_]*`).
|
|
37
|
+
*/
|
|
38
|
+
export function escapeIdent(name) {
|
|
39
|
+
if (typeof name !== "string") {
|
|
40
|
+
throw new TypeError(`escapeIdent: expected string, got ${typeof name}`);
|
|
41
|
+
}
|
|
42
|
+
if (name.length === 0) {
|
|
43
|
+
throw new TypeError("escapeIdent: identifier must not be empty");
|
|
44
|
+
}
|
|
45
|
+
if (!IDENT_RE.test(name)) {
|
|
46
|
+
throw new TypeError(`escapeIdent: invalid identifier ${JSON.stringify(name)} (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`);
|
|
47
|
+
}
|
|
48
|
+
return name;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Render a JS value as a PowQL literal. Supports `string`, `number`, `bigint`,
|
|
52
|
+
* `boolean`, and `null`. Rejects `NaN`/`±Infinity`, `undefined`, symbols,
|
|
53
|
+
* objects, and arrays with `TypeError`.
|
|
54
|
+
*
|
|
55
|
+
* - string → `"..."` with `\` and `"` backslash-escaped (C-style, per the
|
|
56
|
+
* PowQL lexer). Backslash must be escaped first to avoid double-processing.
|
|
57
|
+
* - number → decimal; rejects non-finite
|
|
58
|
+
* - bigint → decimal digits
|
|
59
|
+
* - boolean → `true` / `false`
|
|
60
|
+
* - null → `null`
|
|
61
|
+
*/
|
|
62
|
+
export function escapeLiteral(value) {
|
|
63
|
+
if (value === null)
|
|
64
|
+
return "null";
|
|
65
|
+
const t = typeof value;
|
|
66
|
+
if (t === "string") {
|
|
67
|
+
const escaped = value
|
|
68
|
+
.replace(/\\/g, "\\\\")
|
|
69
|
+
.replace(/"/g, '\\"');
|
|
70
|
+
return `"${escaped}"`;
|
|
71
|
+
}
|
|
72
|
+
if (t === "number") {
|
|
73
|
+
const n = value;
|
|
74
|
+
if (!Number.isFinite(n)) {
|
|
75
|
+
throw new TypeError(`escapeLiteral: non-finite number ${String(n)} cannot be represented as a PowQL literal`);
|
|
76
|
+
}
|
|
77
|
+
return String(n);
|
|
78
|
+
}
|
|
79
|
+
if (t === "bigint") {
|
|
80
|
+
return value.toString(10);
|
|
81
|
+
}
|
|
82
|
+
if (t === "boolean") {
|
|
83
|
+
return value ? "true" : "false";
|
|
84
|
+
}
|
|
85
|
+
throw new TypeError(`escapeLiteral: unsupported type ${describe(value)}`);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Tagged template for safe PowQL composition. Each interpolated value is
|
|
89
|
+
* escaped as a literal by default; wrap it in `ident(...)` to interpolate as
|
|
90
|
+
* an identifier.
|
|
91
|
+
*
|
|
92
|
+
* const q = powql`insert ${ident(table)} { name := ${userName} }`;
|
|
93
|
+
*/
|
|
94
|
+
export function powql(strings, ...values) {
|
|
95
|
+
let out = strings[0] ?? "";
|
|
96
|
+
for (let i = 0; i < values.length; i++) {
|
|
97
|
+
out += renderInterpolation(values[i]);
|
|
98
|
+
out += strings[i + 1] ?? "";
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
// ──────────────────────────────────────────────────────────
|
|
103
|
+
// internals
|
|
104
|
+
// ──────────────────────────────────────────────────────────
|
|
105
|
+
function renderInterpolation(value) {
|
|
106
|
+
if (value instanceof PowqlIdent) {
|
|
107
|
+
return escapeIdent(value.name);
|
|
108
|
+
}
|
|
109
|
+
// `escapeLiteral` enforces the allowed set — for anything else it throws.
|
|
110
|
+
return escapeLiteral(value);
|
|
111
|
+
}
|
|
112
|
+
function describe(value) {
|
|
113
|
+
if (value === undefined)
|
|
114
|
+
return "undefined";
|
|
115
|
+
if (value === null)
|
|
116
|
+
return "null";
|
|
117
|
+
if (Array.isArray(value))
|
|
118
|
+
return "array";
|
|
119
|
+
const t = typeof value;
|
|
120
|
+
if (t === "object")
|
|
121
|
+
return "object";
|
|
122
|
+
return t;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=escape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape.js","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"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"}
|