@lossless.org/client 1.20.0 → 1.20.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.losslessorgclient.js +1 -1
- package/dist_ts/core/classes.error.js +1 -1
- package/dist_ts/core/classes.operation.js +1 -1
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +2 -2
- package/dist_ts/nosqldb/classes.bootstraprecord.js +1 -1
- package/dist_ts/nosqldb/classes.bsonassertion.d.ts +1 -1
- package/dist_ts/nosqldb/classes.bsonsnapshot.js +1 -1
- package/dist_ts/nosqldb/classes.collection.js +39 -23
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +1 -1
- package/dist_ts/nosqldb/classes.collectiontopology.js +1 -1
- package/dist_ts/nosqldb/classes.connection.js +1 -1
- package/dist_ts/nosqldb/classes.cursor.js +1 -1
- package/dist_ts/nosqldb/classes.cursoroperation.d.ts +1 -1
- package/dist_ts/nosqldb/classes.db.js +8 -21
- package/dist_ts/nosqldb/classes.doc.js +1 -1
- package/dist_ts/nosqldb/classes.exactpersistence.js +1 -1
- package/dist_ts/nosqldb/classes.lucene.adapter.js +1 -1
- package/dist_ts/nosqldb/classes.mongoadministration.js +1 -1
- package/dist_ts/nosqldb/classes.namespaceinspection.js +1 -1
- package/dist_ts/nosqldb/classes.operationdeadline.js +1 -1
- package/dist_ts/nosqldb/classes.session.d.ts +2 -2
- package/dist_ts/nosqldb/classes.session.js +1 -1
- package/dist_ts/nosqldb/classes.storedinspection.js +1 -1
- package/dist_ts/nosqldb/classes.testdatabase.js +1 -1
- package/dist_ts/nosqldb/connectionstring.d.ts +15 -0
- package/dist_ts/nosqldb/connectionstring.js +194 -0
- package/dist_ts/objectstorage/classes.bucket.js +1 -1
- package/dist_ts/objectstorage/classes.connection.js +1 -1
- package/dist_ts/objectstorage/classes.directory.js +1 -1
- package/dist_ts/objectstorage/classes.file.js +1 -1
- package/dist_ts/objectstorage/classes.listcursor.js +1 -1
- package/dist_ts/objectstorage/classes.smartbucket.js +1 -1
- package/dist_ts/objectstorage/classes.watcher.js +1 -1
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1 -1
- package/dist_ts/objectstorage/internal.exactupload.capability.js +1 -1
- package/dist_ts/objectstorage/internal.exactupload.operations.js +1 -1
- package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +1 -1
- package/dist_ts/sqldb/classes.clickhouseconnection.js +1 -1
- package/dist_ts/sqldb/classes.sqlconnection.js +1 -1
- package/dist_ts/sqldb/classes.sqltable.js +1 -1
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +1 -1
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +1 -1
- package/dist_ts/sqldb/internal.sqlpool.js +1 -1
- package/package.json +2 -2
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.collection.ts +56 -34
- package/ts/nosqldb/classes.db.ts +11 -22
- package/ts/nosqldb/connectionstring.ts +243 -0
|
@@ -1842,14 +1842,16 @@ const indexEquivalenceSignature = (indexArg: {
|
|
|
1842
1842
|
});
|
|
1843
1843
|
|
|
1844
1844
|
/**
|
|
1845
|
-
* MongoDB reports an index collision without naming
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1845
|
+
* MongoDB reports an index collision without naming either index, yet the
|
|
1846
|
+
* remedy depends on them: adopt the existing name in the declaration or drop
|
|
1847
|
+
* the index. The declarations are checked in order against the catalog and
|
|
1848
|
+
* against one another, since one command creates them all. Only the collision
|
|
1849
|
+
* path pays for the extra catalog read.
|
|
1848
1850
|
*/
|
|
1849
1851
|
const describeIndexCollision = async (
|
|
1850
1852
|
targetCollectionArg: plugins.mongodb.Collection,
|
|
1851
1853
|
collectionNameArg: string,
|
|
1852
|
-
|
|
1854
|
+
expectedIndexesArg: INormalizedCollectionModelSchema['indexes'],
|
|
1853
1855
|
errorArg: unknown,
|
|
1854
1856
|
): Promise<unknown> => {
|
|
1855
1857
|
if (!isMongoIndexConflictError(errorArg)) {
|
|
@@ -1863,31 +1865,49 @@ const describeIndexCollision = async (
|
|
|
1863
1865
|
// own error stays the best evidence the caller can act on.
|
|
1864
1866
|
return errorArg;
|
|
1865
1867
|
}
|
|
1866
|
-
const
|
|
1867
|
-
(
|
|
1868
|
-
actualArg
|
|
1869
|
-
|
|
1870
|
-
stableValue(
|
|
1871
|
-
|
|
1872
|
-
indexEquivalenceSignature(
|
|
1873
|
-
|
|
1874
|
-
if (equivalentIndex) {
|
|
1875
|
-
return new SmartdataPersistenceError(
|
|
1876
|
-
'invalid_configuration',
|
|
1877
|
-
`Collection "${collectionNameArg}" already carries the equivalent index ` +
|
|
1878
|
-
`"${equivalentIndex.name}", so declared index "${expectedArg.name}" ` +
|
|
1879
|
-
'cannot be created. Declare the existing name — an identity declares ' +
|
|
1880
|
-
'it with @unI({ indexName }) — or drop the existing index.',
|
|
1881
|
-
{ cause: errorArg },
|
|
1882
|
-
);
|
|
1883
|
-
}
|
|
1884
|
-
if (actualIndexes.some((actualArg) => actualArg.name === expectedArg.name)) {
|
|
1885
|
-
return new SmartdataPersistenceError(
|
|
1886
|
-
'invalid_configuration',
|
|
1887
|
-
`Collection "${collectionNameArg}" already carries an index named ` +
|
|
1888
|
-
`"${expectedArg.name}" with a different key or options.`,
|
|
1889
|
-
{ cause: errorArg },
|
|
1868
|
+
for (const [position, expected] of expectedIndexesArg.entries()) {
|
|
1869
|
+
const equivalentIndex = actualIndexes.find(
|
|
1870
|
+
(actualArg) =>
|
|
1871
|
+
actualArg.name !== expected.name &&
|
|
1872
|
+
stableValue(Object.entries(actualArg.key || {})) ===
|
|
1873
|
+
stableValue(expected.key) &&
|
|
1874
|
+
indexEquivalenceSignature(actualArg) ===
|
|
1875
|
+
indexEquivalenceSignature(expected.options),
|
|
1890
1876
|
);
|
|
1877
|
+
if (equivalentIndex) {
|
|
1878
|
+
return new SmartdataPersistenceError(
|
|
1879
|
+
'invalid_configuration',
|
|
1880
|
+
`Collection "${collectionNameArg}" already carries the equivalent index ` +
|
|
1881
|
+
`"${equivalentIndex.name}", so declared index "${expected.name}" ` +
|
|
1882
|
+
'cannot be created. Declare the existing name — an identity declares ' +
|
|
1883
|
+
'it with @unI({ indexName }) — or drop the existing index.',
|
|
1884
|
+
{ cause: errorArg },
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
if (actualIndexes.some((actualArg) => actualArg.name === expected.name)) {
|
|
1888
|
+
return new SmartdataPersistenceError(
|
|
1889
|
+
'invalid_configuration',
|
|
1890
|
+
`Collection "${collectionNameArg}" already carries an index named ` +
|
|
1891
|
+
`"${expected.name}" with a different key or options.`,
|
|
1892
|
+
{ cause: errorArg },
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
const equivalentDeclaration = expectedIndexesArg
|
|
1896
|
+
.slice(0, position)
|
|
1897
|
+
.find(
|
|
1898
|
+
(earlierArg) =>
|
|
1899
|
+
stableValue(earlierArg.key) === stableValue(expected.key) &&
|
|
1900
|
+
indexEquivalenceSignature(earlierArg.options) ===
|
|
1901
|
+
indexEquivalenceSignature(expected.options),
|
|
1902
|
+
);
|
|
1903
|
+
if (equivalentDeclaration) {
|
|
1904
|
+
return new SmartdataPersistenceError(
|
|
1905
|
+
'invalid_configuration',
|
|
1906
|
+
`Collection "${collectionNameArg}" declares the equivalent indexes ` +
|
|
1907
|
+
`"${equivalentDeclaration.name}" and "${expected.name}"; declare one name.`,
|
|
1908
|
+
{ cause: errorArg },
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1891
1911
|
}
|
|
1892
1912
|
// An index over the same key that is not interchangeable with the
|
|
1893
1913
|
// declaration cannot be adopted by name, and MongoDB's own error already
|
|
@@ -2133,20 +2153,22 @@ export class SmartdataCollection<T> {
|
|
|
2133
2153
|
if (!modelSchema) {
|
|
2134
2154
|
return;
|
|
2135
2155
|
}
|
|
2136
|
-
for
|
|
2156
|
+
// One command for every declared index: the server builds the missing
|
|
2157
|
+
// ones together, or none of them when one collides.
|
|
2158
|
+
if (modelSchema.indexes.length > 0) {
|
|
2137
2159
|
try {
|
|
2138
|
-
await targetCollection.
|
|
2139
|
-
|
|
2140
|
-
{
|
|
2160
|
+
await targetCollection.createIndexes(
|
|
2161
|
+
modelSchema.indexes.map((index) => ({
|
|
2141
2162
|
...index.options,
|
|
2163
|
+
key: Object.fromEntries(index.key) as plugins.mongodb.IndexDescription['key'],
|
|
2142
2164
|
name: index.name,
|
|
2143
|
-
},
|
|
2165
|
+
})),
|
|
2144
2166
|
);
|
|
2145
2167
|
} catch (errorArg) {
|
|
2146
2168
|
throw await describeIndexCollision(
|
|
2147
2169
|
targetCollection,
|
|
2148
2170
|
this.collectionName,
|
|
2149
|
-
|
|
2171
|
+
modelSchema.indexes,
|
|
2150
2172
|
errorArg,
|
|
2151
2173
|
);
|
|
2152
2174
|
}
|
package/ts/nosqldb/classes.db.ts
CHANGED
|
@@ -40,6 +40,7 @@ export type {
|
|
|
40
40
|
} from './classes.writeobserver.js';
|
|
41
41
|
|
|
42
42
|
import { logger } from './logging.js';
|
|
43
|
+
import { buildSmartdataConnectionUrl, redactSmartdataConnectionError } from './connectionstring.js';
|
|
43
44
|
import { assertDataOptions, SmartdataOperationBudget } from './classes.operationbudget.js';
|
|
44
45
|
import {
|
|
45
46
|
composeSmartdataCapabilities,
|
|
@@ -258,28 +259,11 @@ export class SmartdataDb {
|
|
|
258
259
|
try { budget?.check(); } catch (errorArg) { budget?.close(); throw errorArg; }
|
|
259
260
|
this.beginLifecycleOperation('init');
|
|
260
261
|
this.transitionLifecycle('connecting');
|
|
262
|
+
// Read once, so a failure is redacted with the credentials this attempt connected with.
|
|
263
|
+
const descriptor = this.smartdataOptions;
|
|
261
264
|
let candidateClient: plugins.mongodb.MongoClient | undefined;
|
|
262
265
|
try {
|
|
263
266
|
try {
|
|
264
|
-
// Safely encode credentials to handle special characters
|
|
265
|
-
const encodedUser = this.smartdataOptions.mongoDbUser
|
|
266
|
-
? encodeURIComponent(this.smartdataOptions.mongoDbUser)
|
|
267
|
-
: '';
|
|
268
|
-
const encodedPass = this.smartdataOptions.mongoDbPass
|
|
269
|
-
? encodeURIComponent(this.smartdataOptions.mongoDbPass)
|
|
270
|
-
: '';
|
|
271
|
-
|
|
272
|
-
const finalConnectionUrl = this.smartdataOptions.mongoDbUrl
|
|
273
|
-
.replace('<USERNAME>', encodedUser)
|
|
274
|
-
.replace('<username>', encodedUser)
|
|
275
|
-
.replace('<USER>', encodedUser)
|
|
276
|
-
.replace('<user>', encodedUser)
|
|
277
|
-
.replace('<PASSWORD>', encodedPass)
|
|
278
|
-
.replace('<password>', encodedPass)
|
|
279
|
-
.replace('<DBNAME>', this.smartdataOptions.mongoDbName || '')
|
|
280
|
-
.replace('<dbname>', this.smartdataOptions.mongoDbName || '');
|
|
281
|
-
|
|
282
|
-
const descriptor = this.smartdataOptions;
|
|
283
267
|
const clientOptions: plugins.mongodb.MongoClientOptions = {
|
|
284
268
|
maxPoolSize: descriptor.maxPoolSize ?? 100,
|
|
285
269
|
maxIdleTimeMS: descriptor.maxIdleTimeMS ?? 300000,
|
|
@@ -290,7 +274,8 @@ export class SmartdataDb {
|
|
|
290
274
|
retryWrites: true,
|
|
291
275
|
};
|
|
292
276
|
|
|
293
|
-
candidateClient = new plugins.mongodb.MongoClient(
|
|
277
|
+
candidateClient = new plugins.mongodb.MongoClient(
|
|
278
|
+
buildSmartdataConnectionUrl(descriptor), clientOptions);
|
|
294
279
|
this.pendingClient = candidateClient;
|
|
295
280
|
const connectingClient = candidateClient;
|
|
296
281
|
let cancellationCleanup: Promise<void> | undefined;
|
|
@@ -347,9 +332,13 @@ export class SmartdataDb {
|
|
|
347
332
|
? 'SmartData connection setup and cleanup failed; close must be retried.'
|
|
348
333
|
: 'SmartData connection setup failed or was cancelled.');
|
|
349
334
|
}
|
|
350
|
-
|
|
335
|
+
const setupError = new SmartdataPersistenceError('unsupported_operation',
|
|
336
|
+
'SmartData connection setup failed.',
|
|
337
|
+
{ cause: redactSmartdataConnectionError(error, descriptor) });
|
|
338
|
+
if (cleanupError) throw new AggregateError(
|
|
339
|
+
[setupError, redactSmartdataConnectionError(cleanupError, descriptor)],
|
|
351
340
|
'SmartData connection setup and cleanup failed; close must be retried.');
|
|
352
|
-
throw
|
|
341
|
+
throw setupError;
|
|
353
342
|
}
|
|
354
343
|
} finally {
|
|
355
344
|
this.endLifecycleOperation('init');
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { ISmartdataDbOptions } from './classes.db.js';
|
|
3
|
+
|
|
4
|
+
/** The options that locate and authenticate a database: everything a connection string is built from. */
|
|
5
|
+
export type TSmartdataConnectionStringOptions = Pick<
|
|
6
|
+
ISmartdataDbOptions,
|
|
7
|
+
'mongoDbUrl' | 'mongoDbName' | 'mongoDbUser' | 'mongoDbPass'
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
const redactionMarker = '<redacted>';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The URL the driver connects with: `mongoDbUrl` with its first `<USERNAME>`/`<USER>`,
|
|
14
|
+
* `<PASSWORD>` and `<DBNAME>` placeholder of each case replaced by the URL-encoded
|
|
15
|
+
* user, password and the database name.
|
|
16
|
+
*/
|
|
17
|
+
export const buildSmartdataConnectionUrl = (optionsArg: TSmartdataConnectionStringOptions): string => {
|
|
18
|
+
const encodedUser = optionsArg.mongoDbUser ? encodeURIComponent(optionsArg.mongoDbUser) : '';
|
|
19
|
+
const encodedPass = optionsArg.mongoDbPass ? encodeURIComponent(optionsArg.mongoDbPass) : '';
|
|
20
|
+
return optionsArg.mongoDbUrl
|
|
21
|
+
.replace('<USERNAME>', encodedUser)
|
|
22
|
+
.replace('<username>', encodedUser)
|
|
23
|
+
.replace('<USER>', encodedUser)
|
|
24
|
+
.replace('<user>', encodedUser)
|
|
25
|
+
.replace('<PASSWORD>', encodedPass)
|
|
26
|
+
.replace('<password>', encodedPass)
|
|
27
|
+
.replace('<DBNAME>', optionsArg.mongoDbName || '')
|
|
28
|
+
.replace('<dbname>', optionsArg.mongoDbName || '');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Replaces every credential in a text an error carries. */
|
|
32
|
+
type TRedact = (textArg: string) => string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The URI options whose value is a secret, by the driver's option table
|
|
36
|
+
* (`mongodb/src/connection_string.ts` `OPTIONS`): the `auth` credentials, the
|
|
37
|
+
* `authMechanismProperties` (for example `AWS_SESSION_TOKEN`), the SOCKS5 proxy
|
|
38
|
+
* login, the key file password, and the TLS key material the driver passes through.
|
|
39
|
+
* Lower-cased, because the driver reads option names case-insensitively.
|
|
40
|
+
*/
|
|
41
|
+
const secretUriOptionNames = new Set(
|
|
42
|
+
[
|
|
43
|
+
'auth',
|
|
44
|
+
'authMechanismProperties',
|
|
45
|
+
'proxyUsername',
|
|
46
|
+
'proxyPassword',
|
|
47
|
+
'tlsCertificateKeyFilePassword',
|
|
48
|
+
'key',
|
|
49
|
+
'passphrase',
|
|
50
|
+
'pfx',
|
|
51
|
+
].map((nameArg) => nameArg.toLowerCase()),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/** Its value is a `KEY:value,KEY:value` list, and the driver quotes single values of it. */
|
|
55
|
+
const mechanismPropertiesOptionName = 'authmechanismproperties';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The `authMechanismProperties` entries whose value is a secret, by the driver's
|
|
59
|
+
* `AuthMechanismProperties` (`mongodb/src/cmap/auth/mongo_credentials.ts`).
|
|
60
|
+
*/
|
|
61
|
+
const secretMechanismPropertyNames = new Set(['AWS_SESSION_TOKEN']);
|
|
62
|
+
|
|
63
|
+
/** A credential as written, URL-encoded, URL-decoded and query-decoded: a driver message may quote any of them. */
|
|
64
|
+
const spellingsOf = (credentialArg: string): string[] => [
|
|
65
|
+
credentialArg,
|
|
66
|
+
...[
|
|
67
|
+
encodeURIComponent,
|
|
68
|
+
decodeURIComponent,
|
|
69
|
+
(textArg: string) => decodeURIComponent(textArg.replaceAll('+', ' ')),
|
|
70
|
+
].flatMap((transformArg) => {
|
|
71
|
+
try {
|
|
72
|
+
return [transformArg(credentialArg)];
|
|
73
|
+
} catch {
|
|
74
|
+
// A spelling the credential cannot be transformed into cannot be quoted either.
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The userinfo of a connection string, as a whole and as its user and password halves.
|
|
82
|
+
* A string the driver refuses is malformed by definition, so it is read generously:
|
|
83
|
+
* everything between `://` and the last `@`.
|
|
84
|
+
*/
|
|
85
|
+
const userinfoCredentialsOf = (urlArg: string): string[] => {
|
|
86
|
+
const userinfo = /^[^:/?#]*:\/\/(.*)@/s.exec(urlArg)?.[1];
|
|
87
|
+
if (userinfo === undefined) return [];
|
|
88
|
+
const separator = userinfo.indexOf(':');
|
|
89
|
+
return separator < 0
|
|
90
|
+
? [userinfo]
|
|
91
|
+
: [userinfo, userinfo.slice(0, separator), userinfo.slice(separator + 1)];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
interface IUriOption {
|
|
95
|
+
name: string;
|
|
96
|
+
value: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The `name=value` pairs of a connection string's query, read generously: a pair
|
|
101
|
+
* starts after any `?`, `&` or `;`, and its value runs to the next `&`, as the driver
|
|
102
|
+
* splits, and also to the next `;`, the legacy separator, so both readings are covered.
|
|
103
|
+
*/
|
|
104
|
+
const uriOptionsOf = (urlArg: string): IUriOption[] =>
|
|
105
|
+
[...urlArg.matchAll(/(?<=[?&;])([^?&;=]*)=(?=([^&]*))/g)].flatMap(([, name, value]) =>
|
|
106
|
+
[value, value.split(';')[0]].map((valueArg) => ({ name, value: valueArg })),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
/** The secret entry values in every spelling of an `authMechanismProperties` value, split as the driver splits it. */
|
|
110
|
+
const secretMechanismPropertyValuesOf = (valueArg: string): string[] =>
|
|
111
|
+
spellingsOf(valueArg).flatMap((spellingArg) =>
|
|
112
|
+
spellingArg.split(',').flatMap((entryArg) => {
|
|
113
|
+
const separator = entryArg.indexOf(':');
|
|
114
|
+
return separator >= 0 && secretMechanismPropertyNames.has(entryArg.slice(0, separator).toUpperCase())
|
|
115
|
+
? [entryArg.slice(separator + 1)]
|
|
116
|
+
: [];
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
/** The values of a connection string's secret-bearing URI options, under any spelling of their name. */
|
|
121
|
+
const uriOptionCredentialsOf = (urlArg: string): string[] =>
|
|
122
|
+
uriOptionsOf(urlArg).flatMap(({ name, value }) => {
|
|
123
|
+
const names = spellingsOf(name).map((spellingArg) => spellingArg.toLowerCase());
|
|
124
|
+
if (!names.some((nameArg) => secretUriOptionNames.has(nameArg))) return [];
|
|
125
|
+
return names.includes(mechanismPropertiesOptionName) ? [value, ...secretMechanismPropertyValuesOf(value)] : [value];
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Every credential spelling the options carry, in the connection string as given and as
|
|
130
|
+
* built, longest first so the longest credential starting at a position is the one found.
|
|
131
|
+
*/
|
|
132
|
+
const connectionCredentialsOf = (optionsArg: TSmartdataConnectionStringOptions): string[] => {
|
|
133
|
+
const credentials = [
|
|
134
|
+
...[optionsArg.mongoDbUrl, buildSmartdataConnectionUrl(optionsArg)].flatMap((urlArg) => [
|
|
135
|
+
...userinfoCredentialsOf(urlArg),
|
|
136
|
+
...uriOptionCredentialsOf(urlArg),
|
|
137
|
+
]),
|
|
138
|
+
optionsArg.mongoDbUser,
|
|
139
|
+
optionsArg.mongoDbPass,
|
|
140
|
+
].filter((credentialArg): credentialArg is string => Boolean(credentialArg));
|
|
141
|
+
return [...new Set(credentials.flatMap(spellingsOf))]
|
|
142
|
+
.filter((spellingArg) => spellingArg.length > 0)
|
|
143
|
+
.sort((leftArg, rightArg) => rightArg.length - leftArg.length);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// `RegExp.escape` is ES2025, beyond the ES2024 target.
|
|
147
|
+
const escapeRegExp = (textArg: string): string => textArg.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
148
|
+
|
|
149
|
+
interface ISpan {
|
|
150
|
+
start: number;
|
|
151
|
+
end: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Where credentials occur in a text, overlapping occurrences merged into one span.
|
|
156
|
+
* The pattern only looks ahead, so it reports every start, overlapping ones included.
|
|
157
|
+
*/
|
|
158
|
+
const credentialSpansIn = (textArg: string, occurrenceArg: RegExp): ISpan[] => {
|
|
159
|
+
const spans: ISpan[] = [];
|
|
160
|
+
for (const { index, 1: credential } of textArg.matchAll(occurrenceArg)) {
|
|
161
|
+
const end = index + credential.length;
|
|
162
|
+
const previous = spans.at(-1);
|
|
163
|
+
if (previous !== undefined && index < previous.end) previous.end = Math.max(previous.end, end);
|
|
164
|
+
else spans.push({ start: index, end });
|
|
165
|
+
}
|
|
166
|
+
return spans;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const withSpansRedacted = (textArg: string, spansArg: readonly ISpan[]): string => {
|
|
170
|
+
let redacted = '';
|
|
171
|
+
let copiedUntil = 0;
|
|
172
|
+
for (const { start, end } of spansArg) {
|
|
173
|
+
redacted += textArg.slice(copiedUntil, start) + redactionMarker;
|
|
174
|
+
copiedUntil = end;
|
|
175
|
+
}
|
|
176
|
+
return redacted + textArg.slice(copiedUntil);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Redacts in one pass over the original text, so no credential is matched inside
|
|
181
|
+
* a marker another credential left and overlapping credentials leave no fragment.
|
|
182
|
+
*/
|
|
183
|
+
const redactorFor = (credentialsArg: readonly string[]): TRedact => {
|
|
184
|
+
if (credentialsArg.length === 0) return (textArg) => textArg;
|
|
185
|
+
const occurrence = new RegExp(`(?=(${credentialsArg.map(escapeRegExp).join('|')}))`, 'g');
|
|
186
|
+
return (textArg) => withSpansRedacted(textArg, credentialSpansIn(textArg, occurrence));
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** The fields a caller branches on: the driver's or Node's `code`, the server's `codeName`, the driver's error labels. */
|
|
190
|
+
interface IPreservedErrorFields {
|
|
191
|
+
code?: number | string;
|
|
192
|
+
codeName?: string;
|
|
193
|
+
errorLabels?: string[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Only these named fields: any other field, such as Node's `ERR_INVALID_URL` `input`, may hold the connection string. */
|
|
197
|
+
const preservedFieldsOf = (errorArg: Error, redactArg: TRedact): IPreservedErrorFields => {
|
|
198
|
+
const fields: IPreservedErrorFields = {};
|
|
199
|
+
const code = 'code' in errorArg ? errorArg.code : undefined;
|
|
200
|
+
if (typeof code === 'number') fields.code = code;
|
|
201
|
+
if (typeof code === 'string') fields.code = redactArg(code);
|
|
202
|
+
const codeName = 'codeName' in errorArg ? errorArg.codeName : undefined;
|
|
203
|
+
if (typeof codeName === 'string') fields.codeName = redactArg(codeName);
|
|
204
|
+
if (errorArg instanceof plugins.mongodb.MongoError) fields.errorLabels = errorArg.errorLabels.map(redactArg);
|
|
205
|
+
return fields;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A copy of a thrown value that keeps its error name, its detail, its stack frames,
|
|
210
|
+
* its preserved fields, its cause chain and its aggregated errors, with every
|
|
211
|
+
* credential replaced. Only an error that is its own ancestor is left out, so a
|
|
212
|
+
* cyclic chain still ends.
|
|
213
|
+
*/
|
|
214
|
+
const copyRedacted = (
|
|
215
|
+
thrownArg: unknown,
|
|
216
|
+
redactArg: TRedact,
|
|
217
|
+
ancestorsArg: Set<Error>,
|
|
218
|
+
): Error => {
|
|
219
|
+
if (!(thrownArg instanceof Error)) return new Error(redactArg(String(thrownArg)));
|
|
220
|
+
ancestorsArg.add(thrownArg);
|
|
221
|
+
const copyInner = (innerArg: unknown): Error[] =>
|
|
222
|
+
innerArg instanceof Error && ancestorsArg.has(innerArg) ? [] : [copyRedacted(innerArg, redactArg, ancestorsArg)];
|
|
223
|
+
const [cause] = thrownArg.cause === undefined ? [] : copyInner(thrownArg.cause);
|
|
224
|
+
const options = cause === undefined ? undefined : { cause };
|
|
225
|
+
const message = redactArg(thrownArg.message);
|
|
226
|
+
const copy = thrownArg instanceof AggregateError
|
|
227
|
+
? new AggregateError(thrownArg.errors.flatMap((innerArg: unknown) => copyInner(innerArg)), message, options)
|
|
228
|
+
: new Error(message, options);
|
|
229
|
+
ancestorsArg.delete(thrownArg);
|
|
230
|
+
copy.name = redactArg(thrownArg.name);
|
|
231
|
+
if (thrownArg.stack !== undefined) copy.stack = redactArg(thrownArg.stack);
|
|
232
|
+
return Object.assign(copy, preservedFieldsOf(thrownArg, redactArg));
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* What a connection failure may show: the driver's error, copied with every
|
|
237
|
+
* credential of the options replaced by `<redacted>`. The original is never
|
|
238
|
+
* attached, because printing an error prints its whole cause chain.
|
|
239
|
+
*/
|
|
240
|
+
export const redactSmartdataConnectionError = (
|
|
241
|
+
thrownArg: unknown,
|
|
242
|
+
optionsArg: TSmartdataConnectionStringOptions,
|
|
243
|
+
): Error => copyRedacted(thrownArg, redactorFor(connectionCredentialsOf(optionsArg)), new Set());
|