@proteinjs/db-driver-knex 1.13.9 → 1.14.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/CHANGELOG.md +17 -0
- package/README.md +10 -0
- 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 +66 -0
- package/dist/src/KnexDriver.d.ts.map +1 -1
- package/dist/src/KnexDriver.js +181 -1
- package/dist/src/KnexDriver.js.map +1 -1
- package/dist/src/KnexLogValues.d.ts +38 -0
- package/dist/src/KnexLogValues.d.ts.map +1 -0
- package/dist/src/KnexLogValues.js +47 -0
- package/dist/src/KnexLogValues.js.map +1 -0
- package/dist/test/LogValuesSwitch.test.d.ts +2 -0
- package/dist/test/LogValuesSwitch.test.d.ts.map +1 -0
- package/dist/test/LogValuesSwitch.test.js +255 -0
- package/dist/test/LogValuesSwitch.test.js.map +1 -0
- package/dist/test/StatementLogNeverThrows.test.d.ts +2 -0
- package/dist/test/StatementLogNeverThrows.test.d.ts.map +1 -0
- package/dist/test/StatementLogNeverThrows.test.js +541 -0
- package/dist/test/StatementLogNeverThrows.test.js.map +1 -0
- package/dist/test/StatementParamLogging.test.d.ts +2 -0
- package/dist/test/StatementParamLogging.test.d.ts.map +1 -0
- package/dist/test/StatementParamLogging.test.js +403 -0
- package/dist/test/StatementParamLogging.test.js.map +1 -0
- package/dist/test/util/printedLine.d.ts +20 -0
- package/dist/test/util/printedLine.d.ts.map +1 -0
- package/dist/test/util/printedLine.js +48 -0
- package/dist/test/util/printedLine.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 +4 -4
- package/src/KnexDriver.ts +191 -2
- package/src/KnexLogValues.ts +42 -0
- package/test/LogValuesSwitch.test.ts +201 -0
- package/test/StatementLogNeverThrows.test.ts +466 -0
- package/test/StatementParamLogging.test.ts +313 -0
- package/test/util/printedLine.ts +43 -0
|
@@ -0,0 +1,313 @@
|
|
|
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 driver never PRINTS a bound parameter's value. A statement's parameters are row content: a
|
|
8
|
+
* presented reset token, a password hash, an email address. Before this contract a failed
|
|
9
|
+
* statement put every bound value on the ERROR line twice over — the query layer rewrites a failed
|
|
10
|
+
* query's `message` to the SQL with its bindings INTERPOLATED, and the client library's error
|
|
11
|
+
* carries the same formatted text as `sql` — because the driver handed that vendor error to the
|
|
12
|
+
* logger.
|
|
13
|
+
*
|
|
14
|
+
* The contract: the failure line carries the SQL text (placeholders only), the parameters
|
|
15
|
+
* DESCRIBED — each one's position, the kind of its value and, for strings, arrays and bytes, its
|
|
16
|
+
* length; one private helper (`describeParams`) owns that form — and a summary of the failure (the
|
|
17
|
+
* error's name, the vendor's codes, the server's own message). The vendor error itself no longer
|
|
18
|
+
* rides the line. (The dev-only switch that adds the values back is LogValuesSwitch.test.ts; it is
|
|
19
|
+
* off here.) A statement's bindings are typed as an array, but the query layer also accepts a
|
|
20
|
+
* bindings DICTIONARY (`:name` placeholders) and the driver forwards what it was handed: a
|
|
21
|
+
* dictionary is described by entry name. (Every other shape the driver can be handed, and a log
|
|
22
|
+
* writer that throws, are StatementLogNeverThrows.test.ts.)
|
|
23
|
+
*
|
|
24
|
+
* THE LAW beside it: what the driver THROWS is untouched — the vendor's error itself, the same
|
|
25
|
+
* object with the same message and properties a bare query-layer call rejects with. So a caller
|
|
26
|
+
* that prints what it catches still prints the interpolated SQL; that is the query layer's
|
|
27
|
+
* behaviour and this contract does not change it.
|
|
28
|
+
*
|
|
29
|
+
* SCOPE, by name: this suite holds what the DRIVER prints of the parameters it was handed. It does
|
|
30
|
+
* NOT hold the vendor-echo class — the server's own message can quote a bound value (`Duplicate
|
|
31
|
+
* entry '…' for key 'PRIMARY'`), and that text reaches the line's cause summary and the thrown
|
|
32
|
+
* error exactly as it did before. Closing that class means changing what the driver throws; that
|
|
33
|
+
* work is parked on the branch `fix/driver-logs-never-carry-param-values`, pending a decision. So
|
|
34
|
+
* the canaries below are bound to parameters the server does not echo, and the one value it does
|
|
35
|
+
* echo — the colliding key — is left out of the assertions on purpose.
|
|
36
|
+
*
|
|
37
|
+
* No server is needed: the REAL query layer and dialect run over a stub connection that rejects
|
|
38
|
+
* the way the client library does (its own `format` builds the interpolated `sql`), so the
|
|
39
|
+
* interpolated message under test is the query layer's own.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// Fixture values shaped like the row content that must never be logged (none is a real credential).
|
|
43
|
+
const SECRET_TOKEN = 'rst_5d41402abc4b2a76b9719d911017c592';
|
|
44
|
+
const SECRET_EMAIL = 'casey.rivers@mail.example';
|
|
45
|
+
const ECHOED_KEY = 'c0ffee00-0000-4000-8000-000000000001';
|
|
46
|
+
|
|
47
|
+
type ParamDescription = { type: string; length?: number; null?: true };
|
|
48
|
+
type ParamsDescription = ParamDescription[] | { [name: string]: ParamDescription } | 'unreadable';
|
|
49
|
+
type DriverInternals = {
|
|
50
|
+
logger: Logger;
|
|
51
|
+
describeParams: (params?: unknown) => ParamsDescription | undefined;
|
|
52
|
+
};
|
|
53
|
+
type DriverStatics = { KNEX?: unknown };
|
|
54
|
+
|
|
55
|
+
/** The errors the stub connection rejected with, as they left the client library's callback. */
|
|
56
|
+
const rejected: Error[] = [];
|
|
57
|
+
|
|
58
|
+
/** A connection that lets transaction control through and rejects every statement the way the client library does on a duplicate key. */
|
|
59
|
+
const rejectingConnection = () => {
|
|
60
|
+
const mysql = require('mysql');
|
|
61
|
+
return {
|
|
62
|
+
state: 'authenticated',
|
|
63
|
+
on: () => undefined,
|
|
64
|
+
removeAllListeners: () => undefined,
|
|
65
|
+
connect: (done: (error?: Error) => void) => done(),
|
|
66
|
+
end: (done: (error?: Error) => void) => done(),
|
|
67
|
+
query: (options: { sql: string }, bindings: any[], done: (error: Error | null, rows?: unknown[]) => void) => {
|
|
68
|
+
if (/^\s*(BEGIN|COMMIT|ROLLBACK)/i.test(options.sql)) {
|
|
69
|
+
done(null, []);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const sqlMessage = `Duplicate entry '${bindings[0]}' for key 'PRIMARY'`;
|
|
73
|
+
const error = Object.assign(new Error(`ER_DUP_ENTRY: ${sqlMessage}`), {
|
|
74
|
+
code: 'ER_DUP_ENTRY',
|
|
75
|
+
errno: 1062,
|
|
76
|
+
sqlState: '23000',
|
|
77
|
+
sqlMessage,
|
|
78
|
+
sql: mysql.format(options.sql, bindings),
|
|
79
|
+
});
|
|
80
|
+
rejected.push(error);
|
|
81
|
+
done(error);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const settle = <T>(promise: Promise<T>): Promise<any> =>
|
|
87
|
+
promise.then(() => 'resolved' as const).catch((error) => error);
|
|
88
|
+
|
|
89
|
+
describe('The driver never prints a bound value; what it throws is the vendor`s error, untouched', () => {
|
|
90
|
+
const driver = new KnexDriver({ host: 'localhost', user: 'root', password: '', dbName: 'test' });
|
|
91
|
+
const internals = driver as unknown as DriverInternals;
|
|
92
|
+
const statics = KnexDriver as unknown as DriverStatics;
|
|
93
|
+
let captured: CapturedLog[] = [];
|
|
94
|
+
let instance: ReturnType<typeof knex>;
|
|
95
|
+
let previousInstance: unknown;
|
|
96
|
+
|
|
97
|
+
beforeAll(() => {
|
|
98
|
+
instance = knex({ client: 'mysql', connection: { host: 'localhost', user: 'root', password: '' } });
|
|
99
|
+
// The dialect's own connection lifecycle, over a stub client library.
|
|
100
|
+
(instance as any).client.driver = { createConnection: rejectingConnection };
|
|
101
|
+
previousInstance = statics.KNEX;
|
|
102
|
+
statics.KNEX = instance;
|
|
103
|
+
internals.logger = new Logger({
|
|
104
|
+
name: 'KnexDriver',
|
|
105
|
+
logLevel: 'debug',
|
|
106
|
+
logWriter: { write: (log: CapturedLog) => captured.push(log) } as any,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterAll(async () => {
|
|
111
|
+
statics.KNEX = previousInstance;
|
|
112
|
+
await instance.destroy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
captured = [];
|
|
117
|
+
rejected.length = 0;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const INSERT = 'INSERT INTO `credential` (`id`, `email`, `token`, `attempts`, `cleared`) VALUES (?, ?, ?, ?, ?)';
|
|
121
|
+
const insertStatement = () => ({ sql: INSERT, params: [ECHOED_KEY, SECRET_EMAIL, SECRET_TOKEN, 3, null] });
|
|
122
|
+
/** Everything a caller can read off a caught error. */
|
|
123
|
+
const facts = (error: any) => ({
|
|
124
|
+
class: error.constructor.name,
|
|
125
|
+
message: error.message,
|
|
126
|
+
own: Object.getOwnPropertyNames(error)
|
|
127
|
+
.sort()
|
|
128
|
+
.map((name) => [name, name === 'stack' ? '(stack)' : error[name]]),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('the premise: the vendor error of a failed statement carries the bound values', async () => {
|
|
132
|
+
const vendorError = await settle(instance.raw('INSERT INTO `t` (`token`) VALUES (?)', [SECRET_TOKEN]) as any);
|
|
133
|
+
|
|
134
|
+
expect(vendorError.message).toContain(SECRET_TOKEN);
|
|
135
|
+
expect(vendorError.sql).toContain(SECRET_TOKEN);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('a failed statement: the error line describes each parameter and carries no value it was handed', async () => {
|
|
139
|
+
await settle(driver.runDml(insertStatement));
|
|
140
|
+
|
|
141
|
+
const failures = captured.filter((log) => log.logLevel === 'error' && log.message === 'Failed when executing sql');
|
|
142
|
+
expect(failures).toHaveLength(1);
|
|
143
|
+
// (`ECHOED_KEY` is NOT asserted: the server quotes the colliding key in its own message — the
|
|
144
|
+
// vendor-echo class this suite leaves out by name; see the header.)
|
|
145
|
+
for (const log of captured) {
|
|
146
|
+
expect(lineOf(log)).not.toContain(SECRET_TOKEN);
|
|
147
|
+
expect(lineOf(log)).not.toContain(SECRET_EMAIL);
|
|
148
|
+
}
|
|
149
|
+
const [failure] = failures;
|
|
150
|
+
expect(failure.obj.sql).toBe(INSERT);
|
|
151
|
+
expect(failure.obj.params).toEqual([
|
|
152
|
+
{ type: 'string', length: 36 },
|
|
153
|
+
{ type: 'string', length: SECRET_EMAIL.length },
|
|
154
|
+
{ type: 'string', length: SECRET_TOKEN.length },
|
|
155
|
+
{ type: 'number' },
|
|
156
|
+
{ type: 'null', null: true },
|
|
157
|
+
]);
|
|
158
|
+
expect(failure.obj.cause).toEqual({
|
|
159
|
+
name: 'Error',
|
|
160
|
+
code: 'ER_DUP_ENTRY',
|
|
161
|
+
errno: 1062,
|
|
162
|
+
sqlState: '23000',
|
|
163
|
+
sqlMessage: `Duplicate entry '${ECHOED_KEY}' for key 'PRIMARY'`,
|
|
164
|
+
});
|
|
165
|
+
expect(Object.keys(failure.obj)).not.toContain('paramValues');
|
|
166
|
+
// The vendor error itself — the carrier of the interpolated SQL — is not on the line.
|
|
167
|
+
expect(failure.error).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('the law: the throw is the vendor`s error itself — the object the client library rejected with, reading as a bare query-layer call`s does', async () => {
|
|
171
|
+
const bare = await settle(instance.raw(INSERT, insertStatement().params) as any);
|
|
172
|
+
rejected.length = 0;
|
|
173
|
+
|
|
174
|
+
const outcome = await settle(driver.runDml(insertStatement));
|
|
175
|
+
|
|
176
|
+
expect(rejected).toHaveLength(1);
|
|
177
|
+
expect(outcome).toBe(rejected[0]);
|
|
178
|
+
expect(facts(outcome)).toEqual(facts(bare));
|
|
179
|
+
expect(outcome.code).toBe('ER_DUP_ENTRY');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('a failed query rides the same door', async () => {
|
|
183
|
+
const outcome = await settle(
|
|
184
|
+
driver.runQuery(() => ({ sql: 'SELECT `id` FROM `credential` WHERE `token` = ?', params: [SECRET_TOKEN] }))
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(outcome).toBe(rejected[0]);
|
|
188
|
+
const failures = captured.filter((log) => log.logLevel === 'error');
|
|
189
|
+
expect(failures).toHaveLength(1);
|
|
190
|
+
expect(failures[0].obj.params).toEqual([{ type: 'string', length: SECRET_TOKEN.length }]);
|
|
191
|
+
// This statement's first binding IS the token, so the stub server echoes it: the summary's
|
|
192
|
+
// `sqlMessage` is the vendor-echo class. Everything else on the line is the driver's.
|
|
193
|
+
const { sqlMessage, ...ownFacts } = failures[0].obj.cause;
|
|
194
|
+
expect(sqlMessage).toContain(SECRET_TOKEN);
|
|
195
|
+
expect(lineOf({ ...failures[0], obj: { ...failures[0].obj, cause: ownFacts } })).not.toContain(SECRET_TOKEN);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('a failed statement inside a transaction: the same line, and the transaction rejects with the vendor`s error itself', async () => {
|
|
199
|
+
const outcome = await settle(driver.runTransaction((transaction) => driver.runDml(insertStatement, transaction)));
|
|
200
|
+
|
|
201
|
+
expect(rejected).toHaveLength(1);
|
|
202
|
+
expect(outcome).toBe(rejected[0]);
|
|
203
|
+
expect(captured.filter((log) => log.message === 'Failed when executing sql')).toHaveLength(1);
|
|
204
|
+
for (const log of captured) {
|
|
205
|
+
expect(lineOf(log)).not.toContain(SECRET_TOKEN);
|
|
206
|
+
expect(lineOf(log)).not.toContain(SECRET_EMAIL);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const NAMED_INSERT =
|
|
211
|
+
'INSERT INTO `credential` (`id`, `email`, `token`, `attempts`, `cleared`) VALUES (:id, :email, :token, :attempts, :cleared)';
|
|
212
|
+
/** A hand-written statement whose bindings are a DICTIONARY — wider than `Statement` declares, and what the query layer accepts. */
|
|
213
|
+
const namedStatement = () => ({
|
|
214
|
+
sql: NAMED_INSERT,
|
|
215
|
+
params: { id: ECHOED_KEY, email: SECRET_EMAIL, token: SECRET_TOKEN, attempts: 3, cleared: null } as any,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('the named-bindings door: a failed statement bound by a DICTIONARY throws the vendor`s error itself, and its line describes each entry by name', async () => {
|
|
219
|
+
const bare = await settle(instance.raw(NAMED_INSERT, namedStatement().params) as any);
|
|
220
|
+
// The premise: the query layer takes the dictionary, and the statement reaches the server and fails there.
|
|
221
|
+
expect(bare.code).toBe('ER_DUP_ENTRY');
|
|
222
|
+
rejected.length = 0;
|
|
223
|
+
|
|
224
|
+
const outcome = await settle(driver.runDml(namedStatement));
|
|
225
|
+
|
|
226
|
+
expect(rejected).toHaveLength(1);
|
|
227
|
+
expect(outcome).toBe(rejected[0]);
|
|
228
|
+
expect(facts(outcome)).toEqual(facts(bare));
|
|
229
|
+
const failures = captured.filter((log) => log.logLevel === 'error' && log.message === 'Failed when executing sql');
|
|
230
|
+
expect(failures).toHaveLength(1);
|
|
231
|
+
expect(failures[0].obj.sql).toBe(NAMED_INSERT);
|
|
232
|
+
expect(failures[0].obj.params).toEqual({
|
|
233
|
+
id: { type: 'string', length: 36 },
|
|
234
|
+
email: { type: 'string', length: SECRET_EMAIL.length },
|
|
235
|
+
token: { type: 'string', length: SECRET_TOKEN.length },
|
|
236
|
+
attempts: { type: 'number' },
|
|
237
|
+
cleared: { type: 'null', null: true },
|
|
238
|
+
});
|
|
239
|
+
expect(Object.keys(failures[0].obj)).not.toContain('paramValues');
|
|
240
|
+
expect(failures[0].error).toBeUndefined();
|
|
241
|
+
for (const log of captured) {
|
|
242
|
+
expect(lineOf(log)).not.toContain(SECRET_TOKEN);
|
|
243
|
+
expect(lineOf(log)).not.toContain(SECRET_EMAIL);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('the named-bindings door inside a transaction: the transaction rejects with the vendor`s error itself', async () => {
|
|
248
|
+
const outcome = await settle(driver.runTransaction((transaction) => driver.runDml(namedStatement, transaction)));
|
|
249
|
+
|
|
250
|
+
expect(rejected).toHaveLength(1);
|
|
251
|
+
expect(outcome).toBe(rejected[0]);
|
|
252
|
+
expect(captured.filter((log) => log.message === 'Failed when executing sql')).toHaveLength(1);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('describeParams — positions, kinds and lengths; never a value', () => {
|
|
257
|
+
const internals = new KnexDriver({
|
|
258
|
+
host: 'localhost',
|
|
259
|
+
user: 'root',
|
|
260
|
+
password: '',
|
|
261
|
+
dbName: 'test',
|
|
262
|
+
}) as unknown as DriverInternals;
|
|
263
|
+
|
|
264
|
+
test('each positional parameter: the kind of its value, a length for strings, arrays and bytes, null named', () => {
|
|
265
|
+
const described = internals.describeParams([
|
|
266
|
+
SECRET_TOKEN,
|
|
267
|
+
[SECRET_EMAIL, SECRET_TOKEN],
|
|
268
|
+
3,
|
|
269
|
+
true,
|
|
270
|
+
new Date(0),
|
|
271
|
+
Buffer.from(SECRET_TOKEN),
|
|
272
|
+
{ theme: SECRET_TOKEN },
|
|
273
|
+
null,
|
|
274
|
+
undefined,
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
expect(described).toEqual([
|
|
278
|
+
{ type: 'string', length: SECRET_TOKEN.length },
|
|
279
|
+
{ type: 'array', length: 2 },
|
|
280
|
+
{ type: 'number' },
|
|
281
|
+
{ type: 'boolean' },
|
|
282
|
+
{ type: 'date' },
|
|
283
|
+
{ type: 'bytes', length: Buffer.from(SECRET_TOKEN).length },
|
|
284
|
+
{ type: 'object' },
|
|
285
|
+
{ type: 'null', null: true },
|
|
286
|
+
{ type: 'null', null: true },
|
|
287
|
+
]);
|
|
288
|
+
expect(JSON.stringify(described)).not.toContain(SECRET_TOKEN);
|
|
289
|
+
expect(JSON.stringify(described)).not.toContain(SECRET_EMAIL);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('a statement with no parameters describes nothing', () => {
|
|
293
|
+
expect(internals.describeParams(undefined)).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('a bindings dictionary: each entry by NAME, described the same way', () => {
|
|
297
|
+
const described = internals.describeParams({
|
|
298
|
+
token: SECRET_TOKEN,
|
|
299
|
+
emails: [SECRET_EMAIL, SECRET_TOKEN],
|
|
300
|
+
attempts: 3,
|
|
301
|
+
cleared: null,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(described).toEqual({
|
|
305
|
+
token: { type: 'string', length: SECRET_TOKEN.length },
|
|
306
|
+
emails: { type: 'array', length: 2 },
|
|
307
|
+
attempts: { type: 'number' },
|
|
308
|
+
cleared: { type: 'null', null: true },
|
|
309
|
+
});
|
|
310
|
+
expect(JSON.stringify(described)).not.toContain(SECRET_TOKEN);
|
|
311
|
+
expect(JSON.stringify(described)).not.toContain(SECRET_EMAIL);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { inspect } from 'util';
|
|
2
|
+
|
|
3
|
+
/** A log entry as a capturing log writer receives it. */
|
|
4
|
+
export type CapturedLog = { logLevel: string; message?: string; obj?: any; error?: any };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An error as ANYTHING that prints errors would print it — what a value must never be found in:
|
|
8
|
+
* its message and stack; its `util.inspect` rendering, stock (the default dev log writer,
|
|
9
|
+
* `console.*`), with hidden properties shown and getters run, and the same with the error's own
|
|
10
|
+
* rendering switched OFF (`customInspect: false` — the raw object walk, which runs every accessor
|
|
11
|
+
* on the prototype chain); its serialized form (a structured writer); the standard `cause` chain an
|
|
12
|
+
* error reporter walks, message and `details` at each link; and every own property of the error,
|
|
13
|
+
* enumerable or not.
|
|
14
|
+
*/
|
|
15
|
+
export const printed = (error: any): string => {
|
|
16
|
+
const parts: string[] = [
|
|
17
|
+
String(error?.message),
|
|
18
|
+
String(error?.stack),
|
|
19
|
+
inspect({ error }, { depth: 10, maxStringLength: null }),
|
|
20
|
+
inspect(error, { depth: 10, maxStringLength: null, showHidden: true, getters: true }),
|
|
21
|
+
inspect(error, { depth: 10, maxStringLength: null, showHidden: true, getters: true, customInspect: false }),
|
|
22
|
+
JSON.stringify(error) ?? '',
|
|
23
|
+
];
|
|
24
|
+
for (let link = error?.cause, depth = 0; link && depth < 5; link = link.cause, depth++) {
|
|
25
|
+
parts.push(String(link.message), String(link.details), inspect(link, { depth: 10, maxStringLength: null }));
|
|
26
|
+
}
|
|
27
|
+
if (error && typeof error === 'object') {
|
|
28
|
+
for (const name of Object.getOwnPropertyNames(error)) {
|
|
29
|
+
parts.push(inspect(error[name], { depth: 10, maxStringLength: null }));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return parts.join('\n');
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The text a log writer would print for a captured line. */
|
|
36
|
+
export const lineOf = (log: CapturedLog): string =>
|
|
37
|
+
[
|
|
38
|
+
log.logLevel,
|
|
39
|
+
log.message ?? '',
|
|
40
|
+
inspect(log.obj, { depth: 10, maxStringLength: null, maxArrayLength: null }),
|
|
41
|
+
JSON.stringify(log.obj) ?? '',
|
|
42
|
+
log.error ? printed(log.error) : '',
|
|
43
|
+
].join('\n');
|