From 0cd109f18ae2a0584713f91e69f3b5d9fb6f0b15 Mon Sep 17 00:00:00 2001 From: bombillazo Date: Thu, 8 Feb 2024 12:03:02 -0400 Subject: [PATCH 1/5] feate: add encoding strategy control --- connection/connection.ts | 66 ++++++++++++++++----------------- connection/connection_params.ts | 39 ++++++++++++++++--- query/decode.ts | 18 +++++++-- query/query.ts | 49 ++++++++++++------------ 4 files changed, 105 insertions(+), 67 deletions(-) diff --git a/connection/connection.ts b/connection/connection.ts index e7278c6c..8cc3bcdf 100644 --- a/connection/connection.ts +++ b/connection/connection.ts @@ -139,7 +139,7 @@ export class Connection { constructor( connection_params: ClientConfiguration, - disconnection_callback: () => Promise, + disconnection_callback: () => Promise ) { this.#connection_params = connection_params; this.#onDisconnection = disconnection_callback; @@ -190,7 +190,7 @@ export class Connection { return false; default: throw new Error( - `Could not check if server accepts SSL connections, server responded with: ${response}`, + `Could not check if server accepts SSL connections, server responded with: ${response}` ); } } @@ -220,7 +220,7 @@ export class Connection { .addCString( connection_options .map(([key, value]) => `--${key}=${value}`) - .join(" "), + .join(" ") ); } @@ -266,7 +266,7 @@ export class Connection { } catch (e) { if (e instanceof Deno.errors.NotFound) { throw new ConnectionError( - `Could not open socket in path "${socket_guess}"`, + `Could not open socket in path "${socket_guess}"` ); } throw e; @@ -276,7 +276,7 @@ export class Connection { async #openTlsConnection( connection: Deno.Conn, - options: { hostname: string; caCerts: string[] }, + options: { hostname: string; caCerts: string[] } ) { this.#conn = await Deno.startTls(connection, options); this.#bufWriter = new BufWriter(this.#conn); @@ -345,7 +345,7 @@ export class Connection { bold(yellow("TLS connection failed with message: ")) + e.message + "\n" + - bold("Defaulting to non-encrypted connection"), + bold("Defaulting to non-encrypted connection") ); await this.#openConnection({ hostname, port, transport: "tcp" }); this.#tls = false; @@ -357,7 +357,7 @@ export class Connection { // Make sure to close the connection before erroring this.#closeConnection(); throw new Error( - "The server isn't accepting TLS connections. Change the client configuration so TLS configuration isn't required to connect", + "The server isn't accepting TLS connections. Change the client configuration so TLS configuration isn't required to connect" ); } } @@ -373,14 +373,14 @@ export class Connection { if (e instanceof Deno.errors.InvalidData && tls_enabled) { if (tls_enforced) { throw new Error( - "The certificate used to secure the TLS connection is invalid.", + "The certificate used to secure the TLS connection is invalid." ); } else { console.error( bold(yellow("TLS connection failed with message: ")) + e.message + "\n" + - bold("Defaulting to non-encrypted connection"), + bold("Defaulting to non-encrypted connection") ); await this.#openConnection({ hostname, port, transport: "tcp" }); this.#tls = false; @@ -436,7 +436,7 @@ export class Connection { async startup(is_reconnection: boolean) { if (is_reconnection && this.#connection_params.connection.attempts === 0) { throw new Error( - "The client has been disconnected from the database. Enable reconnection in the client to attempt reconnection after failure", + "The client has been disconnected from the database. Enable reconnection in the client to attempt reconnection after failure" ); } @@ -512,19 +512,19 @@ export class Connection { } case AUTHENTICATION_TYPE.SCM: throw new Error( - "Database server expected SCM authentication, which is not supported at the moment", + "Database server expected SCM authentication, which is not supported at the moment" ); case AUTHENTICATION_TYPE.GSS_STARTUP: throw new Error( - "Database server expected GSS authentication, which is not supported at the moment", + "Database server expected GSS authentication, which is not supported at the moment" ); case AUTHENTICATION_TYPE.GSS_CONTINUE: throw new Error( - "Database server expected GSS authentication, which is not supported at the moment", + "Database server expected GSS authentication, which is not supported at the moment" ); case AUTHENTICATION_TYPE.SSPI: throw new Error( - "Database server expected SSPI authentication, which is not supported at the moment", + "Database server expected SSPI authentication, which is not supported at the moment" ); case AUTHENTICATION_TYPE.SASL_STARTUP: authentication_result = await this.#authenticateWithSasl(); @@ -552,14 +552,14 @@ export class Connection { if (!this.#connection_params.password) { throw new ConnectionParamsError( - "Attempting MD5 authentication with unset password", + "Attempting MD5 authentication with unset password" ); } const password = await hashMd5Password( this.#connection_params.password, this.#connection_params.user, - salt, + salt ); const buffer = this.#packetWriter.addCString(password).flush(0x70); @@ -575,13 +575,13 @@ export class Connection { async #authenticateWithSasl(): Promise { if (!this.#connection_params.password) { throw new ConnectionParamsError( - "Attempting SASL auth with unset password", + "Attempting SASL auth with unset password" ); } const client = new scram.Client( this.#connection_params.user, - this.#connection_params.password, + this.#connection_params.password ); const utf8 = new TextDecoder("utf-8"); @@ -600,7 +600,7 @@ export class Connection { const authentication_type = maybe_sasl_continue.reader.readInt32(); if (authentication_type !== AUTHENTICATION_TYPE.SASL_CONTINUE) { throw new Error( - `Unexpected authentication type in SASL negotiation: ${authentication_type}`, + `Unexpected authentication type in SASL negotiation: ${authentication_type}` ); } break; @@ -609,11 +609,11 @@ export class Connection { throw new PostgresError(parseNoticeMessage(maybe_sasl_continue)); default: throw new Error( - `Unexpected message in SASL negotiation: ${maybe_sasl_continue.type}`, + `Unexpected message in SASL negotiation: ${maybe_sasl_continue.type}` ); } const sasl_continue = utf8.decode( - maybe_sasl_continue.reader.readAllBytes(), + maybe_sasl_continue.reader.readAllBytes() ); await client.receiveChallenge(sasl_continue); @@ -628,7 +628,7 @@ export class Connection { const authentication_type = maybe_sasl_final.reader.readInt32(); if (authentication_type !== AUTHENTICATION_TYPE.SASL_FINAL) { throw new Error( - `Unexpected authentication type in SASL finalization: ${authentication_type}`, + `Unexpected authentication type in SASL finalization: ${authentication_type}` ); } break; @@ -637,7 +637,7 @@ export class Connection { throw new PostgresError(parseNoticeMessage(maybe_sasl_final)); default: throw new Error( - `Unexpected message in SASL finalization: ${maybe_sasl_continue.type}`, + `Unexpected message in SASL finalization: ${maybe_sasl_continue.type}` ); } const sasl_final = utf8.decode(maybe_sasl_final.reader.readAllBytes()); @@ -649,7 +649,7 @@ export class Connection { async #simpleQuery(query: Query): Promise; async #simpleQuery( - query: Query, + query: Query ): Promise; async #simpleQuery(query: Query): Promise { this.#packetWriter.clear(); @@ -678,14 +678,14 @@ export class Connection { break; case INCOMING_QUERY_MESSAGES.COMMAND_COMPLETE: { result.handleCommandComplete( - parseCommandCompleteMessage(current_message), + parseCommandCompleteMessage(current_message) ); break; } case INCOMING_QUERY_MESSAGES.DATA_ROW: { const row_data = parseRowDataMessage(current_message); try { - result.insertRow(row_data); + result.insertRow(row_data, this.#connection_params.controls); } catch (e) { error = e; } @@ -705,13 +705,13 @@ export class Connection { break; case INCOMING_QUERY_MESSAGES.ROW_DESCRIPTION: { result.loadColumnDescriptions( - parseRowDescriptionMessage(current_message), + parseRowDescriptionMessage(current_message) ); break; } default: throw new Error( - `Unexpected simple query message: ${current_message.type}`, + `Unexpected simple query message: ${current_message.type}` ); } @@ -820,7 +820,7 @@ export class Connection { * https://www.postgresql.org/docs/14/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY */ async #preparedQuery( - query: Query, + query: Query ): Promise { // The parse messages declares the statement, query arguments and the cursor used in the transaction // The database will respond with a parse response @@ -855,14 +855,14 @@ export class Connection { break; case INCOMING_QUERY_MESSAGES.COMMAND_COMPLETE: { result.handleCommandComplete( - parseCommandCompleteMessage(current_message), + parseCommandCompleteMessage(current_message) ); break; } case INCOMING_QUERY_MESSAGES.DATA_ROW: { const row_data = parseRowDataMessage(current_message); try { - result.insertRow(row_data); + result.insertRow(row_data, this.#connection_params.controls); } catch (e) { error = e; } @@ -884,13 +884,13 @@ export class Connection { break; case INCOMING_QUERY_MESSAGES.ROW_DESCRIPTION: { result.loadColumnDescriptions( - parseRowDescriptionMessage(current_message), + parseRowDescriptionMessage(current_message) ); break; } default: throw new Error( - `Unexpected prepared query message: ${current_message.type}`, + `Unexpected prepared query message: ${current_message.type}` ); } diff --git a/connection/connection_params.ts b/connection/connection_params.ts index c3037736..76536f29 100644 --- a/connection/connection_params.ts +++ b/connection/connection_params.ts @@ -91,19 +91,43 @@ export interface TLSOptions { caCertificates: string[]; } +/** + * Control the behavior for the client instance + */ +export type ClientControls = { + /** + * The strategy to use when decoding binary fields + * + * `string` : all values are returned as string, and the user has to take care of parsing + * `auto` : deno-postgres parses the data into JS objects (as many as possible implemented, non-implemented parsers would still return strings) + * + * Default: `auto` + * + * Future strategies might include: + * - `strict` : deno-postgres parses the data into JS objects, and if a parser is not implemented, it throws an error + * - `raw` : the data is returned as Uint8Array + */ + decode_strategy?: "string" | "auto"; +}; + /** The Client database connection options */ export type ClientOptions = { /** Name of the application connecing to the database */ applicationName?: string; /** Additional connection options */ connection?: Partial; + /** Control the client behavior */ + controls?: ClientControls; /** The database name */ database?: string; /** The name of the host */ hostname?: string; /** The type of host connection */ host_type?: "tcp" | "socket"; - /** Additional client options */ + /** + * Additional connection URI options + * https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS + */ options?: string | Record; /** The database user password */ password?: string; @@ -118,14 +142,18 @@ export type ClientOptions = { /** The configuration options required to set up a Client instance */ export type ClientConfiguration = & Required< - Omit + Omit< + ClientOptions, + "password" | "port" | "tls" | "connection" | "options" | "controls" + > > & { + connection: ConnectionOptions; + controls?: ClientControls; + options: Record; password?: string; port: number; tls: TLSOptions; - connection: ConnectionOptions; - options: Record; }; function formatMissingParams(missingParams: string[]) { @@ -168,7 +196,7 @@ function assertRequiredOptions( // TODO // Support more options from the spec -/** options from URI per https://www.postgresql.org/docs/14/libpq-connect.html#LIBPQ-CONNSTRING */ +/** options from URI per https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING */ interface PostgresUri { application_name?: string; dbname?: string; @@ -447,6 +475,7 @@ export function createParams( caCertificates: params?.tls?.caCertificates ?? [], }, user: params.user ?? pgEnv.user, + controls: params.controls, }; assertRequiredOptions( diff --git a/query/decode.ts b/query/decode.ts index b09940d6..ad3a8a97 100644 --- a/query/decode.ts +++ b/query/decode.ts @@ -35,6 +35,7 @@ import { decodeTid, decodeTidArray, } from "./decoders.ts"; +import { ClientControls } from "../connection/connection_params.ts"; export class Column { constructor( @@ -44,7 +45,7 @@ export class Column { public typeOid: number, public columnLength: number, public typeModifier: number, - public format: Format, + public format: Format ) {} } @@ -58,7 +59,7 @@ const decoder = new TextDecoder(); // TODO // Decode binary fields function decodeBinary() { - throw new Error("Not implemented!"); + throw new Error("Decoding binary data is not implemented!"); } function decodeText(value: Uint8Array, typeOid: number) { @@ -201,17 +202,26 @@ function decodeText(value: Uint8Array, typeOid: number) { bold(yellow(`Error decoding type Oid ${typeOid} value`)) + _e.message + "\n" + - bold("Defaulting to null."), + bold("Defaulting to null.") ); // If an error occurred during decoding, return null return null; } } -export function decode(value: Uint8Array, column: Column) { +export function decode( + value: Uint8Array, + column: Column, + controls?: ClientControls +) { if (column.format === Format.BINARY) { return decodeBinary(); } else if (column.format === Format.TEXT) { + // If the user has specified a decode strategy, use that + if (controls?.decode_strategy === "string") { + return decoder.decode(value); + } + // default to 'auto' mode, which uses the typeOid to determine the decoding strategy return decodeText(value, column.typeOid); } else { throw new Error(`Unknown column format: ${column.format}`); diff --git a/query/query.ts b/query/query.ts index 46f9b3c5..3faed0e8 100644 --- a/query/query.ts +++ b/query/query.ts @@ -1,6 +1,7 @@ import { encodeArgument, type EncodedArg } from "./encode.ts"; import { type Column, decode } from "./decode.ts"; import { type Notice } from "../connection/message.ts"; +import { type ClientControls } from "../connection/connection_params.ts"; // TODO // Limit the type of parameters that can be passed @@ -62,7 +63,7 @@ export class RowDescription { export function templateStringToQuery( template: TemplateStringsArray, args: unknown[], - result_type: T, + result_type: T ): Query { const text = template.reduce((curr, next, index) => { return `${curr}$${index}${next}`; @@ -73,7 +74,7 @@ export function templateStringToQuery( function objectQueryToQueryArgs( query: string, - args: Record, + args: Record ): [string, unknown[]] { args = normalizeObjectQueryArgs(args); @@ -85,7 +86,7 @@ function objectQueryToQueryArgs( clean_args.push(args[match]); } else { throw new Error( - `No value was provided for the query argument "${match}"`, + `No value was provided for the query argument "${match}"` ); } @@ -97,15 +98,15 @@ function objectQueryToQueryArgs( /** This function lowercases all the keys of the object passed to it and checks for collission names */ function normalizeObjectQueryArgs( - args: Record, + args: Record ): Record { const normalized_args = Object.fromEntries( - Object.entries(args).map(([key, value]) => [key.toLowerCase(), value]), + Object.entries(args).map(([key, value]) => [key.toLowerCase(), value]) ); if (Object.keys(normalized_args).length !== Object.keys(args).length) { throw new Error( - "The arguments provided for the query must be unique (insensitive)", + "The arguments provided for the query must be unique (insensitive)" ); } @@ -232,7 +233,7 @@ export class QueryResult { * This class is used to handle the result of a query that returns an array */ export class QueryArrayResult< - T extends Array = Array, + T extends Array = Array > extends QueryResult { /** * The result rows @@ -242,10 +243,10 @@ export class QueryArrayResult< /** * Insert a row into the result */ - insertRow(row_data: Uint8Array[]) { + insertRow(row_data: Uint8Array[], controls?: ClientControls) { if (!this.rowDescription) { throw new Error( - "The row descriptions required to parse the result data weren't initialized", + "The row descriptions required to parse the result data weren't initialized" ); } @@ -256,7 +257,7 @@ export class QueryArrayResult< if (raw_value === null) { return null; } - return decode(raw_value, column); + return decode(raw_value, column, controls); }) as T; this.rows.push(row); @@ -289,7 +290,7 @@ function snakecaseToCamelcase(input: string) { * This class is used to handle the result of a query that returns an object */ export class QueryObjectResult< - T = Record, + T = Record > extends QueryResult { /** * The column names will be undefined on the first run of insertRow, since @@ -303,10 +304,10 @@ export class QueryObjectResult< /** * Insert a row into the result */ - insertRow(row_data: Uint8Array[]) { + insertRow(row_data: Uint8Array[], controls?: ClientControls) { if (!this.rowDescription) { throw new Error( - "The row description required to parse the result data wasn't initialized", + "The row description required to parse the result data wasn't initialized" ); } @@ -316,7 +317,7 @@ export class QueryObjectResult< if (this.rowDescription.columns.length !== this.query.fields.length) { throw new RangeError( "The fields provided for the query don't match the ones returned as a result " + - `(${this.rowDescription.columns.length} expected, ${this.query.fields.length} received)`, + `(${this.rowDescription.columns.length} expected, ${this.query.fields.length} received)` ); } @@ -329,7 +330,7 @@ export class QueryObjectResult< ); } else { column_names = this.rowDescription.columns.map( - (column) => column.name, + (column) => column.name ); } @@ -337,11 +338,9 @@ export class QueryObjectResult< const duplicates = findDuplicatesInArray(column_names); if (duplicates.length) { throw new Error( - `Field names ${ - duplicates - .map((str) => `"${str}"`) - .join(", ") - } are duplicated in the result of the query`, + `Field names ${duplicates + .map((str) => `"${str}"`) + .join(", ")} are duplicated in the result of the query` ); } @@ -354,7 +353,7 @@ export class QueryObjectResult< if (columns.length !== row_data.length) { throw new RangeError( - "The result fields returned by the database don't match the defined structure of the result", + "The result fields returned by the database don't match the defined structure of the result" ); } @@ -364,7 +363,7 @@ export class QueryObjectResult< if (raw_value === null) { row[columns[index]] = null; } else { - row[columns[index]] = decode(raw_value, current_column); + row[columns[index]] = decode(raw_value, current_column, controls); } return row; @@ -396,7 +395,7 @@ export class Query { constructor( config_or_text: string | QueryObjectOptions, result_type: T, - args: QueryArguments = [], + args: QueryArguments = [] ) { this.result_type = result_type; if (typeof config_or_text === "string") { @@ -418,13 +417,13 @@ export class Query { ); if (!fields_are_clean) { throw new TypeError( - "The fields provided for the query must contain only letters and underscores", + "The fields provided for the query must contain only letters and underscores" ); } if (new Set(fields).size !== fields.length) { throw new TypeError( - "The fields provided for the query must be unique", + "The fields provided for the query must be unique" ); } From b5b877f7d8c0932a0954d1ad11d9e9da28b8879f Mon Sep 17 00:00:00 2001 From: bombillazo Date: Thu, 8 Feb 2024 12:03:13 -0400 Subject: [PATCH 2/5] chore: add encoding strategy tests --- tests/config.ts | 11 +- tests/decode_test.ts | 125 +++++++-- tests/query_client_test.ts | 541 +++++++++++++++++++------------------ 3 files changed, 385 insertions(+), 292 deletions(-) diff --git a/tests/config.ts b/tests/config.ts index fbd2b45f..ebfe3118 100644 --- a/tests/config.ts +++ b/tests/config.ts @@ -1,4 +1,4 @@ -import { ClientConfiguration } from "../connection/connection_params.ts"; +import { ClientConfiguration, ClientOptions } from "../connection/connection_params.ts"; import config_file1 from "./config.json" with { type: "json" }; type TcpConfiguration = Omit & { @@ -67,17 +67,18 @@ export const getClearSocketConfiguration = (): SocketConfiguration => { }; /** MD5 authenticated user with privileged access to the database */ -export const getMainConfiguration = (): TcpConfiguration => { +export const getMainConfiguration = (_config?:ClientOptions ): TcpConfiguration => { return { applicationName: config.postgres_md5.applicationName, database: config.postgres_md5.database, hostname: config.postgres_md5.hostname, - host_type: "tcp", - options: {}, password: config.postgres_md5.password, + user: config.postgres_md5.users.main, + ..._config, + options: {}, port: config.postgres_md5.port, tls: enabled_tls, - user: config.postgres_md5.users.main, + host_type: "tcp", }; }; diff --git a/tests/decode_test.ts b/tests/decode_test.ts index 000cbab4..7ac954ca 100644 --- a/tests/decode_test.ts +++ b/tests/decode_test.ts @@ -1,3 +1,4 @@ +import { Column, decode } from "../query/decode.ts"; import { decodeBigint, decodeBigintArray, @@ -17,6 +18,7 @@ import { decodeTid, } from "../query/decoders.ts"; import { assertEquals, assertThrows } from "./test_deps.ts"; +import { Oid } from "../query/oid.ts"; Deno.test("decodeBigint", function () { assertEquals(decodeBigint("18014398509481984"), 18014398509481984n); @@ -25,9 +27,9 @@ Deno.test("decodeBigint", function () { Deno.test("decodeBigintArray", function () { assertEquals( decodeBigintArray( - "{17365398509481972,9007199254740992,-10414398509481984}", + "{17365398509481972,9007199254740992,-10414398509481984}" ), - [17365398509481972n, 9007199254740992n, -10414398509481984n], + [17365398509481972n, 9007199254740992n, -10414398509481984n] ); }); @@ -62,25 +64,25 @@ Deno.test("decodeBox", function () { assertThrows( () => decodeBox(testValue), Error, - `Invalid Box: "${testValue}". Box must have only 2 point, 1 given.`, + `Invalid Box: "${testValue}". Box must have only 2 point, 1 given.` ); testValue = "(12.4,2),(123,123,123),(9303,33)"; assertThrows( () => decodeBox(testValue), Error, - `Invalid Box: "${testValue}". Box must have only 2 point, 3 given.`, + `Invalid Box: "${testValue}". Box must have only 2 point, 3 given.` ); testValue = "(0,0),(123,123,123)"; assertThrows( () => decodeBox(testValue), Error, - `Invalid Box: "${testValue}" : Invalid Point: "(123,123,123)". Points must have only 2 coordinates, 3 given.`, + `Invalid Box: "${testValue}" : Invalid Point: "(123,123,123)". Points must have only 2 coordinates, 3 given.` ); testValue = "(0,0),(100,r100)"; assertThrows( () => decodeBox(testValue), Error, - `Invalid Box: "${testValue}" : Invalid Point: "(100,r100)". Coordinate "r100" must be a valid number.`, + `Invalid Box: "${testValue}" : Invalid Point: "(100,r100)". Coordinate "r100" must be a valid number.` ); }); @@ -93,13 +95,13 @@ Deno.test("decodeCircle", function () { assertThrows( () => decodeCircle(testValue), Error, - `Invalid Circle: "${testValue}" : Invalid Point: "(c21 23,2)". Coordinate "c21 23" must be a valid number.`, + `Invalid Circle: "${testValue}" : Invalid Point: "(c21 23,2)". Coordinate "c21 23" must be a valid number.` ); testValue = "<(33,2),mn23 3.5>"; assertThrows( () => decodeCircle(testValue), Error, - `Invalid Circle: "${testValue}". Circle radius "mn23 3.5" must be a valid number.`, + `Invalid Circle: "${testValue}". Circle radius "mn23 3.5" must be a valid number.` ); }); @@ -110,11 +112,11 @@ Deno.test("decodeDate", function () { Deno.test("decodeDatetime", function () { assertEquals( decodeDatetime("2021-08-01"), - new Date("2021-08-01 00:00:00-00"), + new Date("2021-08-01 00:00:00-00") ); assertEquals( decodeDatetime("1997-12-17 07:37:16-08"), - new Date("1997-12-17 07:37:16-08"), + new Date("1997-12-17 07:37:16-08") ); }); @@ -131,14 +133,14 @@ Deno.test("decodeInt", function () { Deno.test("decodeJson", function () { assertEquals( decodeJson( - '{"key_1": "MY VALUE", "key_2": null, "key_3": 10, "key_4": {"subkey_1": true, "subkey_2": ["1",2]}}', + '{"key_1": "MY VALUE", "key_2": null, "key_3": 10, "key_4": {"subkey_1": true, "subkey_2": ["1",2]}}' ), { key_1: "MY VALUE", key_2: null, key_3: 10, key_4: { subkey_1: true, subkey_2: ["1", 2] }, - }, + } ); assertThrows(() => decodeJson("{ 'eqw' ; ddd}")); }); @@ -149,13 +151,13 @@ Deno.test("decodeLine", function () { assertThrows( () => decodeLine("{100,50,0,100}"), Error, - `Invalid Line: "${testValue}". Line in linear equation format must have 3 constants, 4 given.`, + `Invalid Line: "${testValue}". Line in linear equation format must have 3 constants, 4 given.` ); testValue = "{100,d3km,0}"; assertThrows( () => decodeLine(testValue), Error, - `Invalid Line: "${testValue}". Line constant "d3km" must be a valid number.`, + `Invalid Line: "${testValue}". Line constant "d3km" must be a valid number.` ); }); @@ -168,25 +170,25 @@ Deno.test("decodeLineSegment", function () { assertThrows( () => decodeLineSegment(testValue), Error, - `Invalid Line Segment: "${testValue}" : Invalid Point: "(r344,350)". Coordinate "r344" must be a valid number.`, + `Invalid Line Segment: "${testValue}" : Invalid Point: "(r344,350)". Coordinate "r344" must be a valid number.` ); testValue = "((100),(r344,350))"; assertThrows( () => decodeLineSegment(testValue), Error, - `Invalid Line Segment: "${testValue}" : Invalid Point: "(100)". Points must have only 2 coordinates, 1 given.`, + `Invalid Line Segment: "${testValue}" : Invalid Point: "(100)". Points must have only 2 coordinates, 1 given.` ); testValue = "((100,50))"; assertThrows( () => decodeLineSegment(testValue), Error, - `Invalid Line Segment: "${testValue}". Line segments must have only 2 point, 1 given.`, + `Invalid Line Segment: "${testValue}". Line segments must have only 2 point, 1 given.` ); testValue = "((100,50),(350,350),(100,100))"; assertThrows( () => decodeLineSegment(testValue), Error, - `Invalid Line Segment: "${testValue}". Line segments must have only 2 point, 3 given.`, + `Invalid Line Segment: "${testValue}". Line segments must have only 2 point, 3 given.` ); }); @@ -204,13 +206,13 @@ Deno.test("decodePath", function () { assertThrows( () => decodePath(testValue), Error, - `Invalid Path: "${testValue}" : Invalid Point: "(350,kjf334)". Coordinate "kjf334" must be a valid number.`, + `Invalid Path: "${testValue}" : Invalid Point: "(350,kjf334)". Coordinate "kjf334" must be a valid number.` ); testValue = "((100,50,9949))"; assertThrows( () => decodePath(testValue), Error, - `Invalid Path: "${testValue}" : Invalid Point: "(100,50,9949)". Points must have only 2 coordinates, 3 given.`, + `Invalid Path: "${testValue}" : Invalid Point: "(100,50,9949)". Points must have only 2 coordinates, 3 given.` ); }); @@ -220,25 +222,25 @@ Deno.test("decodePoint", function () { assertThrows( () => decodePoint(testValue), Error, - `Invalid Point: "${testValue}". Points must have only 2 coordinates, 1 given.`, + `Invalid Point: "${testValue}". Points must have only 2 coordinates, 1 given.` ); testValue = "(100.100,50,350)"; assertThrows( () => decodePoint(testValue), Error, - `Invalid Point: "${testValue}". Points must have only 2 coordinates, 3 given.`, + `Invalid Point: "${testValue}". Points must have only 2 coordinates, 3 given.` ); testValue = "(1,r344)"; assertThrows( () => decodePoint(testValue), Error, - `Invalid Point: "${testValue}". Coordinate "r344" must be a valid number.`, + `Invalid Point: "${testValue}". Coordinate "r344" must be a valid number.` ); testValue = "(cd 213ee,100)"; assertThrows( () => decodePoint(testValue), Error, - `Invalid Point: "${testValue}". Coordinate "cd 213ee" must be a valid number.`, + `Invalid Point: "${testValue}". Coordinate "cd 213ee" must be a valid number.` ); }); @@ -248,3 +250,78 @@ Deno.test("decodeTid", function () { 29383838509481984n, ]); }); + +Deno.test("decode_strategy", function () { + const testValues = [ + { + value: "40", + column: new Column("test", 0, 0, Oid.int4, 0, 0, 0), + parsed: 40, + }, + { + value: "my_value", + column: new Column("test", 0, 0, Oid.text, 0, 0, 0), + parsed: "my_value", + }, + { + value: "[(100,50),(350,350)]", + column: new Column("test", 0, 0, Oid.path, 0, 0, 0), + parsed: [ + { x: "100", y: "50" }, + { x: "350", y: "350" }, + ], + }, + { + value: '{"value_1","value_2","value_3"}', + column: new Column("test", 0, 0, Oid.text_array, 0, 0, 0), + parsed: ["value_1", "value_2", "value_3"], + }, + { + value: "1997-12-17 07:37:16-08", + column: new Column("test", 0, 0, Oid.timestamp, 0, 0, 0), + parsed: new Date("1997-12-17 07:37:16-08"), + }, + { + value: "Yes", + column: new Column("test", 0, 0, Oid.bool, 0, 0, 0), + parsed: true, + }, + { + value: "<(12.4,2),3.5>", + column: new Column("test", 0, 0, Oid.circle, 0, 0, 0), + parsed: { point: { x: "12.4", y: "2" }, radius: "3.5" }, + }, + { + value: '{"test":1,"val":"foo","example":[1,2,false]}', + column: new Column("test", 0, 0, Oid.jsonb, 0, 0, 0), + parsed: { test: 1, val: "foo", example: [1, 2, false] }, + }, + { + value: "18014398509481984", + column: new Column("test", 0, 0, Oid.int8, 0, 0, 0), + parsed: 18014398509481984n, + }, + { + value: "{3.14,1.11,0.43,200}", + column: new Column("test", 0, 0, Oid.float4_array, 0, 0, 0), + parsed: [3.14, 1.11, 0.43, 200], + }, + ]; + + for (const testValue of testValues) { + const encodedValue = new TextEncoder().encode(testValue.value); + + // check default behavior + assertEquals(decode(encodedValue, testValue.column), testValue.parsed); + // check 'auto' behavior + assertEquals( + decode(encodedValue, testValue.column, { decode_strategy: "auto" }), + testValue.parsed + ); + // check 'string' behavior + assertEquals( + decode(encodedValue, testValue.column, { decode_strategy: "string" }), + testValue.value + ); + } +}); diff --git a/tests/query_client_test.ts b/tests/query_client_test.ts index bd6c5014..1d1b4648 100644 --- a/tests/query_client_test.ts +++ b/tests/query_client_test.ts @@ -14,12 +14,15 @@ import { } from "./test_deps.ts"; import { getMainConfiguration } from "./config.ts"; import { PoolClient, QueryClient } from "../client.ts"; +import { ClientConfiguration } from "../connection/connection_params.ts"; +import { ClientOptions } from "../connection/connection_params.ts"; function withClient( t: (client: QueryClient) => void | Promise, + config?: ClientOptions ) { async function clientWrapper() { - const client = new Client(getMainConfiguration()); + const client = new Client(getMainConfiguration(config)); try { await client.connect(); await t(client); @@ -29,7 +32,7 @@ function withClient( } async function poolWrapper() { - const pool = new Pool(getMainConfiguration(), 1); + const pool = new Pool(getMainConfiguration(config), 1); let client; try { client = await pool.connect(); @@ -48,7 +51,7 @@ function withClient( function withClientGenerator( t: (getClient: () => Promise) => void | Promise, - pool_size = 10, + pool_size = 10 ) { async function clientWrapper() { const clients: Client[] = []; @@ -98,18 +101,62 @@ Deno.test( withClient(async (client) => { const result = await client.queryArray("SELECT UNNEST(ARRAY[1, 2])"); assertEquals(result.rows.length, 2); - }), + }) ); Deno.test( "Object query", withClient(async (client) => { const result = await client.queryObject( - "SELECT ARRAY[1, 2, 3] AS ID, 'DATA' AS TYPE", + "SELECT ARRAY[1, 2, 3] AS ID, 'DATA' AS TYPE" ); assertEquals(result.rows, [{ id: [1, 2, 3], type: "DATA" }]); - }), + }) +); + +Deno.test( + "Decode strategy - auto", + withClient( + async (client) => { + const result = await client.queryObject( + `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` + ); + + assertEquals(result.rows, [ + { + _bool: true, + _float: 3.14, + _int_array: [1, 2, 3], + _json: { test: "foo", arr: [1, 2, 3] }, + _text: "DATA", + }, + ]); + }, + { controls: { decode_strategy: "auto" } } + ) +); + +Deno.test( + "Decode strategy - string", + withClient( + async (client) => { + const result = await client.queryObject( + `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` + ); + + assertEquals(result.rows, [ + { + _bool: "t", + _float: "3.14", + _int_array: "{1,2,3}", + _json: '{"arr": [1, 2, 3], "test": "foo"}', + _text: "DATA", + }, + ]); + }, + { controls: { decode_strategy: "string" } } + ) ); Deno.test( @@ -117,10 +164,7 @@ Deno.test( withClient(async (client) => { { const value = "1"; - const result = await client.queryArray( - "SELECT $1", - [value], - ); + const result = await client.queryArray("SELECT $1", [value]); assertEquals(result.rows, [[value]]); } @@ -135,10 +179,7 @@ Deno.test( { const value = "3"; - const result = await client.queryObject( - "SELECT $1 AS ID", - [value], - ); + const result = await client.queryObject("SELECT $1 AS ID", [value]); assertEquals(result.rows, [{ id: value }]); } @@ -150,7 +191,7 @@ Deno.test( }); assertEquals(result.rows, [{ id: value }]); } - }), + }) ); Deno.test( @@ -158,10 +199,7 @@ Deno.test( withClient(async (client) => { { const value = "1"; - const result = await client.queryArray( - "SELECT $id", - { id: value }, - ); + const result = await client.queryArray("SELECT $id", { id: value }); assertEquals(result.rows, [[value]]); } @@ -176,10 +214,9 @@ Deno.test( { const value = "3"; - const result = await client.queryObject( - "SELECT $id as ID", - { id: value }, - ); + const result = await client.queryObject("SELECT $id as ID", { + id: value, + }); assertEquals(result.rows, [{ id: value }]); } @@ -191,7 +228,7 @@ Deno.test( }); assertEquals(result.rows, [{ id: value }]); } - }), + }) ); Deno.test( @@ -200,16 +237,16 @@ Deno.test( const value = "some_value"; const { rows: res } = await client.queryArray( "SELECT $value, $VaLue, $VALUE", - { value }, + { value } ); assertEquals(res, [[value, value, value]]); await assertRejects( () => client.queryArray("SELECT $A", { a: 1, A: 2 }), Error, - "The arguments provided for the query must be unique (insensitive)", + "The arguments provided for the query must be unique (insensitive)" ); - }), + }) ); Deno.test( @@ -218,10 +255,9 @@ Deno.test( await client.queryArray`CREATE TEMP TABLE PREPARED_STATEMENT_ERROR (X INT)`; await assertRejects(() => - client.queryArray( - "INSERT INTO PREPARED_STATEMENT_ERROR VALUES ($1)", - ["TEXT"], - ) + client.queryArray("INSERT INTO PREPARED_STATEMENT_ERROR VALUES ($1)", [ + "TEXT", + ]) ); const { rows } = await client.queryObject<{ result: number }>({ @@ -230,19 +266,16 @@ Deno.test( }); assertEquals(rows[0], { result: 1 }); - }), + }) ); Deno.test( "Array query can handle multiple query failures at once", withClient(async (client) => { await assertRejects( - () => - client.queryArray( - "SELECT 1; SELECT '2'::INT; SELECT 'A'::INT", - ), + () => client.queryArray("SELECT 1; SELECT '2'::INT; SELECT 'A'::INT"), PostgresError, - "invalid input syntax for type integer", + "invalid input syntax for type integer" ); const { rows } = await client.queryObject<{ result: number }>({ @@ -251,20 +284,18 @@ Deno.test( }); assertEquals(rows[0], { result: 1 }); - }), + }) ); Deno.test( "Array query handles error during data processing", withClient(async (client) => { - await assertRejects( - () => client.queryObject`SELECT 'A' AS X, 'B' AS X`, - ); + await assertRejects(() => client.queryObject`SELECT 'A' AS X, 'B' AS X`); const value = "193"; const { rows: result_2 } = await client.queryObject`SELECT ${value} AS B`; assertEquals(result_2[0], { b: value }); - }), + }) ); Deno.test( @@ -276,7 +307,7 @@ Deno.test( }); assertEquals(result, [{ result: 1 }, { result: 2 }]); - }), + }) ); Deno.test( @@ -284,7 +315,7 @@ Deno.test( withClient(async (client) => { const { rows: result } = await client.queryArray(""); assertEquals(result, []); - }), + }) ); Deno.test( @@ -292,11 +323,13 @@ Deno.test( withClient(async (client) => { await client.queryArray`CREATE TEMP TABLE PREPARED_STATEMENT_ERROR (X INT)`; - await assertRejects(() => - client.queryArray( - "INSERT INTO PREPARED_STATEMENT_ERROR VALUES ($1)", - ["TEXT"], - ), PostgresError); + await assertRejects( + () => + client.queryArray("INSERT INTO PREPARED_STATEMENT_ERROR VALUES ($1)", [ + "TEXT", + ]), + PostgresError + ); const result = "handled"; @@ -307,20 +340,18 @@ Deno.test( }); assertEquals(rows[0], { result }); - }), + }) ); Deno.test( "Prepared query handles error during data processing", withClient(async (client) => { - await assertRejects( - () => client.queryObject`SELECT ${1} AS A, ${2} AS A`, - ); + await assertRejects(() => client.queryObject`SELECT ${1} AS A, ${2} AS A`); const value = "z"; const { rows: result_2 } = await client.queryObject`SELECT ${value} AS B`; assertEquals(result_2[0], { b: value }); - }), + }) ); Deno.test( @@ -329,19 +360,19 @@ Deno.test( const item_1 = "Test;Azer"; const item_2 = "123;456"; - const { rows: result_1 } = await client.queryArray( - `SELECT ARRAY[$1, $2]`, - [item_1, item_2], - ); + const { rows: result_1 } = await client.queryArray(`SELECT ARRAY[$1, $2]`, [ + item_1, + item_2, + ]); assertEquals(result_1[0], [[item_1, item_2]]); - }), + }) ); Deno.test( "Handles parameter status messages on array query", withClient(async (client) => { - const { rows: result_1 } = await client - .queryArray`SET TIME ZONE 'HongKong'`; + const { rows: result_1 } = + await client.queryArray`SET TIME ZONE 'HongKong'`; assertEquals(result_1, []); @@ -351,7 +382,7 @@ Deno.test( }); assertEquals(result_2, [{ result: 1 }]); - }), + }) ); Deno.test( @@ -359,8 +390,7 @@ Deno.test( withClient(async (client) => { const result = 10; - await client - .queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE(RES INTEGER) RETURNS INT AS $$ + await client.queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE(RES INTEGER) RETURNS INT AS $$ BEGIN SET TIME ZONE 'HongKong'; END; @@ -372,11 +402,10 @@ Deno.test( result, ]), PostgresError, - "control reached end of function without RETURN", + "control reached end of function without RETURN" ); - await client - .queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE(RES INTEGER) RETURNS INT AS $$ + await client.queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE(RES INTEGER) RETURNS INT AS $$ BEGIN SET TIME ZONE 'HongKong'; RETURN RES; @@ -390,14 +419,13 @@ Deno.test( }); assertEquals(result_1, [{ result }]); - }), + }) ); Deno.test( "Handles parameter status after error", withClient(async (client) => { - await client - .queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE() RETURNS INT AS $$ + await client.queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE() RETURNS INT AS $$ BEGIN SET TIME ZONE 'HongKong'; END; @@ -406,9 +434,9 @@ Deno.test( await assertRejects( () => client.queryArray`SELECT * FROM PG_TEMP.CHANGE_TIMEZONE()`, PostgresError, - "control reached end of function without RETURN", + "control reached end of function without RETURN" ); - }), + }) ); Deno.test( @@ -421,9 +449,9 @@ Deno.test( await client.queryArray`SELECT 1`; }, Error, - "Connection to the database has been terminated", + "Connection to the database has been terminated" ); - }), + }) ); // This test depends on the assumption that all clients will default to @@ -434,35 +462,31 @@ Deno.test( await assertRejects( () => client.queryArray`SELECT PG_TERMINATE_BACKEND(${client.session.pid})`, - ConnectionError, + ConnectionError ); const { rows: result } = await client.queryObject<{ res: number }>({ text: `SELECT 1`, fields: ["res"], }); - assertEquals( - result[0].res, - 1, - ); + assertEquals(result[0].res, 1); assertEquals(client.connected, true); - }), + }) ); Deno.test( "Handling of debug notices", withClient(async (client) => { // Create temporary function - await client - .queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CREATE_NOTICE () RETURNS INT AS $$ BEGIN RAISE NOTICE 'NOTICED'; RETURN (SELECT 1); END; $$ LANGUAGE PLPGSQL;`; + await client.queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CREATE_NOTICE () RETURNS INT AS $$ BEGIN RAISE NOTICE 'NOTICED'; RETURN (SELECT 1); END; $$ LANGUAGE PLPGSQL;`; const { rows, warnings } = await client.queryArray( - "SELECT * FROM PG_TEMP.CREATE_NOTICE();", + "SELECT * FROM PG_TEMP.CREATE_NOTICE();" ); assertEquals(rows[0][0], 1); assertEquals(warnings[0].message, "NOTICED"); - }), + }) ); // This query doesn't recreate the table and outputs @@ -470,22 +494,19 @@ Deno.test( Deno.test( "Handling of query notices", withClient(async (client) => { - await client.queryArray( - "CREATE TEMP TABLE NOTICE_TEST (ABC INT);", - ); + await client.queryArray("CREATE TEMP TABLE NOTICE_TEST (ABC INT);"); const { warnings } = await client.queryArray( - "CREATE TEMP TABLE IF NOT EXISTS NOTICE_TEST (ABC INT);", + "CREATE TEMP TABLE IF NOT EXISTS NOTICE_TEST (ABC INT);" ); assert(warnings[0].message.includes("already exists")); - }), + }) ); Deno.test( "Handling of messages between data fetching", withClient(async (client) => { - await client - .queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.MESSAGE_BETWEEN_DATA(MESSAGE VARCHAR) RETURNS VARCHAR AS $$ + await client.queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.MESSAGE_BETWEEN_DATA(MESSAGE VARCHAR) RETURNS VARCHAR AS $$ BEGIN RAISE NOTICE '%', MESSAGE; RETURN MESSAGE; @@ -517,7 +538,7 @@ Deno.test( assertEquals(result[2], { result: message_3 }); assertObjectMatch(warnings[2], { message: message_3 }); - }), + }) ); Deno.test( @@ -531,39 +552,37 @@ Deno.test( const expectedDate = Date.UTC(2019, 1, 10, 6, 0, 40, 5); assertEquals(row[0].toUTCString(), new Date(expectedDate).toUTCString()); - }), + }) ); Deno.test( "Binary data is parsed correctly", withClient(async (client) => { - const { rows: result_1 } = await client - .queryArray`SELECT E'foo\\\\000\\\\200\\\\\\\\\\\\377'::BYTEA`; + const { rows: result_1 } = + await client.queryArray`SELECT E'foo\\\\000\\\\200\\\\\\\\\\\\377'::BYTEA`; const expectedBytes = new Uint8Array([102, 111, 111, 0, 128, 92, 255]); assertEquals(result_1[0][0], expectedBytes); - const { rows: result_2 } = await client.queryArray( - "SELECT $1::BYTEA", - [expectedBytes], - ); + const { rows: result_2 } = await client.queryArray("SELECT $1::BYTEA", [ + expectedBytes, + ]); assertEquals(result_2[0][0], expectedBytes); - }), + }) ); Deno.test( "Result object metadata", withClient(async (client) => { await client.queryArray`CREATE TEMP TABLE METADATA (VALUE INTEGER)`; - await client - .queryArray`INSERT INTO METADATA VALUES (100), (200), (300), (400), (500), (600)`; + await client.queryArray`INSERT INTO METADATA VALUES (100), (200), (300), (400), (500), (600)`; let result; // simple select result = await client.queryArray( - "SELECT * FROM METADATA WHERE VALUE = 100", + "SELECT * FROM METADATA WHERE VALUE = 100" ); assertEquals(result.command, "SELECT"); assertEquals(result.rowCount, 1); @@ -571,23 +590,22 @@ Deno.test( // parameterized select result = await client.queryArray( "SELECT * FROM METADATA WHERE VALUE IN ($1, $2)", - [200, 300], + [200, 300] ); assertEquals(result.command, "SELECT"); assertEquals(result.rowCount, 2); // simple delete result = await client.queryArray( - "DELETE FROM METADATA WHERE VALUE IN (100, 200)", + "DELETE FROM METADATA WHERE VALUE IN (100, 200)" ); assertEquals(result.command, "DELETE"); assertEquals(result.rowCount, 2); // parameterized delete - result = await client.queryArray( - "DELETE FROM METADATA WHERE VALUE = $1", - [300], - ); + result = await client.queryArray("DELETE FROM METADATA WHERE VALUE = $1", [ + 300, + ]); assertEquals(result.command, "DELETE"); assertEquals(result.rowCount, 1); @@ -603,7 +621,7 @@ Deno.test( // simple update result = await client.queryArray( - "UPDATE METADATA SET VALUE = 500 WHERE VALUE IN (500, 600)", + "UPDATE METADATA SET VALUE = 500 WHERE VALUE IN (500, 600)" ); assertEquals(result.command, "UPDATE"); assertEquals(result.rowCount, 2); @@ -611,11 +629,11 @@ Deno.test( // parameterized update result = await client.queryArray( "UPDATE METADATA SET VALUE = 400 WHERE VALUE = $1", - [400], + [400] ); assertEquals(result.command, "UPDATE"); assertEquals(result.rowCount, 1); - }), + }) ); Deno.test( @@ -626,11 +644,11 @@ Deno.test( `); assertEquals(result, [ - { "very_very_very_very_very_very_very_very_very_very_very_long_nam": 1 }, + { very_very_very_very_very_very_very_very_very_very_very_long_nam: 1 }, ]); assert(warnings[0].message.includes("will be truncated")); - }), + }) ); Deno.test( @@ -643,7 +661,7 @@ Deno.test( >`SELECT ${value_1}, ${value_2}`; assertEquals(rows[0], [value_1, value_2]); - }), + }) ); Deno.test( @@ -662,7 +680,7 @@ Deno.test( }); assertEquals(result[0], record); - }), + }) ); Deno.test( @@ -681,7 +699,7 @@ Deno.test( }); assertEquals(result[0], record); - }), + }) ); Deno.test( @@ -693,7 +711,7 @@ Deno.test( }); assertEquals(result.rows, [{ ID: [1, 2, 3], type: "DATA" }]); - }), + }) ); Deno.test( @@ -709,7 +727,7 @@ Deno.test( }); assertEquals(result[0], record); - }), + }) ); Deno.test( @@ -722,9 +740,9 @@ Deno.test( fields: ["FIELD_1", "FIELD_1"], }), TypeError, - "The fields provided for the query must be unique", + "The fields provided for the query must be unique" ); - }), + }) ); Deno.test( @@ -733,7 +751,7 @@ Deno.test( await assertRejects( () => client.queryObject`SELECT 1 AS "a", 2 AS A`, Error, - `Field names "a" are duplicated in the result of the query`, + `Field names "a" are duplicated in the result of the query` ); await assertRejects( @@ -743,9 +761,9 @@ Deno.test( text: `SELECT 1 AS "fieldX", 2 AS field_x`, }), Error, - `Field names "fieldX" are duplicated in the result of the query`, + `Field names "fieldX" are duplicated in the result of the query` ); - }), + }) ); Deno.test( @@ -756,10 +774,7 @@ Deno.test( fields: ["a"], }); - assertEquals( - result_1[0].a, - 1, - ); + assertEquals(result_1[0].a, 1); await assertRejects( async () => { @@ -769,9 +784,9 @@ Deno.test( }); }, TypeError, - "The fields provided for the query must contain only letters and underscores", + "The fields provided for the query must contain only letters and underscores" ); - }), + }) ); Deno.test( @@ -785,7 +800,7 @@ Deno.test( }); }, TypeError, - "The fields provided for the query must contain only letters and underscores", + "The fields provided for the query must contain only letters and underscores" ); await assertRejects( @@ -796,7 +811,7 @@ Deno.test( }); }, TypeError, - "The fields provided for the query must contain only letters and underscores", + "The fields provided for the query must contain only letters and underscores" ); await assertRejects( @@ -807,9 +822,9 @@ Deno.test( }); }, TypeError, - "The fields provided for the query must contain only letters and underscores", + "The fields provided for the query must contain only letters and underscores" ); - }), + }) ); Deno.test( @@ -823,9 +838,9 @@ Deno.test( }); }, RangeError, - "The fields provided for the query don't match the ones returned as a result (1 expected, 2 received)", + "The fields provided for the query don't match the ones returned as a result (1 expected, 2 received)" ); - }), + }) ); Deno.test( @@ -838,9 +853,9 @@ Deno.test( fields: ["result"], }), RangeError, - "The result fields returned by the database don't match the defined structure of the result", + "The result fields returned by the database don't match the defined structure of the result" ); - }), + }) ); Deno.test( @@ -848,12 +863,13 @@ Deno.test( withClient(async (client) => { const value = { x: "A", y: "B" }; - const { rows } = await client.queryObject< - { x: string; y: string } - >`SELECT ${value.x} AS x, ${value.y} AS y`; + const { rows } = await client.queryObject<{ + x: string; + y: string; + }>`SELECT ${value.x} AS x, ${value.y} AS y`; assertEquals(rows[0], value); - }), + }) ); Deno.test( @@ -863,9 +879,9 @@ Deno.test( // deno-lint-ignore ban-ts-comment // @ts-expect-error () => client.createTransaction(), - "Transaction name must be a non-empty string", + "Transaction name must be a non-empty string" ); - }), + }) ); Deno.test( @@ -878,35 +894,35 @@ Deno.test( assertEquals( client.session.current_transaction, transaction_name, - "Client is locked out during transaction", + "Client is locked out during transaction" ); await transaction.queryArray`CREATE TEMP TABLE TEST (X INTEGER)`; const savepoint = await transaction.savepoint("table_creation"); await transaction.queryArray`INSERT INTO TEST (X) VALUES (1)`; - const query_1 = await transaction.queryObject< - { x: number } - >`SELECT X FROM TEST`; + const query_1 = await transaction.queryObject<{ + x: number; + }>`SELECT X FROM TEST`; assertEquals( query_1.rows[0].x, 1, - "Operation was not executed inside transaction", + "Operation was not executed inside transaction" ); await transaction.rollback(savepoint); - const query_2 = await transaction.queryObject< - { x: number } - >`SELECT X FROM TEST`; + const query_2 = await transaction.queryObject<{ + x: number; + }>`SELECT X FROM TEST`; assertEquals( query_2.rowCount, 0, - "Rollback was not succesful inside transaction", + "Rollback was not succesful inside transaction" ); await transaction.commit(); assertEquals( client.session.current_transaction, null, - "Client was not released after transaction", + "Client was not released after transaction" ); - }), + }) ); Deno.test( @@ -918,8 +934,8 @@ Deno.test( const data = 1; { - const { rows: result } = await transaction - .queryArray`SELECT ${data}::INTEGER`; + const { rows: result } = + await transaction.queryArray`SELECT ${data}::INTEGER`; assertEquals(result[0], [data]); } { @@ -932,7 +948,7 @@ Deno.test( } await transaction.commit(); - }), + }) ); Deno.test( @@ -948,45 +964,45 @@ Deno.test( const transaction_rr = client_1.createTransaction( "transactionIsolationLevelRepeatableRead", - { isolation_level: "repeatable_read" }, + { isolation_level: "repeatable_read" } ); await transaction_rr.begin(); // This locks the current value of the test table - await transaction_rr.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + await transaction_rr.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; // Modify data outside the transaction await client_2.queryArray`UPDATE FOR_TRANSACTION_TEST SET X = 2`; - const { rows: query_1 } = await client_2.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_1 } = await client_2.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals(query_1, [{ x: 2 }]); - const { rows: query_2 } = await transaction_rr.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_2 } = await transaction_rr.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals( query_2, [{ x: 1 }], - "Repeatable read transaction should not be able to observe changes that happened after the transaction start", + "Repeatable read transaction should not be able to observe changes that happened after the transaction start" ); await transaction_rr.commit(); - const { rows: query_3 } = await client_1.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_3 } = await client_1.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals( query_3, [{ x: 2 }], - "Main session should be able to observe changes after transaction ended", + "Main session should be able to observe changes after transaction ended" ); await client_1.queryArray`DROP TABLE FOR_TRANSACTION_TEST`; - }), + }) ); Deno.test( @@ -1002,14 +1018,14 @@ Deno.test( const transaction_rr = client_1.createTransaction( "transactionIsolationLevelRepeatableRead", - { isolation_level: "serializable" }, + { isolation_level: "serializable" } ); await transaction_rr.begin(); // This locks the current value of the test table - await transaction_rr.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + await transaction_rr.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; // Modify data outside the transaction await client_2.queryArray`UPDATE FOR_TRANSACTION_TEST SET X = 2`; @@ -1018,20 +1034,20 @@ Deno.test( () => transaction_rr.queryArray`UPDATE FOR_TRANSACTION_TEST SET X = 3`, TransactionError, undefined, - "A serializable transaction should throw if the data read in the transaction has been modified externally", + "A serializable transaction should throw if the data read in the transaction has been modified externally" ); - const { rows: query_3 } = await client_1.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_3 } = await client_1.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals( query_3, [{ x: 2 }], - "Main session should be able to observe changes after transaction ended", + "Main session should be able to observe changes after transaction ended" ); await client_1.queryArray`DROP TABLE FOR_TRANSACTION_TEST`; - }), + }) ); Deno.test( @@ -1048,11 +1064,11 @@ Deno.test( () => transaction.queryArray`DELETE FROM FOR_TRANSACTION_TEST`, TransactionError, undefined, - "DELETE shouldn't be able to be used in a read-only transaction", + "DELETE shouldn't be able to be used in a read-only transaction" ); await client.queryArray`DROP TABLE FOR_TRANSACTION_TEST`; - }), + }) ); Deno.test( @@ -1064,51 +1080,50 @@ Deno.test( await client_1.queryArray`DROP TABLE IF EXISTS FOR_TRANSACTION_TEST`; await client_1.queryArray`CREATE TABLE FOR_TRANSACTION_TEST (X INTEGER)`; await client_1.queryArray`INSERT INTO FOR_TRANSACTION_TEST (X) VALUES (1)`; - const transaction_1 = client_1.createTransaction( - "transactionSnapshot1", - { isolation_level: "repeatable_read" }, - ); + const transaction_1 = client_1.createTransaction("transactionSnapshot1", { + isolation_level: "repeatable_read", + }); await transaction_1.begin(); // This locks the current value of the test table - await transaction_1.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + await transaction_1.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; // Modify data outside the transaction await client_2.queryArray`UPDATE FOR_TRANSACTION_TEST SET X = 2`; - const { rows: query_1 } = await transaction_1.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_1 } = await transaction_1.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals( query_1, [{ x: 1 }], - "External changes shouldn't affect repeatable read transaction", + "External changes shouldn't affect repeatable read transaction" ); const snapshot = await transaction_1.getSnapshot(); - const transaction_2 = client_2.createTransaction( - "transactionSnapshot2", - { isolation_level: "repeatable_read", snapshot }, - ); + const transaction_2 = client_2.createTransaction("transactionSnapshot2", { + isolation_level: "repeatable_read", + snapshot, + }); await transaction_2.begin(); - const { rows: query_2 } = await transaction_2.queryObject< - { x: number } - >`SELECT X FROM FOR_TRANSACTION_TEST`; + const { rows: query_2 } = await transaction_2.queryObject<{ + x: number; + }>`SELECT X FROM FOR_TRANSACTION_TEST`; assertEquals( query_2, [{ x: 1 }], - "External changes shouldn't affect repeatable read transaction with previous snapshot", + "External changes shouldn't affect repeatable read transaction with previous snapshot" ); await transaction_1.commit(); await transaction_2.commit(); await client_1.queryArray`DROP TABLE FOR_TRANSACTION_TEST`; - }), + }) ); Deno.test( @@ -1123,7 +1138,7 @@ Deno.test( () => client.queryArray`SELECT 1`, Error, `This connection is currently locked by the "${name}" transaction`, - "The connection is not being locked by the transaction", + "The connection is not being locked by the transaction" ); await transaction.commit(); @@ -1131,9 +1146,9 @@ Deno.test( assertEquals( client.session.current_transaction, null, - "Client was not released after transaction", + "Client was not released after transaction" ); - }), + }) ); Deno.test( @@ -1148,16 +1163,16 @@ Deno.test( assertEquals( client.session.current_transaction, name, - "Client shouldn't have been released on chained commit", + "Client shouldn't have been released on chained commit" ); await transaction.commit(); assertEquals( client.session.current_transaction, null, - "Client was not released after transaction ended", + "Client was not released after transaction ended" ); - }), + }) ); Deno.test( @@ -1170,9 +1185,9 @@ Deno.test( await transaction.begin(); await transaction.queryArray`INSERT INTO MY_TEST (X) VALUES (1)`; - const { rows: query_1 } = await transaction.queryObject< - { x: number } - >`SELECT X FROM MY_TEST`; + const { rows: query_1 } = await transaction.queryObject<{ + x: number; + }>`SELECT X FROM MY_TEST`; assertEquals(query_1, [{ x: 1 }]); await transaction.rollback({ chain: true }); @@ -1180,29 +1195,29 @@ Deno.test( assertEquals( client.session.current_transaction, name, - "Client shouldn't have been released after chained rollback", + "Client shouldn't have been released after chained rollback" ); await transaction.rollback(); - const { rowCount: query_2 } = await client.queryObject< - { x: number } - >`SELECT X FROM MY_TEST`; + const { rowCount: query_2 } = await client.queryObject<{ + x: number; + }>`SELECT X FROM MY_TEST`; assertEquals(query_2, 0); assertEquals( client.session.current_transaction, null, - "Client was not released after rollback", + "Client was not released after rollback" ); - }), + }) ); Deno.test( "Transaction rollback validations", withClient(async (client) => { const transaction = client.createTransaction( - "transactionRollbackValidations", + "transactionRollbackValidations" ); await transaction.begin(); @@ -1210,11 +1225,11 @@ Deno.test( // @ts-ignore This is made to check the two properties aren't passed at once () => transaction.rollback({ savepoint: "unexistent", chain: true }), Error, - "The chain option can't be used alongside a savepoint on a rollback operation", + "The chain option can't be used alongside a savepoint on a rollback operation" ); await transaction.commit(); - }), + }) ); Deno.test( @@ -1227,7 +1242,7 @@ Deno.test( await assertRejects( () => transaction.queryArray`SELECT []`, TransactionError, - `The transaction "${name}" has been aborted`, + `The transaction "${name}" has been aborted` ); assertEquals(client.session.current_transaction, null); @@ -1235,10 +1250,10 @@ Deno.test( await assertRejects( () => transaction.queryObject`SELECT []`, TransactionError, - `The transaction "${name}" has been aborted`, + `The transaction "${name}" has been aborted` ); assertEquals(client.session.current_transaction, null); - }), + }) ); Deno.test( @@ -1250,54 +1265,54 @@ Deno.test( await transaction.begin(); await transaction.queryArray`CREATE TEMP TABLE X (Y INT)`; await transaction.queryArray`INSERT INTO X VALUES (1)`; - const { rows: query_1 } = await transaction.queryObject< - { y: number } - >`SELECT Y FROM X`; + const { rows: query_1 } = await transaction.queryObject<{ + y: number; + }>`SELECT Y FROM X`; assertEquals(query_1, [{ y: 1 }]); const savepoint = await transaction.savepoint(savepoint_name); await transaction.queryArray`DELETE FROM X`; - const { rowCount: query_2 } = await transaction.queryObject< - { y: number } - >`SELECT Y FROM X`; + const { rowCount: query_2 } = await transaction.queryObject<{ + y: number; + }>`SELECT Y FROM X`; assertEquals(query_2, 0); await savepoint.update(); await transaction.queryArray`INSERT INTO X VALUES (2)`; - const { rows: query_3 } = await transaction.queryObject< - { y: number } - >`SELECT Y FROM X`; + const { rows: query_3 } = await transaction.queryObject<{ + y: number; + }>`SELECT Y FROM X`; assertEquals(query_3, [{ y: 2 }]); await transaction.rollback(savepoint); - const { rowCount: query_4 } = await transaction.queryObject< - { y: number } - >`SELECT Y FROM X`; + const { rowCount: query_4 } = await transaction.queryObject<{ + y: number; + }>`SELECT Y FROM X`; assertEquals(query_4, 0); assertEquals( savepoint.instances, 2, - "An incorrect number of instances were created for a transaction savepoint", + "An incorrect number of instances were created for a transaction savepoint" ); await savepoint.release(); assertEquals( savepoint.instances, 1, - "The instance for the savepoint was not released", + "The instance for the savepoint was not released" ); // This checks that the savepoint can be called by name as well await transaction.rollback(savepoint_name); - const { rows: query_5 } = await transaction.queryObject< - { y: number } - >`SELECT Y FROM X`; + const { rows: query_5 } = await transaction.queryObject<{ + y: number; + }>`SELECT Y FROM X`; assertEquals(query_5, [{ y: 1 }]); await transaction.commit(); - }), + }) ); Deno.test( @@ -1309,22 +1324,22 @@ Deno.test( await assertRejects( () => transaction.savepoint("1"), Error, - "The savepoint name can't begin with a number", + "The savepoint name can't begin with a number" ); await assertRejects( () => transaction.savepoint( - "this_savepoint_is_going_to_be_longer_than_sixty_three_characters", + "this_savepoint_is_going_to_be_longer_than_sixty_three_characters" ), Error, - "The savepoint name can't be longer than 63 characters", + "The savepoint name can't be longer than 63 characters" ); await assertRejects( () => transaction.savepoint("+"), Error, - "The savepoint name can only contain alphanumeric characters", + "The savepoint name can only contain alphanumeric characters" ); const savepoint = await transaction.savepoint("ABC1"); @@ -1333,7 +1348,7 @@ Deno.test( assertEquals( savepoint, await transaction.savepoint("abc1"), - "Creating a savepoint with the same name should return the original one", + "Creating a savepoint with the same name should return the original one" ); await savepoint.release(); @@ -1342,23 +1357,23 @@ Deno.test( await assertRejects( () => savepoint.release(), Error, - "This savepoint has no instances to release", + "This savepoint has no instances to release" ); await assertRejects( () => transaction.rollback(savepoint), Error, - `There are no savepoints of "abc1" left to rollback to`, + `There are no savepoints of "abc1" left to rollback to` ); await assertRejects( () => transaction.rollback("UNEXISTENT"), Error, - `There is no "unexistent" savepoint registered in this transaction`, + `There is no "unexistent" savepoint registered in this transaction` ); await transaction.commit(); - }), + }) ); Deno.test( @@ -1373,7 +1388,7 @@ Deno.test( await assertRejects( () => transaction_y.begin(), Error, - `This client already has an ongoing transaction "x"`, + `This client already has an ongoing transaction "x"` ); await transaction_x.commit(); @@ -1381,44 +1396,44 @@ Deno.test( await assertRejects( () => transaction_y.begin(), Error, - "This transaction is already open", + "This transaction is already open" ); await transaction_y.commit(); await assertRejects( () => transaction_y.commit(), Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so`, + `This transaction has not been started yet, make sure to use the "begin" method to do so` ); await assertRejects( () => transaction_y.commit(), Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so`, + `This transaction has not been started yet, make sure to use the "begin" method to do so` ); await assertRejects( () => transaction_y.queryArray`SELECT 1`, Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so`, + `This transaction has not been started yet, make sure to use the "begin" method to do so` ); await assertRejects( () => transaction_y.queryObject`SELECT 1`, Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so`, + `This transaction has not been started yet, make sure to use the "begin" method to do so` ); await assertRejects( () => transaction_y.rollback(), Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so`, + `This transaction has not been started yet, make sure to use the "begin" method to do so` ); await assertRejects( () => transaction_y.savepoint("SOME"), Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so`, + `This transaction has not been started yet, make sure to use the "begin" method to do so` ); - }), + }) ); From 692b7f344db729221c4b070adb5cca7723d1a44e Mon Sep 17 00:00:00 2001 From: bombillazo Date: Thu, 8 Feb 2024 12:04:30 -0400 Subject: [PATCH 3/5] chore: fix file formatting --- connection/connection.ts | 62 +++---- connection/connection_params.ts | 2 +- query/decode.ts | 6 +- query/query.ts | 40 ++--- tests/config.ts | 9 +- tests/decode_test.ts | 52 +++--- tests/query_client_test.ts | 284 ++++++++++++++++---------------- 7 files changed, 234 insertions(+), 221 deletions(-) diff --git a/connection/connection.ts b/connection/connection.ts index 8cc3bcdf..c062553c 100644 --- a/connection/connection.ts +++ b/connection/connection.ts @@ -139,7 +139,7 @@ export class Connection { constructor( connection_params: ClientConfiguration, - disconnection_callback: () => Promise + disconnection_callback: () => Promise, ) { this.#connection_params = connection_params; this.#onDisconnection = disconnection_callback; @@ -190,7 +190,7 @@ export class Connection { return false; default: throw new Error( - `Could not check if server accepts SSL connections, server responded with: ${response}` + `Could not check if server accepts SSL connections, server responded with: ${response}`, ); } } @@ -220,7 +220,7 @@ export class Connection { .addCString( connection_options .map(([key, value]) => `--${key}=${value}`) - .join(" ") + .join(" "), ); } @@ -266,7 +266,7 @@ export class Connection { } catch (e) { if (e instanceof Deno.errors.NotFound) { throw new ConnectionError( - `Could not open socket in path "${socket_guess}"` + `Could not open socket in path "${socket_guess}"`, ); } throw e; @@ -276,7 +276,7 @@ export class Connection { async #openTlsConnection( connection: Deno.Conn, - options: { hostname: string; caCerts: string[] } + options: { hostname: string; caCerts: string[] }, ) { this.#conn = await Deno.startTls(connection, options); this.#bufWriter = new BufWriter(this.#conn); @@ -345,7 +345,7 @@ export class Connection { bold(yellow("TLS connection failed with message: ")) + e.message + "\n" + - bold("Defaulting to non-encrypted connection") + bold("Defaulting to non-encrypted connection"), ); await this.#openConnection({ hostname, port, transport: "tcp" }); this.#tls = false; @@ -357,7 +357,7 @@ export class Connection { // Make sure to close the connection before erroring this.#closeConnection(); throw new Error( - "The server isn't accepting TLS connections. Change the client configuration so TLS configuration isn't required to connect" + "The server isn't accepting TLS connections. Change the client configuration so TLS configuration isn't required to connect", ); } } @@ -373,14 +373,14 @@ export class Connection { if (e instanceof Deno.errors.InvalidData && tls_enabled) { if (tls_enforced) { throw new Error( - "The certificate used to secure the TLS connection is invalid." + "The certificate used to secure the TLS connection is invalid.", ); } else { console.error( bold(yellow("TLS connection failed with message: ")) + e.message + "\n" + - bold("Defaulting to non-encrypted connection") + bold("Defaulting to non-encrypted connection"), ); await this.#openConnection({ hostname, port, transport: "tcp" }); this.#tls = false; @@ -436,7 +436,7 @@ export class Connection { async startup(is_reconnection: boolean) { if (is_reconnection && this.#connection_params.connection.attempts === 0) { throw new Error( - "The client has been disconnected from the database. Enable reconnection in the client to attempt reconnection after failure" + "The client has been disconnected from the database. Enable reconnection in the client to attempt reconnection after failure", ); } @@ -512,19 +512,19 @@ export class Connection { } case AUTHENTICATION_TYPE.SCM: throw new Error( - "Database server expected SCM authentication, which is not supported at the moment" + "Database server expected SCM authentication, which is not supported at the moment", ); case AUTHENTICATION_TYPE.GSS_STARTUP: throw new Error( - "Database server expected GSS authentication, which is not supported at the moment" + "Database server expected GSS authentication, which is not supported at the moment", ); case AUTHENTICATION_TYPE.GSS_CONTINUE: throw new Error( - "Database server expected GSS authentication, which is not supported at the moment" + "Database server expected GSS authentication, which is not supported at the moment", ); case AUTHENTICATION_TYPE.SSPI: throw new Error( - "Database server expected SSPI authentication, which is not supported at the moment" + "Database server expected SSPI authentication, which is not supported at the moment", ); case AUTHENTICATION_TYPE.SASL_STARTUP: authentication_result = await this.#authenticateWithSasl(); @@ -552,14 +552,14 @@ export class Connection { if (!this.#connection_params.password) { throw new ConnectionParamsError( - "Attempting MD5 authentication with unset password" + "Attempting MD5 authentication with unset password", ); } const password = await hashMd5Password( this.#connection_params.password, this.#connection_params.user, - salt + salt, ); const buffer = this.#packetWriter.addCString(password).flush(0x70); @@ -575,13 +575,13 @@ export class Connection { async #authenticateWithSasl(): Promise { if (!this.#connection_params.password) { throw new ConnectionParamsError( - "Attempting SASL auth with unset password" + "Attempting SASL auth with unset password", ); } const client = new scram.Client( this.#connection_params.user, - this.#connection_params.password + this.#connection_params.password, ); const utf8 = new TextDecoder("utf-8"); @@ -600,7 +600,7 @@ export class Connection { const authentication_type = maybe_sasl_continue.reader.readInt32(); if (authentication_type !== AUTHENTICATION_TYPE.SASL_CONTINUE) { throw new Error( - `Unexpected authentication type in SASL negotiation: ${authentication_type}` + `Unexpected authentication type in SASL negotiation: ${authentication_type}`, ); } break; @@ -609,11 +609,11 @@ export class Connection { throw new PostgresError(parseNoticeMessage(maybe_sasl_continue)); default: throw new Error( - `Unexpected message in SASL negotiation: ${maybe_sasl_continue.type}` + `Unexpected message in SASL negotiation: ${maybe_sasl_continue.type}`, ); } const sasl_continue = utf8.decode( - maybe_sasl_continue.reader.readAllBytes() + maybe_sasl_continue.reader.readAllBytes(), ); await client.receiveChallenge(sasl_continue); @@ -628,7 +628,7 @@ export class Connection { const authentication_type = maybe_sasl_final.reader.readInt32(); if (authentication_type !== AUTHENTICATION_TYPE.SASL_FINAL) { throw new Error( - `Unexpected authentication type in SASL finalization: ${authentication_type}` + `Unexpected authentication type in SASL finalization: ${authentication_type}`, ); } break; @@ -637,7 +637,7 @@ export class Connection { throw new PostgresError(parseNoticeMessage(maybe_sasl_final)); default: throw new Error( - `Unexpected message in SASL finalization: ${maybe_sasl_continue.type}` + `Unexpected message in SASL finalization: ${maybe_sasl_continue.type}`, ); } const sasl_final = utf8.decode(maybe_sasl_final.reader.readAllBytes()); @@ -649,7 +649,7 @@ export class Connection { async #simpleQuery(query: Query): Promise; async #simpleQuery( - query: Query + query: Query, ): Promise; async #simpleQuery(query: Query): Promise { this.#packetWriter.clear(); @@ -678,7 +678,7 @@ export class Connection { break; case INCOMING_QUERY_MESSAGES.COMMAND_COMPLETE: { result.handleCommandComplete( - parseCommandCompleteMessage(current_message) + parseCommandCompleteMessage(current_message), ); break; } @@ -705,13 +705,13 @@ export class Connection { break; case INCOMING_QUERY_MESSAGES.ROW_DESCRIPTION: { result.loadColumnDescriptions( - parseRowDescriptionMessage(current_message) + parseRowDescriptionMessage(current_message), ); break; } default: throw new Error( - `Unexpected simple query message: ${current_message.type}` + `Unexpected simple query message: ${current_message.type}`, ); } @@ -820,7 +820,7 @@ export class Connection { * https://www.postgresql.org/docs/14/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY */ async #preparedQuery( - query: Query + query: Query, ): Promise { // The parse messages declares the statement, query arguments and the cursor used in the transaction // The database will respond with a parse response @@ -855,7 +855,7 @@ export class Connection { break; case INCOMING_QUERY_MESSAGES.COMMAND_COMPLETE: { result.handleCommandComplete( - parseCommandCompleteMessage(current_message) + parseCommandCompleteMessage(current_message), ); break; } @@ -884,13 +884,13 @@ export class Connection { break; case INCOMING_QUERY_MESSAGES.ROW_DESCRIPTION: { result.loadColumnDescriptions( - parseRowDescriptionMessage(current_message) + parseRowDescriptionMessage(current_message), ); break; } default: throw new Error( - `Unexpected prepared query message: ${current_message.type}` + `Unexpected prepared query message: ${current_message.type}`, ); } diff --git a/connection/connection_params.ts b/connection/connection_params.ts index 76536f29..deb4dd05 100644 --- a/connection/connection_params.ts +++ b/connection/connection_params.ts @@ -102,7 +102,7 @@ export type ClientControls = { * `auto` : deno-postgres parses the data into JS objects (as many as possible implemented, non-implemented parsers would still return strings) * * Default: `auto` - * + * * Future strategies might include: * - `strict` : deno-postgres parses the data into JS objects, and if a parser is not implemented, it throws an error * - `raw` : the data is returned as Uint8Array diff --git a/query/decode.ts b/query/decode.ts index ad3a8a97..dc6e517c 100644 --- a/query/decode.ts +++ b/query/decode.ts @@ -45,7 +45,7 @@ export class Column { public typeOid: number, public columnLength: number, public typeModifier: number, - public format: Format + public format: Format, ) {} } @@ -202,7 +202,7 @@ function decodeText(value: Uint8Array, typeOid: number) { bold(yellow(`Error decoding type Oid ${typeOid} value`)) + _e.message + "\n" + - bold("Defaulting to null.") + bold("Defaulting to null."), ); // If an error occurred during decoding, return null return null; @@ -212,7 +212,7 @@ function decodeText(value: Uint8Array, typeOid: number) { export function decode( value: Uint8Array, column: Column, - controls?: ClientControls + controls?: ClientControls, ) { if (column.format === Format.BINARY) { return decodeBinary(); diff --git a/query/query.ts b/query/query.ts index 3faed0e8..0bb39d7b 100644 --- a/query/query.ts +++ b/query/query.ts @@ -63,7 +63,7 @@ export class RowDescription { export function templateStringToQuery( template: TemplateStringsArray, args: unknown[], - result_type: T + result_type: T, ): Query { const text = template.reduce((curr, next, index) => { return `${curr}$${index}${next}`; @@ -74,7 +74,7 @@ export function templateStringToQuery( function objectQueryToQueryArgs( query: string, - args: Record + args: Record, ): [string, unknown[]] { args = normalizeObjectQueryArgs(args); @@ -86,7 +86,7 @@ function objectQueryToQueryArgs( clean_args.push(args[match]); } else { throw new Error( - `No value was provided for the query argument "${match}"` + `No value was provided for the query argument "${match}"`, ); } @@ -98,15 +98,15 @@ function objectQueryToQueryArgs( /** This function lowercases all the keys of the object passed to it and checks for collission names */ function normalizeObjectQueryArgs( - args: Record + args: Record, ): Record { const normalized_args = Object.fromEntries( - Object.entries(args).map(([key, value]) => [key.toLowerCase(), value]) + Object.entries(args).map(([key, value]) => [key.toLowerCase(), value]), ); if (Object.keys(normalized_args).length !== Object.keys(args).length) { throw new Error( - "The arguments provided for the query must be unique (insensitive)" + "The arguments provided for the query must be unique (insensitive)", ); } @@ -233,7 +233,7 @@ export class QueryResult { * This class is used to handle the result of a query that returns an array */ export class QueryArrayResult< - T extends Array = Array + T extends Array = Array, > extends QueryResult { /** * The result rows @@ -246,7 +246,7 @@ export class QueryArrayResult< insertRow(row_data: Uint8Array[], controls?: ClientControls) { if (!this.rowDescription) { throw new Error( - "The row descriptions required to parse the result data weren't initialized" + "The row descriptions required to parse the result data weren't initialized", ); } @@ -290,7 +290,7 @@ function snakecaseToCamelcase(input: string) { * This class is used to handle the result of a query that returns an object */ export class QueryObjectResult< - T = Record + T = Record, > extends QueryResult { /** * The column names will be undefined on the first run of insertRow, since @@ -307,7 +307,7 @@ export class QueryObjectResult< insertRow(row_data: Uint8Array[], controls?: ClientControls) { if (!this.rowDescription) { throw new Error( - "The row description required to parse the result data wasn't initialized" + "The row description required to parse the result data wasn't initialized", ); } @@ -317,7 +317,7 @@ export class QueryObjectResult< if (this.rowDescription.columns.length !== this.query.fields.length) { throw new RangeError( "The fields provided for the query don't match the ones returned as a result " + - `(${this.rowDescription.columns.length} expected, ${this.query.fields.length} received)` + `(${this.rowDescription.columns.length} expected, ${this.query.fields.length} received)`, ); } @@ -330,7 +330,7 @@ export class QueryObjectResult< ); } else { column_names = this.rowDescription.columns.map( - (column) => column.name + (column) => column.name, ); } @@ -338,9 +338,11 @@ export class QueryObjectResult< const duplicates = findDuplicatesInArray(column_names); if (duplicates.length) { throw new Error( - `Field names ${duplicates - .map((str) => `"${str}"`) - .join(", ")} are duplicated in the result of the query` + `Field names ${ + duplicates + .map((str) => `"${str}"`) + .join(", ") + } are duplicated in the result of the query`, ); } @@ -353,7 +355,7 @@ export class QueryObjectResult< if (columns.length !== row_data.length) { throw new RangeError( - "The result fields returned by the database don't match the defined structure of the result" + "The result fields returned by the database don't match the defined structure of the result", ); } @@ -395,7 +397,7 @@ export class Query { constructor( config_or_text: string | QueryObjectOptions, result_type: T, - args: QueryArguments = [] + args: QueryArguments = [], ) { this.result_type = result_type; if (typeof config_or_text === "string") { @@ -417,13 +419,13 @@ export class Query { ); if (!fields_are_clean) { throw new TypeError( - "The fields provided for the query must contain only letters and underscores" + "The fields provided for the query must contain only letters and underscores", ); } if (new Set(fields).size !== fields.length) { throw new TypeError( - "The fields provided for the query must be unique" + "The fields provided for the query must be unique", ); } diff --git a/tests/config.ts b/tests/config.ts index ebfe3118..17bf701c 100644 --- a/tests/config.ts +++ b/tests/config.ts @@ -1,4 +1,7 @@ -import { ClientConfiguration, ClientOptions } from "../connection/connection_params.ts"; +import { + ClientConfiguration, + ClientOptions, +} from "../connection/connection_params.ts"; import config_file1 from "./config.json" with { type: "json" }; type TcpConfiguration = Omit & { @@ -67,7 +70,9 @@ export const getClearSocketConfiguration = (): SocketConfiguration => { }; /** MD5 authenticated user with privileged access to the database */ -export const getMainConfiguration = (_config?:ClientOptions ): TcpConfiguration => { +export const getMainConfiguration = ( + _config?: ClientOptions, +): TcpConfiguration => { return { applicationName: config.postgres_md5.applicationName, database: config.postgres_md5.database, diff --git a/tests/decode_test.ts b/tests/decode_test.ts index 7ac954ca..438e5d84 100644 --- a/tests/decode_test.ts +++ b/tests/decode_test.ts @@ -27,9 +27,9 @@ Deno.test("decodeBigint", function () { Deno.test("decodeBigintArray", function () { assertEquals( decodeBigintArray( - "{17365398509481972,9007199254740992,-10414398509481984}" + "{17365398509481972,9007199254740992,-10414398509481984}", ), - [17365398509481972n, 9007199254740992n, -10414398509481984n] + [17365398509481972n, 9007199254740992n, -10414398509481984n], ); }); @@ -64,25 +64,25 @@ Deno.test("decodeBox", function () { assertThrows( () => decodeBox(testValue), Error, - `Invalid Box: "${testValue}". Box must have only 2 point, 1 given.` + `Invalid Box: "${testValue}". Box must have only 2 point, 1 given.`, ); testValue = "(12.4,2),(123,123,123),(9303,33)"; assertThrows( () => decodeBox(testValue), Error, - `Invalid Box: "${testValue}". Box must have only 2 point, 3 given.` + `Invalid Box: "${testValue}". Box must have only 2 point, 3 given.`, ); testValue = "(0,0),(123,123,123)"; assertThrows( () => decodeBox(testValue), Error, - `Invalid Box: "${testValue}" : Invalid Point: "(123,123,123)". Points must have only 2 coordinates, 3 given.` + `Invalid Box: "${testValue}" : Invalid Point: "(123,123,123)". Points must have only 2 coordinates, 3 given.`, ); testValue = "(0,0),(100,r100)"; assertThrows( () => decodeBox(testValue), Error, - `Invalid Box: "${testValue}" : Invalid Point: "(100,r100)". Coordinate "r100" must be a valid number.` + `Invalid Box: "${testValue}" : Invalid Point: "(100,r100)". Coordinate "r100" must be a valid number.`, ); }); @@ -95,13 +95,13 @@ Deno.test("decodeCircle", function () { assertThrows( () => decodeCircle(testValue), Error, - `Invalid Circle: "${testValue}" : Invalid Point: "(c21 23,2)". Coordinate "c21 23" must be a valid number.` + `Invalid Circle: "${testValue}" : Invalid Point: "(c21 23,2)". Coordinate "c21 23" must be a valid number.`, ); testValue = "<(33,2),mn23 3.5>"; assertThrows( () => decodeCircle(testValue), Error, - `Invalid Circle: "${testValue}". Circle radius "mn23 3.5" must be a valid number.` + `Invalid Circle: "${testValue}". Circle radius "mn23 3.5" must be a valid number.`, ); }); @@ -112,11 +112,11 @@ Deno.test("decodeDate", function () { Deno.test("decodeDatetime", function () { assertEquals( decodeDatetime("2021-08-01"), - new Date("2021-08-01 00:00:00-00") + new Date("2021-08-01 00:00:00-00"), ); assertEquals( decodeDatetime("1997-12-17 07:37:16-08"), - new Date("1997-12-17 07:37:16-08") + new Date("1997-12-17 07:37:16-08"), ); }); @@ -133,14 +133,14 @@ Deno.test("decodeInt", function () { Deno.test("decodeJson", function () { assertEquals( decodeJson( - '{"key_1": "MY VALUE", "key_2": null, "key_3": 10, "key_4": {"subkey_1": true, "subkey_2": ["1",2]}}' + '{"key_1": "MY VALUE", "key_2": null, "key_3": 10, "key_4": {"subkey_1": true, "subkey_2": ["1",2]}}', ), { key_1: "MY VALUE", key_2: null, key_3: 10, key_4: { subkey_1: true, subkey_2: ["1", 2] }, - } + }, ); assertThrows(() => decodeJson("{ 'eqw' ; ddd}")); }); @@ -151,13 +151,13 @@ Deno.test("decodeLine", function () { assertThrows( () => decodeLine("{100,50,0,100}"), Error, - `Invalid Line: "${testValue}". Line in linear equation format must have 3 constants, 4 given.` + `Invalid Line: "${testValue}". Line in linear equation format must have 3 constants, 4 given.`, ); testValue = "{100,d3km,0}"; assertThrows( () => decodeLine(testValue), Error, - `Invalid Line: "${testValue}". Line constant "d3km" must be a valid number.` + `Invalid Line: "${testValue}". Line constant "d3km" must be a valid number.`, ); }); @@ -170,25 +170,25 @@ Deno.test("decodeLineSegment", function () { assertThrows( () => decodeLineSegment(testValue), Error, - `Invalid Line Segment: "${testValue}" : Invalid Point: "(r344,350)". Coordinate "r344" must be a valid number.` + `Invalid Line Segment: "${testValue}" : Invalid Point: "(r344,350)". Coordinate "r344" must be a valid number.`, ); testValue = "((100),(r344,350))"; assertThrows( () => decodeLineSegment(testValue), Error, - `Invalid Line Segment: "${testValue}" : Invalid Point: "(100)". Points must have only 2 coordinates, 1 given.` + `Invalid Line Segment: "${testValue}" : Invalid Point: "(100)". Points must have only 2 coordinates, 1 given.`, ); testValue = "((100,50))"; assertThrows( () => decodeLineSegment(testValue), Error, - `Invalid Line Segment: "${testValue}". Line segments must have only 2 point, 1 given.` + `Invalid Line Segment: "${testValue}". Line segments must have only 2 point, 1 given.`, ); testValue = "((100,50),(350,350),(100,100))"; assertThrows( () => decodeLineSegment(testValue), Error, - `Invalid Line Segment: "${testValue}". Line segments must have only 2 point, 3 given.` + `Invalid Line Segment: "${testValue}". Line segments must have only 2 point, 3 given.`, ); }); @@ -206,13 +206,13 @@ Deno.test("decodePath", function () { assertThrows( () => decodePath(testValue), Error, - `Invalid Path: "${testValue}" : Invalid Point: "(350,kjf334)". Coordinate "kjf334" must be a valid number.` + `Invalid Path: "${testValue}" : Invalid Point: "(350,kjf334)". Coordinate "kjf334" must be a valid number.`, ); testValue = "((100,50,9949))"; assertThrows( () => decodePath(testValue), Error, - `Invalid Path: "${testValue}" : Invalid Point: "(100,50,9949)". Points must have only 2 coordinates, 3 given.` + `Invalid Path: "${testValue}" : Invalid Point: "(100,50,9949)". Points must have only 2 coordinates, 3 given.`, ); }); @@ -222,25 +222,25 @@ Deno.test("decodePoint", function () { assertThrows( () => decodePoint(testValue), Error, - `Invalid Point: "${testValue}". Points must have only 2 coordinates, 1 given.` + `Invalid Point: "${testValue}". Points must have only 2 coordinates, 1 given.`, ); testValue = "(100.100,50,350)"; assertThrows( () => decodePoint(testValue), Error, - `Invalid Point: "${testValue}". Points must have only 2 coordinates, 3 given.` + `Invalid Point: "${testValue}". Points must have only 2 coordinates, 3 given.`, ); testValue = "(1,r344)"; assertThrows( () => decodePoint(testValue), Error, - `Invalid Point: "${testValue}". Coordinate "r344" must be a valid number.` + `Invalid Point: "${testValue}". Coordinate "r344" must be a valid number.`, ); testValue = "(cd 213ee,100)"; assertThrows( () => decodePoint(testValue), Error, - `Invalid Point: "${testValue}". Coordinate "cd 213ee" must be a valid number.` + `Invalid Point: "${testValue}". Coordinate "cd 213ee" must be a valid number.`, ); }); @@ -316,12 +316,12 @@ Deno.test("decode_strategy", function () { // check 'auto' behavior assertEquals( decode(encodedValue, testValue.column, { decode_strategy: "auto" }), - testValue.parsed + testValue.parsed, ); // check 'string' behavior assertEquals( decode(encodedValue, testValue.column, { decode_strategy: "string" }), - testValue.value + testValue.value, ); } }); diff --git a/tests/query_client_test.ts b/tests/query_client_test.ts index 1d1b4648..0d4d7bf8 100644 --- a/tests/query_client_test.ts +++ b/tests/query_client_test.ts @@ -19,7 +19,7 @@ import { ClientOptions } from "../connection/connection_params.ts"; function withClient( t: (client: QueryClient) => void | Promise, - config?: ClientOptions + config?: ClientOptions, ) { async function clientWrapper() { const client = new Client(getMainConfiguration(config)); @@ -51,7 +51,7 @@ function withClient( function withClientGenerator( t: (getClient: () => Promise) => void | Promise, - pool_size = 10 + pool_size = 10, ) { async function clientWrapper() { const clients: Client[] = []; @@ -101,18 +101,18 @@ Deno.test( withClient(async (client) => { const result = await client.queryArray("SELECT UNNEST(ARRAY[1, 2])"); assertEquals(result.rows.length, 2); - }) + }), ); Deno.test( "Object query", withClient(async (client) => { const result = await client.queryObject( - "SELECT ARRAY[1, 2, 3] AS ID, 'DATA' AS TYPE" + "SELECT ARRAY[1, 2, 3] AS ID, 'DATA' AS TYPE", ); assertEquals(result.rows, [{ id: [1, 2, 3], type: "DATA" }]); - }) + }), ); Deno.test( @@ -120,7 +120,7 @@ Deno.test( withClient( async (client) => { const result = await client.queryObject( - `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` + `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`, ); assertEquals(result.rows, [ @@ -133,8 +133,8 @@ Deno.test( }, ]); }, - { controls: { decode_strategy: "auto" } } - ) + { controls: { decode_strategy: "auto" } }, + ), ); Deno.test( @@ -142,7 +142,7 @@ Deno.test( withClient( async (client) => { const result = await client.queryObject( - `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` + `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`, ); assertEquals(result.rows, [ @@ -155,8 +155,8 @@ Deno.test( }, ]); }, - { controls: { decode_strategy: "string" } } - ) + { controls: { decode_strategy: "string" } }, + ), ); Deno.test( @@ -191,7 +191,7 @@ Deno.test( }); assertEquals(result.rows, [{ id: value }]); } - }) + }), ); Deno.test( @@ -228,7 +228,7 @@ Deno.test( }); assertEquals(result.rows, [{ id: value }]); } - }) + }), ); Deno.test( @@ -237,16 +237,16 @@ Deno.test( const value = "some_value"; const { rows: res } = await client.queryArray( "SELECT $value, $VaLue, $VALUE", - { value } + { value }, ); assertEquals(res, [[value, value, value]]); await assertRejects( () => client.queryArray("SELECT $A", { a: 1, A: 2 }), Error, - "The arguments provided for the query must be unique (insensitive)" + "The arguments provided for the query must be unique (insensitive)", ); - }) + }), ); Deno.test( @@ -266,7 +266,7 @@ Deno.test( }); assertEquals(rows[0], { result: 1 }); - }) + }), ); Deno.test( @@ -275,7 +275,7 @@ Deno.test( await assertRejects( () => client.queryArray("SELECT 1; SELECT '2'::INT; SELECT 'A'::INT"), PostgresError, - "invalid input syntax for type integer" + "invalid input syntax for type integer", ); const { rows } = await client.queryObject<{ result: number }>({ @@ -284,7 +284,7 @@ Deno.test( }); assertEquals(rows[0], { result: 1 }); - }) + }), ); Deno.test( @@ -295,7 +295,7 @@ Deno.test( const value = "193"; const { rows: result_2 } = await client.queryObject`SELECT ${value} AS B`; assertEquals(result_2[0], { b: value }); - }) + }), ); Deno.test( @@ -307,7 +307,7 @@ Deno.test( }); assertEquals(result, [{ result: 1 }, { result: 2 }]); - }) + }), ); Deno.test( @@ -315,7 +315,7 @@ Deno.test( withClient(async (client) => { const { rows: result } = await client.queryArray(""); assertEquals(result, []); - }) + }), ); Deno.test( @@ -328,7 +328,7 @@ Deno.test( client.queryArray("INSERT INTO PREPARED_STATEMENT_ERROR VALUES ($1)", [ "TEXT", ]), - PostgresError + PostgresError, ); const result = "handled"; @@ -340,7 +340,7 @@ Deno.test( }); assertEquals(rows[0], { result }); - }) + }), ); Deno.test( @@ -351,7 +351,7 @@ Deno.test( const value = "z"; const { rows: result_2 } = await client.queryObject`SELECT ${value} AS B`; assertEquals(result_2[0], { b: value }); - }) + }), ); Deno.test( @@ -365,14 +365,14 @@ Deno.test( item_2, ]); assertEquals(result_1[0], [[item_1, item_2]]); - }) + }), ); Deno.test( "Handles parameter status messages on array query", withClient(async (client) => { - const { rows: result_1 } = - await client.queryArray`SET TIME ZONE 'HongKong'`; + const { rows: result_1 } = await client + .queryArray`SET TIME ZONE 'HongKong'`; assertEquals(result_1, []); @@ -382,7 +382,7 @@ Deno.test( }); assertEquals(result_2, [{ result: 1 }]); - }) + }), ); Deno.test( @@ -390,7 +390,8 @@ Deno.test( withClient(async (client) => { const result = 10; - await client.queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE(RES INTEGER) RETURNS INT AS $$ + await client + .queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE(RES INTEGER) RETURNS INT AS $$ BEGIN SET TIME ZONE 'HongKong'; END; @@ -402,10 +403,11 @@ Deno.test( result, ]), PostgresError, - "control reached end of function without RETURN" + "control reached end of function without RETURN", ); - await client.queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE(RES INTEGER) RETURNS INT AS $$ + await client + .queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE(RES INTEGER) RETURNS INT AS $$ BEGIN SET TIME ZONE 'HongKong'; RETURN RES; @@ -419,13 +421,14 @@ Deno.test( }); assertEquals(result_1, [{ result }]); - }) + }), ); Deno.test( "Handles parameter status after error", withClient(async (client) => { - await client.queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE() RETURNS INT AS $$ + await client + .queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CHANGE_TIMEZONE() RETURNS INT AS $$ BEGIN SET TIME ZONE 'HongKong'; END; @@ -434,9 +437,9 @@ Deno.test( await assertRejects( () => client.queryArray`SELECT * FROM PG_TEMP.CHANGE_TIMEZONE()`, PostgresError, - "control reached end of function without RETURN" + "control reached end of function without RETURN", ); - }) + }), ); Deno.test( @@ -449,9 +452,9 @@ Deno.test( await client.queryArray`SELECT 1`; }, Error, - "Connection to the database has been terminated" + "Connection to the database has been terminated", ); - }) + }), ); // This test depends on the assumption that all clients will default to @@ -462,7 +465,7 @@ Deno.test( await assertRejects( () => client.queryArray`SELECT PG_TERMINATE_BACKEND(${client.session.pid})`, - ConnectionError + ConnectionError, ); const { rows: result } = await client.queryObject<{ res: number }>({ @@ -472,21 +475,22 @@ Deno.test( assertEquals(result[0].res, 1); assertEquals(client.connected, true); - }) + }), ); Deno.test( "Handling of debug notices", withClient(async (client) => { // Create temporary function - await client.queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CREATE_NOTICE () RETURNS INT AS $$ BEGIN RAISE NOTICE 'NOTICED'; RETURN (SELECT 1); END; $$ LANGUAGE PLPGSQL;`; + await client + .queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.CREATE_NOTICE () RETURNS INT AS $$ BEGIN RAISE NOTICE 'NOTICED'; RETURN (SELECT 1); END; $$ LANGUAGE PLPGSQL;`; const { rows, warnings } = await client.queryArray( - "SELECT * FROM PG_TEMP.CREATE_NOTICE();" + "SELECT * FROM PG_TEMP.CREATE_NOTICE();", ); assertEquals(rows[0][0], 1); assertEquals(warnings[0].message, "NOTICED"); - }) + }), ); // This query doesn't recreate the table and outputs @@ -496,17 +500,18 @@ Deno.test( withClient(async (client) => { await client.queryArray("CREATE TEMP TABLE NOTICE_TEST (ABC INT);"); const { warnings } = await client.queryArray( - "CREATE TEMP TABLE IF NOT EXISTS NOTICE_TEST (ABC INT);" + "CREATE TEMP TABLE IF NOT EXISTS NOTICE_TEST (ABC INT);", ); assert(warnings[0].message.includes("already exists")); - }) + }), ); Deno.test( "Handling of messages between data fetching", withClient(async (client) => { - await client.queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.MESSAGE_BETWEEN_DATA(MESSAGE VARCHAR) RETURNS VARCHAR AS $$ + await client + .queryArray`CREATE OR REPLACE FUNCTION PG_TEMP.MESSAGE_BETWEEN_DATA(MESSAGE VARCHAR) RETURNS VARCHAR AS $$ BEGIN RAISE NOTICE '%', MESSAGE; RETURN MESSAGE; @@ -538,7 +543,7 @@ Deno.test( assertEquals(result[2], { result: message_3 }); assertObjectMatch(warnings[2], { message: message_3 }); - }) + }), ); Deno.test( @@ -552,14 +557,14 @@ Deno.test( const expectedDate = Date.UTC(2019, 1, 10, 6, 0, 40, 5); assertEquals(row[0].toUTCString(), new Date(expectedDate).toUTCString()); - }) + }), ); Deno.test( "Binary data is parsed correctly", withClient(async (client) => { - const { rows: result_1 } = - await client.queryArray`SELECT E'foo\\\\000\\\\200\\\\\\\\\\\\377'::BYTEA`; + const { rows: result_1 } = await client + .queryArray`SELECT E'foo\\\\000\\\\200\\\\\\\\\\\\377'::BYTEA`; const expectedBytes = new Uint8Array([102, 111, 111, 0, 128, 92, 255]); @@ -569,20 +574,21 @@ Deno.test( expectedBytes, ]); assertEquals(result_2[0][0], expectedBytes); - }) + }), ); Deno.test( "Result object metadata", withClient(async (client) => { await client.queryArray`CREATE TEMP TABLE METADATA (VALUE INTEGER)`; - await client.queryArray`INSERT INTO METADATA VALUES (100), (200), (300), (400), (500), (600)`; + await client + .queryArray`INSERT INTO METADATA VALUES (100), (200), (300), (400), (500), (600)`; let result; // simple select result = await client.queryArray( - "SELECT * FROM METADATA WHERE VALUE = 100" + "SELECT * FROM METADATA WHERE VALUE = 100", ); assertEquals(result.command, "SELECT"); assertEquals(result.rowCount, 1); @@ -590,14 +596,14 @@ Deno.test( // parameterized select result = await client.queryArray( "SELECT * FROM METADATA WHERE VALUE IN ($1, $2)", - [200, 300] + [200, 300], ); assertEquals(result.command, "SELECT"); assertEquals(result.rowCount, 2); // simple delete result = await client.queryArray( - "DELETE FROM METADATA WHERE VALUE IN (100, 200)" + "DELETE FROM METADATA WHERE VALUE IN (100, 200)", ); assertEquals(result.command, "DELETE"); assertEquals(result.rowCount, 2); @@ -621,7 +627,7 @@ Deno.test( // simple update result = await client.queryArray( - "UPDATE METADATA SET VALUE = 500 WHERE VALUE IN (500, 600)" + "UPDATE METADATA SET VALUE = 500 WHERE VALUE IN (500, 600)", ); assertEquals(result.command, "UPDATE"); assertEquals(result.rowCount, 2); @@ -629,11 +635,11 @@ Deno.test( // parameterized update result = await client.queryArray( "UPDATE METADATA SET VALUE = 400 WHERE VALUE = $1", - [400] + [400], ); assertEquals(result.command, "UPDATE"); assertEquals(result.rowCount, 1); - }) + }), ); Deno.test( @@ -648,7 +654,7 @@ Deno.test( ]); assert(warnings[0].message.includes("will be truncated")); - }) + }), ); Deno.test( @@ -661,7 +667,7 @@ Deno.test( >`SELECT ${value_1}, ${value_2}`; assertEquals(rows[0], [value_1, value_2]); - }) + }), ); Deno.test( @@ -680,7 +686,7 @@ Deno.test( }); assertEquals(result[0], record); - }) + }), ); Deno.test( @@ -699,7 +705,7 @@ Deno.test( }); assertEquals(result[0], record); - }) + }), ); Deno.test( @@ -711,7 +717,7 @@ Deno.test( }); assertEquals(result.rows, [{ ID: [1, 2, 3], type: "DATA" }]); - }) + }), ); Deno.test( @@ -727,7 +733,7 @@ Deno.test( }); assertEquals(result[0], record); - }) + }), ); Deno.test( @@ -740,9 +746,9 @@ Deno.test( fields: ["FIELD_1", "FIELD_1"], }), TypeError, - "The fields provided for the query must be unique" + "The fields provided for the query must be unique", ); - }) + }), ); Deno.test( @@ -751,7 +757,7 @@ Deno.test( await assertRejects( () => client.queryObject`SELECT 1 AS "a", 2 AS A`, Error, - `Field names "a" are duplicated in the result of the query` + `Field names "a" are duplicated in the result of the query`, ); await assertRejects( @@ -761,9 +767,9 @@ Deno.test( text: `SELECT 1 AS "fieldX", 2 AS field_x`, }), Error, - `Field names "fieldX" are duplicated in the result of the query` + `Field names "fieldX" are duplicated in the result of the query`, ); - }) + }), ); Deno.test( @@ -784,9 +790,9 @@ Deno.test( }); }, TypeError, - "The fields provided for the query must contain only letters and underscores" + "The fields provided for the query must contain only letters and underscores", ); - }) + }), ); Deno.test( @@ -800,7 +806,7 @@ Deno.test( }); }, TypeError, - "The fields provided for the query must contain only letters and underscores" + "The fields provided for the query must contain only letters and underscores", ); await assertRejects( @@ -811,7 +817,7 @@ Deno.test( }); }, TypeError, - "The fields provided for the query must contain only letters and underscores" + "The fields provided for the query must contain only letters and underscores", ); await assertRejects( @@ -822,9 +828,9 @@ Deno.test( }); }, TypeError, - "The fields provided for the query must contain only letters and underscores" + "The fields provided for the query must contain only letters and underscores", ); - }) + }), ); Deno.test( @@ -838,9 +844,9 @@ Deno.test( }); }, RangeError, - "The fields provided for the query don't match the ones returned as a result (1 expected, 2 received)" + "The fields provided for the query don't match the ones returned as a result (1 expected, 2 received)", ); - }) + }), ); Deno.test( @@ -853,9 +859,9 @@ Deno.test( fields: ["result"], }), RangeError, - "The result fields returned by the database don't match the defined structure of the result" + "The result fields returned by the database don't match the defined structure of the result", ); - }) + }), ); Deno.test( @@ -869,7 +875,7 @@ Deno.test( }>`SELECT ${value.x} AS x, ${value.y} AS y`; assertEquals(rows[0], value); - }) + }), ); Deno.test( @@ -879,9 +885,9 @@ Deno.test( // deno-lint-ignore ban-ts-comment // @ts-expect-error () => client.createTransaction(), - "Transaction name must be a non-empty string" + "Transaction name must be a non-empty string", ); - }) + }), ); Deno.test( @@ -894,7 +900,7 @@ Deno.test( assertEquals( client.session.current_transaction, transaction_name, - "Client is locked out during transaction" + "Client is locked out during transaction", ); await transaction.queryArray`CREATE TEMP TABLE TEST (X INTEGER)`; const savepoint = await transaction.savepoint("table_creation"); @@ -905,7 +911,7 @@ Deno.test( assertEquals( query_1.rows[0].x, 1, - "Operation was not executed inside transaction" + "Operation was not executed inside transaction", ); await transaction.rollback(savepoint); const query_2 = await transaction.queryObject<{ @@ -914,15 +920,15 @@ Deno.test( assertEquals( query_2.rowCount, 0, - "Rollback was not succesful inside transaction" + "Rollback was not succesful inside transaction", ); await transaction.commit(); assertEquals( client.session.current_transaction, null, - "Client was not released after transaction" + "Client was not released after transaction", ); - }) + }), ); Deno.test( @@ -934,8 +940,8 @@ Deno.test( const data = 1; { - const { rows: result } = - await transaction.queryArray`SELECT ${data}::INTEGER`; + const { rows: result } = await transaction + .queryArray`SELECT ${data}::INTEGER`; assertEquals(result[0], [data]); } { @@ -948,7 +954,7 @@ Deno.test( } await transaction.commit(); - }) + }), ); Deno.test( @@ -964,7 +970,7 @@ Deno.test( const transaction_rr = client_1.createTransaction( "transactionIsolationLevelRepeatableRead", - { isolation_level: "repeatable_read" } + { isolation_level: "repeatable_read" }, ); await transaction_rr.begin(); @@ -987,7 +993,7 @@ Deno.test( assertEquals( query_2, [{ x: 1 }], - "Repeatable read transaction should not be able to observe changes that happened after the transaction start" + "Repeatable read transaction should not be able to observe changes that happened after the transaction start", ); await transaction_rr.commit(); @@ -998,11 +1004,11 @@ Deno.test( assertEquals( query_3, [{ x: 2 }], - "Main session should be able to observe changes after transaction ended" + "Main session should be able to observe changes after transaction ended", ); await client_1.queryArray`DROP TABLE FOR_TRANSACTION_TEST`; - }) + }), ); Deno.test( @@ -1018,7 +1024,7 @@ Deno.test( const transaction_rr = client_1.createTransaction( "transactionIsolationLevelRepeatableRead", - { isolation_level: "serializable" } + { isolation_level: "serializable" }, ); await transaction_rr.begin(); @@ -1034,7 +1040,7 @@ Deno.test( () => transaction_rr.queryArray`UPDATE FOR_TRANSACTION_TEST SET X = 3`, TransactionError, undefined, - "A serializable transaction should throw if the data read in the transaction has been modified externally" + "A serializable transaction should throw if the data read in the transaction has been modified externally", ); const { rows: query_3 } = await client_1.queryObject<{ @@ -1043,11 +1049,11 @@ Deno.test( assertEquals( query_3, [{ x: 2 }], - "Main session should be able to observe changes after transaction ended" + "Main session should be able to observe changes after transaction ended", ); await client_1.queryArray`DROP TABLE FOR_TRANSACTION_TEST`; - }) + }), ); Deno.test( @@ -1064,11 +1070,11 @@ Deno.test( () => transaction.queryArray`DELETE FROM FOR_TRANSACTION_TEST`, TransactionError, undefined, - "DELETE shouldn't be able to be used in a read-only transaction" + "DELETE shouldn't be able to be used in a read-only transaction", ); await client.queryArray`DROP TABLE FOR_TRANSACTION_TEST`; - }) + }), ); Deno.test( @@ -1099,7 +1105,7 @@ Deno.test( assertEquals( query_1, [{ x: 1 }], - "External changes shouldn't affect repeatable read transaction" + "External changes shouldn't affect repeatable read transaction", ); const snapshot = await transaction_1.getSnapshot(); @@ -1116,14 +1122,14 @@ Deno.test( assertEquals( query_2, [{ x: 1 }], - "External changes shouldn't affect repeatable read transaction with previous snapshot" + "External changes shouldn't affect repeatable read transaction with previous snapshot", ); await transaction_1.commit(); await transaction_2.commit(); await client_1.queryArray`DROP TABLE FOR_TRANSACTION_TEST`; - }) + }), ); Deno.test( @@ -1138,7 +1144,7 @@ Deno.test( () => client.queryArray`SELECT 1`, Error, `This connection is currently locked by the "${name}" transaction`, - "The connection is not being locked by the transaction" + "The connection is not being locked by the transaction", ); await transaction.commit(); @@ -1146,9 +1152,9 @@ Deno.test( assertEquals( client.session.current_transaction, null, - "Client was not released after transaction" + "Client was not released after transaction", ); - }) + }), ); Deno.test( @@ -1163,16 +1169,16 @@ Deno.test( assertEquals( client.session.current_transaction, name, - "Client shouldn't have been released on chained commit" + "Client shouldn't have been released on chained commit", ); await transaction.commit(); assertEquals( client.session.current_transaction, null, - "Client was not released after transaction ended" + "Client was not released after transaction ended", ); - }) + }), ); Deno.test( @@ -1195,7 +1201,7 @@ Deno.test( assertEquals( client.session.current_transaction, name, - "Client shouldn't have been released after chained rollback" + "Client shouldn't have been released after chained rollback", ); await transaction.rollback(); @@ -1208,16 +1214,16 @@ Deno.test( assertEquals( client.session.current_transaction, null, - "Client was not released after rollback" + "Client was not released after rollback", ); - }) + }), ); Deno.test( "Transaction rollback validations", withClient(async (client) => { const transaction = client.createTransaction( - "transactionRollbackValidations" + "transactionRollbackValidations", ); await transaction.begin(); @@ -1225,11 +1231,11 @@ Deno.test( // @ts-ignore This is made to check the two properties aren't passed at once () => transaction.rollback({ savepoint: "unexistent", chain: true }), Error, - "The chain option can't be used alongside a savepoint on a rollback operation" + "The chain option can't be used alongside a savepoint on a rollback operation", ); await transaction.commit(); - }) + }), ); Deno.test( @@ -1242,7 +1248,7 @@ Deno.test( await assertRejects( () => transaction.queryArray`SELECT []`, TransactionError, - `The transaction "${name}" has been aborted` + `The transaction "${name}" has been aborted`, ); assertEquals(client.session.current_transaction, null); @@ -1250,10 +1256,10 @@ Deno.test( await assertRejects( () => transaction.queryObject`SELECT []`, TransactionError, - `The transaction "${name}" has been aborted` + `The transaction "${name}" has been aborted`, ); assertEquals(client.session.current_transaction, null); - }) + }), ); Deno.test( @@ -1295,13 +1301,13 @@ Deno.test( assertEquals( savepoint.instances, 2, - "An incorrect number of instances were created for a transaction savepoint" + "An incorrect number of instances were created for a transaction savepoint", ); await savepoint.release(); assertEquals( savepoint.instances, 1, - "The instance for the savepoint was not released" + "The instance for the savepoint was not released", ); // This checks that the savepoint can be called by name as well @@ -1312,7 +1318,7 @@ Deno.test( assertEquals(query_5, [{ y: 1 }]); await transaction.commit(); - }) + }), ); Deno.test( @@ -1324,22 +1330,22 @@ Deno.test( await assertRejects( () => transaction.savepoint("1"), Error, - "The savepoint name can't begin with a number" + "The savepoint name can't begin with a number", ); await assertRejects( () => transaction.savepoint( - "this_savepoint_is_going_to_be_longer_than_sixty_three_characters" + "this_savepoint_is_going_to_be_longer_than_sixty_three_characters", ), Error, - "The savepoint name can't be longer than 63 characters" + "The savepoint name can't be longer than 63 characters", ); await assertRejects( () => transaction.savepoint("+"), Error, - "The savepoint name can only contain alphanumeric characters" + "The savepoint name can only contain alphanumeric characters", ); const savepoint = await transaction.savepoint("ABC1"); @@ -1348,7 +1354,7 @@ Deno.test( assertEquals( savepoint, await transaction.savepoint("abc1"), - "Creating a savepoint with the same name should return the original one" + "Creating a savepoint with the same name should return the original one", ); await savepoint.release(); @@ -1357,23 +1363,23 @@ Deno.test( await assertRejects( () => savepoint.release(), Error, - "This savepoint has no instances to release" + "This savepoint has no instances to release", ); await assertRejects( () => transaction.rollback(savepoint), Error, - `There are no savepoints of "abc1" left to rollback to` + `There are no savepoints of "abc1" left to rollback to`, ); await assertRejects( () => transaction.rollback("UNEXISTENT"), Error, - `There is no "unexistent" savepoint registered in this transaction` + `There is no "unexistent" savepoint registered in this transaction`, ); await transaction.commit(); - }) + }), ); Deno.test( @@ -1388,7 +1394,7 @@ Deno.test( await assertRejects( () => transaction_y.begin(), Error, - `This client already has an ongoing transaction "x"` + `This client already has an ongoing transaction "x"`, ); await transaction_x.commit(); @@ -1396,44 +1402,44 @@ Deno.test( await assertRejects( () => transaction_y.begin(), Error, - "This transaction is already open" + "This transaction is already open", ); await transaction_y.commit(); await assertRejects( () => transaction_y.commit(), Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so` + `This transaction has not been started yet, make sure to use the "begin" method to do so`, ); await assertRejects( () => transaction_y.commit(), Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so` + `This transaction has not been started yet, make sure to use the "begin" method to do so`, ); await assertRejects( () => transaction_y.queryArray`SELECT 1`, Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so` + `This transaction has not been started yet, make sure to use the "begin" method to do so`, ); await assertRejects( () => transaction_y.queryObject`SELECT 1`, Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so` + `This transaction has not been started yet, make sure to use the "begin" method to do so`, ); await assertRejects( () => transaction_y.rollback(), Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so` + `This transaction has not been started yet, make sure to use the "begin" method to do so`, ); await assertRejects( () => transaction_y.savepoint("SOME"), Error, - `This transaction has not been started yet, make sure to use the "begin" method to do so` + `This transaction has not been started yet, make sure to use the "begin" method to do so`, ); - }) + }), ); From 3230a8e12e919dd2fd53786b4e431cfe4a772d22 Mon Sep 17 00:00:00 2001 From: bombillazo Date: Thu, 8 Feb 2024 12:05:35 -0400 Subject: [PATCH 4/5] chore: fix lint issue of unused import --- tests/query_client_test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/query_client_test.ts b/tests/query_client_test.ts index 0d4d7bf8..65a81215 100644 --- a/tests/query_client_test.ts +++ b/tests/query_client_test.ts @@ -14,7 +14,6 @@ import { } from "./test_deps.ts"; import { getMainConfiguration } from "./config.ts"; import { PoolClient, QueryClient } from "../client.ts"; -import { ClientConfiguration } from "../connection/connection_params.ts"; import { ClientOptions } from "../connection/connection_params.ts"; function withClient( From 0c2ad267e49847029c4fd0cf4369fcd0ac36d621 Mon Sep 17 00:00:00 2001 From: bombillazo Date: Sun, 11 Feb 2024 18:23:14 -0400 Subject: [PATCH 5/5] chore: fix variable anem to make camelcase --- connection/connection_params.ts | 2 +- query/decode.ts | 2 +- tests/decode_test.ts | 6 +++--- tests/query_client_test.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/connection/connection_params.ts b/connection/connection_params.ts index deb4dd05..bf006a21 100644 --- a/connection/connection_params.ts +++ b/connection/connection_params.ts @@ -107,7 +107,7 @@ export type ClientControls = { * - `strict` : deno-postgres parses the data into JS objects, and if a parser is not implemented, it throws an error * - `raw` : the data is returned as Uint8Array */ - decode_strategy?: "string" | "auto"; + decodeStrategy?: "string" | "auto"; }; /** The Client database connection options */ diff --git a/query/decode.ts b/query/decode.ts index dc6e517c..1afe82a0 100644 --- a/query/decode.ts +++ b/query/decode.ts @@ -218,7 +218,7 @@ export function decode( return decodeBinary(); } else if (column.format === Format.TEXT) { // If the user has specified a decode strategy, use that - if (controls?.decode_strategy === "string") { + if (controls?.decodeStrategy === "string") { return decoder.decode(value); } // default to 'auto' mode, which uses the typeOid to determine the decoding strategy diff --git a/tests/decode_test.ts b/tests/decode_test.ts index 438e5d84..06512911 100644 --- a/tests/decode_test.ts +++ b/tests/decode_test.ts @@ -251,7 +251,7 @@ Deno.test("decodeTid", function () { ]); }); -Deno.test("decode_strategy", function () { +Deno.test("decode strategy", function () { const testValues = [ { value: "40", @@ -315,12 +315,12 @@ Deno.test("decode_strategy", function () { assertEquals(decode(encodedValue, testValue.column), testValue.parsed); // check 'auto' behavior assertEquals( - decode(encodedValue, testValue.column, { decode_strategy: "auto" }), + decode(encodedValue, testValue.column, { decodeStrategy: "auto" }), testValue.parsed, ); // check 'string' behavior assertEquals( - decode(encodedValue, testValue.column, { decode_strategy: "string" }), + decode(encodedValue, testValue.column, { decodeStrategy: "string" }), testValue.value, ); } diff --git a/tests/query_client_test.ts b/tests/query_client_test.ts index 65a81215..4c4217bf 100644 --- a/tests/query_client_test.ts +++ b/tests/query_client_test.ts @@ -132,7 +132,7 @@ Deno.test( }, ]); }, - { controls: { decode_strategy: "auto" } }, + { controls: { decodeStrategy: "auto" } }, ), ); @@ -154,7 +154,7 @@ Deno.test( }, ]); }, - { controls: { decode_strategy: "string" } }, + { controls: { decodeStrategy: "string" } }, ), );