@lossless.org/client 1.19.0 → 1.20.1
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/nosqldb/classes.atomicdelete.js +12 -4
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +11 -2
- package/dist_ts/nosqldb/classes.atomicupdate.d.ts +2 -0
- package/dist_ts/nosqldb/classes.atomicupdate.js +32 -5
- package/dist_ts/nosqldb/classes.bootstraprecord.js +7 -2
- package/dist_ts/nosqldb/classes.collection.js +52 -7
- package/dist_ts/nosqldb/classes.db.d.ts +19 -0
- package/dist_ts/nosqldb/classes.db.js +28 -21
- package/dist_ts/nosqldb/classes.doc.js +2 -2
- package/dist_ts/nosqldb/classes.exactpersistence.js +17 -2
- package/dist_ts/nosqldb/classes.persistence.d.ts +6 -0
- package/dist_ts/nosqldb/classes.persistence.js +16 -1
- package/dist_ts/nosqldb/classes.session.d.ts +11 -0
- package/dist_ts/nosqldb/classes.session.js +51 -1
- package/dist_ts/nosqldb/classes.writeobserver.d.ts +57 -0
- package/dist_ts/nosqldb/classes.writeobserver.js +139 -0
- package/dist_ts/nosqldb/connectionstring.d.ts +15 -0
- package/dist_ts/nosqldb/connectionstring.js +194 -0
- package/package.json +3 -3
- package/readme.md +3 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.atomicdelete.ts +11 -2
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +22 -0
- package/ts/nosqldb/classes.atomicupdate.ts +53 -3
- package/ts/nosqldb/classes.bootstraprecord.ts +7 -1
- package/ts/nosqldb/classes.collection.ts +63 -4
- package/ts/nosqldb/classes.db.ts +42 -22
- package/ts/nosqldb/classes.doc.ts +1 -1
- package/ts/nosqldb/classes.exactpersistence.ts +24 -0
- package/ts/nosqldb/classes.persistence.ts +16 -0
- package/ts/nosqldb/classes.session.ts +73 -0
- package/ts/nosqldb/classes.writeobserver.ts +224 -0
- package/ts/nosqldb/connectionstring.ts +243 -0
|
@@ -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());
|