1import type * as rdsdata from "@distilled.cloud/aws/rds-data";
2
3/**
4 * JS value → Data API `SqlParameter` marshalling, matching the Postgres
5 * wire expectations: integers as `longValue`, floats as `doubleValue`,
6 * `Date`s as `TIMESTAMP`-hinted strings, `Uint8Array` as blobs, and
7 * anything else JSON-stringified.
8 */
9export const toSqlParameter = (
10 name: string,
11 value: unknown,
12): rdsdata.SqlParameter => {
13 if (value === null || value === undefined) {
14 return { name, value: { isNull: true } };
15 }
16 if (typeof value === "string") {
17 return { name, value: { stringValue: value } };
18 }
19 if (typeof value === "number") {
20 return Number.isInteger(value)
21 ? { name, value: { longValue: value } }
22 : { name, value: { doubleValue: value } };
23 }
24 if (typeof value === "bigint") {
25 return { name, value: { longValue: Number(value) } };
26 }
27 if (typeof value === "boolean") {
28 return { name, value: { booleanValue: value } };
29 }
30 if (value instanceof Date) {
31 return {
32 name,
33 typeHint: "TIMESTAMP",
34 value: {
35 stringValue: value.toISOString().replace("T", " ").replace("Z", ""),
36 },
37 };
38 }
39 if (value instanceof Uint8Array) {
40 return { name, value: { blobValue: value } as rdsdata.Field };
41 }
42 return { name, value: { stringValue: JSON.stringify(value) } };
43};
44
45const TIMESTAMP_TYPES = new Set([
46 "timestamp",
47 "timestamptz",
48 "date",
49 "datetime",
50]);
51
52/**
53 * Data API `Field` → JS value. Timestamp-typed columns (per the response's
54 * `columnMetadata.typeName`) revive as `Date`s — the Data API returns UTC
55 * timestamps as `"YYYY-MM-DD HH:MM:SS[.FFF]"` strings.
56 */
57export const fromField = (
58 field: rdsdata.Field,
59 typeName: string | undefined,
60): unknown => {
61 const f = field;
62 if (f.isNull) {
63 return null;
64 }
65 if (f.stringValue !== undefined) {
66 if (typeName !== undefined && TIMESTAMP_TYPES.has(typeName.toLowerCase())) {
67 const iso = f.stringValue.replace(" ", "T");
68 return new Date(/[Z+]/.test(iso.slice(10)) ? iso : `${iso}Z`);
69 }
70 return f.stringValue;
71 }
72 if (f.longValue !== undefined) return f.longValue;
73 if (f.doubleValue !== undefined) return f.doubleValue;
74 if (f.booleanValue !== undefined) return f.booleanValue;
75 if (f.blobValue !== undefined) return f.blobValue;
76 if (f.arrayValue !== undefined) return fromArrayValue(f.arrayValue);
77 return null;
78};
79
80const fromArrayValue = (value: rdsdata.ArrayValue): unknown[] =>
81 value.arrayValues?.map(fromArrayValue) ??
82 value.booleanValues ??
83 value.longValues ??
84 value.doubleValues ??
85 value.stringValues ??
86 [];
87
88/**
89 * Map an `executeStatement` response (with `includeResultMetadata: true`)
90 * to rows keyed by column label.
91 */
92export const toRows = (response: {
93 records?: rdsdata.Field[][];
94 columnMetadata?: rdsdata.ColumnMetadata[];
95}): Record<string, unknown>[] => {
96 const metadata = response.columnMetadata ?? [];
97 return (response.records ?? []).map((record) => {
98 const row: Record<string, unknown> = {};
99 record.forEach((field, index) => {
100 const column = metadata[index];
101 row[column?.label ?? column?.name ?? `column${index}`] = fromField(
102 field,
103 column?.typeName ?? undefined,
104 );
105 });
106 return row;
107 });
108};