Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: gajus/slonik
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v31.2.5
Choose a base ref
...
head repository: gajus/slonik
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v31.2.6
Choose a head ref
  • 5 commits
  • 9 files changed
  • 1 contributor

Commits on Oct 5, 2022

  1. Copy the full SHA
    6764e80 View commit details
  2. Copy the full SHA
    76ef6cb View commit details

Commits on Oct 6, 2022

  1. Copy the full SHA
    3bfdb80 View commit details

Commits on Oct 10, 2022

  1. Copy the full SHA
    7af8a1e View commit details
  2. fix: update roarr

    gajus committed Oct 10, 2022
    Copy the full SHA
    fcfa0fe View commit details
Showing with 83 additions and 26 deletions.
  1. +9 −7 .README/RECIPES.md
  2. +9 −7 README.md
  3. +7 −7 package-lock.json
  4. +1 −1 package.json
  5. +3 −2 src/routines/executeQuery.ts
  6. +2 −2 src/types.ts
  7. +3 −0 src/utilities/index.ts
  8. +7 −0 src/utilities/sanitizeObject.ts
  9. +42 −0 test/helpers/createIntegrationTests.ts
16 changes: 9 additions & 7 deletions .README/RECIPES.md
Original file line number Diff line number Diff line change
@@ -63,11 +63,13 @@ Note: How you determine which queries are safe to route to a read-only instance

Note: `beforePoolConnection` only works for connections initiated by a query, i.e. `pool#query` and not `pool#connect()`.

Note: `pool#transaction` triggers `beforePoolConnection` but has no `query`.

Note: This particular implementation does not handle [`SELECT INTO`](https://www.postgresql.org/docs/current/sql-selectinto.html).

```ts
const slavePool = await createPool('postgres://slave');
const masterPool = await createPool('postgres://master', {
const readOnlyPool = await createPool('postgres://read-only');
const pool = await createPool('postgres://main', {
interceptors: [
{
beforePoolConnection: (connectionContext) => {
@@ -88,17 +90,17 @@ const masterPool = await createPool('postgres://master', {

// Returning an instance of DatabasePool will attempt to run the query using the other connection pool.
// Note that all other interceptors of the pool that the query originated from are short-circuited.
return slavePool;
return readOnlyPool;
}
}
]
});

// This query will use `postgres://slave` connection.
masterPool.query(sql`SELECT 1`);
// This query will use `postgres://read-only` connection.
pool.query(sql`SELECT 1`);

// This query will use `postgres://master` connection.
masterPool.query(sql`UPDATE 1`);
// This query will use `postgres://main` connection.
pool.query(sql`UPDATE 1`);
```

### Building Utility Statements
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1170,11 +1170,13 @@ Note: How you determine which queries are safe to route to a read-only instance

Note: `beforePoolConnection` only works for connections initiated by a query, i.e. `pool#query` and not `pool#connect()`.

Note: `pool#transaction` triggers `beforePoolConnection` but has no `query`.

Note: This particular implementation does not handle [`SELECT INTO`](https://www.postgresql.org/docs/current/sql-selectinto.html).

```ts
const slavePool = await createPool('postgres://slave');
const masterPool = await createPool('postgres://master', {
const readOnlyPool = await createPool('postgres://read-only');
const pool = await createPool('postgres://main', {
interceptors: [
{
beforePoolConnection: (connectionContext) => {
@@ -1195,17 +1197,17 @@ const masterPool = await createPool('postgres://master', {

// Returning an instance of DatabasePool will attempt to run the query using the other connection pool.
// Note that all other interceptors of the pool that the query originated from are short-circuited.
return slavePool;
return readOnlyPool;
}
}
]
});

// This query will use `postgres://slave` connection.
masterPool.query(sql`SELECT 1`);
// This query will use `postgres://read-only` connection.
pool.query(sql`SELECT 1`);

// This query will use `postgres://master` connection.
masterPool.query(sql`UPDATE 1`);
// This query will use `postgres://main` connection.
pool.query(sql`UPDATE 1`);
```

<a name="user-content-slonik-recipes-building-utility-statements"></a>
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
"pg-types": "^4.0.0",
"postgres-array": "^3.0.1",
"postgres-interval": "^4.0.0",
"roarr": "^7.12.3",
"roarr": "^7.13.0",
"serialize-error": "^8.0.0",
"through2": "^4.0.2",
"zod": "^3.19.1"
5 changes: 3 additions & 2 deletions src/routines/executeQuery.ts
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@ import {
} from '../types';
import {
createQueryId,
sanitizeObject,
} from '../utilities';

type GenericQueryResult = QueryResult<QueryResultRow>;
@@ -76,13 +77,13 @@ const createParseInterceptor = (parser: ZodTypeAny): Interceptor => {
if (!validationResult.success) {
log.error({
error: serializeError(validationResult.error),
row: JSON.parse(JSON.stringify(row)),
row: sanitizeObject(row),
sql: actualQuery.sql,
}, 'row failed validation');

throw new SchemaValidationError(
actualQuery.sql,
JSON.parse(JSON.stringify(row)),
sanitizeObject(row),
validationResult.error.issues,
);
}
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -57,8 +57,8 @@ export type TypeNameIdentifier =
| 'uuid';

export type SerializableValue =
boolean | number | string | readonly SerializableValue[] | {
[key: string]: SerializableValue | undefined,
SerializableValue[] | boolean | number | string | {
[key: string]: SerializableValue,
} | null;

export type QueryId = string;
3 changes: 3 additions & 0 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,9 @@ export {
export {
escapeLiteralValue,
} from './escapeLiteralValue';
export {
sanitizeObject,
} from './sanitizeObject';
export {
isPrimitiveValueExpression,
} from './isPrimitiveValueExpression';
7 changes: 7 additions & 0 deletions src/utilities/sanitizeObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {
type SerializableValue,
} from '../types';

export const sanitizeObject = (object: Record<string, unknown>): SerializableValue => {
return JSON.parse(JSON.stringify(object));
};
42 changes: 42 additions & 0 deletions test/helpers/createIntegrationTests.ts
Original file line number Diff line number Diff line change
@@ -101,6 +101,7 @@ export const createTestRunner = (PgPool: new (poolConfig: PoolConfig) => PgPoolT
CREATE TABLE person (
id SERIAL PRIMARY KEY,
name text,
tags text[],
birth_date date,
payload bytea
)
@@ -147,6 +148,47 @@ export const createIntegrationTests = (
test: TestFn<TestContextType>,
PgPool: new () => PgPoolType,
) => {
test('inserting records using jsonb_to_recordset', async (t) => {
const pool = await createPool(t.context.dsn, {
PgPool,
});

const persons = await pool.any(sql`
INSERT INTO person
(
name,
tags
)
SELECT *
FROM jsonb_to_recordset(${sql.jsonb([
{
name: 'foo',
tags: [
'a',
'b',
'c',
],
},
])}) AS t(name text, tags text[])
RETURNING
name,
tags
`);

t.deepEqual(persons, [
{
name: 'foo',
tags: [
'a',
'b',
'c',
],
},
]);

await pool.end();
});

test('re-routes query to a different pool', async (t) => {
const readOnlyBeforeTransformQuery = sinon.stub().resolves(null);
const beforeTransformQuery = sinon.stub().throws();