@proteinjs/db-driver-knex 1.15.0 → 1.15.2
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/CHANGELOG.md +21 -0
- package/README.md +5 -3
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/test/index.js +1 -1
- package/dist/generated/test/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/KnexDriver.d.ts +0 -7
- package/dist/src/KnexDriver.d.ts.map +1 -1
- package/dist/src/KnexDriver.js +37 -34
- package/dist/src/KnexDriver.js.map +1 -1
- package/dist/src/KnexFailureLine.d.ts +58 -0
- package/dist/src/KnexFailureLine.d.ts.map +1 -0
- package/dist/src/KnexFailureLine.js +124 -0
- package/dist/src/KnexFailureLine.js.map +1 -0
- package/dist/test/LogValuesSwitch.test.js +13 -7
- package/dist/test/LogValuesSwitch.test.js.map +1 -1
- package/dist/test/StatementLogNeverThrows.test.js +23 -29
- package/dist/test/StatementLogNeverThrows.test.js.map +1 -1
- package/dist/test/StatementParamLogging.test.js +16 -41
- package/dist/test/StatementParamLogging.test.js.map +1 -1
- package/dist/test/VendorTextNeverOnALine.test.d.ts +2 -0
- package/dist/test/VendorTextNeverOnALine.test.d.ts.map +1 -0
- package/dist/test/VendorTextNeverOnALine.test.js +330 -0
- package/dist/test/VendorTextNeverOnALine.test.js.map +1 -0
- package/generated/index.ts +1 -1
- package/generated/test/index.ts +1 -1
- package/index.ts +1 -0
- package/package.json +6 -6
- package/src/KnexDriver.ts +23 -36
- package/src/KnexFailureLine.ts +127 -0
- package/test/LogValuesSwitch.test.ts +3 -1
- package/test/StatementLogNeverThrows.test.ts +24 -20
- package/test/StatementParamLogging.test.ts +12 -17
- package/test/VendorTextNeverOnALine.test.ts +257 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import knex from 'knex';
|
|
2
|
+
import { KnexDriver } from '@proteinjs/db-driver-knex';
|
|
3
|
+
import { Logger } from '@proteinjs/logger';
|
|
4
|
+
import { CapturedLog, lineOf } from './util/printedLine';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The vendor's text never reaches a LOG LINE — the driver's own line, and any line a caller
|
|
8
|
+
* writes about the error it caught — while what the driver THROWS is the vendor's error itself,
|
|
9
|
+
* untouched (StatementParamLogging.test.ts holds that law; here the premise of every scenario is
|
|
10
|
+
* that the caught error still carries the values).
|
|
11
|
+
*
|
|
12
|
+
* The vendor error carries a statement's values three ways: the query layer rewrites its
|
|
13
|
+
* `message` to the SQL with every binding INTERPOLATED, the client library carries the same text
|
|
14
|
+
* as `sql`, and the server's own message quotes the value it refused (`Duplicate entry '<key>'
|
|
15
|
+
* for key 'PRIMARY'`). The driver's line never carried the first two; it did carry the third, and
|
|
16
|
+
* a caller that logged what it caught — whole, as a line's `error` or inside its `obj` — printed
|
|
17
|
+
* all three.
|
|
18
|
+
*
|
|
19
|
+
* So a line carries the vendor's CODES and the driver's own sentence. No server is needed: the
|
|
20
|
+
* REAL query layer and dialect run over a stub connection that rejects the way the client library
|
|
21
|
+
* does; lines are captured at the log WRITER and judged as the text a writer would print.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Fixture values shaped like the row content that must never be logged (none is a real credential).
|
|
25
|
+
const SECRET_TOKEN = 'rst_5d41402abc4b2a76b9719d911017c592';
|
|
26
|
+
const SECRET_EMAIL = 'casey.rivers@mail.example';
|
|
27
|
+
const ECHOED_KEY = 'c0ffee00-0000-4000-8000-000000000001';
|
|
28
|
+
const VALUES = [SECRET_TOKEN, SECRET_EMAIL, ECHOED_KEY];
|
|
29
|
+
|
|
30
|
+
type DriverStatics = { KNEX?: unknown };
|
|
31
|
+
|
|
32
|
+
/** What the stub's COMMIT answers with; nothing, unless a test says otherwise. */
|
|
33
|
+
let commitRejection: (() => Error) | undefined;
|
|
34
|
+
/** Whether the stub leaves every statement unanswered (a stuck server) instead of rejecting it. */
|
|
35
|
+
let silentStatements = false;
|
|
36
|
+
|
|
37
|
+
/** A connection that rejects every statement the way the client library does on a duplicate key. */
|
|
38
|
+
const rejectingConnection = () => {
|
|
39
|
+
const mysql = require('mysql');
|
|
40
|
+
return {
|
|
41
|
+
state: 'authenticated',
|
|
42
|
+
on: () => undefined,
|
|
43
|
+
removeAllListeners: () => undefined,
|
|
44
|
+
connect: (done: (error?: Error) => void) => done(),
|
|
45
|
+
end: (done: (error?: Error) => void) => done(),
|
|
46
|
+
query: (options: { sql: string }, bindings: any[], done: (error: Error | null, rows?: unknown[]) => void) => {
|
|
47
|
+
if (/^\s*COMMIT/i.test(options.sql) && commitRejection) {
|
|
48
|
+
done(commitRejection());
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (/^\s*(BEGIN|COMMIT|ROLLBACK)/i.test(options.sql)) {
|
|
52
|
+
done(null, []);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (/^\s*SELECT 1/i.test(options.sql)) {
|
|
56
|
+
done(null, [[{ 1: 1 }], []]);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (/^\s*KILL QUERY/i.test(options.sql)) {
|
|
60
|
+
done(null, []);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (silentStatements) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const sqlMessage = `Duplicate entry '${bindings[0]}' for key 'PRIMARY'`;
|
|
67
|
+
done(
|
|
68
|
+
Object.assign(new Error(`ER_DUP_ENTRY: ${sqlMessage}`), {
|
|
69
|
+
code: 'ER_DUP_ENTRY',
|
|
70
|
+
errno: 1062,
|
|
71
|
+
sqlState: '23000',
|
|
72
|
+
sqlMessage,
|
|
73
|
+
sql: mysql.format(options.sql, bindings),
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const settle = <T>(promise: Promise<T>): Promise<any> =>
|
|
81
|
+
promise.then(() => 'resolved' as const).catch((error) => error);
|
|
82
|
+
|
|
83
|
+
describe('the vendor`s text never reaches a log line; the thrown error still carries it', () => {
|
|
84
|
+
const driver = new KnexDriver({ host: 'localhost', user: 'root', password: '', dbName: 'test' });
|
|
85
|
+
const internals = driver as unknown as { logger: Logger };
|
|
86
|
+
const statics = KnexDriver as unknown as DriverStatics;
|
|
87
|
+
let captured: CapturedLog[] = [];
|
|
88
|
+
let instance: ReturnType<typeof knex>;
|
|
89
|
+
let previousInstance: unknown;
|
|
90
|
+
const capturing = (name: string) =>
|
|
91
|
+
new Logger({
|
|
92
|
+
name,
|
|
93
|
+
logLevel: 'debug',
|
|
94
|
+
logWriter: { write: (log: CapturedLog) => captured.push(log) } as any,
|
|
95
|
+
});
|
|
96
|
+
const callerLogger = capturing('ACaller');
|
|
97
|
+
|
|
98
|
+
beforeAll(() => {
|
|
99
|
+
instance = knex({ client: 'mysql', connection: { host: 'localhost', user: 'root', password: '' } });
|
|
100
|
+
(instance as any).client.driver = { createConnection: rejectingConnection };
|
|
101
|
+
previousInstance = statics.KNEX;
|
|
102
|
+
statics.KNEX = instance;
|
|
103
|
+
internals.logger = capturing('KnexDriver');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterAll(async () => {
|
|
107
|
+
statics.KNEX = previousInstance;
|
|
108
|
+
await instance.destroy();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
captured = [];
|
|
113
|
+
commitRejection = undefined;
|
|
114
|
+
silentStatements = false;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const INSERT = 'INSERT INTO `credential` (`id`, `email`, `token`, `attempts`, `cleared`) VALUES (?, ?, ?, ?, ?)';
|
|
118
|
+
const insertStatement = () => ({ sql: INSERT, params: [ECHOED_KEY, SECRET_EMAIL, SECRET_TOKEN, 3, null] });
|
|
119
|
+
|
|
120
|
+
/** The caller logs what it caught; no line — the driver's or the caller's — carries a value. */
|
|
121
|
+
const assertNeverOnALine = (caught: any) => {
|
|
122
|
+
callerLogger.error({ message: 'A caller`s own line about what it caught', error: caught });
|
|
123
|
+
callerLogger.warn({ message: 'A caller`s own line about what it caught', obj: { caught, list: [caught] } });
|
|
124
|
+
|
|
125
|
+
expect(captured.filter((log) => log.message === 'A caller`s own line about what it caught')).toHaveLength(2);
|
|
126
|
+
for (const log of captured) {
|
|
127
|
+
for (const value of VALUES) {
|
|
128
|
+
expect(lineOf(log)).not.toContain(value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
test('a failed statement: the caught error carries every value; the driver`s line and a caller`s lines carry the codes and the sentence', async () => {
|
|
134
|
+
const caught = await settle(driver.runDml(insertStatement));
|
|
135
|
+
|
|
136
|
+
// The premise: the vendor error, as thrown, carries the values all three ways.
|
|
137
|
+
expect(caught.message).toContain(SECRET_TOKEN);
|
|
138
|
+
expect(caught.sql).toContain(SECRET_EMAIL);
|
|
139
|
+
expect(caught.sqlMessage).toContain(ECHOED_KEY);
|
|
140
|
+
|
|
141
|
+
assertNeverOnALine(caught);
|
|
142
|
+
const [failure] = captured.filter((log) => log.message === 'Failed when executing sql');
|
|
143
|
+
expect(failure.obj.cause).toEqual({
|
|
144
|
+
name: 'Error',
|
|
145
|
+
code: 'ER_DUP_ENTRY',
|
|
146
|
+
errno: 1062,
|
|
147
|
+
sqlState: '23000',
|
|
148
|
+
message: 'a row with that key already exists',
|
|
149
|
+
});
|
|
150
|
+
const [callersLine] = captured.filter((log) => log.message === 'A caller`s own line about what it caught');
|
|
151
|
+
expect(callersLine.error.message).toBe(
|
|
152
|
+
'Failed when executing sql (ER_DUP_ENTRY, errno 1062): a row with that key already exists'
|
|
153
|
+
);
|
|
154
|
+
expect(callersLine.error).toMatchObject({ code: 'ER_DUP_ENTRY', errno: 1062, sqlState: '23000' });
|
|
155
|
+
expect(String(callersLine.error.stack).split('\n').length).toBeGreaterThan(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test.each([
|
|
159
|
+
['DEVELOPMENT set AND DB_LOG_PARAM_VALUES=1', { DEVELOPMENT: 'true', DB_LOG_PARAM_VALUES: '1' }, true],
|
|
160
|
+
['DEVELOPMENT unset, DB_LOG_PARAM_VALUES=1', { DB_LOG_PARAM_VALUES: '1' }, false],
|
|
161
|
+
['DEVELOPMENT set, DB_LOG_PARAM_VALUES unset', { DEVELOPMENT: 'true' }, false],
|
|
162
|
+
])(
|
|
163
|
+
'the vendor`s text rides a line only behind the dev-only values switch — %s',
|
|
164
|
+
async (_gate, env: { [name: string]: string }, open) => {
|
|
165
|
+
const before = { DEVELOPMENT: process.env.DEVELOPMENT, DB_LOG_PARAM_VALUES: process.env.DB_LOG_PARAM_VALUES };
|
|
166
|
+
delete process.env.DEVELOPMENT;
|
|
167
|
+
delete process.env.DB_LOG_PARAM_VALUES;
|
|
168
|
+
Object.assign(process.env, env);
|
|
169
|
+
try {
|
|
170
|
+
const caught = await settle(driver.runDml(insertStatement));
|
|
171
|
+
callerLogger.error({ message: 'A caller`s own line', error: caught });
|
|
172
|
+
|
|
173
|
+
const [callersLine] = captured.filter((log) => log.message === 'A caller`s own line');
|
|
174
|
+
const [failure] = captured.filter((log) => log.message === 'Failed when executing sql');
|
|
175
|
+
if (open) {
|
|
176
|
+
expect(callersLine.error).toBe(caught);
|
|
177
|
+
expect(failure.obj.cause.sqlMessage).toContain(ECHOED_KEY);
|
|
178
|
+
} else {
|
|
179
|
+
expect(callersLine.error).not.toBe(caught);
|
|
180
|
+
expect(Object.keys(failure.obj.cause)).not.toContain('sqlMessage');
|
|
181
|
+
for (const log of captured) {
|
|
182
|
+
expect(lineOf(log)).not.toContain(ECHOED_KEY);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} finally {
|
|
186
|
+
for (const [name, value] of Object.entries(before)) {
|
|
187
|
+
if (value === undefined) {
|
|
188
|
+
delete process.env[name];
|
|
189
|
+
} else {
|
|
190
|
+
process.env[name] = value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
test('a statement past the driver`s deadline: the caught error carries the bound values; every line says the statement timed out, and carries none', async () => {
|
|
198
|
+
const timedOut = 'the statement or its wait for a connection timed out';
|
|
199
|
+
silentStatements = true;
|
|
200
|
+
const deadlined = new KnexDriver({
|
|
201
|
+
host: 'localhost',
|
|
202
|
+
user: 'root',
|
|
203
|
+
password: '',
|
|
204
|
+
dbName: 'test',
|
|
205
|
+
operationDeadlineMs: 300,
|
|
206
|
+
});
|
|
207
|
+
(deadlined as unknown as { logger: Logger }).logger = capturing('KnexDriver');
|
|
208
|
+
|
|
209
|
+
const caught = await settle(deadlined.runDml(insertStatement));
|
|
210
|
+
|
|
211
|
+
// The premise: the query layer's timeout error, as thrown, names the deadline and carries the bindings.
|
|
212
|
+
expect(caught.name).toBe('KnexTimeoutError');
|
|
213
|
+
expect(caught.message).toMatch(/Defined query timeout of 300ms exceeded/);
|
|
214
|
+
expect(caught.bindings).toContain(SECRET_TOKEN);
|
|
215
|
+
|
|
216
|
+
assertNeverOnALine(caught);
|
|
217
|
+
const [failure] = captured.filter((log) => log.message === 'Failed when executing sql');
|
|
218
|
+
expect(failure.obj.cause).toEqual({ name: 'KnexTimeoutError', message: timedOut });
|
|
219
|
+
const [callersLine] = captured.filter((log) => log.message === 'A caller`s own line about what it caught');
|
|
220
|
+
expect(callersLine.error.name).toBe('KnexTimeoutError');
|
|
221
|
+
expect(callersLine.error.message).toBe(`Failed when executing sql: ${timedOut}`);
|
|
222
|
+
}, 10_000);
|
|
223
|
+
|
|
224
|
+
test('the same inside a transaction', async () => {
|
|
225
|
+
const caught = await settle(driver.runTransaction((transaction) => driver.runDml(insertStatement, transaction)));
|
|
226
|
+
|
|
227
|
+
expect(caught.message).toContain(SECRET_TOKEN);
|
|
228
|
+
assertNeverOnALine(caught);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('a commit the server refuses leaves through the transaction`s door, marked there; a caller`s own error thrown in the body prints as it is', async () => {
|
|
232
|
+
commitRejection = () =>
|
|
233
|
+
Object.assign(new Error(`ER_DUP_ENTRY: Duplicate entry '${ECHOED_KEY}' for key 'email'`), {
|
|
234
|
+
code: 'ER_DUP_ENTRY',
|
|
235
|
+
errno: 1062,
|
|
236
|
+
sqlState: '23000',
|
|
237
|
+
sqlMessage: `Duplicate entry '${ECHOED_KEY}' for key 'email'`,
|
|
238
|
+
});
|
|
239
|
+
const refusedCommit = await settle(driver.runTransaction(async () => 'the body ran clean'));
|
|
240
|
+
|
|
241
|
+
expect(String(refusedCommit.message)).toContain(ECHOED_KEY);
|
|
242
|
+
assertNeverOnALine(refusedCommit);
|
|
243
|
+
|
|
244
|
+
captured = [];
|
|
245
|
+
commitRejection = undefined;
|
|
246
|
+
const callersOwn = new Error('The order is already closed');
|
|
247
|
+
const outcome = await settle(
|
|
248
|
+
driver.runTransaction(async () => {
|
|
249
|
+
throw callersOwn;
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
callerLogger.error({ message: 'A caller`s own line', error: outcome });
|
|
253
|
+
|
|
254
|
+
expect(outcome).toBe(callersOwn);
|
|
255
|
+
expect(captured[0].error).toBe(callersOwn);
|
|
256
|
+
});
|
|
257
|
+
});
|