Skip to content

Commit b125ea4

Browse files
committed
feat: add custom parsers
1 parent 3230a8e commit b125ea4

File tree

3 files changed

+111
-7
lines changed

3 files changed

+111
-7
lines changed

connection/connection_params.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { parseConnectionUri } from "../utils/utils.ts";
22
import { ConnectionParamsError } from "../client/error.ts";
33
import { fromFileUrl, isAbsolute } from "../deps.ts";
4+
import { Oid } from "../query/oid.ts";
45

56
/**
67
* The connection string must match the following URI structure. All parameters but database and user are optional
@@ -91,6 +92,10 @@ export interface TLSOptions {
9192
caCertificates: string[];
9293
}
9394

95+
export type DecoderFunction = (value: string) => unknown;
96+
97+
export type Decoders = Record<number, DecoderFunction>;
98+
9499
/**
95100
* Control the behavior for the client instance
96101
*/
@@ -108,6 +113,19 @@ export type ClientControls = {
108113
* - `raw` : the data is returned as Uint8Array
109114
*/
110115
decode_strategy?: "string" | "auto";
116+
117+
/**
118+
* Overide to decoder used to decode text fields. The key is the OID type number
119+
* and the value is the decoder function
120+
*
121+
* You can use the Oid map to set the decoder:
122+
* ```ts
123+
* {
124+
* [Oid.date]: (value: string) => new Date(value),
125+
* }
126+
* ```
127+
*/
128+
decoders?: Decoders;
111129
};
112130

113131
/** The Client database connection options */

query/decode.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
decodeTid,
3636
decodeTidArray,
3737
} from "./decoders.ts";
38-
import { ClientControls } from "../connection/connection_params.ts";
38+
import { ClientControls, Decoders } from "../connection/connection_params.ts";
3939

4040
export class Column {
4141
constructor(
@@ -62,9 +62,13 @@ function decodeBinary() {
6262
throw new Error("Decoding binary data is not implemented!");
6363
}
6464

65-
function decodeText(value: Uint8Array, typeOid: number) {
65+
function decodeText(value: Uint8Array, typeOid: number, decoders?: Decoders) {
6666
const strValue = decoder.decode(value);
6767

68+
if (!!decoders?.[typeOid]) {
69+
return decoders[typeOid](strValue);
70+
}
71+
6872
try {
6973
switch (typeOid) {
7074
case Oid.bpchar:
@@ -222,7 +226,7 @@ export function decode(
222226
return decoder.decode(value);
223227
}
224228
// default to 'auto' mode, which uses the typeOid to determine the decoding strategy
225-
return decodeText(value, column.typeOid);
229+
return decodeText(value, column.typeOid, controls?.decoders);
226230
} else {
227231
throw new Error(`Unknown column format: ${column.format}`);
228232
}

tests/query_client_test.ts

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { getMainConfiguration } from "./config.ts";
1616
import { PoolClient, QueryClient } from "../client.ts";
1717
import { ClientOptions } from "../connection/connection_params.ts";
18+
import { Oid } from "../query/oid.ts";
1819

1920
function withClient(
2021
t: (client: QueryClient) => void | Promise<void>,
@@ -119,15 +120,21 @@ Deno.test(
119120
withClient(
120121
async (client) => {
121122
const result = await client.queryObject(
122-
`SELECT ARRAY[1, 2, 3] AS _int_array, 3.14::REAL AS _float, 'DATA' AS _text, '{"test": "foo", "arr": [1,2,3]}'::JSONB AS _json, 'Y'::BOOLEAN AS _bool`,
123+
`SELECT
124+
'Y'::BOOLEAN AS _bool,
125+
3.14::REAL AS _float,
126+
ARRAY[1, 2, 3] AS _int_array,
127+
'{"test": "foo", "arr": [1,2,3]}'::JSONB AS _jsonb,
128+
'DATA' AS _text
129+
;`,
123130
);
124131

125132
assertEquals(result.rows, [
126133
{
127134
_bool: true,
128135
_float: 3.14,
129136
_int_array: [1, 2, 3],
130-
_json: { test: "foo", arr: [1, 2, 3] },
137+
_jsonb: { test: "foo", arr: [1, 2, 3] },
131138
_text: "DATA",
132139
},
133140
]);
@@ -141,15 +148,21 @@ Deno.test(
141148
withClient(
142149
async (client) => {
143150
const result = await client.queryObject(
144-
`SELECT ARRAY[1, 2, 3] AS _int_array, 3.14::REAL AS _float, 'DATA' AS _text, '{"test": "foo", "arr": [1,2,3]}'::JSONB AS _json, 'Y'::BOOLEAN AS _bool`,
151+
`SELECT
152+
'Y'::BOOLEAN AS _bool,
153+
3.14::REAL AS _float,
154+
ARRAY[1, 2, 3] AS _int_array,
155+
'{"test": "foo", "arr": [1,2,3]}'::JSONB AS _jsonb,
156+
'DATA' AS _text
157+
;`,
145158
);
146159

147160
assertEquals(result.rows, [
148161
{
149162
_bool: "t",
150163
_float: "3.14",
151164
_int_array: "{1,2,3}",
152-
_json: '{"arr": [1, 2, 3], "test": "foo"}',
165+
_jsonb: '{"arr": [1, 2, 3], "test": "foo"}',
153166
_text: "DATA",
154167
},
155168
]);
@@ -158,6 +171,75 @@ Deno.test(
158171
),
159172
);
160173

174+
Deno.test(
175+
"Custom decoders",
176+
withClient(
177+
async (client) => {
178+
const result = await client.queryObject(
179+
`SELECT
180+
0::BOOLEAN AS _bool,
181+
(DATE '2024-01-01' + INTERVAL '2 months')::DATE AS _date,
182+
7.90::REAL AS _float,
183+
100 AS _int,
184+
'{"foo": "a", "bar": [1,2,3], "baz": null}'::JSONB AS _jsonb,
185+
'MY_VALUE' AS _text,
186+
DATE '2024-10-01' + INTERVAL '2 years' - INTERVAL '2 months' AS _timestamp
187+
;`,
188+
);
189+
190+
assertEquals(result.rows, [
191+
{
192+
_bool: { boolean: false },
193+
_date: new Date("2024-03-03T00:00:00.000Z"),
194+
_float: 785,
195+
_int: 200,
196+
_jsonb: { id: "999", foo: "A", bar: [2, 4, 6], baz: "initial" },
197+
_text: ["E", "U", "L", "A", "V", "_", "Y", "M"],
198+
_timestamp: { year: 2126, month: "---08" },
199+
},
200+
]);
201+
},
202+
{
203+
controls: {
204+
decoders: {
205+
// convert to object
206+
[Oid.bool]: (value: string) => ({ boolean: value === "t" }),
207+
// convert to date and add 2 days
208+
[Oid.date]: (value: string) => {
209+
const d = new Date(value);
210+
return new Date(d.setDate(d.getDate() + 2));
211+
},
212+
// multiply by 100 - 5 = 785
213+
[Oid.float4]: (value: string) => parseFloat(value) * 100 - 5,
214+
// convert to int and add 100 = 200
215+
[Oid.int4]: (value: string) => parseInt(value, 10) + 100,
216+
// parse with multiple conditions
217+
[Oid.jsonb]: (value: string) => {
218+
const obj = JSON.parse(value);
219+
obj.foo = obj.foo.toUpperCase();
220+
obj.id = "999";
221+
obj.bar = obj.bar.map((v: number) => v * 2);
222+
if (obj.baz === null) obj.baz = "initial";
223+
return obj;
224+
},
225+
// split string and reverse
226+
[Oid.text]: (value: string) => value.split("").reverse(),
227+
// format timestamp into custom object
228+
[Oid.timestamp]: (value: string) => {
229+
const d = new Date(value);
230+
return {
231+
year: d.getFullYear() + 100,
232+
month: `---${d.getMonth() + 1 < 10 ? "0" : ""}${
233+
d.getMonth() + 1
234+
}`,
235+
};
236+
},
237+
},
238+
},
239+
},
240+
),
241+
);
242+
161243
Deno.test(
162244
"Array arguments",
163245
withClient(async (client) => {

0 commit comments

Comments
 (0)