From 1edfcbf0ab1a6f365b7d20e7dcd659ba332ee288 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 12 Sep 2024 18:15:44 -0400 Subject: [PATCH] don't let pg-promise type parsers override each other --- apps/docs/pages/packages/admin.md | 1 + apps/docs/pages/packages/client.md | 349 +++++++++++++----- apps/docs/pages/packages/typegen.md | 2 +- packages/client/contributing.md | 1 + packages/client/package.json | 3 +- packages/client/readme.md | 241 ++++++++---- packages/client/src/client.ts | 46 ++- packages/client/src/type-parsers.ts | 34 +- packages/client/src/types.ts | 25 +- packages/client/test/api-usage.test.ts | 73 ++-- packages/client/test/errors.test.ts | 98 ++--- packages/client/test/pg-promise-usage.test.ts | 20 + packages/client/test/recipes.test.ts | 52 +-- packages/client/test/snapshots.ts | 27 +- packages/migrator/test/pool-helper.ts | 17 +- packages/typegen/test/helper.ts | 7 +- pnpm-lock.yaml | 35 +- 17 files changed, 709 insertions(+), 322 deletions(-) create mode 100644 packages/client/contributing.md create mode 100644 packages/client/test/pg-promise-usage.test.ts diff --git a/apps/docs/pages/packages/admin.md b/apps/docs/pages/packages/admin.md index 3c559161..235801ff 100644 --- a/apps/docs/pages/packages/admin.md +++ b/apps/docs/pages/packages/admin.md @@ -1,6 +1,7 @@ # @pgkit/admin + A no-config admin UI for running queries against PostgreSQL database, with autocomplete for tables, columns, views, functions etc. ![demo](/gifs/demo.gif) diff --git a/apps/docs/pages/packages/client.md b/apps/docs/pages/packages/client.md index da222e9c..9773900b 100644 --- a/apps/docs/pages/packages/client.md +++ b/apps/docs/pages/packages/client.md @@ -65,6 +65,7 @@ Note that @pgkit/migra and @pgkit/schemainspect are pure ports of their Python e - [sql.literalValue](#sqlliteralvalue) - [transaction savepoints](#transaction-savepoints) - [sql.type](#sqltype) + - [sql.type with custom error message](#sqltype-with-custom-error-message) - [createSqlTag + sql.typeAlias](#createsqltag--sqltypealias) - [Types](#types) - [Automatic type generation](#automatic-type-generation) @@ -469,7 +470,7 @@ expect(newRecords).toEqual([{id: 10, name: 'ten'}]) ```typescript const StringId = z.object({id: z.string()}) -await expect(client.any(sql.type(StringId)`select text(id) id from usage_test`)).resolves.toMatchObject([ +await expect(client.any(sql.type(StringId)`select id::text from usage_test`)).resolves.toMatchObject([ {id: '1'}, {id: '2'}, {id: '3'}, @@ -490,6 +491,63 @@ expect(error.cause).toMatchInlineSnapshot(` "message": "Expected string, received number" } ]], + "message": "[ + { + "code": "invalid_type", + "expected": "string", + "received": "number", + "path": [ + "id" + ], + "message": "Expected string, received number" + } + ]", + "name": "QueryErrorCause", + "query": { + "name": "select-usage_test_8729cac", + "parse": [Function], + "sql": "select id from usage_test", + "templateArgs": [Function], + "token": "sql", + "values": [], + }, + } +`) +``` + +### sql.type with custom error message + +Wrap the query function to customize the error message + +```typescript +client = createClient(client.connectionString(), { + ...client.options, + wrapQueryFn: query => { + const parentWrapper = client.options.wrapQueryFn || (x => x) + return async (...args) => { + const parentQueryFn = parentWrapper(query) + try { + return await parentQueryFn(...args) + } catch (e) { + if (e instanceof QueryError && e.message.endsWith('Parsing rows failed')) { + throw new QueryError(e.message, { + cause: {query: e.cause.query, error: fromError(e.cause.error)}, + }) + } + throw e + } + } + }, +}) +const StringId = z.object({id: z.string()}) + +const error = await client.any(sql.type(StringId)`select id from usage_test`).catch(e => e) + +expect(error.cause).toMatchInlineSnapshot(` + { + "error": [ZodValidationError: Validation error: Expected string, received number at "id"], + "message": "Validation error: Expected string, received number at "id"", + "name": "QueryErrorCause", "query": { "name": "select-usage_test_8729cac", "parse": [Function], @@ -522,17 +580,9 @@ expect(result).toEqual({name: 'Bob'}) const err = await client.any(sql.typeAlias('Profile')`select 123 as name`).catch(e => e) expect(err.cause).toMatchInlineSnapshot(` { - "error": [ZodError: [ - { - "code": "invalid_type", - "expected": "string", - "received": "number", - "path": [ - "name" - ], - "message": "Expected string, received number" - } - ]], + "error": [ZodValidationError: Validation error: Expected string, received number at "name"], + "message": "Validation error: Expected string, received number at "name"", + "name": "QueryErrorCause", "query": { "name": "select_245d49b", "parse": [Function], @@ -651,6 +701,7 @@ Transform rows: ```typescript const Row = z.object({ id: z.number(), + label: z.string().nullable(), location: z .string() .regex(/^-?\d+,-?\d+$/) @@ -664,10 +715,19 @@ const result = await client.any(sql.type(Row)` select * from zod_test `) +expectTypeOf(result).toEqualTypeOf<{id: number; label: string | null; location: {lat: number; lon: number}}[]>() + +const result2 = await client.any(sql.type(Row)` + select * from ${sql.identifier(['zod_test'])} +`) + +expect(result2).toEqual(result) + expect(result).toMatchInlineSnapshot(` [ { "id": 1, + "label": "a", "location": { "lat": 70, "lon": -108 @@ -675,6 +735,7 @@ expect(result).toMatchInlineSnapshot(` }, { "id": 2, + "label": "b", "location": { "lat": 71, "lon": -102 @@ -682,6 +743,7 @@ expect(result).toMatchInlineSnapshot(` }, { "id": 3, + "label": null, "location": { "lat": 66, "lon": -90 @@ -701,20 +763,29 @@ const Row = z.object({ const getResult = () => client.any(sql.type(Row)` - select * from recipes_test + select * from zod_test `) await expect(getResult()).rejects.toMatchInlineSnapshot(` { "cause": { "query": { - "name": "select-recipes_test_6e1b6e6", - "sql": "\\n select * from recipes_test\\n ", + "name": "select-zod_test_83bbed1", + "sql": "\\n select * from zod_test\\n ", "token": "sql", "values": [] }, "error": { "issues": [ + { + "code": "invalid_type", + "expected": "string", + "received": "undefined", + "path": [ + "name" + ], + "message": "Required" + }, { "code": "custom", "message": "id must be even", @@ -724,7 +795,9 @@ await expect(getResult()).rejects.toMatchInlineSnapshot(` } ], "name": "ZodError" - } + }, + "message": "[\\n {\\n \\"code\\": \\"invalid_type\\",\\n \\"expected\\": \\"string\\",\\n \\"received\\": \\"undefined\\",\\n \\"path\\": [\\n \\"name\\"\\n ],\\n \\"message\\": \\"Required\\"\\n },\\n {\\n \\"code\\": \\"custom\\",\\n \\"message\\": \\"id must be even\\",\\n \\"path\\": [\\n \\"id\\"\\n ]\\n }\\n]", + "name": "QueryErrorCause" } } `) @@ -752,7 +825,25 @@ await client.query(sql` )} `) -expect(sqlProduced).toMatchInlineSnapshot(`{}`) +expect(sqlProduced).toMatchInlineSnapshot(` + [ + { + "sql": "\\n insert into recipes_test(id, name)\\n select *\\n from unnest($1::int4[], $2::text[])\\n ", + "values": [ + [ + 1, + 2, + 3 + ], + [ + "one", + "two", + "three" + ] + ] + } + ] +`) ``` ### Query logging @@ -777,7 +868,65 @@ expect(log.mock.calls[0][0]).toMatchInlineSnapshot( start: expect.any(Number), end: expect.any(Number), took: expect.any(Number), - }, `{}`) + }, + ` + { + "start": { + "inverse": false + }, + "end": { + "inverse": false + }, + "took": { + "inverse": false + }, + "query": { + "name": "select-recipes_test_8d7ce25", + "sql": "select * from recipes_test", + "token": "sql", + "values": [] + }, + "result": { + "rows": [ + { + "id": 1, + "name": "one" + }, + { + "id": 2, + "name": "two" + }, + { + "id": 3, + "name": "three" + } + ], + "command": "SELECT", + "rowCount": 3, + "fields": [ + { + "name": "id", + "tableID": 123456789, + "columnID": 1, + "dataTypeID": 123456789, + "dataTypeSize": 4, + "dataTypeModifier": -1, + "format": "text" + }, + { + "name": "name", + "tableID": 123456789, + "columnID": 2, + "dataTypeID": 123456789, + "dataTypeSize": -1, + "dataTypeModifier": -1, + "format": "text" + } + ] + } + } + `, +) ``` ### query timeouts @@ -786,15 +935,15 @@ expect(log.mock.calls[0][0]).toMatchInlineSnapshot( const shortTimeoutMs = 20 const impatient = createClient(client.connectionString() + '?shortTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs + connect: { + query_timeout: shortTimeoutMs, }, }, }) const patient = createClient(client.connectionString() + '?longTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs * 3 + connect: { + query_timeout: shortTimeoutMs * 3, }, }, }) @@ -802,23 +951,25 @@ const patient = createClient(client.connectionString() + '?longTimeout', { const sleepSeconds = (shortTimeoutMs * 2) / 1000 await expect(impatient.one(sql`select pg_sleep(${sleepSeconds})`)).rejects.toThrowErrorMatchingInlineSnapshot( ` - { - "message": "[Query select_9dcc021]: Query read timeout", - "cause": { - "query": { - "name": "select_9dcc021", - "sql": "select pg_sleep($1)", - "token": "sql", - "values": [ - 0.04 - ] - }, - "error": { - "query": "select pg_sleep(0.04)" + [[Query select_9dcc021]: Query read timeout] + { + "cause": { + "query": { + "name": "select_9dcc021", + "sql": "select pg_sleep($1)", + "token": "sql", + "values": [ + 0.04 + ] + }, + "error": { + "query": "select pg_sleep(0.04)" + }, + "message": "Query read timeout", + "name": "QueryErrorCause" } } - } -`, + `, ) await expect(patient.one(sql`select pg_sleep(${sleepSeconds})`)).resolves.toMatchObject({ pg_sleep: '', @@ -833,15 +984,15 @@ You can use `wrapQueryFn` to dynamically choose different clients depending on t const shortTimeoutMs = 20 const impatientClient = createClient(client.connectionString() + '?shortTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs + connect: { + query_timeout: shortTimeoutMs, }, }, }) const patientClient = createClient(client.connectionString() + '?longTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs * 3 + connect: { + query_timeout: shortTimeoutMs * 3, }, }, }) @@ -874,8 +1025,8 @@ await expect( select pg_sleep(${sleepSeconds}) `), ).rejects.toThrowErrorMatchingInlineSnapshot(` + [[Query select_6289211]: Query read timeout] { - "message": "[Query select_6289211]: Query read timeout", "cause": { "query": { "name": "select_6289211", @@ -887,7 +1038,9 @@ await expect( }, "error": { "query": "\\n select pg_sleep(0.04)\\n " - } + }, + "message": "Query read timeout", + "name": "QueryErrorCause" } } `) @@ -1024,52 +1177,55 @@ For errors based on the number of rows returned (for `one`, `oneFirst`, `many`, ```typescript await expect(pool.one(sql`select * from test_errors where id > 1`)).rejects.toMatchInlineSnapshot( ` - { - "message": "[Query select-test_errors_36f5f64]: Expected one row", - "cause": { - "query": { - "name": "select-test_errors_36f5f64", - "sql": "select * from test_errors where id > 1", - "token": "sql", - "values": [] - }, - "result": { - "rows": [ - { - "id": 2, - "name": "two" - }, - { - "id": 3, - "name": "three" - } - ], - "command": "SELECT", - "rowCount": 2, - "fields": [ - { - "name": "id", - "tableID": 123456789, - "columnID": 1, - "dataTypeID": 123456789, - "dataTypeSize": 4, - "dataTypeModifier": -1, - "format": "text" - }, - { - "name": "name", - "tableID": 123456789, - "columnID": 2, - "dataTypeID": 123456789, - "dataTypeSize": -1, - "dataTypeModifier": -1, - "format": "text" - } - ] - } - } + [[Query select-test_errors_36f5f64]: Expected one row] + { + "message": "[Query select-test_errors_36f5f64]: Expected one row", + "cause": { + "query": { + "name": "select-test_errors_36f5f64", + "sql": "select * from test_errors where id > 1", + "token": "sql", + "values": [] + }, + "result": { + "rows": [ + { + "id": 2, + "name": "two" + }, + { + "id": 3, + "name": "three" + } + ], + "command": "SELECT", + "rowCount": 2, + "fields": [ + { + "name": "id", + "tableID": 123456789, + "columnID": 1, + "dataTypeID": 123456789, + "dataTypeSize": 4, + "dataTypeModifier": -1, + "format": "text" + }, + { + "name": "name", + "tableID": 123456789, + "columnID": 2, + "dataTypeID": 123456789, + "dataTypeSize": -1, + "dataTypeModifier": -1, + "format": "text" + } + ] + }, + "message": "", + "name": "QueryErrorCause" } - `, + } +`, ) ``` @@ -1077,6 +1233,7 @@ await expect(pool.one(sql`select * from test_errors where id > 1`)).rejects.toMa ```typescript await expect(pool.maybeOne(sql`select * from test_errors where id > 1`)).rejects.toMatchInlineSnapshot(` + [[Query select-test_errors_36f5f64]: Expected at most one row] { "message": "[Query select-test_errors_36f5f64]: Expected at most one row", "cause": { @@ -1119,7 +1276,9 @@ await expect(pool.maybeOne(sql`select * from test_errors where id > 1`)).rejects "format": "text" } ] - } + }, + "message": "", + "name": "QueryErrorCause" } } `) @@ -1129,6 +1288,7 @@ await expect(pool.maybeOne(sql`select * from test_errors where id > 1`)).rejects ```typescript await expect(pool.many(sql`select * from test_errors where id > 100`)).rejects.toMatchInlineSnapshot(` + [[Query select-test_errors_34cad85]: Expected at least one row] { "message": "[Query select-test_errors_34cad85]: Expected at least one row", "cause": { @@ -1162,7 +1322,9 @@ await expect(pool.many(sql`select * from test_errors where id > 100`)).rejects.t "format": "text" } ] - } + }, + "message": "", + "name": "QueryErrorCause" } } `) @@ -1172,10 +1334,9 @@ await expect(pool.many(sql`select * from test_errors where id > 100`)).rejects.t ```typescript await expect(pool.query(sql`select * frooom test_errors`)).rejects.toMatchInlineSnapshot(` + [[Query select_fb83277]: syntax error at or near "frooom"] { "message": "[Query select_fb83277]: syntax error at or near \\"frooom\\"", - "pg_code": "42601", - "pg_code_name": "syntax_error", "cause": { "query": { "name": "select_fb83277", @@ -1193,7 +1354,9 @@ await expect(pool.query(sql`select * frooom test_errors`)).rejects.toMatchInline "line": "123456789", "routine": "scanner_yyerror", "query": "select * frooom test_errors" - } + }, + "message": "syntax error at or near \\"frooom\\"", + "name": "QueryErrorCause" } } `) diff --git a/apps/docs/pages/packages/typegen.md b/apps/docs/pages/packages/typegen.md index 45ab9f84..d66f9911 100644 --- a/apps/docs/pages/packages/typegen.md +++ b/apps/docs/pages/packages/typegen.md @@ -133,7 +133,7 @@ CLI arguments will always have precedence over config options. |`include`|`--include`|`string[]`|`['**/*.{ts,sql}']`|Glob patterns for files to include in processing. Repeatable in CLI.| |`exclude`|`--exclude`|`string[]`|`['**/node_modules/**']`|Glob patterns for files to exclude from processing. Repeatable in CLI.| |`since`|`--since`|`string \| undefined`|`undefined`|Limit matched files to those which have been changed since the given git ref. Use `"HEAD"` for files changed since the last commit, `"main"` for files changed in a branch, etc.| -|`connectionURI`|`--connection-uri`|`string`|`'postgresql://` `postgres:postgres` `@localhost:5432/` `postgres'`|URI for connecting to psql. Note that if you are using `psql` inside docker, you should make sure that the container and host port match, since this will be used both by `psql` and pgkit to connect to the database.| +|`connectionURI`|`--connection-string`|`string`|`'postgresql://` `postgres:postgres` `@localhost:5432/` `postgres'`|URI for connecting to psql. Note that if you are using `psql` inside docker, you should make sure that the container and host port match, since this will be used both by `psql` and pgkit to connect to the database.| |`psqlCommand`|`--psql`|`string`|`'psql'`|The CLI command for running the official postgres `psql` CLI client.
Note that right now this can't contain single quotes. This should also be configured to talk to the same database as the `pool` variable (and it should be a development database - don't run this tool in production!). If you are using docker compose, you can use a command like `docker-compose exec -T postgres psql`| |`defaultType`|`--default-type`|`string`|`'unknown'`|TypeScript type when no mapping is found. This should usually be `unknown` (or `any` if you like to live dangerously).| |`poolConfig`||`PoolConfig \| undefined`
(see [below](#complex-config-types))|`undefined`|Pgkit database pool configuration. Will be used to create a pool which issues queries to the database as the tool is running, and will have its type parsers inspected to ensure the generated types are correct. It's important to pass in a pool confguration which is the same as the one used in your application.| diff --git a/packages/client/contributing.md b/packages/client/contributing.md new file mode 100644 index 00000000..64110c94 --- /dev/null +++ b/packages/client/contributing.md @@ -0,0 +1 @@ +Important that any *library* using this can accept an instantiated client, not just a connection string. https://stackoverflow.com/questions/64238590/pg-promise-recommended-pattern-for-passing-connections-to-different-libraries diff --git a/packages/client/package.json b/packages/client/package.json index b7326e5e..68546f9e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -53,7 +53,8 @@ "tsup": "^8.0.1", "typescript-eslint": "^7.1.0", "vitest": "^1.2.2", - "zod": "^3.22.4" + "zod": "^3.22.4", + "zod-validation-error": "^3.3.0" }, "dependencies": { "pg": "^8.11.3", diff --git a/packages/client/readme.md b/packages/client/readme.md index 2f1c5c3b..a969a71a 100644 --- a/packages/client/readme.md +++ b/packages/client/readme.md @@ -66,6 +66,7 @@ Note that @pgkit/migra and @pgkit/schemainspect are pure ports of their Python e - [sql.literalValue](#sqlliteralvalue) - [transaction savepoints](#transaction-savepoints) - [sql.type](#sqltype) + - [sql.type with custom error message](#sqltype-with-custom-error-message) - [createSqlTag + sql.typeAlias](#createsqltag--sqltypealias) - [Types](#types) - [Automatic type generation](#automatic-type-generation) @@ -470,7 +471,7 @@ expect(newRecords).toEqual([{id: 10, name: 'ten'}]) ```typescript const StringId = z.object({id: z.string()}) -await expect(client.any(sql.type(StringId)`select text(id) id from usage_test`)).resolves.toMatchObject([ +await expect(client.any(sql.type(StringId)`select id::text from usage_test`)).resolves.toMatchObject([ {id: '1'}, {id: '2'}, {id: '3'}, @@ -491,6 +492,63 @@ expect(error.cause).toMatchInlineSnapshot(` "message": "Expected string, received number" } ]], + "message": "[ + { + "code": "invalid_type", + "expected": "string", + "received": "number", + "path": [ + "id" + ], + "message": "Expected string, received number" + } + ]", + "name": "QueryErrorCause", + "query": { + "name": "select-usage_test_8729cac", + "parse": [Function], + "sql": "select id from usage_test", + "templateArgs": [Function], + "token": "sql", + "values": [], + }, + } +`) +``` + +### sql.type with custom error message + +Wrap the query function to customize the error message + +```typescript +client = createClient(client.connectionString(), { + ...client.options, + wrapQueryFn: query => { + const parentWrapper = client.options.wrapQueryFn || (x => x) + return async (...args) => { + const parentQueryFn = parentWrapper(query) + try { + return await parentQueryFn(...args) + } catch (e) { + if (e instanceof QueryError && e.message.endsWith('Parsing rows failed')) { + throw new QueryError(e.message, { + cause: {query: e.cause.query, error: fromError(e.cause.error)}, + }) + } + throw e + } + } + }, +}) +const StringId = z.object({id: z.string()}) + +const error = await client.any(sql.type(StringId)`select id from usage_test`).catch(e => e) + +expect(error.cause).toMatchInlineSnapshot(` + { + "error": [ZodValidationError: Validation error: Expected string, received number at "id"], + "message": "Validation error: Expected string, received number at "id"", + "name": "QueryErrorCause", "query": { "name": "select-usage_test_8729cac", "parse": [Function], @@ -523,17 +581,9 @@ expect(result).toEqual({name: 'Bob'}) const err = await client.any(sql.typeAlias('Profile')`select 123 as name`).catch(e => e) expect(err.cause).toMatchInlineSnapshot(` { - "error": [ZodError: [ - { - "code": "invalid_type", - "expected": "string", - "received": "number", - "path": [ - "name" - ], - "message": "Expected string, received number" - } - ]], + "error": [ZodValidationError: Validation error: Expected string, received number at "name"], + "message": "Validation error: Expected string, received number at "name"", + "name": "QueryErrorCause", "query": { "name": "select_245d49b", "parse": [Function], @@ -652,6 +702,7 @@ Transform rows: ```typescript const Row = z.object({ id: z.number(), + label: z.string().nullable(), location: z .string() .regex(/^-?\d+,-?\d+$/) @@ -665,10 +716,19 @@ const result = await client.any(sql.type(Row)` select * from zod_test `) +expectTypeOf(result).toEqualTypeOf<{id: number; label: string | null; location: {lat: number; lon: number}}[]>() + +const result2 = await client.any(sql.type(Row)` + select * from ${sql.identifier(['zod_test'])} +`) + +expect(result2).toEqual(result) + expect(result).toMatchInlineSnapshot(` [ { "id": 1, + "label": "a", "location": { "lat": 70, "lon": -108 @@ -676,6 +736,7 @@ expect(result).toMatchInlineSnapshot(` }, { "id": 2, + "label": "b", "location": { "lat": 71, "lon": -102 @@ -683,6 +744,7 @@ expect(result).toMatchInlineSnapshot(` }, { "id": 3, + "label": null, "location": { "lat": 66, "lon": -90 @@ -702,20 +764,29 @@ const Row = z.object({ const getResult = () => client.any(sql.type(Row)` - select * from recipes_test + select * from zod_test `) await expect(getResult()).rejects.toMatchInlineSnapshot(` { "cause": { "query": { - "name": "select-recipes_test_6e1b6e6", - "sql": "\\n select * from recipes_test\\n ", + "name": "select-zod_test_83bbed1", + "sql": "\\n select * from zod_test\\n ", "token": "sql", "values": [] }, "error": { "issues": [ + { + "code": "invalid_type", + "expected": "string", + "received": "undefined", + "path": [ + "name" + ], + "message": "Required" + }, { "code": "custom", "message": "id must be even", @@ -725,7 +796,9 @@ await expect(getResult()).rejects.toMatchInlineSnapshot(` } ], "name": "ZodError" - } + }, + "message": "[\\n {\\n \\"code\\": \\"invalid_type\\",\\n \\"expected\\": \\"string\\",\\n \\"received\\": \\"undefined\\",\\n \\"path\\": [\\n \\"name\\"\\n ],\\n \\"message\\": \\"Required\\"\\n },\\n {\\n \\"code\\": \\"custom\\",\\n \\"message\\": \\"id must be even\\",\\n \\"path\\": [\\n \\"id\\"\\n ]\\n }\\n]", + "name": "QueryErrorCause" } } `) @@ -863,15 +936,15 @@ expect(log.mock.calls[0][0]).toMatchInlineSnapshot( const shortTimeoutMs = 20 const impatient = createClient(client.connectionString() + '?shortTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs + connect: { + query_timeout: shortTimeoutMs, }, }, }) const patient = createClient(client.connectionString() + '?longTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs * 3 + connect: { + query_timeout: shortTimeoutMs * 3, }, }, }) @@ -879,6 +952,7 @@ const patient = createClient(client.connectionString() + '?longTimeout', { const sleepSeconds = (shortTimeoutMs * 2) / 1000 await expect(impatient.one(sql`select pg_sleep(${sleepSeconds})`)).rejects.toThrowErrorMatchingInlineSnapshot( ` + [[Query select_9dcc021]: Query read timeout] { "cause": { "query": { @@ -891,7 +965,9 @@ await expect(impatient.one(sql`select pg_sleep(${sleepSeconds})`)).rejects.toThr }, "error": { "query": "select pg_sleep(0.04)" - } + }, + "message": "Query read timeout", + "name": "QueryErrorCause" } } `, @@ -909,15 +985,15 @@ You can use `wrapQueryFn` to dynamically choose different clients depending on t const shortTimeoutMs = 20 const impatientClient = createClient(client.connectionString() + '?shortTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs + connect: { + query_timeout: shortTimeoutMs, }, }, }) const patientClient = createClient(client.connectionString() + '?longTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs * 3 + connect: { + query_timeout: shortTimeoutMs * 3, }, }, }) @@ -950,6 +1026,7 @@ await expect( select pg_sleep(${sleepSeconds}) `), ).rejects.toThrowErrorMatchingInlineSnapshot(` + [[Query select_6289211]: Query read timeout] { "cause": { "query": { @@ -962,7 +1039,9 @@ await expect( }, "error": { "query": "\\n select pg_sleep(0.04)\\n " - } + }, + "message": "Query read timeout", + "name": "QueryErrorCause" } } `) @@ -1099,52 +1178,55 @@ For errors based on the number of rows returned (for `one`, `oneFirst`, `many`, ```typescript await expect(pool.one(sql`select * from test_errors where id > 1`)).rejects.toMatchInlineSnapshot( ` - { - "message": "[Query select-test_errors_36f5f64]: Expected one row", - "cause": { - "query": { - "name": "select-test_errors_36f5f64", - "sql": "select * from test_errors where id > 1", - "token": "sql", - "values": [] - }, - "result": { - "rows": [ - { - "id": 2, - "name": "two" - }, - { - "id": 3, - "name": "three" - } - ], - "command": "SELECT", - "rowCount": 2, - "fields": [ - { - "name": "id", - "tableID": 123456789, - "columnID": 1, - "dataTypeID": 123456789, - "dataTypeSize": 4, - "dataTypeModifier": -1, - "format": "text" - }, - { - "name": "name", - "tableID": 123456789, - "columnID": 2, - "dataTypeID": 123456789, - "dataTypeSize": -1, - "dataTypeModifier": -1, - "format": "text" - } - ] - } - } + [[Query select-test_errors_36f5f64]: Expected one row] + { + "message": "[Query select-test_errors_36f5f64]: Expected one row", + "cause": { + "query": { + "name": "select-test_errors_36f5f64", + "sql": "select * from test_errors where id > 1", + "token": "sql", + "values": [] + }, + "result": { + "rows": [ + { + "id": 2, + "name": "two" + }, + { + "id": 3, + "name": "three" + } + ], + "command": "SELECT", + "rowCount": 2, + "fields": [ + { + "name": "id", + "tableID": 123456789, + "columnID": 1, + "dataTypeID": 123456789, + "dataTypeSize": 4, + "dataTypeModifier": -1, + "format": "text" + }, + { + "name": "name", + "tableID": 123456789, + "columnID": 2, + "dataTypeID": 123456789, + "dataTypeSize": -1, + "dataTypeModifier": -1, + "format": "text" + } + ] + }, + "message": "", + "name": "QueryErrorCause" } - `, + } +`, ) ``` @@ -1152,6 +1234,7 @@ await expect(pool.one(sql`select * from test_errors where id > 1`)).rejects.toMa ```typescript await expect(pool.maybeOne(sql`select * from test_errors where id > 1`)).rejects.toMatchInlineSnapshot(` + [[Query select-test_errors_36f5f64]: Expected at most one row] { "message": "[Query select-test_errors_36f5f64]: Expected at most one row", "cause": { @@ -1194,7 +1277,9 @@ await expect(pool.maybeOne(sql`select * from test_errors where id > 1`)).rejects "format": "text" } ] - } + }, + "message": "", + "name": "QueryErrorCause" } } `) @@ -1204,6 +1289,7 @@ await expect(pool.maybeOne(sql`select * from test_errors where id > 1`)).rejects ```typescript await expect(pool.many(sql`select * from test_errors where id > 100`)).rejects.toMatchInlineSnapshot(` + [[Query select-test_errors_34cad85]: Expected at least one row] { "message": "[Query select-test_errors_34cad85]: Expected at least one row", "cause": { @@ -1237,7 +1323,9 @@ await expect(pool.many(sql`select * from test_errors where id > 100`)).rejects.t "format": "text" } ] - } + }, + "message": "", + "name": "QueryErrorCause" } } `) @@ -1247,10 +1335,9 @@ await expect(pool.many(sql`select * from test_errors where id > 100`)).rejects.t ```typescript await expect(pool.query(sql`select * frooom test_errors`)).rejects.toMatchInlineSnapshot(` + [[Query select_fb83277]: syntax error at or near "frooom"] { "message": "[Query select_fb83277]: syntax error at or near \\"frooom\\"", - "pg_code": "42601", - "pg_code_name": "syntax_error", "cause": { "query": { "name": "select_fb83277", @@ -1268,7 +1355,9 @@ await expect(pool.query(sql`select * frooom test_errors`)).rejects.toMatchInline "line": "123456789", "routine": "scanner_yyerror", "query": "select * frooom test_errors" - } + }, + "message": "syntax error at or near \\"frooom\\"", + "name": "QueryErrorCause" } } `) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 170a2fd9..a98f2830 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -1,8 +1,19 @@ import * as crypto from 'node:crypto' +import TypeOverrides from 'pg/lib/type-overrides' import pgPromise from 'pg-promise' import {QueryError, errorFromUnknown} from './errors' import {setRecommendedTypeParsers} from './type-parsers' -import {Client, First, Queryable, SQLQueryRowType, ClientOptions, Connection, Transaction, Result} from './types' +import { + Client, + First, + Queryable, + SQLQueryRowType, + ClientOptions, + Connection, + Transaction, + Result, + PGTypes, +} from './types' export const identityParser = (input: unknown): T => input as T @@ -103,17 +114,30 @@ export const createClient = (connectionString: string, options: ClientOptions = if (typeof connectionString !== 'string') throw new Error(`Expected connectionString, got ${typeof connectionString}`) if (!connectionString) throw new Error(`Expected a valid connectionString, got "${connectionString}"`) - const {pgpOptions = {}, setTypeParsers = setRecommendedTypeParsers, wrapQueryFn} = options - const pgp = pgPromise(pgpOptions) + options = { + applyTypeParsers: setRecommendedTypeParsers, + pgpOptions: {}, + ...options, + } + + const types = new TypeOverrides() + const pgp = pgPromise(options.pgpOptions?.initialize) - setTypeParsers(pgp.pg.types) + options.applyTypeParsers?.({ + setTypeParser: (id, parseFn) => types.setTypeParser(id, parseFn as (input: unknown) => unknown), + builtins: pgp.pg.types.builtins, + } as PGTypes) const createWrappedQueryFn: typeof createQueryFn = queryable => { const queryFn = createQueryFn(queryable) - return wrapQueryFn ? wrapQueryFn(queryFn) : queryFn + return options.wrapQueryFn ? options.wrapQueryFn(queryFn) : queryFn } - const client = pgp(connectionString) + const client = pgp({ + connectionString, + types, + ...options.pgpOptions?.connect, + }) const transactionFnFromTask = (task: pgPromise.ITask | pgPromise.IDatabase): Connection['transaction'] => @@ -145,10 +169,14 @@ export const createClient = (connectionString: string, options: ClientOptions = return { options, pgp: client, - pgpOptions, + pgpOptions: options.pgpOptions, ...createQueryable(createWrappedQueryFn(client)), - // eslint-disable-next-line @typescript-eslint/no-base-to-string - connectionString: () => client.$cn.toString(), + connectionString: () => { + const cn = client.$cn + const result = typeof cn === 'string' ? cn : cn.connectionString + if (!result) throw new Error('Expected connection string') + return result + }, end: async () => client.$pool.end(), connect, transaction: transactionFnFromTask(client), diff --git a/packages/client/src/type-parsers.ts b/packages/client/src/type-parsers.ts index d6ad7944..b8799602 100644 --- a/packages/client/src/type-parsers.ts +++ b/packages/client/src/type-parsers.ts @@ -1,27 +1,27 @@ import pgPromise from 'pg-promise' -import {PGTypes, SetTypeParsers} from './types' +import {PGTypes, ApplyTypeParsers} from './types' export const pgTypes: PGTypes = pgPromise().pg.types -export const setRecommendedTypeParsers: SetTypeParsers = types => { - types.setTypeParser(types.builtins.DATE, value => new Date(value)) - types.setTypeParser(types.builtins.TIMESTAMPTZ, value => new Date(value)) - types.setTypeParser(types.builtins.TIMESTAMP, value => value) - types.setTypeParser(types.builtins.INTERVAL, value => value) - types.setTypeParser(types.builtins.NUMERIC, Number) - types.setTypeParser(types.builtins.INT2, Number) - types.setTypeParser(types.builtins.INT4, Number) - types.setTypeParser(types.builtins.INT8, Number) - types.setTypeParser(types.builtins.BOOL, value => value === 't') +export const setRecommendedTypeParsers: ApplyTypeParsers = ({setTypeParser, builtins}) => { + setTypeParser(builtins.DATE, value => new Date(value)) + setTypeParser(builtins.TIMESTAMPTZ, value => new Date(value)) + setTypeParser(builtins.TIMESTAMP, value => value) + setTypeParser(builtins.INTERVAL, value => value) + setTypeParser(builtins.NUMERIC, Number) + setTypeParser(builtins.INT2, Number) + setTypeParser(builtins.INT4, Number) + setTypeParser(builtins.INT8, Number) + setTypeParser(builtins.BOOL, value => value === 't') } /** * Equivalent of slonik type parsers in `createTypeParserPreset`. [Docs](https://www.npmjs.com/package/slonik#default-configuration) */ -export const setSlonik37TypeParsers: SetTypeParsers = types => { - types.setTypeParser(types.builtins.DATE, value => new Date(value)) - types.setTypeParser(types.builtins.TIMESTAMPTZ, value => new Date(value).getTime()) - types.setTypeParser(types.builtins.TIMESTAMP, value => new Date(value).getTime()) - types.setTypeParser(types.builtins.INTERVAL, value => value) - types.setTypeParser(types.builtins.NUMERIC, Number) +export const setSlonik37TypeParsers: ApplyTypeParsers = ({setTypeParser, builtins}) => { + setTypeParser(builtins.DATE, value => new Date(value)) + setTypeParser(builtins.TIMESTAMPTZ, value => new Date(value).getTime()) + setTypeParser(builtins.TIMESTAMP, value => new Date(value).getTime()) + setTypeParser(builtins.INTERVAL, value => value) + setTypeParser(builtins.NUMERIC, Number) } diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index e5491dad..55fce10e 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -65,7 +65,7 @@ export interface Transaction extends Connection { export interface Client extends Queryable { options: ClientOptions pgp: ReturnType> - pgpOptions: Parameters[0] + pgpOptions: PGPOptions connectionString(): string end(): Promise connect(callback: (connection: Connection) => Promise): Promise @@ -158,15 +158,32 @@ export type SQLMethodHelpers = { ) => SQLQuery } +/** Called `pgp` in pg-promise docs */ +export type PGPromiseInitializer = typeof pgPromise +/** Called `IMain` in pg-promise */ +export type PGPromiseDBConnector = ReturnType +/** Looks like `[cn: string | pg.IConnectionParameters, dc?: any]` in pg-promise */ +export type PGPromiseDBConnectorParameters = Parameters +/** Looks like `pg.IConnectionParameters` in pg-promise */ +export type PGPromiseDBConnectionOptions = Exclude + export type PGTypes = ReturnType['pg']['types'] export type ParseFn = Extract[number], Function> export type PGTypesBuiltins = PGTypes['builtins'] export type PGTypesBuiltinOid = PGTypesBuiltins[keyof PGTypesBuiltins] -export type SetTypeParsers = (types: Pick) => void +export type ApplyTypeParsers = (params: { + setTypeParser: (oid: PGTypesBuiltinOid, parseFn: ParseFn) => void + builtins: PGTypes['builtins'] +}) => void + +export type PGPOptions = { + initialize?: pgPromise.IInitOptions + connect?: PGPromiseDBConnectionOptions +} export interface ClientOptions { - pgpOptions?: Parameters[0] - setTypeParsers?: (types: PGTypes) => void + pgpOptions?: PGPOptions + applyTypeParsers?: ApplyTypeParsers wrapQueryFn?: (queryFn: Queryable['query']) => Queryable['query'] } diff --git a/packages/client/test/api-usage.test.ts b/packages/client/test/api-usage.test.ts index 18469817..61342839 100644 --- a/packages/client/test/api-usage.test.ts +++ b/packages/client/test/api-usage.test.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-shadow */ import {beforeAll, beforeEach, expect, expectTypeOf, test, vi} from 'vitest' import {z} from 'zod' -import {createClient, createSqlTag, sql} from '../src' +import {fromError} from 'zod-validation-error' +import {createClient, createSqlTag, QueryError, sql} from '../src' export let client: Awaited> @@ -261,7 +262,7 @@ test('transaction savepoints', async () => { */ test('sql.type', async () => { const StringId = z.object({id: z.string()}) - await expect(client.any(sql.type(StringId)`select text(id) id from usage_test`)).resolves.toMatchObject([ + await expect(client.any(sql.type(StringId)`select id::text from usage_test`)).resolves.toMatchObject([ {id: '1'}, {id: '2'}, {id: '3'}, @@ -306,6 +307,50 @@ test('sql.type', async () => { `) }) +/** + * Wrap the query function to customize the error message + */ +test('sql.type with custom error message', async () => { + client = createClient(client.connectionString(), { + ...client.options, + wrapQueryFn: query => { + const parentWrapper = client.options.wrapQueryFn || (x => x) + return async (...args) => { + const parentQueryFn = parentWrapper(query) + try { + return await parentQueryFn(...args) + } catch (e) { + if (e instanceof QueryError && e.message.endsWith('Parsing rows failed')) { + throw new QueryError(e.message, { + cause: {query: e.cause.query, error: fromError(e.cause.error)}, + }) + } + throw e + } + } + }, + }) + const StringId = z.object({id: z.string()}) + + const error = await client.any(sql.type(StringId)`select id from usage_test`).catch(e => e) + + expect(error.cause).toMatchInlineSnapshot(` + { + "error": [ZodValidationError: Validation error: Expected string, received number at "id"], + "message": "Validation error: Expected string, received number at "id"", + "name": "QueryErrorCause", + "query": { + "name": "select-usage_test_8729cac", + "parse": [Function], + "sql": "select id from usage_test", + "templateArgs": [Function], + "token": "sql", + "values": [], + }, + } + `) +}) + /** * `createSqlTag` lets you create your own `sql` tag, which you can export and use instead of the deafult one, * to add commonly-used schemas, which can be referred to by their key in the `createSqlTag` definition. @@ -326,28 +371,8 @@ test('createSqlTag + sql.typeAlias', async () => { const err = await client.any(sql.typeAlias('Profile')`select 123 as name`).catch(e => e) expect(err.cause).toMatchInlineSnapshot(` { - "error": [ZodError: [ - { - "code": "invalid_type", - "expected": "string", - "received": "number", - "path": [ - "name" - ], - "message": "Expected string, received number" - } - ]], - "message": "[ - { - "code": "invalid_type", - "expected": "string", - "received": "number", - "path": [ - "name" - ], - "message": "Expected string, received number" - } - ]", + "error": [ZodValidationError: Validation error: Expected string, received number at "name"], + "message": "Validation error: Expected string, received number at "name"", "name": "QueryErrorCause", "query": { "name": "select_245d49b", diff --git a/packages/client/test/errors.test.ts b/packages/client/test/errors.test.ts index af70127d..0512e125 100644 --- a/packages/client/test/errors.test.ts +++ b/packages/client/test/errors.test.ts @@ -38,59 +38,61 @@ beforeAll(async () => { test('one error', async () => { await expect(pool.one(sql`select * from test_errors where id > 1`)).rejects.toMatchInlineSnapshot( ` - { - "message": "[Query select-test_errors_36f5f64]: Expected one row", - "cause": { - "query": { - "name": "select-test_errors_36f5f64", - "sql": "select * from test_errors where id > 1", - "token": "sql", - "values": [] - }, - "result": { - "rows": [ - { - "id": 2, - "name": "two" - }, - { - "id": 3, - "name": "three" - } - ], - "command": "SELECT", - "rowCount": 2, - "fields": [ - { - "name": "id", - "tableID": 123456789, - "columnID": 1, - "dataTypeID": 123456789, - "dataTypeSize": 4, - "dataTypeModifier": -1, - "format": "text" - }, - { - "name": "name", - "tableID": 123456789, - "columnID": 2, - "dataTypeID": 123456789, - "dataTypeSize": -1, - "dataTypeModifier": -1, - "format": "text" - } - ] - }, - "message": "", - "name": "QueryErrorCause" - } + [[Query select-test_errors_36f5f64]: Expected one row] + { + "message": "[Query select-test_errors_36f5f64]: Expected one row", + "cause": { + "query": { + "name": "select-test_errors_36f5f64", + "sql": "select * from test_errors where id > 1", + "token": "sql", + "values": [] + }, + "result": { + "rows": [ + { + "id": 2, + "name": "two" + }, + { + "id": 3, + "name": "three" + } + ], + "command": "SELECT", + "rowCount": 2, + "fields": [ + { + "name": "id", + "tableID": 123456789, + "columnID": 1, + "dataTypeID": 123456789, + "dataTypeSize": 4, + "dataTypeModifier": -1, + "format": "text" + }, + { + "name": "name", + "tableID": 123456789, + "columnID": 2, + "dataTypeID": 123456789, + "dataTypeSize": -1, + "dataTypeModifier": -1, + "format": "text" + } + ] + }, + "message": "", + "name": "QueryErrorCause" } - `, + } + `, ) }) test('maybeOne error', async () => { await expect(pool.maybeOne(sql`select * from test_errors where id > 1`)).rejects.toMatchInlineSnapshot(` + [[Query select-test_errors_36f5f64]: Expected at most one row] { "message": "[Query select-test_errors_36f5f64]: Expected at most one row", "cause": { @@ -143,6 +145,7 @@ test('maybeOne error', async () => { test('many error', async () => { await expect(pool.many(sql`select * from test_errors where id > 100`)).rejects.toMatchInlineSnapshot(` + [[Query select-test_errors_34cad85]: Expected at least one row] { "message": "[Query select-test_errors_34cad85]: Expected at least one row", "cause": { @@ -186,6 +189,7 @@ test('many error', async () => { test('syntax error', async () => { await expect(pool.query(sql`select * frooom test_errors`)).rejects.toMatchInlineSnapshot(` + [[Query select_fb83277]: syntax error at or near "frooom"] { "message": "[Query select_fb83277]: syntax error at or near \\"frooom\\"", "cause": { diff --git a/packages/client/test/pg-promise-usage.test.ts b/packages/client/test/pg-promise-usage.test.ts new file mode 100644 index 00000000..a4515f40 --- /dev/null +++ b/packages/client/test/pg-promise-usage.test.ts @@ -0,0 +1,20 @@ +import {test, expect, vi} from 'vitest' +import {createClient, sql} from '../src' + +test("type parsers don't override each other", async () => { + const client1 = createClient('postgresql://postgres:postgres@localhost:5432/postgres', { + applyTypeParsers: types => { + types.setTypeParser(types.builtins.INT8, Number) + }, + }) + const client2 = createClient('postgresql://postgres:postgres@localhost:5432/postgres', { + applyTypeParsers: types => { + types.setTypeParser(types.builtins.INT8, BigInt) + }, + }) + + const result1 = await client1.one(sql`select 1::int8 as one`) + const result2 = await client2.one(sql`select 1::int8 as two`) + expect(result1).toEqual({one: 1}) + expect(result2).toEqual({two: 1n}) +}) diff --git a/packages/client/test/recipes.test.ts b/packages/client/test/recipes.test.ts index 1e94d421..0dc1bd27 100644 --- a/packages/client/test/recipes.test.ts +++ b/packages/client/test/recipes.test.ts @@ -155,15 +155,15 @@ test('query timeouts', async () => { const shortTimeoutMs = 20 const impatient = createClient(client.connectionString() + '?shortTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs + connect: { + query_timeout: shortTimeoutMs, }, }, }) const patient = createClient(client.connectionString() + '?longTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs * 3 + connect: { + query_timeout: shortTimeoutMs * 3, }, }, }) @@ -171,24 +171,25 @@ test('query timeouts', async () => { const sleepSeconds = (shortTimeoutMs * 2) / 1000 await expect(impatient.one(sql`select pg_sleep(${sleepSeconds})`)).rejects.toThrowErrorMatchingInlineSnapshot( ` - { - "cause": { - "query": { - "name": "select_9dcc021", - "sql": "select pg_sleep($1)", - "token": "sql", - "values": [ - 0.04 - ] - }, - "error": { - "query": "select pg_sleep(0.04)" - }, - "message": "Query read timeout", - "name": "QueryErrorCause" + [[Query select_9dcc021]: Query read timeout] + { + "cause": { + "query": { + "name": "select_9dcc021", + "sql": "select pg_sleep($1)", + "token": "sql", + "values": [ + 0.04 + ] + }, + "error": { + "query": "select pg_sleep(0.04)" + }, + "message": "Query read timeout", + "name": "QueryErrorCause" + } } - } - `, + `, ) await expect(patient.one(sql`select pg_sleep(${sleepSeconds})`)).resolves.toMatchObject({ pg_sleep: '', @@ -200,15 +201,15 @@ test('switchable clients', async () => { const shortTimeoutMs = 20 const impatientClient = createClient(client.connectionString() + '?shortTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs + connect: { + query_timeout: shortTimeoutMs, }, }, }) const patientClient = createClient(client.connectionString() + '?longTimeout', { pgpOptions: { - connect: ({client}) => { - client.connectionParameters.query_timeout = shortTimeoutMs * 3 + connect: { + query_timeout: shortTimeoutMs * 3, }, }, }) @@ -241,6 +242,7 @@ test('switchable clients', async () => { select pg_sleep(${sleepSeconds}) `), ).rejects.toThrowErrorMatchingInlineSnapshot(` + [[Query select_6289211]: Query read timeout] { "cause": { "query": { diff --git a/packages/client/test/snapshots.ts b/packages/client/test/snapshots.ts index 52a8df44..b44375bb 100644 --- a/packages/client/test/snapshots.ts +++ b/packages/client/test/snapshots.ts @@ -1,16 +1,19 @@ export function printPostgresErrorSnapshot(val: any): string { - return JSON.stringify( - val, - function (key, value) { - if (key === 'dataTypeID' || key === 'tableID') { - return 123_456_789 // avoid unstable pg generated ids - } - if (this.name === 'error' && key === 'line') { - return '123456789' // avoid unstable line numbers of generated statements - } + return ( + `[${val.message}]\n`.replace('[undefined]\n', '') + + JSON.stringify( + val, + function (key, value) { + if (key === 'dataTypeID' || key === 'tableID') { + return 123_456_789 // avoid unstable pg generated ids + } + if (this.name === 'error' && key === 'line') { + return '123456789' // avoid unstable line numbers of generated statements + } - return value - }, - 2, + return value + }, + 2, + ) ) } diff --git a/packages/migrator/test/pool-helper.ts b/packages/migrator/test/pool-helper.ts index bf541637..7c1bdf45 100644 --- a/packages/migrator/test/pool-helper.ts +++ b/packages/migrator/test/pool-helper.ts @@ -20,14 +20,15 @@ export const getPoolHelper2 = (dbName: string, {lockTimeout = '', statementTimeo const pool = createClient(connectionString(dbName), { pgpOptions: { - // schema: schemaName, - noWarnings: true, - // eslint-disable-next-line @typescript-eslint/no-misused-promises - connect: async ({client, useCount}) => { - if (useCount === 0) { - if (statementTimeout) await client.query(`set statement_timeout to '${statementTimeout}'`) - if (lockTimeout) await client.query(`set lock_timeout = '${lockTimeout}'`) - } + initialize: { + noWarnings: true, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + connect: async ({client, useCount}) => { + if (useCount === 0) { + if (statementTimeout) await client.query(`set statement_timeout to '${statementTimeout}'`) + if (lockTimeout) await client.query(`set lock_timeout = '${lockTimeout}'`) + } + }, }, }, // idleTimeout: 1, diff --git a/packages/typegen/test/helper.ts b/packages/typegen/test/helper.ts index 114e9c49..dd0cf4ec 100644 --- a/packages/typegen/test/helper.ts +++ b/packages/typegen/test/helper.ts @@ -59,8 +59,13 @@ export const getPoolHelper = (params: {__filename: string; baseConnectionURI: st ...params.config, pgpOptions: { // schema: schemaName, - noWarnings: true, + // noWarnings: true, + // ...params.config?.pgpOptions, ...params.config?.pgpOptions, + initialize: { + ...params.config?.pgpOptions?.initialize, + noWarnings: true, // todo: remove + }, // connect: async ({client, useCount}) => { // if (useCount === 0) { // if (statementTimeout) await client.query(`set statement_timeout to '${statementTimeout}'`) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1067da0..efbda8e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -401,6 +401,9 @@ importers: zod: specifier: ^3.22.4 version: 3.22.4 + zod-validation-error: + specifier: ^3.3.0 + version: 3.3.0(zod@3.22.4) packages/formatter: dependencies: @@ -10519,6 +10522,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)': + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 7.3.1 + '@typescript-eslint/type-utils': 7.3.1(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 7.3.1 + debug: 4.3.4 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@5.3.3) + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.10.0 @@ -12258,7 +12281,7 @@ snapshots: eslint-config-xo-typescript@1.0.1(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3): dependencies: - '@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 typescript: 5.3.3 @@ -12360,7 +12383,7 @@ snapshots: '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 optionalDependencies: - '@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) transitivePeerDependencies: - supports-color - typescript @@ -12401,7 +12424,7 @@ snapshots: '@rushstack/eslint-plugin-packlets': 0.8.1(eslint@8.57.0)(typescript@5.3.3) '@rushstack/eslint-plugin-security': 0.7.1(eslint@8.57.0)(typescript@5.3.3) '@types/eslint': 8.56.6 - '@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.3.3) eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-config-xo: 0.43.1(eslint@8.57.0) @@ -12654,7 +12677,7 @@ snapshots: '@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 optionalDependencies: - '@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) vitest: 1.2.2(@types/node@20.11.17)(sass@1.71.0) transitivePeerDependencies: - supports-color @@ -17712,6 +17735,10 @@ snapshots: dependencies: zod: 3.23.8 + zod-validation-error@3.3.0(zod@3.22.4): + dependencies: + zod: 3.22.4 + zod-validation-error@3.3.0(zod@3.23.8): dependencies: zod: 3.23.8