@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 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
@@ -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"}
@@ -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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"}