pqb 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/db.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  } from './columnSchema';
26
26
  import { applyMixins, pushOrNewArray } from './utils';
27
27
  import { StringKey } from './common';
28
+ import { QueryError, QueryErrorName } from './errors';
28
29
 
29
30
  export type DbTableOptions = {
30
31
  schema?: string;
@@ -76,6 +77,11 @@ export interface Db<
76
77
  columnsParsers?: ColumnsParsers;
77
78
  relations: Relations;
78
79
  withData: Query['withData'];
80
+ error: new (
81
+ message: string,
82
+ length: number,
83
+ name: QueryErrorName,
84
+ ) => QueryError<this>;
79
85
  [defaultsKey]: Record<
80
86
  {
81
87
  [K in keyof Shape]: Shape[K]['hasDefault'] extends true ? K : never;
@@ -139,7 +145,7 @@ export class Db<
139
145
 
140
146
  const columnsParsers = {} as ColumnsParsers;
141
147
  let hasParsers = false;
142
- let modifyQuery: ((q: Query) => void)[] | undefined;
148
+ let modifyQuery: ((q: Query) => void)[] | undefined = undefined;
143
149
  for (const key in shape) {
144
150
  const column = shape[key];
145
151
  if (column.parseFn) {
@@ -166,6 +172,8 @@ export class Db<
166
172
  this.relations = {} as Relations;
167
173
 
168
174
  modifyQuery?.forEach((cb) => cb(this));
175
+
176
+ this.error = class extends QueryError {};
169
177
  }
170
178
  }
171
179
 
@@ -0,0 +1,79 @@
1
+ import { UniqueTable, User, useTestDatabase } from './test-utils';
2
+ import { raw } from './common';
3
+ import { columnTypes } from './columnSchema';
4
+ import { QueryError } from './errors';
5
+
6
+ describe('errors', () => {
7
+ useTestDatabase();
8
+
9
+ it('should capture stack trace properly', async () => {
10
+ let err: Error | undefined;
11
+
12
+ try {
13
+ await User.select({ column: raw(columnTypes.boolean(), 'koko') });
14
+ } catch (error) {
15
+ err = error as Error;
16
+ }
17
+
18
+ expect(err?.stack).toContain('errors.test.ts');
19
+ });
20
+
21
+ it('should have isUnique and column names map when violating unique error over single column', async () => {
22
+ await UniqueTable.insert({
23
+ one: 'one',
24
+ two: 1,
25
+ thirdColumn: 'three',
26
+ fourthColumn: 1,
27
+ });
28
+
29
+ let err: InstanceType<typeof UniqueTable.error> | undefined;
30
+
31
+ try {
32
+ await UniqueTable.insert({
33
+ one: 'one',
34
+ two: 2,
35
+ thirdColumn: 'three',
36
+ fourthColumn: 2,
37
+ });
38
+ } catch (error) {
39
+ if (error instanceof UniqueTable.error) {
40
+ err = error;
41
+ }
42
+ }
43
+
44
+ expect(err?.isUnique).toBe(true);
45
+ expect(err?.columns).toEqual({
46
+ one: true,
47
+ });
48
+ });
49
+
50
+ it('should have isUnique and column names map when violating unique error over multiple columns', async () => {
51
+ await UniqueTable.insert({
52
+ one: 'one',
53
+ two: 1,
54
+ thirdColumn: 'three',
55
+ fourthColumn: 1,
56
+ });
57
+
58
+ let err: QueryError | undefined;
59
+
60
+ try {
61
+ await UniqueTable.insert({
62
+ one: 'two',
63
+ two: 2,
64
+ thirdColumn: 'three',
65
+ fourthColumn: 1,
66
+ });
67
+ } catch (error) {
68
+ if (error instanceof QueryError) {
69
+ err = error;
70
+ }
71
+ }
72
+
73
+ expect(err?.isUnique).toBe(true);
74
+ expect(err?.columns).toEqual({
75
+ thirdColumn: true,
76
+ fourthColumn: true,
77
+ });
78
+ });
79
+ });
package/src/errors.ts CHANGED
@@ -1,5 +1,87 @@
1
+ import { DatabaseError } from 'pg';
2
+ import { ColumnsShape } from './columnSchema';
3
+
1
4
  export class PormError extends Error {}
2
5
 
6
+ export type QueryErrorName =
7
+ | 'parseComplete'
8
+ | 'bindComplete'
9
+ | 'closeComplete'
10
+ | 'noData'
11
+ | 'portalSuspended'
12
+ | 'replicationStart'
13
+ | 'emptyQuery'
14
+ | 'copyDone'
15
+ | 'copyData'
16
+ | 'rowDescription'
17
+ | 'parameterDescription'
18
+ | 'parameterStatus'
19
+ | 'backendKeyData'
20
+ | 'notification'
21
+ | 'readyForQuery'
22
+ | 'commandComplete'
23
+ | 'dataRow'
24
+ | 'copyInResponse'
25
+ | 'copyOutResponse'
26
+ | 'authenticationOk'
27
+ | 'authenticationMD5Password'
28
+ | 'authenticationCleartextPassword'
29
+ | 'authenticationSASL'
30
+ | 'authenticationSASLContinue'
31
+ | 'authenticationSASLFinal'
32
+ | 'error'
33
+ | 'notice';
34
+
35
+ export class QueryError<
36
+ T extends { shape: ColumnsShape } = { shape: ColumnsShape },
37
+ > extends DatabaseError {
38
+ message!: string;
39
+ name!: QueryErrorName;
40
+ stack: string | undefined;
41
+ code: string | undefined;
42
+ detail: string | undefined;
43
+ severity: string | undefined;
44
+ hint: string | undefined;
45
+ position: string | undefined;
46
+ internalPosition: string | undefined;
47
+ internalQuery: string | undefined;
48
+ where: string | undefined;
49
+ schema: string | undefined;
50
+ table: string | undefined;
51
+ column: string | undefined;
52
+ dataType: string | undefined;
53
+ constraint: string | undefined;
54
+ file: string | undefined;
55
+ line: string | undefined;
56
+ routine: string | undefined;
57
+
58
+ get isUnique() {
59
+ return this.code === '23505';
60
+ }
61
+
62
+ columnsCache?: { [K in keyof T['shape']]?: true };
63
+ get columns() {
64
+ if (this.columnsCache) return this.columnsCache;
65
+
66
+ const columns: { [K in keyof T['shape']]?: true } = {};
67
+
68
+ if (this.detail) {
69
+ const list = this.detail.match(/\((.*)\)=/)?.[1];
70
+ if (list) {
71
+ list.split(', ').forEach((item) => {
72
+ const column = (
73
+ item.startsWith('"') ? item.slice(1, -1) : item
74
+ ) as keyof T['shape'];
75
+
76
+ columns[column] = true;
77
+ });
78
+ }
79
+ }
80
+
81
+ return (this.columnsCache = columns);
82
+ }
83
+ }
84
+
3
85
  export class NotFoundError extends PormError {
4
86
  constructor(message = 'Record is not found') {
5
87
  super(message);
package/src/query.ts CHANGED
@@ -13,6 +13,7 @@ import { EmptyObject, Spread } from './utils';
13
13
  import { AliasOrTable, RawExpression, StringKey } from './common';
14
14
  import { Db } from './db';
15
15
  import { RelationQueryBase, RelationsBase } from './relations';
16
+ import { QueryError, QueryErrorName } from './errors';
16
17
 
17
18
  export type ColumnParser = (input: unknown) => unknown;
18
19
  export type ColumnsParsers = Record<string | getValueKey, ColumnParser>;
@@ -64,6 +65,11 @@ export type Query = QueryMethods & {
64
65
  columnsParsers?: ColumnsParsers;
65
66
  relations: RelationsBase;
66
67
  withData: WithDataBase;
68
+ error: new (
69
+ message: string,
70
+ length: number,
71
+ name: QueryErrorName,
72
+ ) => QueryError;
67
73
  // eslint-disable-next-line @typescript-eslint/ban-types
68
74
  [defaultsKey]: {};
69
75
  };
@@ -1,8 +1,10 @@
1
- import { assertType, User } from '../test-utils';
1
+ import { assertType, User, useTestDatabase } from '../test-utils';
2
2
  import { raw } from '../common';
3
3
  import { columnTypes } from '../columnSchema';
4
4
 
5
5
  describe('then', () => {
6
+ useTestDatabase();
7
+
6
8
  describe('catch', () => {
7
9
  it('should catch error', (done) => {
8
10
  const query = User.select({
@@ -1,10 +1,11 @@
1
1
  import { ColumnsParsers, Query, QueryReturnType } from '../query';
2
2
  import { getQueryParsers } from '../common';
3
- import { NotFoundError } from '../errors';
3
+ import { NotFoundError, QueryError } from '../errors';
4
4
  import { QueryArraysResult, QueryResult } from '../adapter';
5
5
  import { CommonQueryData, Sql } from '../sql';
6
6
  import { AfterCallback, BeforeCallback } from './callbacks';
7
7
  import { getValueKey } from './get';
8
+ import { DatabaseError } from 'pg';
8
9
 
9
10
  export type ThenResult<Res> = (
10
11
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -28,20 +29,17 @@ export const queryMethodByReturnType: Record<
28
29
  void: 'arrays',
29
30
  };
30
31
 
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ type Resolve = (result: any) => any;
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ type Reject = (error: any) => any;
36
+
37
+ let queryError: Error = undefined as unknown as Error;
38
+
31
39
  export class Then {
32
- then(
33
- this: Query,
34
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
- resolve?: (result: any) => any,
36
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
- reject?: (error: any) => any,
38
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
- ): Promise<any> {
40
- if (this.query.wrapInTransaction && !this.query.inTransaction) {
41
- return this.transaction((q) => then(q, resolve, reject));
42
- } else {
43
- return then(this, resolve, reject);
44
- }
40
+ get then() {
41
+ queryError = new Error();
42
+ return maybeWrappedThen;
45
43
  }
46
44
 
47
45
  async catch<T extends Query, Result>(
@@ -60,6 +58,14 @@ export const handleResult: CommonQueryData['handleResult'] = async (
60
58
  return parseResult(q, q.query.returnType || 'all', result);
61
59
  };
62
60
 
61
+ function maybeWrappedThen(this: Query, resolve?: Resolve, reject?: Reject) {
62
+ if (this.query.wrapInTransaction && !this.query.inTransaction) {
63
+ return this.transaction((q) => then(q, resolve, reject));
64
+ } else {
65
+ return then(this, resolve, reject);
66
+ }
67
+ }
68
+
63
69
  const then = async (
64
70
  q: Query,
65
71
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -117,7 +123,9 @@ const then = async (
117
123
  }
118
124
 
119
125
  resolve?.(result);
120
- } catch (error) {
126
+ } catch (err) {
127
+ const error = err instanceof DatabaseError ? assignError(q, err) : err;
128
+
121
129
  if (q.query.log && sql && logData) {
122
130
  q.query.log.onError(error as Error, sql, logData);
123
131
  }
@@ -125,6 +133,32 @@ const then = async (
125
133
  }
126
134
  };
127
135
 
136
+ const assignError = (q: Query, from: DatabaseError) => {
137
+ const to = new (q.error as unknown as new () => QueryError)();
138
+ to.stack = queryError.stack;
139
+ to.message = from.message;
140
+ (to as { length?: number }).length = from.length;
141
+ (to as { name?: string }).name = from.name;
142
+ to.severity = from.severity;
143
+ to.code = from.code;
144
+ to.detail = from.detail;
145
+ to.hint = from.hint;
146
+ to.position = from.position;
147
+ to.internalPosition = from.internalPosition;
148
+ to.internalQuery = from.internalQuery;
149
+ to.where = from.where;
150
+ to.schema = from.schema;
151
+ to.table = from.table;
152
+ to.column = from.column;
153
+ to.dataType = from.dataType;
154
+ to.constraint = from.constraint;
155
+ to.file = from.file;
156
+ to.line = from.line;
157
+ to.routine = from.routine;
158
+
159
+ return to;
160
+ };
161
+
128
162
  export const parseResult = (
129
163
  q: Query,
130
164
  returnType: QueryReturnType | undefined = 'all',
package/src/test-utils.ts CHANGED
@@ -60,6 +60,14 @@ export const Chat = db('chat', (t) => ({
60
60
  ...t.timestamps(),
61
61
  }));
62
62
 
63
+ export const UniqueTable = db('uniqueTable', (t) => ({
64
+ one: t.text().unique(),
65
+ two: t.integer().unique(),
66
+ thirdColumn: t.text(),
67
+ fourthColumn: t.integer(),
68
+ ...t.unique(['thirdColumn', 'fourthColumn']),
69
+ }));
70
+
63
71
  export type MessageRecord = typeof Message['type'];
64
72
  export const Message = db('message', (t) => ({
65
73
  id: t.serial().primaryKey(),