@zvndev/powdb-client 0.11.0 → 0.13.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.
@@ -8,7 +8,7 @@
8
8
  * This mirrors crates/server/src/protocol.rs — keep them in sync.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.MAX_SYNC_PULL_BYTES = exports.MAX_SYNC_PULL_UNITS = exports.MAX_SYNC_UNITS = exports.MAX_PARAMS = exports.MAX_COLUMNS = exports.MAX_ROWS = exports.MAX_PAYLOAD_SIZE = exports.MSG_PONG = exports.MSG_PING = exports.MSG_DISCONNECT = exports.MSG_RESULT_MSG = exports.MSG_ERROR = exports.MSG_RESULT_OK = exports.MSG_RESULT_SCALAR = exports.MSG_RESULT_ROWS = exports.MSG_SYNC_ACK_RESULT = exports.MSG_SYNC_PULL_RESULT = exports.MSG_SYNC_STATUS_RESULT = exports.MSG_SYNC_ACK = exports.MSG_SYNC_PULL = exports.MSG_SYNC_STATUS = exports.MSG_QUERY_SQL = exports.MSG_QUERY_PARAMS = exports.MSG_QUERY = exports.MSG_CONNECT_OK = exports.MSG_CONNECT = void 0;
11
+ exports.MAX_SYNC_PULL_BYTES = exports.MAX_SYNC_PULL_UNITS = exports.MAX_SYNC_UNITS = exports.MAX_PARAMS = exports.MAX_COLUMNS = exports.MAX_ROWS = exports.MAX_PAYLOAD_SIZE = exports.MSG_RESULT_SCALAR_NATIVE = exports.MSG_RESULT_ROWS_NATIVE = exports.MSG_QUERY_SQL_NATIVE = exports.MSG_QUERY_PARAMS_NATIVE = exports.MSG_QUERY_NATIVE = exports.MSG_PONG = exports.MSG_PING = exports.MSG_DISCONNECT = exports.MSG_RESULT_MSG = exports.MSG_ERROR = exports.MSG_RESULT_OK = exports.MSG_RESULT_SCALAR = exports.MSG_RESULT_ROWS = exports.MSG_SYNC_ACK_RESULT = exports.MSG_SYNC_PULL_RESULT = exports.MSG_SYNC_STATUS_RESULT = exports.MSG_SYNC_ACK = exports.MSG_SYNC_PULL = exports.MSG_SYNC_STATUS = exports.MSG_QUERY_SQL = exports.MSG_QUERY_PARAMS = exports.MSG_QUERY = exports.MSG_CONNECT_OK = exports.MSG_CONNECT = void 0;
12
12
  exports.encode = encode;
13
13
  exports.tryDecode = tryDecode;
14
14
  exports.MSG_CONNECT = 0x01;
@@ -37,6 +37,11 @@ exports.MSG_RESULT_MSG = 0x0b;
37
37
  exports.MSG_DISCONNECT = 0x10;
38
38
  exports.MSG_PING = 0x11;
39
39
  exports.MSG_PONG = 0x12;
40
+ exports.MSG_QUERY_NATIVE = 0x13;
41
+ exports.MSG_QUERY_PARAMS_NATIVE = 0x14;
42
+ exports.MSG_QUERY_SQL_NATIVE = 0x15;
43
+ exports.MSG_RESULT_ROWS_NATIVE = 0x16;
44
+ exports.MSG_RESULT_SCALAR_NATIVE = 0x17;
40
45
  // ───── Size limits (mirror crates/server/src/protocol.rs) ──────────────────
41
46
  /** Maximum payload size accepted from the wire (64 MB). */
42
47
  exports.MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
@@ -85,41 +90,22 @@ function encode(msg) {
85
90
  msgType = exports.MSG_QUERY_SQL;
86
91
  break;
87
92
  case "QueryWithParams": {
88
- const parts = [encodeString(msg.query)];
89
- const count = Buffer.alloc(2);
90
- count.writeUInt16LE(msg.params.length, 0);
91
- parts.push(count);
92
- for (const p of msg.params) {
93
- switch (p.tag) {
94
- case "null":
95
- parts.push(Buffer.from([0]));
96
- break;
97
- case "int": {
98
- const b = Buffer.alloc(9);
99
- b.writeUInt8(1, 0);
100
- b.writeBigInt64LE(p.value, 1);
101
- parts.push(b);
102
- break;
103
- }
104
- case "float": {
105
- const b = Buffer.alloc(9);
106
- b.writeUInt8(2, 0);
107
- b.writeDoubleLE(p.value, 1);
108
- parts.push(b);
109
- break;
110
- }
111
- case "bool":
112
- parts.push(Buffer.from([3, p.value ? 1 : 0]));
113
- break;
114
- case "str":
115
- parts.push(Buffer.from([4]), encodeString(p.value));
116
- break;
117
- }
118
- }
119
- payload = Buffer.concat(parts);
93
+ payload = encodeQueryWithParams(msg.query, msg.params);
120
94
  msgType = exports.MSG_QUERY_PARAMS;
121
95
  break;
122
96
  }
97
+ case "QueryNative":
98
+ payload = encodeString(msg.query);
99
+ msgType = exports.MSG_QUERY_NATIVE;
100
+ break;
101
+ case "QueryWithParamsNative":
102
+ payload = encodeQueryWithParams(msg.query, msg.params);
103
+ msgType = exports.MSG_QUERY_PARAMS_NATIVE;
104
+ break;
105
+ case "QuerySqlNative":
106
+ payload = encodeString(msg.query);
107
+ msgType = exports.MSG_QUERY_SQL_NATIVE;
108
+ break;
123
109
  case "SyncStatus":
124
110
  payload = encodeString(msg.replicaId);
125
111
  msgType = exports.MSG_SYNC_STATUS;
@@ -197,6 +183,23 @@ function encode(msg) {
197
183
  payload = encodeString(msg.value);
198
184
  msgType = exports.MSG_RESULT_SCALAR;
199
185
  break;
186
+ case "ResultRowsNative": {
187
+ const parts = [u16LE(msg.columns.length)];
188
+ for (const column of msg.columns)
189
+ parts.push(encodeString(column));
190
+ parts.push(u32LE(msg.rows.length));
191
+ for (const row of msg.rows) {
192
+ for (const value of row)
193
+ parts.push(encodeWireValue(value));
194
+ }
195
+ payload = Buffer.concat(parts);
196
+ msgType = exports.MSG_RESULT_ROWS_NATIVE;
197
+ break;
198
+ }
199
+ case "ResultScalarNative":
200
+ payload = encodeWireValue(msg.value);
201
+ msgType = exports.MSG_RESULT_SCALAR_NATIVE;
202
+ break;
200
203
  case "ResultOk": {
201
204
  payload = Buffer.alloc(8);
202
205
  payload.writeBigUInt64LE(msg.affected, 0);
@@ -231,6 +234,83 @@ function encode(msg) {
231
234
  payload.copy(frame, 6);
232
235
  return frame;
233
236
  }
237
+ function encodeQueryWithParams(query, params) {
238
+ const parts = [encodeString(query), u16LE(params.length)];
239
+ for (const param of params) {
240
+ switch (param.tag) {
241
+ case "null":
242
+ parts.push(Buffer.from([0]));
243
+ break;
244
+ case "int": {
245
+ const body = Buffer.alloc(9);
246
+ body.writeUInt8(1, 0);
247
+ body.writeBigInt64LE(param.value, 1);
248
+ parts.push(body);
249
+ break;
250
+ }
251
+ case "float": {
252
+ const body = Buffer.alloc(9);
253
+ body.writeUInt8(2, 0);
254
+ body.writeDoubleLE(param.value, 1);
255
+ parts.push(body);
256
+ break;
257
+ }
258
+ case "bool":
259
+ parts.push(Buffer.from([3, param.value ? 1 : 0]));
260
+ break;
261
+ case "str":
262
+ parts.push(Buffer.from([4]), encodeString(param.value));
263
+ break;
264
+ }
265
+ }
266
+ return Buffer.concat(parts);
267
+ }
268
+ function encodeWireValue(value) {
269
+ let tag;
270
+ let body;
271
+ switch (value.type) {
272
+ case "empty":
273
+ tag = 0;
274
+ body = Buffer.alloc(0);
275
+ break;
276
+ case "int":
277
+ case "datetime":
278
+ tag = value.type === "int" ? 1 : 5;
279
+ body = Buffer.alloc(8);
280
+ body.writeBigInt64LE(value.value, 0);
281
+ break;
282
+ case "float":
283
+ tag = 2;
284
+ body = Buffer.alloc(8);
285
+ body.writeDoubleLE(value.value, 0);
286
+ break;
287
+ case "bool":
288
+ tag = 3;
289
+ body = Buffer.from([value.value ? 1 : 0]);
290
+ break;
291
+ case "str":
292
+ tag = 4;
293
+ body = Buffer.from(value.value, "utf8");
294
+ break;
295
+ case "uuid":
296
+ if (value.value.length !== 16) {
297
+ throw new Error(`native UUID must be exactly 16 bytes, got ${value.value.length}`);
298
+ }
299
+ tag = 6;
300
+ body = Buffer.from(value.value);
301
+ break;
302
+ case "bytes":
303
+ tag = 7;
304
+ body = Buffer.from(value.value);
305
+ break;
306
+ case "json":
307
+ tag = 8;
308
+ body = Buffer.from(value.pj1);
309
+ decodePj1(body);
310
+ break;
311
+ }
312
+ return Buffer.concat([Buffer.from([tag]), u32LE(body.length), body]);
313
+ }
234
314
  // ───── Decoding ────────────────────────────────────────────────────────────
235
315
  /**
236
316
  * Attempts to parse a single frame from the start of `buf`. Returns the parsed
@@ -310,6 +390,21 @@ function decodePayload(msgType, payload) {
310
390
  }
311
391
  return { type: "QueryWithParams", query, params };
312
392
  }
393
+ case exports.MSG_QUERY_NATIVE: {
394
+ const query = decodeStringStrict(payload, cursor, "native PowQL query");
395
+ requirePayloadEnd(payload, cursor, "native PowQL query");
396
+ return { type: "QueryNative", query };
397
+ }
398
+ case exports.MSG_QUERY_PARAMS_NATIVE: {
399
+ const { query, params } = decodeNativeQueryWithParams(payload, cursor);
400
+ requirePayloadEnd(payload, cursor, "native parameterized query");
401
+ return { type: "QueryWithParamsNative", query, params };
402
+ }
403
+ case exports.MSG_QUERY_SQL_NATIVE: {
404
+ const query = decodeStringStrict(payload, cursor, "native SQL query");
405
+ requirePayloadEnd(payload, cursor, "native SQL query");
406
+ return { type: "QuerySqlNative", query };
407
+ }
313
408
  case exports.MSG_SYNC_STATUS:
314
409
  return { type: "SyncStatus", replicaId: decodeString(payload, cursor) };
315
410
  case exports.MSG_SYNC_PULL: {
@@ -407,6 +502,42 @@ function decodePayload(msgType, payload) {
407
502
  }
408
503
  case exports.MSG_RESULT_SCALAR:
409
504
  return { type: "ResultScalar", value: decodeString(payload, cursor) };
505
+ case exports.MSG_RESULT_ROWS_NATIVE: {
506
+ const colCount = readU16(payload, cursor, "native column count");
507
+ if (colCount > exports.MAX_COLUMNS) {
508
+ throw new Error(`too many columns: ${colCount} (max ${exports.MAX_COLUMNS})`);
509
+ }
510
+ const columns = [];
511
+ for (let i = 0; i < colCount; i++) {
512
+ columns.push(decodeStringStrict(payload, cursor, "native column name"));
513
+ }
514
+ const rowCount = readU32(payload, cursor, "native row count");
515
+ if (rowCount > exports.MAX_ROWS) {
516
+ throw new Error(`too many rows: ${rowCount} (max ${exports.MAX_ROWS})`);
517
+ }
518
+ if (colCount === 0 && rowCount > 0) {
519
+ throw new Error("nonzero native row count with zero columns");
520
+ }
521
+ const minimumBytes = BigInt(rowCount) * BigInt(colCount) * 5n;
522
+ if (BigInt(payload.length - cursor.pos) < minimumBytes) {
523
+ throw new Error(`native row data too short for declared shape: ${rowCount} rows x ${colCount} columns`);
524
+ }
525
+ const rows = [];
526
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
527
+ const row = [];
528
+ for (let columnIndex = 0; columnIndex < colCount; columnIndex++) {
529
+ row.push(decodeWireValue(payload, cursor));
530
+ }
531
+ rows.push(row);
532
+ }
533
+ requirePayloadEnd(payload, cursor, "native rows");
534
+ return { type: "ResultRowsNative", columns, rows };
535
+ }
536
+ case exports.MSG_RESULT_SCALAR_NATIVE: {
537
+ const value = decodeWireValue(payload, cursor);
538
+ requirePayloadEnd(payload, cursor, "native scalar");
539
+ return { type: "ResultScalarNative", value };
540
+ }
410
541
  case exports.MSG_RESULT_OK: {
411
542
  const affected = readU64(payload, cursor, "affected count");
412
543
  return { type: "ResultOk", affected };
@@ -425,6 +556,94 @@ function decodePayload(msgType, payload) {
425
556
  throw new Error(`unknown message type: 0x${msgType.toString(16)}`);
426
557
  }
427
558
  }
559
+ function decodeNativeQueryWithParams(payload, cursor) {
560
+ const query = decodeStringStrict(payload, cursor, "native query");
561
+ const count = readU16(payload, cursor, "native param count");
562
+ if (count > exports.MAX_PARAMS) {
563
+ throw new Error(`too many parameters: ${count} (max ${exports.MAX_PARAMS})`);
564
+ }
565
+ if (count > payload.length - cursor.pos) {
566
+ throw new Error("parameter count exceeds payload size");
567
+ }
568
+ const params = [];
569
+ for (let index = 0; index < count; index++) {
570
+ const tag = readU8(payload, cursor, "param tag");
571
+ switch (tag) {
572
+ case 0:
573
+ params.push({ tag: "null" });
574
+ break;
575
+ case 1:
576
+ params.push({ tag: "int", value: readI64(payload, cursor, "int param") });
577
+ break;
578
+ case 2:
579
+ params.push({ tag: "float", value: readF64(payload, cursor, "float param") });
580
+ break;
581
+ case 3:
582
+ params.push({ tag: "bool", value: readBool(payload, cursor, "bool param") });
583
+ break;
584
+ case 4:
585
+ params.push({
586
+ tag: "str",
587
+ value: decodeStringStrict(payload, cursor, "string param"),
588
+ });
589
+ break;
590
+ default:
591
+ throw new Error(`unknown param tag: ${tag}`);
592
+ }
593
+ }
594
+ return { query, params };
595
+ }
596
+ function decodeWireValue(buf, cursor) {
597
+ const tag = readU8(buf, cursor, "typed value tag");
598
+ const bodyLength = readU32(buf, cursor, "typed value body length");
599
+ const body = readFixedBytes(buf, cursor, bodyLength, "typed value body");
600
+ const requireLength = (expected, label) => {
601
+ if (body.length !== expected) {
602
+ throw new Error(`invalid ${label} typed value length: expected ${expected}, got ${body.length}`);
603
+ }
604
+ };
605
+ switch (tag) {
606
+ case 0:
607
+ requireLength(0, "Empty");
608
+ return { type: "empty" };
609
+ case 1:
610
+ requireLength(8, "Int");
611
+ return { type: "int", value: body.readBigInt64LE(0) };
612
+ case 2: {
613
+ requireLength(8, "Float");
614
+ return { type: "float", value: body.readDoubleLE(0) };
615
+ }
616
+ case 3:
617
+ requireLength(1, "Bool");
618
+ if (body[0] !== 0 && body[0] !== 1) {
619
+ throw new Error(`invalid typed boolean: ${body[0]}`);
620
+ }
621
+ return { type: "bool", value: body[0] === 1 };
622
+ case 4:
623
+ return { type: "str", value: decodeUtf8Strict(body, "typed string") };
624
+ case 5:
625
+ requireLength(8, "DateTime");
626
+ return { type: "datetime", value: body.readBigInt64LE(0) };
627
+ case 6:
628
+ requireLength(16, "UUID");
629
+ return { type: "uuid", value: new Uint8Array(body) };
630
+ case 7:
631
+ return { type: "bytes", value: new Uint8Array(body) };
632
+ case 8:
633
+ return {
634
+ type: "json",
635
+ value: decodePj1(body),
636
+ pj1: new Uint8Array(body),
637
+ };
638
+ default:
639
+ throw new Error(`unknown typed value tag: ${tag}`);
640
+ }
641
+ }
642
+ function requirePayloadEnd(payload, cursor, label) {
643
+ if (cursor.pos !== payload.length) {
644
+ throw new Error(`trailing bytes in ${label} payload`);
645
+ }
646
+ }
428
647
  function encodeSyncStatus(status) {
429
648
  return Buffer.concat([
430
649
  encodeString(status.replicaId),
@@ -551,6 +770,159 @@ function decodeString(buf, cursor) {
551
770
  cursor.pos += len;
552
771
  return s;
553
772
  }
773
+ const strictUtf8 = new TextDecoder("utf-8", { fatal: true });
774
+ function decodeUtf8Strict(bytes, label) {
775
+ try {
776
+ return strictUtf8.decode(bytes);
777
+ }
778
+ catch {
779
+ throw new Error(`invalid UTF-8 in ${label}`);
780
+ }
781
+ }
782
+ function decodeStringStrict(buf, cursor, label) {
783
+ const len = readU32(buf, cursor, `${label} length`);
784
+ const bytes = readFixedBytes(buf, cursor, len, label);
785
+ return decodeUtf8Strict(bytes, label);
786
+ }
787
+ function decodePj1(doc) {
788
+ const [value, end] = decodePj1Node(doc, 0, 0);
789
+ if (end !== doc.length)
790
+ throw new Error("invalid typed PJ1 JSON: trailing bytes");
791
+ return value;
792
+ }
793
+ function decodePj1Node(doc, start, depth) {
794
+ if (depth > 128)
795
+ throw new Error("invalid typed PJ1 JSON: nesting too deep");
796
+ if (start >= doc.length)
797
+ throw new Error("invalid typed PJ1 JSON: truncated node");
798
+ const tag = doc[start];
799
+ switch (tag) {
800
+ case 0:
801
+ return [null, start + 1];
802
+ case 1:
803
+ return [false, start + 1];
804
+ case 2:
805
+ return [true, start + 1];
806
+ case 3: {
807
+ ensurePj1Range(doc, start + 1, 8);
808
+ const integer = doc.readBigInt64LE(start + 1);
809
+ const value = integer >= BigInt(Number.MIN_SAFE_INTEGER) &&
810
+ integer <= BigInt(Number.MAX_SAFE_INTEGER)
811
+ ? Number(integer)
812
+ : integer;
813
+ return [value, start + 9];
814
+ }
815
+ case 4: {
816
+ ensurePj1Range(doc, start + 1, 8);
817
+ const value = doc.readDoubleLE(start + 1);
818
+ if (!Number.isFinite(value)) {
819
+ throw new Error("invalid typed PJ1 JSON: non-finite float");
820
+ }
821
+ return [value, start + 9];
822
+ }
823
+ case 5: {
824
+ const [value, end] = decodePj1String(doc, start + 1);
825
+ return [value, end];
826
+ }
827
+ case 6:
828
+ return decodePj1Array(doc, start, depth);
829
+ case 7:
830
+ return decodePj1Object(doc, start, depth);
831
+ default:
832
+ throw new Error(`invalid typed PJ1 JSON: reserved/invalid tag ${tag}`);
833
+ }
834
+ }
835
+ function decodePj1Array(doc, start, depth) {
836
+ const count = pj1U32(doc, start + 1);
837
+ const headerSize = 5 + 4 * (count + 1);
838
+ ensurePj1Range(doc, start, headerSize);
839
+ let expectedOffset = headerSize;
840
+ const values = [];
841
+ for (let index = 0; index < count; index++) {
842
+ const offset = pj1U32(doc, start + 5 + index * 4);
843
+ const nextOffset = pj1U32(doc, start + 5 + (index + 1) * 4);
844
+ if (offset !== expectedOffset || nextOffset < offset) {
845
+ throw new Error("invalid typed PJ1 JSON: array offsets not canonical");
846
+ }
847
+ const [value, end] = decodePj1Node(doc, start + offset, depth + 1);
848
+ if (end !== start + nextOffset) {
849
+ throw new Error("invalid typed PJ1 JSON: array element length mismatch");
850
+ }
851
+ values.push(value);
852
+ expectedOffset = nextOffset;
853
+ }
854
+ const total = pj1U32(doc, start + 5 + count * 4);
855
+ if (total !== expectedOffset) {
856
+ throw new Error("invalid typed PJ1 JSON: array end offset mismatch");
857
+ }
858
+ ensurePj1Range(doc, start, total);
859
+ return [values, start + total];
860
+ }
861
+ function decodePj1Object(doc, start, depth) {
862
+ const count = pj1U32(doc, start + 1);
863
+ const headerSize = 5 + count * 8 + 4;
864
+ ensurePj1Range(doc, start, headerSize);
865
+ const keys = [];
866
+ let expectedKeyOffset = headerSize;
867
+ for (let index = 0; index < count; index++) {
868
+ const keyOffset = pj1U32(doc, start + 5 + index * 8);
869
+ if (keyOffset !== expectedKeyOffset) {
870
+ throw new Error("invalid typed PJ1 JSON: object key offset mismatch");
871
+ }
872
+ const [text, end, bytes] = decodePj1String(doc, start + keyOffset);
873
+ const previous = keys[keys.length - 1];
874
+ if (previous && Buffer.compare(previous.bytes, bytes) >= 0) {
875
+ throw new Error("invalid typed PJ1 JSON: object keys not strictly sorted");
876
+ }
877
+ keys.push({ text, bytes });
878
+ expectedKeyOffset = end - start;
879
+ }
880
+ const result = {};
881
+ let expectedValueOffset = expectedKeyOffset;
882
+ for (let index = 0; index < count; index++) {
883
+ const valueOffset = pj1U32(doc, start + 5 + index * 8 + 4);
884
+ if (valueOffset !== expectedValueOffset) {
885
+ throw new Error("invalid typed PJ1 JSON: object value offset mismatch");
886
+ }
887
+ const [value, end] = decodePj1Node(doc, start + valueOffset, depth + 1);
888
+ const key = keys[index];
889
+ if (!key)
890
+ throw new Error("invalid typed PJ1 JSON: missing object key");
891
+ Object.defineProperty(result, key.text, {
892
+ value,
893
+ enumerable: true,
894
+ configurable: true,
895
+ writable: true,
896
+ });
897
+ expectedValueOffset = end - start;
898
+ }
899
+ const total = pj1U32(doc, start + 5 + count * 8);
900
+ if (total !== expectedValueOffset) {
901
+ throw new Error("invalid typed PJ1 JSON: object end offset mismatch");
902
+ }
903
+ ensurePj1Range(doc, start, total);
904
+ return [result, start + total];
905
+ }
906
+ function decodePj1String(doc, lengthOffset) {
907
+ const len = pj1U32(doc, lengthOffset);
908
+ const dataStart = lengthOffset + 4;
909
+ ensurePj1Range(doc, dataStart, len);
910
+ const bytes = doc.subarray(dataStart, dataStart + len);
911
+ return [decodeUtf8Strict(bytes, "PJ1 string"), dataStart + len, bytes];
912
+ }
913
+ function pj1U32(doc, offset) {
914
+ ensurePj1Range(doc, offset, 4);
915
+ return doc.readUInt32LE(offset);
916
+ }
917
+ function ensurePj1Range(doc, offset, len) {
918
+ if (!Number.isSafeInteger(offset) ||
919
+ !Number.isSafeInteger(len) ||
920
+ offset < 0 ||
921
+ len < 0 ||
922
+ offset + len > doc.length) {
923
+ throw new Error("invalid typed PJ1 JSON: truncated or overflowing range");
924
+ }
925
+ }
554
926
  function readU8(buf, cursor, label) {
555
927
  if (cursor.pos + 1 > buf.length) {
556
928
  throw new Error(`truncated ${label}`);
@@ -9,6 +9,7 @@
9
9
  * Str → raw UTF-8 (may contain anything)
10
10
  * DateTime → microseconds since Unix epoch as decimal, e.g. "1718928000000000"
11
11
  * Uuid → canonical "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
12
+ * Json → canonical JSON text (keys sorted bytewise, no whitespace)
12
13
  * Bytes → "<N bytes>" (LOSSY — see note below)
13
14
  *
14
15
  * `queryTyped` takes a user-supplied schema and coerces each column string
@@ -24,14 +25,18 @@
24
25
  * schema if you just want the `<N bytes>` placeholder.
25
26
  */
26
27
  /**
27
- * Supported column types. Mirrors the server's `DataType` enum minus the
28
+ * Supported column types. Mirrors the server's `TypeId` enum minus the
28
29
  * `Bytes` variant, which is intentionally unsupported here.
29
30
  */
30
- export type ColumnType = "int" | "float" | "bool" | "str" | "datetime" | "uuid";
31
+ export type ColumnType = "int" | "float" | "bool" | "str" | "datetime" | "uuid" | "json";
31
32
  /** Map of column name → declared type. Columns not in the map pass through as strings. */
32
33
  export type TypedSchema = Record<string, ColumnType>;
34
+ /** Any value produced by `JSON.parse` of a canonical JSON document. */
35
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
36
+ [key: string]: JsonValue;
37
+ };
33
38
  /** Coerced value type. `int` may return `bigint` if the value exceeds Number.MAX_SAFE_INTEGER. */
34
- export type Coerced = number | bigint | boolean | string | Date | null;
39
+ export type Coerced = number | bigint | boolean | string | Date | null | JsonValue;
35
40
  /** A row of coerced values keyed by column name. */
36
41
  export type TypedRow = Record<string, Coerced>;
37
42
  /**
package/dist/cjs/typed.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * Str → raw UTF-8 (may contain anything)
11
11
  * DateTime → microseconds since Unix epoch as decimal, e.g. "1718928000000000"
12
12
  * Uuid → canonical "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
13
+ * Json → canonical JSON text (keys sorted bytewise, no whitespace)
13
14
  * Bytes → "<N bytes>" (LOSSY — see note below)
14
15
  *
15
16
  * `queryTyped` takes a user-supplied schema and coerces each column string
@@ -85,6 +86,25 @@ function coerceValue(column, raw, type) {
85
86
  }
86
87
  return raw;
87
88
  }
89
+ case "json": {
90
+ // The server renders json cells as canonical JSON text, so JSON.parse
91
+ // reconstructs the document (object, array, or scalar). A JSON `null`
92
+ // document already returned above via the shared null-sentinel guard.
93
+ //
94
+ // Unlike the other typed columns, a parse failure does NOT throw: it
95
+ // returns the raw string. Rationale: json is the one column type whose
96
+ // wire text is not a fixed, server-controlled shape a client can fully
97
+ // predict, and a future server that ever emits a non-canonical cell (or
98
+ // a proxy that rewrites it) must not turn a readable result into an
99
+ // exception. Canonical output always parses, so this fallback is inert
100
+ // in normal operation and exists purely as a safety net.
101
+ try {
102
+ return JSON.parse(raw);
103
+ }
104
+ catch {
105
+ return raw;
106
+ }
107
+ }
88
108
  }
89
109
  }
90
110
  /**