Skip to content

Commit

Permalink
feat: Named parameters (#374)
Browse files Browse the repository at this point in the history
  • Loading branch information
Soremwar committed Jan 14, 2022
1 parent 2e88d1e commit 1cb7563
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 161 deletions.
38 changes: 23 additions & 15 deletions client.ts
Expand Up @@ -9,9 +9,9 @@ import {
Query,
QueryArguments,
QueryArrayResult,
QueryConfig,
QueryObjectConfig,
QueryObjectOptions,
QueryObjectResult,
QueryOptions,
QueryResult,
ResultType,
templateStringToQuery,
Expand Down Expand Up @@ -282,18 +282,18 @@ export abstract class QueryClient {
*/
queryArray<T extends Array<unknown>>(
query: string,
...args: QueryArguments
args?: QueryArguments,
): Promise<QueryArrayResult<T>>;
queryArray<T extends Array<unknown>>(
config: QueryConfig,
config: QueryOptions,
): Promise<QueryArrayResult<T>>;
queryArray<T extends Array<unknown>>(
strings: TemplateStringsArray,
...args: QueryArguments
...args: unknown[]
): Promise<QueryArrayResult<T>>;
queryArray<T extends Array<unknown> = Array<unknown>>(
query_template_or_config: TemplateStringsArray | string | QueryConfig,
...args: QueryArguments
query_template_or_config: TemplateStringsArray | string | QueryOptions,
...args: unknown[] | [QueryArguments | undefined]
): Promise<QueryArrayResult<T>> {
this.#assertOpenConnection();

Expand All @@ -305,7 +305,11 @@ export abstract class QueryClient {

let query: Query<ResultType.ARRAY>;
if (typeof query_template_or_config === "string") {
query = new Query(query_template_or_config, ResultType.ARRAY, ...args);
query = new Query(
query_template_or_config,
ResultType.ARRAY,
args[0] as QueryArguments | undefined,
);
} else if (isTemplateString(query_template_or_config)) {
query = templateStringToQuery(
query_template_or_config,
Expand Down Expand Up @@ -380,23 +384,23 @@ export abstract class QueryClient {
*/
queryObject<T>(
query: string,
...args: QueryArguments
args?: QueryArguments,
): Promise<QueryObjectResult<T>>;
queryObject<T>(
config: QueryObjectConfig,
config: QueryObjectOptions,
): Promise<QueryObjectResult<T>>;
queryObject<T>(
query: TemplateStringsArray,
...args: QueryArguments
...args: unknown[]
): Promise<QueryObjectResult<T>>;
queryObject<
T = Record<string, unknown>,
>(
query_template_or_config:
| string
| QueryObjectConfig
| QueryObjectOptions
| TemplateStringsArray,
...args: QueryArguments
...args: unknown[] | [QueryArguments | undefined]
): Promise<QueryObjectResult<T>> {
this.#assertOpenConnection();

Expand All @@ -408,7 +412,11 @@ export abstract class QueryClient {

let query: Query<ResultType.OBJECT>;
if (typeof query_template_or_config === "string") {
query = new Query(query_template_or_config, ResultType.OBJECT, ...args);
query = new Query(
query_template_or_config,
ResultType.OBJECT,
args[0] as QueryArguments | undefined,
);
} else if (isTemplateString(query_template_or_config)) {
query = templateStringToQuery(
query_template_or_config,
Expand All @@ -417,7 +425,7 @@ export abstract class QueryClient {
);
} else {
query = new Query(
query_template_or_config as QueryObjectConfig,
query_template_or_config as QueryObjectOptions,
ResultType.OBJECT,
);
}
Expand Down
70 changes: 58 additions & 12 deletions docs/README.md
Expand Up @@ -496,14 +496,12 @@ async function runQuery(query: string) {
return result;
}

await runQuery("SELECT ID, NAME FROM users"); // [{id: 1, name: 'Carlos'}, {id: 2, name: 'John'}, ...]
await runQuery("SELECT ID, NAME FROM users WHERE id = '1'"); // [{id: 1, name: 'Carlos'}, {id: 2, name: 'John'}, ...]
await runQuery("SELECT ID, NAME FROM USERS"); // [{id: 1, name: 'Carlos'}, {id: 2, name: 'John'}, ...]
await runQuery("SELECT ID, NAME FROM USERS WHERE ID = '1'"); // [{id: 1, name: 'Carlos'}, {id: 2, name: 'John'}, ...]
```

## Executing queries

### Executing simple queries

Executing a query is as simple as providing the raw SQL to your client, it will
automatically be queued, validated and processed so you can get a human
readable, blazing fast result
Expand All @@ -513,36 +511,84 @@ const result = await client.queryArray("SELECT ID, NAME FROM PEOPLE");
console.log(result.rows); // [[1, "Laura"], [2, "Jason"]]
```

### Executing prepared statements
### Prepared statements and query arguments

Prepared statements are a Postgres mechanism designed to prevent SQL injection
and maximize query performance for multiple queries (see
https://security.stackexchange.com/questions/15214/are-prepared-statements-100-safe-against-sql-injection).
https://security.stackexchange.com/questions/15214/are-prepared-statements-100-safe-against-sql-injection)

The idea is simple, provide a base sql statement with placeholders for any
variables required, and then provide said variables as arguments for the query
call
variables required, and then provide said variables in an array of arguments

```ts
// Example using the simplified argument interface
{
const result = await client.queryArray(
"SELECT ID, NAME FROM PEOPLE WHERE AGE > $1 AND AGE < $2",
10,
20,
[10, 20],
);
console.log(result.rows);
}

// Example using the advanced query interface
{
const result = await client.queryArray({
text: "SELECT ID, NAME FROM PEOPLE WHERE AGE > $1 AND AGE < $2",
args: [10, 20],
text: "SELECT ID, NAME FROM PEOPLE WHERE AGE > $1 AND AGE < $2",
});
console.log(result.rows);
}
```

#### Named arguments

Alternatively, you can provide such placeholders in the form of variables to be
replaced at runtime with an argument object

```ts
{
const result = await client.queryArray(
"SELECT ID, NAME FROM PEOPLE WHERE AGE > $MIN AND AGE < $MAX",
{ min: 10, max: 20 },
);
console.log(result.rows);
}

{
const result = await client.queryArray({
args: { min: 10, max: 20 },
text: "SELECT ID, NAME FROM PEOPLE WHERE AGE > $MIN AND AGE < $MAX",
});
console.log(result.rows);
}
```

Behind the scenes, `deno-postgres` will replace the variables names in your
query for Postgres-readable placeholders making it easy to reuse values in
multiple places in your query

```ts
{
const result = await client.queryArray(
`SELECT
ID,
NAME||LASTNAME
FROM PEOPLE
WHERE NAME ILIKE $SEARCH
OR LASTNAME ILIKE $SEARCH`,
{ search: "JACKSON" },
);
console.log(result.rows);
}
```

The placeholders in the query will be looked up in the argument object without
taking case into account, so having a variable named `$Value` and an object
argument like `{value: 1}` will still match the values together

**Note**: This feature has a little overhead when compared to the array of
arguments, since it needs to transform the SQL and validate the structure of the
arguments object

#### Template strings

Even thought the previous call is already pretty simple, it can be simplified
Expand Down
2 changes: 1 addition & 1 deletion mod.ts
Expand Up @@ -17,6 +17,6 @@ export type {
} from "./connection/connection_params.ts";
export type { Session } from "./client.ts";
export { PoolClient, QueryClient } from "./client.ts";
export type { QueryConfig, QueryObjectConfig } from "./query/query.ts";
export type { QueryObjectOptions, QueryOptions } from "./query/query.ts";
export { Savepoint, Transaction } from "./query/transaction.ts";
export type { TransactionOptions } from "./query/transaction.ts";
4 changes: 2 additions & 2 deletions query/encode.ts
Expand Up @@ -63,7 +63,7 @@ function encodeArray(array: Array<unknown>): string {
// TODO: it should be encoded as bytea?
throw new Error("Can't encode array of buffers.");
} else {
const encodedElement = encode(element);
const encodedElement = encodeArgument(element);
encodedArray += escapeArrayElement(encodedElement as string);
}
});
Expand All @@ -81,7 +81,7 @@ function encodeBytes(value: Uint8Array): string {

export type EncodedArg = null | string | Uint8Array;

export function encode(value: unknown): EncodedArg {
export function encodeArgument(value: unknown): EncodedArg {
if (value === null || typeof value === "undefined") {
return null;
} else if (value instanceof Uint8Array) {
Expand Down

0 comments on commit 1cb7563

Please sign in to comment.