@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.
Files changed (34) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/nosqldb/classes.atomicdelete.js +12 -4
  3. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +11 -2
  4. package/dist_ts/nosqldb/classes.atomicupdate.d.ts +2 -0
  5. package/dist_ts/nosqldb/classes.atomicupdate.js +32 -5
  6. package/dist_ts/nosqldb/classes.bootstraprecord.js +7 -2
  7. package/dist_ts/nosqldb/classes.collection.js +52 -7
  8. package/dist_ts/nosqldb/classes.db.d.ts +19 -0
  9. package/dist_ts/nosqldb/classes.db.js +28 -21
  10. package/dist_ts/nosqldb/classes.doc.js +2 -2
  11. package/dist_ts/nosqldb/classes.exactpersistence.js +17 -2
  12. package/dist_ts/nosqldb/classes.persistence.d.ts +6 -0
  13. package/dist_ts/nosqldb/classes.persistence.js +16 -1
  14. package/dist_ts/nosqldb/classes.session.d.ts +11 -0
  15. package/dist_ts/nosqldb/classes.session.js +51 -1
  16. package/dist_ts/nosqldb/classes.writeobserver.d.ts +57 -0
  17. package/dist_ts/nosqldb/classes.writeobserver.js +139 -0
  18. package/dist_ts/nosqldb/connectionstring.d.ts +15 -0
  19. package/dist_ts/nosqldb/connectionstring.js +194 -0
  20. package/package.json +3 -3
  21. package/readme.md +3 -2
  22. package/ts/00_commitinfo_data.ts +1 -1
  23. package/ts/nosqldb/classes.atomicdelete.ts +11 -2
  24. package/ts/nosqldb/classes.atomicfindoneandupdate.ts +22 -0
  25. package/ts/nosqldb/classes.atomicupdate.ts +53 -3
  26. package/ts/nosqldb/classes.bootstraprecord.ts +7 -1
  27. package/ts/nosqldb/classes.collection.ts +63 -4
  28. package/ts/nosqldb/classes.db.ts +42 -22
  29. package/ts/nosqldb/classes.doc.ts +1 -1
  30. package/ts/nosqldb/classes.exactpersistence.ts +24 -0
  31. package/ts/nosqldb/classes.persistence.ts +16 -0
  32. package/ts/nosqldb/classes.session.ts +73 -0
  33. package/ts/nosqldb/classes.writeobserver.ts +224 -0
  34. package/ts/nosqldb/connectionstring.ts +243 -0
@@ -0,0 +1,194 @@
1
+ import * as plugins from './plugins.js';
2
+ const redactionMarker = '<redacted>';
3
+ /**
4
+ * The URL the driver connects with: `mongoDbUrl` with its first `<USERNAME>`/`<USER>`,
5
+ * `<PASSWORD>` and `<DBNAME>` placeholder of each case replaced by the URL-encoded
6
+ * user, password and the database name.
7
+ */
8
+ export const buildSmartdataConnectionUrl = (optionsArg) => {
9
+ const encodedUser = optionsArg.mongoDbUser ? encodeURIComponent(optionsArg.mongoDbUser) : '';
10
+ const encodedPass = optionsArg.mongoDbPass ? encodeURIComponent(optionsArg.mongoDbPass) : '';
11
+ return optionsArg.mongoDbUrl
12
+ .replace('<USERNAME>', encodedUser)
13
+ .replace('<username>', encodedUser)
14
+ .replace('<USER>', encodedUser)
15
+ .replace('<user>', encodedUser)
16
+ .replace('<PASSWORD>', encodedPass)
17
+ .replace('<password>', encodedPass)
18
+ .replace('<DBNAME>', optionsArg.mongoDbName || '')
19
+ .replace('<dbname>', optionsArg.mongoDbName || '');
20
+ };
21
+ /**
22
+ * The URI options whose value is a secret, by the driver's option table
23
+ * (`mongodb/src/connection_string.ts` `OPTIONS`): the `auth` credentials, the
24
+ * `authMechanismProperties` (for example `AWS_SESSION_TOKEN`), the SOCKS5 proxy
25
+ * login, the key file password, and the TLS key material the driver passes through.
26
+ * Lower-cased, because the driver reads option names case-insensitively.
27
+ */
28
+ const secretUriOptionNames = new Set([
29
+ 'auth',
30
+ 'authMechanismProperties',
31
+ 'proxyUsername',
32
+ 'proxyPassword',
33
+ 'tlsCertificateKeyFilePassword',
34
+ 'key',
35
+ 'passphrase',
36
+ 'pfx',
37
+ ].map((nameArg) => nameArg.toLowerCase()));
38
+ /** Its value is a `KEY:value,KEY:value` list, and the driver quotes single values of it. */
39
+ const mechanismPropertiesOptionName = 'authmechanismproperties';
40
+ /**
41
+ * The `authMechanismProperties` entries whose value is a secret, by the driver's
42
+ * `AuthMechanismProperties` (`mongodb/src/cmap/auth/mongo_credentials.ts`).
43
+ */
44
+ const secretMechanismPropertyNames = new Set(['AWS_SESSION_TOKEN']);
45
+ /** A credential as written, URL-encoded, URL-decoded and query-decoded: a driver message may quote any of them. */
46
+ const spellingsOf = (credentialArg) => [
47
+ credentialArg,
48
+ ...[
49
+ encodeURIComponent,
50
+ decodeURIComponent,
51
+ (textArg) => decodeURIComponent(textArg.replaceAll('+', ' ')),
52
+ ].flatMap((transformArg) => {
53
+ try {
54
+ return [transformArg(credentialArg)];
55
+ }
56
+ catch {
57
+ // A spelling the credential cannot be transformed into cannot be quoted either.
58
+ return [];
59
+ }
60
+ }),
61
+ ];
62
+ /**
63
+ * The userinfo of a connection string, as a whole and as its user and password halves.
64
+ * A string the driver refuses is malformed by definition, so it is read generously:
65
+ * everything between `://` and the last `@`.
66
+ */
67
+ const userinfoCredentialsOf = (urlArg) => {
68
+ const userinfo = /^[^:/?#]*:\/\/(.*)@/s.exec(urlArg)?.[1];
69
+ if (userinfo === undefined)
70
+ return [];
71
+ const separator = userinfo.indexOf(':');
72
+ return separator < 0
73
+ ? [userinfo]
74
+ : [userinfo, userinfo.slice(0, separator), userinfo.slice(separator + 1)];
75
+ };
76
+ /**
77
+ * The `name=value` pairs of a connection string's query, read generously: a pair
78
+ * starts after any `?`, `&` or `;`, and its value runs to the next `&`, as the driver
79
+ * splits, and also to the next `;`, the legacy separator, so both readings are covered.
80
+ */
81
+ const uriOptionsOf = (urlArg) => [...urlArg.matchAll(/(?<=[?&;])([^?&;=]*)=(?=([^&]*))/g)].flatMap(([, name, value]) => [value, value.split(';')[0]].map((valueArg) => ({ name, value: valueArg })));
82
+ /** The secret entry values in every spelling of an `authMechanismProperties` value, split as the driver splits it. */
83
+ const secretMechanismPropertyValuesOf = (valueArg) => spellingsOf(valueArg).flatMap((spellingArg) => spellingArg.split(',').flatMap((entryArg) => {
84
+ const separator = entryArg.indexOf(':');
85
+ return separator >= 0 && secretMechanismPropertyNames.has(entryArg.slice(0, separator).toUpperCase())
86
+ ? [entryArg.slice(separator + 1)]
87
+ : [];
88
+ }));
89
+ /** The values of a connection string's secret-bearing URI options, under any spelling of their name. */
90
+ const uriOptionCredentialsOf = (urlArg) => uriOptionsOf(urlArg).flatMap(({ name, value }) => {
91
+ const names = spellingsOf(name).map((spellingArg) => spellingArg.toLowerCase());
92
+ if (!names.some((nameArg) => secretUriOptionNames.has(nameArg)))
93
+ return [];
94
+ return names.includes(mechanismPropertiesOptionName) ? [value, ...secretMechanismPropertyValuesOf(value)] : [value];
95
+ });
96
+ /**
97
+ * Every credential spelling the options carry, in the connection string as given and as
98
+ * built, longest first so the longest credential starting at a position is the one found.
99
+ */
100
+ const connectionCredentialsOf = (optionsArg) => {
101
+ const credentials = [
102
+ ...[optionsArg.mongoDbUrl, buildSmartdataConnectionUrl(optionsArg)].flatMap((urlArg) => [
103
+ ...userinfoCredentialsOf(urlArg),
104
+ ...uriOptionCredentialsOf(urlArg),
105
+ ]),
106
+ optionsArg.mongoDbUser,
107
+ optionsArg.mongoDbPass,
108
+ ].filter((credentialArg) => Boolean(credentialArg));
109
+ return [...new Set(credentials.flatMap(spellingsOf))]
110
+ .filter((spellingArg) => spellingArg.length > 0)
111
+ .sort((leftArg, rightArg) => rightArg.length - leftArg.length);
112
+ };
113
+ // `RegExp.escape` is ES2025, beyond the ES2024 target.
114
+ const escapeRegExp = (textArg) => textArg.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
115
+ /**
116
+ * Where credentials occur in a text, overlapping occurrences merged into one span.
117
+ * The pattern only looks ahead, so it reports every start, overlapping ones included.
118
+ */
119
+ const credentialSpansIn = (textArg, occurrenceArg) => {
120
+ const spans = [];
121
+ for (const { index, 1: credential } of textArg.matchAll(occurrenceArg)) {
122
+ const end = index + credential.length;
123
+ const previous = spans.at(-1);
124
+ if (previous !== undefined && index < previous.end)
125
+ previous.end = Math.max(previous.end, end);
126
+ else
127
+ spans.push({ start: index, end });
128
+ }
129
+ return spans;
130
+ };
131
+ const withSpansRedacted = (textArg, spansArg) => {
132
+ let redacted = '';
133
+ let copiedUntil = 0;
134
+ for (const { start, end } of spansArg) {
135
+ redacted += textArg.slice(copiedUntil, start) + redactionMarker;
136
+ copiedUntil = end;
137
+ }
138
+ return redacted + textArg.slice(copiedUntil);
139
+ };
140
+ /**
141
+ * Redacts in one pass over the original text, so no credential is matched inside
142
+ * a marker another credential left and overlapping credentials leave no fragment.
143
+ */
144
+ const redactorFor = (credentialsArg) => {
145
+ if (credentialsArg.length === 0)
146
+ return (textArg) => textArg;
147
+ const occurrence = new RegExp(`(?=(${credentialsArg.map(escapeRegExp).join('|')}))`, 'g');
148
+ return (textArg) => withSpansRedacted(textArg, credentialSpansIn(textArg, occurrence));
149
+ };
150
+ /** Only these named fields: any other field, such as Node's `ERR_INVALID_URL` `input`, may hold the connection string. */
151
+ const preservedFieldsOf = (errorArg, redactArg) => {
152
+ const fields = {};
153
+ const code = 'code' in errorArg ? errorArg.code : undefined;
154
+ if (typeof code === 'number')
155
+ fields.code = code;
156
+ if (typeof code === 'string')
157
+ fields.code = redactArg(code);
158
+ const codeName = 'codeName' in errorArg ? errorArg.codeName : undefined;
159
+ if (typeof codeName === 'string')
160
+ fields.codeName = redactArg(codeName);
161
+ if (errorArg instanceof plugins.mongodb.MongoError)
162
+ fields.errorLabels = errorArg.errorLabels.map(redactArg);
163
+ return fields;
164
+ };
165
+ /**
166
+ * A copy of a thrown value that keeps its error name, its detail, its stack frames,
167
+ * its preserved fields, its cause chain and its aggregated errors, with every
168
+ * credential replaced. Only an error that is its own ancestor is left out, so a
169
+ * cyclic chain still ends.
170
+ */
171
+ const copyRedacted = (thrownArg, redactArg, ancestorsArg) => {
172
+ if (!(thrownArg instanceof Error))
173
+ return new Error(redactArg(String(thrownArg)));
174
+ ancestorsArg.add(thrownArg);
175
+ const copyInner = (innerArg) => innerArg instanceof Error && ancestorsArg.has(innerArg) ? [] : [copyRedacted(innerArg, redactArg, ancestorsArg)];
176
+ const [cause] = thrownArg.cause === undefined ? [] : copyInner(thrownArg.cause);
177
+ const options = cause === undefined ? undefined : { cause };
178
+ const message = redactArg(thrownArg.message);
179
+ const copy = thrownArg instanceof AggregateError
180
+ ? new AggregateError(thrownArg.errors.flatMap((innerArg) => copyInner(innerArg)), message, options)
181
+ : new Error(message, options);
182
+ ancestorsArg.delete(thrownArg);
183
+ copy.name = redactArg(thrownArg.name);
184
+ if (thrownArg.stack !== undefined)
185
+ copy.stack = redactArg(thrownArg.stack);
186
+ return Object.assign(copy, preservedFieldsOf(thrownArg, redactArg));
187
+ };
188
+ /**
189
+ * What a connection failure may show: the driver's error, copied with every
190
+ * credential of the options replaced by `<redacted>`. The original is never
191
+ * attached, because printing an error prints its whole cause chain.
192
+ */
193
+ export const redactSmartdataConnectionError = (thrownArg, optionsArg) => copyRedacted(thrownArg, redactorFor(connectionCredentialsOf(optionsArg)), new Set());
194
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29ubmVjdGlvbnN0cmluZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL25vc3FsZGIvY29ubmVjdGlvbnN0cmluZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQVN4QyxNQUFNLGVBQWUsR0FBRyxZQUFZLENBQUM7QUFFckM7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLDJCQUEyQixHQUFHLENBQUMsVUFBNkMsRUFBVSxFQUFFO0lBQ25HLE1BQU0sV0FBVyxHQUFHLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLGtCQUFrQixDQUFDLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQzdGLE1BQU0sV0FBVyxHQUFHLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLGtCQUFrQixDQUFDLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQzdGLE9BQU8sVUFBVSxDQUFDLFVBQVU7U0FDekIsT0FBTyxDQUFDLFlBQVksRUFBRSxXQUFXLENBQUM7U0FDbEMsT0FBTyxDQUFDLFlBQVksRUFBRSxXQUFXLENBQUM7U0FDbEMsT0FBTyxDQUFDLFFBQVEsRUFBRSxXQUFXLENBQUM7U0FDOUIsT0FBTyxDQUFDLFFBQVEsRUFBRSxXQUFXLENBQUM7U0FDOUIsT0FBTyxDQUFDLFlBQVksRUFBRSxXQUFXLENBQUM7U0FDbEMsT0FBTyxDQUFDLFlBQVksRUFBRSxXQUFXLENBQUM7U0FDbEMsT0FBTyxDQUFDLFVBQVUsRUFBRSxVQUFVLENBQUMsV0FBVyxJQUFJLEVBQUUsQ0FBQztTQUNqRCxPQUFPLENBQUMsVUFBVSxFQUFFLFVBQVUsQ0FBQyxXQUFXLElBQUksRUFBRSxDQUFDLENBQUM7QUFDdkQsQ0FBQyxDQUFDO0FBS0Y7Ozs7OztHQU1HO0FBQ0gsTUFBTSxvQkFBb0IsR0FBRyxJQUFJLEdB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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lossless.org/client",
3
- "version": "1.19.0",
3
+ "version": "1.20.1",
4
4
  "private": false,
5
5
  "description": "One typed client for NoSQLDB, MongoDB, SQLDB, MariaDB, ClickHouse and S3 object storage.",
6
6
  "exports": {
@@ -81,9 +81,9 @@
81
81
  "@aws-sdk/client-s3": "^3.1039.0",
82
82
  "@clickhouse/client": "1.23.1",
83
83
  "@git.zone/tsbuild": "5.1.2",
84
- "@git.zone/tsbundle": "3.1.3",
84
+ "@git.zone/tsbundle": "3.1.4",
85
85
  "@git.zone/tsrun": "3.0.2",
86
- "@git.zone/tstest": "7.1.0",
86
+ "@git.zone/tstest": "7.2.0",
87
87
  "@lossless.org/nosqldb": "8.0.2",
88
88
  "@lossless.org/nosqldb-qualified": "npm:@lossless.org/nosqldb@10.7.2",
89
89
  "@lossless.org/objectstorage": "10.3.1",
package/readme.md CHANGED
@@ -116,6 +116,7 @@ Family imports preserve the established SmartData and SmartBucket names and cons
116
116
  | Query-plan inspection | `Model.inspectQueryPlan(db, { filter, sort?, projection?, limit?, timeoutMS?, signal? })` — the winning plan's stages (`stage`, `depth`, `indexName`, `direction`) and `nReturned`/`totalKeysExamined`/`totalDocsExamined`, read-only and bounded, so a consumer proves a model query uses its intended index without raw driver access |
117
117
  | Per-instance model binding | `bindDocToManager(manager, Model)` returns a subclass of a model declared with `@managed()` and no manager argument, named like the model and bound to that manager, so several databases use the same models at once; any other binding is refused with `invalid_configuration` |
118
118
  | Subclasses need their own binding | a subclass of a bound model that declares decorators or a persistence policy of its own and no `@Collection()`, `@managed()` or `defineCollectionModel()` binding is refused with `invalid_configuration` on every read and write, so it never writes through the parent's schema; an undecorated subclass keeps the inherited binding |
119
+ | Observing this process's writes | `db.observeWrites({ fields, listener })` — one frozen report per acknowledged model write, `{ collection, kind, documents, anchors }` with the requested top-level fields of the written documents and of the selector's equality anchors; writes inside a `db.createSession()` transaction are reported after the commit, never after a rollback; works on NoSQLDB, where `watch()` is unsupported |
119
120
  | Disposable test databases on a unix socket | `SmartdataTestDatabase.create({ connection: { socketPath } })` from `@lossless.org/client/testsupport` reaches an engine that listens only on a unix-domain socket, fault helpers included; the loopback `host`/`port` form is unchanged |
120
121
 
121
122
  `docs/source/smartdata/readme.md` documents each one with its refusals.
@@ -146,7 +147,7 @@ The document model runs on both NoSQLDB and MongoDB, but several APIs do not mea
146
147
  | `getGroupedTotals()` / `getGroupedTotalsPage()` / `iterateGroupedTotals()` with one `groupBy` field | exact, including `sums`, declared nested paths, `unwind` of a declared array and declared fields of its element, `elementFilter`, group key order and `cursor` paging, on 8.0.2 and 10.7.2 alike | exact |
147
148
  | the same with **two** `groupBy` fields | exact, `sums`, nested paths, `unwind`, two element fields of one unwound array (`lines.accountNumber` with `lines.side`), `elementFilter` and `cursor` paging included, on an engine that identifies itself as NoSQLDB 10.5.0 or newer: that release evaluates the composite group key per document. Below it the call is refused with `unsupported_operation` naming both fields and the single-field form, because the engine leaves the composite group key unresolved and answers with a single row whose group values are the literal field paths (`'$owner'`) and whose count is the whole filtered collection; the refusal is the guard against that wrong number, not a fix for it | exact; the client wraps each key member in `$ifNull`, because MongoDB leaves a missing member out of the key and would otherwise report it as a second null group |
148
149
  | guarded `$expr` atomic filters (`$add`, `$lte`, `$ifNull`) | exact on 10.6.0 and newer: the engine evaluates the client's fail-closed compilation (`$cond`, `$let`, `$isNumber`, `$type`), so the guard admits within the limit, refuses beyond it and never matches a stored value that is not a finite number. 8.0.2 rejects the compilation with `invalid argument: $cond` and leaves the document untouched | exact |
149
- | `watch()` change streams | unsupported. `NoSqlConnection` refuses with `unsupported_capability`, and so does a plain `SmartdataDb` whose engine identified itself; against an engine that answers no identity the refusal instead arrives from the engine as `CommandNotFound` once the change stream is read | supported on replica sets |
150
+ | `watch()` change streams | unsupported. `NoSqlConnection` refuses with `unsupported_capability`, and so does a plain `SmartdataDb` whose engine identified itself; against an engine that answers no identity the refusal instead arrives from the engine as `CommandNotFound` once the change stream is read. `db.observeWrites()` reports the process's own model writes on both backends | supported on replica sets |
150
151
  | model-declared partial indexes (`partialFilterExpression`) | not expressible: the index-option type rejects it and the runtime refuses it with `invalid_configuration`. A client decision, not an engine limit on every release — 10.6.0 and newer create partial indexes, 8.0.2 refuses the option with `InvalidOptions` (72) — because one model declaration has to hold on every qualified backend. A migration may create one on 10.7.2; `toleratedIndexNames` accepts it and inspection reports its filter | not expressible, but a migration may own one; inspection reports its filter |
151
152
  | collection validators (`validator`, `validationLevel`, `validationAction`) | no client API; a migration owns a validator through the driver. 10.7.0 and newer create the collection, list the options exactly as given and enforce them on every model write: a violating `insert()` or `atomicUpdate()` rejects with the engine's `DocumentValidationFailure` (121) and its `errInfo`, which the client passes through unchanged rather than mapping to a `SmartdataPersistenceError` code, and stores nothing. Models initialize on a validated collection like on any other. 8.0.2 refuses the option with `InvalidOptions` (72) and creates no collection, so a validator is never silently dropped | enforced; not exercised by the client's MongoDB suite |
152
153
  | write conflicts between transactions, and writes outside a transaction to a document a transaction holds | 10.7.2 (since 10.6.1) detects a conflict at the statement, as MongoDB does: the first writer wins, and the later transaction's statement rejects with the engine's `WriteConflict` (112) labelled `TransientTransactionError`, which the client passes through so `session.withTransaction()` re-runs that callback. A write outside a transaction waits until the transaction holding its document ends, then applies on top; the wait is bounded by the write's own `timeoutMS` and by the engine's fixed 60-second transaction lifetime, after which a waiting writer aborts the holder. Awaiting such a write inside the holding transaction's callback therefore stalls that transaction until one of the two expires. 8.0.2 detects the conflict at commit: the first transaction to commit wins, a write outside a transaction applies at once, and the transaction that wrote first re-runs. Both writes land on either engine; a callback must be safe to re-run and must not swallow the statement error | first writer wins at the statement and outside writes wait, MongoDB's documented rule; not exercised by the client's MongoDB suite |
@@ -222,7 +223,7 @@ SmartClickHouse migration changes: replace RxJS `next/complete` insertion with a
222
223
 
223
224
  `@lossless.org/client/nosqldb` is a fork of @push.rocks/smartdata 11.14.2 (commit `9868817a63de72caef787bd8e68bac3deb9d6131`), taken on 2026-09-10; this repository continues that package's git history, so every upstream fix up to and including 11.14.2 is present here. `nosqldbLineage`, exported from the family entry point, carries the same baseline for runtime and tooling checks, and `docs/source/smartdata` keeps the upstream readme, hints and changelog.
224
225
 
225
- The fork departs from that baseline in five places: `NoSqlConnection` adds the client backend, capability and readiness contract; the family entry point exports it together with `nosqldbLineage`; `watch()` refuses to open a change stream when the connected backend reports change streams as unsupported; `init()` identifies the engine and states the capabilities that identity implies; and two-field grouped totals are refused on a backend that does not group composite keys exactly. The package version file was dropped, because versioning belongs to `@lossless.org/client`. Document-model capabilities added after the fork — `$max`/`$min`, identity seeding through `$setOnInsert`, `atomicUpsertMany()` and `identityAsDocumentId` — are this package's own additions and have no upstream counterpart.
226
+ The fork departs from that baseline in six places: `NoSqlConnection` adds the client backend, capability and readiness contract; the family entry point exports it together with `nosqldbLineage`; `watch()` refuses to open a change stream when the connected backend reports change streams as unsupported; `init()` identifies the engine and states the capabilities that identity implies; a failed `init()` rejects with `SmartdataPersistenceError` (`unsupported_operation`) instead of the driver's own error, and without a deadline carries that error as `cause`, copied with every connection credential redacted, so code that matched the driver's error class (`MongoServerSelectionError`, …) reads `cause.name` (a cleanup that fails as well makes the rejection an `AggregateError` of both); and two-field grouped totals are refused on a backend that does not group composite keys exactly. The package version file was dropped, because versioning belongs to `@lossless.org/client`. Document-model capabilities added after the fork — `$max`/`$min`, identity seeding through `$setOnInsert`, `atomicUpsertMany()` and `identityAsDocumentId` — are this package's own additions and have no upstream counterpart.
226
227
 
227
228
  Fixes land here. @push.rocks/smartdata is no longer a release channel for this implementation, so a later upstream version is not a source to merge from and not an upgrade path for consumers of this package.
228
229
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@lossless.org/client',
6
- version: '1.19.0',
6
+ version: '1.20.1',
7
7
  description: 'One typed client for NoSQLDB, MongoDB, SQLDB, MariaDB, ClickHouse and S3 object storage.'
8
8
  }
@@ -1,6 +1,7 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type { SmartdataCollection } from './classes.collection.js';
3
3
  import {
4
+ reportSmartdataCollectionWrite,
4
5
  runWithOrdinarySmartdataSession,
5
6
  type TSmartdataOrdinarySession,
6
7
  } from './classes.session.js';
@@ -31,13 +32,17 @@ export const executeAtomicDelete = async <T>(
31
32
  },
32
33
  async (rawSessionArg, deadlineArg) => {
33
34
  await deadlineArg.join(collectionArg.init());
34
- return collectionArg.mongoDbCollection.deleteOne(
35
+ const result = await collectionArg.mongoDbCollection.deleteOne(
35
36
  filterArg,
36
37
  {
37
38
  session: rawSessionArg,
38
39
  ...deadlineArg.commandOptions(),
39
40
  },
40
41
  );
42
+ if (result.deletedCount > 0) {
43
+ reportSmartdataCollectionWrite(collectionArg, optsArg?.session, 'delete', null, filterArg);
44
+ }
45
+ return result;
41
46
  },
42
47
  );
43
48
  };
@@ -71,13 +76,17 @@ export const executeAtomicDeleteMany = async <T>(
71
76
  },
72
77
  async (rawSessionArg, deadlineArg) => {
73
78
  await deadlineArg.join(collectionArg.init());
74
- return collectionArg.mongoDbCollection.deleteMany(
79
+ const result = await collectionArg.mongoDbCollection.deleteMany(
75
80
  filterArg,
76
81
  {
77
82
  session: rawSessionArg,
78
83
  ...deadlineArg.commandOptions(),
79
84
  },
80
85
  );
86
+ if (result.deletedCount > 0) {
87
+ reportSmartdataCollectionWrite(collectionArg, optsArg?.session, 'delete', null, filterArg);
88
+ }
89
+ return result;
81
90
  },
82
91
  );
83
92
  };
@@ -5,6 +5,7 @@ import {
5
5
  SmartdataPersistenceError,
6
6
  } from './classes.persistence.js';
7
7
  import {
8
+ reportSmartdataCollectionWrite,
8
9
  runWithOrdinarySmartdataSession,
9
10
  isSmartdataSession,
10
11
  failSmartdataTransaction,
@@ -138,6 +139,15 @@ export const executeAtomicFindOneAndUpdate = async <T>(
138
139
  classified.document, policy, collectionArg.getBoundModelSchema()!.persistedFields,
139
140
  ) as TRawDocument;
140
141
  }
142
+ if (classified.status !== 'not_matched') {
143
+ reportSmartdataCollectionWrite(
144
+ collectionArg,
145
+ optionsArg.session,
146
+ classified.status === 'upserted' ? 'insert' : 'update',
147
+ classified.document ? [classified.document] : null,
148
+ filterArg,
149
+ );
150
+ }
141
151
  return classified;
142
152
  } catch (errorArg) {
143
153
  if (policy) {
@@ -213,6 +223,18 @@ export const executeAtomicFindOneAndReplace = async <T>(
213
223
  // Unreachable: this operation never requests an upsert.
214
224
  return unsupportedResult();
215
225
  }
226
+ if (classified.status === 'matched') {
227
+ // A before image leaves the replacement as the only image of the new state.
228
+ reportSmartdataCollectionWrite(
229
+ collectionArg,
230
+ optionsArg.session,
231
+ 'replace',
232
+ optionsArg.returnDocument === 'after'
233
+ ? [classified.document]
234
+ : [classified.document, replacementArg],
235
+ filterArg,
236
+ );
237
+ }
216
238
  return classified;
217
239
  } catch (errorArg) {
218
240
  return normalizeOrdinaryPersistenceError(
@@ -1,7 +1,11 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type { SmartdataCollection } from './classes.collection.js';
3
- import { normalizeOrdinaryPersistenceError } from './classes.persistence.js';
4
3
  import {
4
+ getMongoBulkWriteErrorIndexes,
5
+ normalizeOrdinaryPersistenceError,
6
+ } from './classes.persistence.js';
7
+ import {
8
+ reportSmartdataCollectionWrite,
5
9
  runWithOrdinarySmartdataSession,
6
10
  type TSmartdataOrdinarySession,
7
11
  } from './classes.session.js';
@@ -21,6 +25,8 @@ export const executeAtomicUpdate = async <T>(
21
25
  upsert?: boolean;
22
26
  session?: TSmartdataOrdinarySession;
23
27
  timeoutMS?: number;
28
+ /** The document image the update writes, when the caller holds one: a fenced save's instance. */
29
+ observedDocument?: object;
24
30
  },
25
31
  ): Promise<plugins.mongodb.UpdateResult> => {
26
32
  return runWithOrdinarySmartdataSession(
@@ -35,7 +41,7 @@ export const executeAtomicUpdate = async <T>(
35
41
  async (rawSessionArg, deadlineArg) => {
36
42
  await deadlineArg.join(collectionArg.init());
37
43
  try {
38
- return await collectionArg.mongoDbCollection.updateOne(
44
+ const result = await collectionArg.mongoDbCollection.updateOne(
39
45
  filterArg,
40
46
  updateArg,
41
47
  {
@@ -44,6 +50,16 @@ export const executeAtomicUpdate = async <T>(
44
50
  ...deadlineArg.commandOptions(),
45
51
  },
46
52
  );
53
+ if (result.upsertedCount > 0 || result.matchedCount > 0) {
54
+ reportSmartdataCollectionWrite(
55
+ collectionArg,
56
+ optsArg?.session,
57
+ result.upsertedCount > 0 ? 'insert' : 'update',
58
+ optsArg?.observedDocument ? [optsArg.observedDocument] : null,
59
+ filterArg,
60
+ );
61
+ }
62
+ return result;
47
63
  } catch (errorArg) {
48
64
  return normalizeOrdinaryPersistenceError(
49
65
  errorArg,
@@ -90,6 +106,25 @@ export const executeAtomicUpsertMany = async <T>(
90
106
  },
91
107
  async (rawSessionArg, deadlineArg) => {
92
108
  await deadlineArg.join(collectionArg.init());
109
+ /**
110
+ * Every entry upserts, so an entry that neither inserted nor failed
111
+ * matched its document.
112
+ */
113
+ const reportEntries = (
114
+ upsertedIdsArg: Record<number, unknown> | undefined,
115
+ failedIndexesArg: ReadonlySet<number>,
116
+ ): void => {
117
+ operationsArg.forEach((operationArg, indexArg) => {
118
+ if (failedIndexesArg.has(indexArg)) return;
119
+ reportSmartdataCollectionWrite(
120
+ collectionArg,
121
+ optsArg?.session,
122
+ Object.hasOwn(upsertedIdsArg ?? {}, indexArg) ? 'insert' : 'update',
123
+ null,
124
+ operationArg.filter,
125
+ );
126
+ });
127
+ };
93
128
  try {
94
129
  const result = await collectionArg.mongoDbCollection.bulkWrite(
95
130
  operationsArg.map((operationArg) => ({
@@ -105,6 +140,9 @@ export const executeAtomicUpsertMany = async <T>(
105
140
  ...deadlineArg.commandOptions(),
106
141
  },
107
142
  );
143
+ if (result.isOk()) {
144
+ reportEntries(result.upsertedIds, new Set());
145
+ }
108
146
  return {
109
147
  acknowledged: result.isOk(),
110
148
  matchedCount: result.matchedCount,
@@ -112,6 +150,14 @@ export const executeAtomicUpsertMany = async <T>(
112
150
  upsertedCount: result.upsertedCount,
113
151
  };
114
152
  } catch (errorArg) {
153
+ // An unordered batch that failed on write errors attempted every entry.
154
+ const failedIndexes = getMongoBulkWriteErrorIndexes(errorArg);
155
+ if (failedIndexes) {
156
+ reportEntries(
157
+ (errorArg as plugins.mongodb.MongoBulkWriteError).result?.upsertedIds,
158
+ failedIndexes,
159
+ );
160
+ }
115
161
  return normalizeOrdinaryPersistenceError(
116
162
  errorArg,
117
163
  `Atomic upsert-many conflicts with a unique index in collection "${collectionArg.collectionName}".`,
@@ -151,7 +197,7 @@ export const executeAtomicUpdateMany = async <T>(
151
197
  async (rawSessionArg, deadlineArg) => {
152
198
  await deadlineArg.join(collectionArg.init());
153
199
  try {
154
- return await collectionArg.mongoDbCollection.updateMany(
200
+ const result = await collectionArg.mongoDbCollection.updateMany(
155
201
  filterArg,
156
202
  updateArg,
157
203
  {
@@ -159,6 +205,10 @@ export const executeAtomicUpdateMany = async <T>(
159
205
  ...deadlineArg.commandOptions(),
160
206
  },
161
207
  );
208
+ if (result.matchedCount > 0) {
209
+ reportSmartdataCollectionWrite(collectionArg, optsArg?.session, 'update', null, filterArg);
210
+ }
211
+ return result;
162
212
  } catch (errorArg) {
163
213
  return normalizeOrdinaryPersistenceError(
164
214
  errorArg,
@@ -6,6 +6,7 @@ import { snapshotBoundedBsonDocument } from './classes.bsonsnapshot.js';
6
6
  import { validateOrdinaryStoredDocument } from './classes.ordinarypersistence.js';
7
7
  import { SmartdataPersistenceError, isMongoDuplicateKeyError } from './classes.persistence.js';
8
8
  import { readBoundedInspectionCursor } from './classes.namespaceinspection.js';
9
+ import { reportSmartdataWrite } from './classes.session.js';
9
10
  import {
10
11
  collectionModelSchemaResolverSymbol, collectionTopologyDbInspectionSymbol,
11
12
  getExpectedCollectionTopologyForSchema, inspectCollectionTopologySnapshot,
@@ -96,7 +97,12 @@ export const insertBootstrapRecord = async (
96
97
  dispatched ? 'Bootstrap insertion outcome is uncertain; inspect its explicit ID before proceeding.'
97
98
  : 'Bootstrap insertion could not inspect the current database connection.');
98
99
  }
99
- if (outcome === 'inserted') return Object.freeze({ acknowledged: true, insertedId: document._id as string });
100
+ if (outcome === 'inserted') {
101
+ reportSmartdataWrite(dbArg, undefined, {
102
+ collection: schema.collectionName, kind: 'insert', documents: [document],
103
+ });
104
+ return Object.freeze({ acknowledged: true, insertedId: document._id as string });
105
+ }
100
106
  const messages = {
101
107
  unique_conflict: 'Bootstrap record conflicts with a unique index.',
102
108
  invalid_configuration: 'Bootstrap insertion requires absent or exact primary-only collection topology.',
@@ -24,13 +24,16 @@ import { CollectionFactory } from './classes.collectionfactory.js';
24
24
  import { logger } from './logging.js';
25
25
  import {
26
26
  SmartdataPersistenceError,
27
+ getMongoBulkWriteErrorIndexes,
27
28
  isMongoIndexConflictError,
28
29
  normalizeOrdinaryPersistenceError,
29
30
  } from './classes.persistence.js';
30
31
  import { notifyCollectionReconnect } from './classes.collectionlifecycle.js';
31
32
  import type { SmartdataOperationDeadline } from './classes.operationdeadline.js';
33
+ import { hasSmartdataWriteObservers } from './classes.writeobserver.js';
32
34
  import {
33
35
  leaseOrdinarySmartdataSession,
36
+ reportSmartdataCollectionWrite,
34
37
  runWithOrdinarySmartdataSession,
35
38
  type TSmartdataOrdinarySession,
36
39
  } from './classes.session.js';
@@ -2595,10 +2598,14 @@ export class SmartdataCollection<T> {
2595
2598
 
2596
2599
  const saveableObject = preparedObject ?? await dbDocArg.createSavableObject();
2597
2600
  try {
2598
- return await this.mongoDbCollection.insertOne(saveableObject, {
2601
+ const result = await this.mongoDbCollection.insertOne(saveableObject, {
2599
2602
  session: rawSessionArg,
2600
2603
  ...deadlineArg.commandOptions(),
2601
2604
  });
2605
+ if (result.acknowledged) {
2606
+ reportSmartdataCollectionWrite(this, opts?.session, 'insert', [saveableObject]);
2607
+ }
2608
+ return result;
2602
2609
  } catch (err: any) {
2603
2610
  return normalizeOrdinaryPersistenceError(
2604
2611
  err,
@@ -2707,13 +2714,29 @@ export class SmartdataCollection<T> {
2707
2714
  saveableObjects.push(await dbDocArg.createSavableObject());
2708
2715
  }
2709
2716
  }
2717
+ const reportInserted = (insertedIdsArg: Record<number, unknown> | undefined): void => {
2718
+ const inserted = Object.keys(insertedIdsArg ?? {})
2719
+ .map((indexArg) => saveableObjects[Number(indexArg)])
2720
+ .filter((documentArg) => documentArg !== undefined);
2721
+ if (inserted.length > 0) {
2722
+ reportSmartdataCollectionWrite(this, opts?.session, 'insert', inserted);
2723
+ }
2724
+ };
2710
2725
  try {
2711
- return await this.mongoDbCollection.insertMany(saveableObjects, {
2726
+ const result = await this.mongoDbCollection.insertMany(saveableObjects, {
2712
2727
  session: rawSessionArg,
2713
2728
  ordered: opts?.ordered !== false,
2714
2729
  ...deadlineArg.commandOptions(),
2715
2730
  });
2731
+ if (result.acknowledged) {
2732
+ reportInserted(result.insertedIds);
2733
+ }
2734
+ return result;
2716
2735
  } catch (err: any) {
2736
+ // A batch that failed on write errors names the documents it still inserted.
2737
+ if (getMongoBulkWriteErrorIndexes(err)) {
2738
+ reportInserted((err as plugins.mongodb.MongoBulkWriteError).result?.insertedIds);
2739
+ }
2717
2740
  return normalizeOrdinaryPersistenceError(
2718
2741
  err,
2719
2742
  `Insert conflicts with a unique index in collection "${this.collectionName}".`,
@@ -2756,6 +2779,14 @@ export class SmartdataCollection<T> {
2756
2779
  // seeded body must not repeat the immutable primary key: MongoDB derives
2757
2780
  // it from the filter's equality condition on the insert branch.
2758
2781
  const documentIdIdentity = this.modelSchema?.identityAsDocumentId === identityFieldArg;
2782
+ const reportUpserted = (upsertedIdsArg: Record<number, unknown> | undefined): void => {
2783
+ const inserted = Object.keys(upsertedIdsArg ?? {})
2784
+ .map((indexArg) => prepared[Number(indexArg)])
2785
+ .filter((documentArg) => documentArg !== undefined);
2786
+ if (inserted.length > 0) {
2787
+ reportSmartdataCollectionWrite(this, optsArg.session, 'insert', inserted);
2788
+ }
2789
+ };
2759
2790
  try {
2760
2791
  const result = await this.mongoDbCollection.bulkWrite(prepared.map((document) => {
2761
2792
  const { _id: storedId, ...body } = document as Record<string, unknown>;
@@ -2769,12 +2800,16 @@ export class SmartdataCollection<T> {
2769
2800
  },
2770
2801
  };
2771
2802
  }), { ordered: false, session: rawSessionArg, ...deadlineArg.commandOptions() });
2803
+ reportUpserted(result.upsertedIds);
2772
2804
  if (result.upsertedCount + result.matchedCount !== prepared.length) {
2773
2805
  throw new SmartdataPersistenceError('unsupported_operation',
2774
2806
  'insertManyIfAbsent requires an acknowledged result for every identity.');
2775
2807
  }
2776
2808
  return { insertedCount: result.upsertedCount, matchedCount: result.matchedCount };
2777
2809
  } catch (errorArg) {
2810
+ if (getMongoBulkWriteErrorIndexes(errorArg)) {
2811
+ reportUpserted((errorArg as plugins.mongodb.MongoBulkWriteError).result?.upsertedIds);
2812
+ }
2778
2813
  return normalizeOrdinaryPersistenceError(errorArg,
2779
2814
  `Insert conflicts with a unique index in collection "${this.collectionName}".`);
2780
2815
  }
@@ -2825,7 +2860,7 @@ export class SmartdataCollection<T> {
2825
2860
  updateableObject[key] = saveableObject[key];
2826
2861
  }
2827
2862
  try {
2828
- return await this.mongoDbCollection.updateOne(
2863
+ const result = await this.mongoDbCollection.updateOne(
2829
2864
  identifiableObject,
2830
2865
  {
2831
2866
  $set: updateableObject,
@@ -2835,6 +2870,16 @@ export class SmartdataCollection<T> {
2835
2870
  },
2836
2871
  { upsert: true, session: rawSessionArg, ...deadlineArg.commandOptions() },
2837
2872
  );
2873
+ if (result.upsertedCount > 0 || result.matchedCount > 0) {
2874
+ reportSmartdataCollectionWrite(
2875
+ this,
2876
+ opts?.session,
2877
+ result.upsertedCount > 0 ? 'insert' : 'update',
2878
+ [saveableObject],
2879
+ identifiableObject,
2880
+ );
2881
+ }
2882
+ return result;
2838
2883
  } catch (errorArg) {
2839
2884
  return normalizeOrdinaryPersistenceError(
2840
2885
  errorArg,
@@ -2857,10 +2902,24 @@ export class SmartdataCollection<T> {
2857
2902
  await this.checkDoc(dbDocArg);
2858
2903
  const identifiableObject = await dbDocArg.createIdentifiableObject();
2859
2904
  this.assertConstrainingIdentifiableObject(identifiableObject, 'delete');
2860
- return this.mongoDbCollection.deleteOne(identifiableObject, {
2905
+ // Serialized before the delete, so a failing serializer never fails an acknowledged delete.
2906
+ const observedDocument = hasSmartdataWriteObservers(this.smartdataDb)
2907
+ ? await dbDocArg.createSavableObject() as object
2908
+ : undefined;
2909
+ const result = await this.mongoDbCollection.deleteOne(identifiableObject, {
2861
2910
  session: rawSessionArg,
2862
2911
  ...deadlineArg.commandOptions(),
2863
2912
  });
2913
+ if (result.deletedCount > 0) {
2914
+ reportSmartdataCollectionWrite(
2915
+ this,
2916
+ opts?.session,
2917
+ 'delete',
2918
+ observedDocument ? [observedDocument] : null,
2919
+ identifiableObject,
2920
+ );
2921
+ }
2922
+ return result;
2864
2923
  },
2865
2924
  );
2866
2925
  }