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/dist/index.d.ts +52 -16
- package/dist/index.esm.js +63 -10
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +62 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/pnpm-lock.yaml +2821 -0
- package/src/db.ts +9 -1
- package/src/errors.test.ts +79 -0
- package/src/errors.ts +82 -0
- package/src/query.ts +6 -0
- package/src/queryMethods/then.test.ts +3 -1
- package/src/queryMethods/then.ts +49 -15
- package/src/test-utils.ts +8 -0
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({
|
package/src/queryMethods/then.ts
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
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 (
|
|
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(),
|