@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.
Files changed (55) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.losslessorgclient.js +1 -1
  3. package/dist_ts/core/classes.error.js +1 -1
  4. package/dist_ts/core/classes.operation.js +1 -1
  5. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +2 -2
  6. package/dist_ts/nosqldb/classes.bootstraprecord.js +1 -1
  7. package/dist_ts/nosqldb/classes.bsonassertion.d.ts +1 -1
  8. package/dist_ts/nosqldb/classes.bsonsnapshot.js +1 -1
  9. package/dist_ts/nosqldb/classes.collection.js +39 -23
  10. package/dist_ts/nosqldb/classes.collectiontopology.d.ts +1 -1
  11. package/dist_ts/nosqldb/classes.collectiontopology.js +1 -1
  12. package/dist_ts/nosqldb/classes.connection.js +1 -1
  13. package/dist_ts/nosqldb/classes.cursor.js +1 -1
  14. package/dist_ts/nosqldb/classes.cursoroperation.d.ts +1 -1
  15. package/dist_ts/nosqldb/classes.db.js +8 -21
  16. package/dist_ts/nosqldb/classes.doc.js +1 -1
  17. package/dist_ts/nosqldb/classes.exactpersistence.js +1 -1
  18. package/dist_ts/nosqldb/classes.lucene.adapter.js +1 -1
  19. package/dist_ts/nosqldb/classes.mongoadministration.js +1 -1
  20. package/dist_ts/nosqldb/classes.namespaceinspection.js +1 -1
  21. package/dist_ts/nosqldb/classes.operationdeadline.js +1 -1
  22. package/dist_ts/nosqldb/classes.session.d.ts +2 -2
  23. package/dist_ts/nosqldb/classes.session.js +1 -1
  24. package/dist_ts/nosqldb/classes.storedinspection.js +1 -1
  25. package/dist_ts/nosqldb/classes.testdatabase.js +1 -1
  26. package/dist_ts/nosqldb/connectionstring.d.ts +15 -0
  27. package/dist_ts/nosqldb/connectionstring.js +194 -0
  28. package/dist_ts/objectstorage/classes.bucket.js +1 -1
  29. package/dist_ts/objectstorage/classes.connection.js +1 -1
  30. package/dist_ts/objectstorage/classes.directory.js +1 -1
  31. package/dist_ts/objectstorage/classes.file.js +1 -1
  32. package/dist_ts/objectstorage/classes.listcursor.js +1 -1
  33. package/dist_ts/objectstorage/classes.smartbucket.js +1 -1
  34. package/dist_ts/objectstorage/classes.watcher.js +1 -1
  35. package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1 -1
  36. package/dist_ts/objectstorage/internal.exactupload.capability.js +1 -1
  37. package/dist_ts/objectstorage/internal.exactupload.operations.js +1 -1
  38. package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +1 -1
  39. package/dist_ts/sqldb/classes.clickhouseconnection.js +1 -1
  40. package/dist_ts/sqldb/classes.sqlconnection.js +1 -1
  41. package/dist_ts/sqldb/classes.sqltable.js +1 -1
  42. package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +1 -1
  43. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +1 -1
  44. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +1 -1
  45. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +1 -1
  46. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +1 -1
  47. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +1 -1
  48. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +1 -1
  49. package/dist_ts/sqldb/internal.sqlpool.js +1 -1
  50. package/package.json +2 -2
  51. package/readme.md +1 -1
  52. package/ts/00_commitinfo_data.ts +1 -1
  53. package/ts/nosqldb/classes.collection.ts +56 -34
  54. package/ts/nosqldb/classes.db.ts +11 -22
  55. 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 the index already in the
1846
- * namespace, yet the remedy depends on it: adopt that name in the declaration
1847
- * or drop the index. Only the collision path pays for the extra catalog read.
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
- expectedArg: INormalizedCollectionModelSchema['indexes'][number],
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 equivalentIndex = actualIndexes.find(
1867
- (actualArg) =>
1868
- actualArg.name !== expectedArg.name &&
1869
- stableValue(Object.entries(actualArg.key || {})) ===
1870
- stableValue(expectedArg.key) &&
1871
- indexEquivalenceSignature(actualArg) ===
1872
- indexEquivalenceSignature(expectedArg.options),
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 (const index of modelSchema.indexes) {
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.createIndex(
2139
- Object.fromEntries(index.key) as plugins.mongodb.IndexSpecification,
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
- index,
2171
+ modelSchema.indexes,
2150
2172
  errorArg,
2151
2173
  );
2152
2174
  }
@@ -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(finalConnectionUrl, clientOptions);
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
- if (cleanupError) throw new AggregateError([error, cleanupError],
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 error;
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());