@lossless.org/client 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lossless.org/client",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "description": "One typed client for NoSQLDB, MongoDB, SQLDB, MariaDB, ClickHouse and S3 object storage.",
6
6
  "exports": {
@@ -57,7 +57,7 @@
57
57
  "@push.rocks/smartstring": "^4.1.0",
58
58
  "@push.rocks/smarttime": "^4.2.3",
59
59
  "@push.rocks/smartunique": "^3.0.9",
60
- "@push.rocks/taskbuffer": "^8.0.2",
60
+ "@push.rocks/taskbuffer": "^9.0.3",
61
61
  "@tsclass/tsclass": "^9.5.1",
62
62
  "minimatch": "^10.2.5",
63
63
  "mongodb": "^7.2.0"
@@ -81,15 +81,15 @@
81
81
  "devDependencies": {
82
82
  "@aws-sdk/client-s3": "^3.1039.0",
83
83
  "@clickhouse/client": "1.23.1",
84
- "@git.zone/tsbuild": "4.5.0",
85
- "@git.zone/tsrun": "2.0.6",
86
- "@git.zone/tstest": "6.1.1",
84
+ "@git.zone/tsbuild": "5.0.0",
85
+ "@git.zone/tsrun": "3.0.0",
86
+ "@git.zone/tstest": "6.3.0",
87
87
  "@lossless.org/nosqldb": "8.0.2",
88
- "@lossless.org/nosqldb-qualified": "npm:@lossless.org/nosqldb@10.5.0",
88
+ "@lossless.org/nosqldb-qualified": "npm:@lossless.org/nosqldb@10.6.0",
89
89
  "@lossless.org/objectstorage": "10.1.0",
90
90
  "@lossless.org/sqldb": "0.2.3",
91
91
  "@push.rocks/qenv": "^6.1.3",
92
- "@types/node": "26.5.1",
92
+ "@types/node": "26.6.1",
93
93
  "mariadb": "3.5.4",
94
94
  "mongodb-memory-server": "11.1.0"
95
95
  },
package/readme.md CHANGED
@@ -91,6 +91,8 @@ Family imports preserve the established SmartData and SmartBucket names and cons
91
91
  | Identities on a migrated unique index | `@unI({ indexName })` names the index that backs the identity instead of `<field>_1` |
92
92
  | Typed `null` predicates | read and atomic filters accept `null` for a field declared optional, so a value stored as an explicit null stays addressable |
93
93
  | Owned sessions on cursors | `getCursor()` takes the handle from `db.createSession()` and keeps it leased until the cursor is closed |
94
+ | Owned sessions on instance writes | `instance.save({ session })` and `instance.delete({ session })` take the handle from `db.createSession()`, leased for the one write like every other model API |
95
+ | Typed connection pool options | `new SmartdataDb(options: ISmartdataDbOptions)` types `maxPoolSize`, `maxIdleTimeMS`, `serverSelectionTimeoutMS` and `socketTimeoutMS` beside the descriptor fields, so no cast through `IMongoDescriptor` is needed |
94
96
  | Named plural selectors | a non-empty `$in` anchors `atomicUpdateMany()`, `atomicDeleteMany()` and `atomicUpsertMany()`; singular operations are unchanged |
95
97
  | Fields only atomic writes may update | `@svDb({ atomicOnly: true })` and `atomicOnlyFields` — seeded by every write that creates the document, never overwritten by a later `save()` |
96
98
  | Defaults inside `$expr` | `{ $ifNull: ['$field', literal] }` operands, so a guard covers rows written before the field existed |
@@ -98,13 +100,14 @@ Family imports preserve the established SmartData and SmartBucket names and cons
98
100
  | Identity rotation | `Model.atomicFindOneAndReplace(filter, replacement, opts?)` — the one write that moves an `@unI()` identity, under the matched document's `_id` |
99
101
  | Fail-fast on unprepared collections | a session-carrying call inside a transaction refuses instead of stalling on lazy index DDL until the client deadline |
100
102
  | Migration-owned indexes | `toleratedIndexNames` on the model — named undeclared indexes are observed, never created, dropped or verified, and no longer make the topology divergent |
103
+ | Partial index filters in inspection | `inspectNamespace()`, `inspectCollectionTopology()` and `getIndexInfo()` report a partial index's `partialFilterExpression` exactly as the backend lists it, as a frozen snapshot present only on a partial index |
101
104
  | Key-value store readiness | `EasyStore.ensureInitialized()` — installs the store's declared index at startup, outside any transaction window, so a first `readKey()`/`writeKey()` needs no index DDL |
102
105
 
103
106
  `docs/source/smartdata/readme.md` documents each one with its refusals.
104
107
 
105
108
  ### Document APIs that need a MongoDB backend
106
109
 
107
- The document model runs on both NoSQLDB and MongoDB, but four APIs do not mean the same thing on both. The NoSQLDB engine is qualified at 8.0.2 and 10.5.0 (see [Verification](#verification)); the two releases part on one row below — 10.5.0 groups composite keys exactly, 8.0.2 does not — and answer every other row alike.
110
+ The document model runs on both NoSQLDB and MongoDB, but four APIs do not mean the same thing on both. The NoSQLDB engine is qualified at 8.0.2 and 10.6.0 (see [Verification](#verification)); the two releases part on the grouped-totals and guarded-`$expr` rows below — 10.6.0 groups composite keys exactly and evaluates the guard, 8.0.2 does neither — and on whether a migration can create a partial index at all.
108
111
 
109
112
  `init()` asks the connected engine what it is with one `buildInfo` command and records the answer on the database as `engineIdentity` (`{ product, version }`). NoSQLDB answers with its own `nosqldb` identity document from 10.3.0 onward; a MongoDB server answers `buildInfo` without one; an engine that refuses the command — NoSQLDB 8.0.2 answers `CommandNotFound` — stays `{ product: 'unknown', version: null }`. The identity is what states `capabilities` on a plain `SmartdataDb`, and an unidentified engine states none at all, so every capability-gated call refuses. `NoSqlConnection` keeps stating the capabilities of its declared backend from construction on.
110
113
 
@@ -112,12 +115,12 @@ The document model runs on both NoSQLDB and MongoDB, but four APIs do not mean t
112
115
  | --- | --- | --- |
113
116
  | `getGroupedTotals()` / `getGroupedTotalsPage()` with one `groupBy` field | exact, including `sums` | exact |
114
117
  | the same with **two** `groupBy` fields | exact, `sums` 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 |
115
- | guarded `$expr` atomic filters (`$add`, `$lte`, `$ifNull`) | refused by the engine: the fail-closed compilation uses `$cond`, which the engine rejects with `invalid argument: $cond`. The document is left untouched | exact |
118
+ | guarded `$expr` atomic filters (`$add`, `$lte`, `$ifNull`) | exact on 10.6.0: 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 |
116
119
  | `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 |
117
- | model-declared partial indexes (`partialFilterExpression`) | not expressible: the index-option type rejects it and the runtime refuses it with `invalid_configuration`; the engine cannot create one either and refuses the option itself with `InvalidOptions` (72) on both qualified releases. Migration-owned partial indexes stay reachable through `toleratedIndexNames` | not expressible, but a migration may own one |
120
+ | 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 creates 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.6.0; `toleratedIndexNames` accepts it and inspection reports its filter | not expressible, but a migration may own one; inspection reports its filter |
118
121
  | aggregation-pipeline (array) updates | refused by the client with `invalid_argument` before any backend is contacted. A client-owned refusal, not an engine limit: both qualified engines apply a pipeline update themselves | same client refusal |
119
122
 
120
- `test/nosqldb/smartdb/test.engine-capability-boundary.node.ts` asserts the identity, grouped-totals, guarded-`$expr`, partial-index and pipeline-update rows against both qualified engines, so a boundary that moves shows up as a failing suite rather than as a changed number in a consumer. The two halves of the grouped-totals row are separate tests, each bound to the engine that answers it, and the engine's own composite-key answer stays under assertion through the raw `aggregateGroupedTotals()` boundary — one literal row on 8.0.2, the two exact pairs on 10.5.0. The same boundary pins what 10.5.0 gained with it, a validated `$group` stage: a `$cond` inside a composite key and a non-document accumulator are refused with `TypeMismatch` (14) there, while 8.0.2 answers both. The `watch()` row is the contract refusal `ts/nosqldb/classes.collection.ts` states; the engine suite opens no change stream.
123
+ `test/nosqldb/smartdb/test.engine-capability-boundary.node.ts` asserts the identity, grouped-totals, guarded-`$expr`, partial-index and pipeline-update rows against both qualified engines, so a boundary that moves shows up as a failing suite rather than as a changed number in a consumer. The two halves of the grouped-totals and guarded-`$expr` rows are separate tests, each bound to the engine that answers it, and the engine's own composite-key answer stays under assertion through the raw `aggregateGroupedTotals()` boundary — one literal row on 8.0.2, the two exact pairs on 10.6.0. The same boundary pins the rest of the engine's `$group` stage: a non-document accumulator is refused with `TypeMismatch` (14) on 10.6.0 while 8.0.2 answers it, and a `$cond` inside a composite key is evaluated per document on 10.6.0 while 8.0.2 answers its single literal row. It also pins whether a migration can create a partial index — 10.6.0 creates it and lists the filter, 8.0.2 refuses with 72. The `watch()` row is the contract refusal `ts/nosqldb/classes.collection.ts` states; the engine suite opens no change stream.
121
124
 
122
125
  ## Relational SQL
123
126
 
@@ -197,14 +200,14 @@ Qualified engines:
197
200
 
198
201
  | Component | Qualified versions |
199
202
  | --- | --- |
200
- | NoSQLDB engine | **8.0.2 and 10.5.0** — the same `test/nosqldb/smartdb/` suite runs against both, through the single engine seam in `test/nosqldb/helpers/smartdb.ts`; `NOSQLDB_ENGINE=qualified` selects 10.5.0 |
203
+ | NoSQLDB engine | **8.0.2 and 10.6.0** — the same `test/nosqldb/smartdb/` suite runs against both, through the single engine seam in `test/nosqldb/helpers/smartdb.ts`; `NOSQLDB_ENGINE=qualified` selects 10.6.0 |
201
204
  | MongoDB | 8.0.26 replica sets |
202
205
  | ObjectStorage | 10.1.0 |
203
206
  | SQLDB | 0.2.3 |
204
207
  | MariaDB | 11.8 |
205
208
  | ClickHouse | 25.8 |
206
209
 
207
- Both engine versions run the whole document suite. They part on one asserted boundary — 10.5.0 groups composite keys exactly and answers two-field grouped totals, 8.0.2 does neither and the client refuses there — which [Document APIs that need a MongoDB backend](#document-apis-that-need-a-mongodb-backend) states row by row. Text search, the Lucene adapter and the broad read-filter surface are exercised against MongoDB only. `pnpm run test:minio` uses a digest-pinned MinIO image: `sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e`. That MinIO version enforces the tested conditional uploads but does not enforce conditional deletion; its exact-purge capability is correctly unavailable. ObjectStorage passes both live probes. AWS S3 has not been qualified against a live account in this migration; no universal S3 exact-operation guarantee is inferred from the SDK.
210
+ Both engine versions run the whole document suite. They part on asserted boundaries — 10.6.0 groups composite keys exactly and answers two-field grouped totals, evaluates guarded `$expr` filters and creates migration-owned partial indexes, 8.0.2 does none of these and the client refuses the two-field form there — which [Document APIs that need a MongoDB backend](#document-apis-that-need-a-mongodb-backend) states row by row. Text search, the Lucene adapter and the broad read-filter surface are exercised against MongoDB only. `pnpm run test:minio` uses a digest-pinned MinIO image: `sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e`. That MinIO version enforces the tested conditional uploads but does not enforce conditional deletion; its exact-purge capability is correctly unavailable. ObjectStorage passes both live probes. AWS S3 has not been qualified against a live account in this migration; no universal S3 exact-operation guarantee is inferred from the SDK.
208
211
 
209
212
  ## License and Legal Information
210
213
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@lossless.org/client',
6
- version: '1.5.0',
6
+ version: '1.6.0',
7
7
  description: 'One typed client for NoSQLDB, MongoDB, SQLDB, MariaDB, ClickHouse and S3 object storage.'
8
8
  }
@@ -39,6 +39,7 @@ import {
39
39
  getExpectedCollectionTopologyForSchema,
40
40
  getIdentityIndexName,
41
41
  isIdentityIndexFor,
42
+ isPlainIndexDocument,
42
43
  compareSmartdataTopologyStrings,
43
44
  } from './classes.collectiontopology.js';
44
45
 
@@ -210,6 +211,8 @@ export interface ISmartdataIndexInfo {
210
211
  unique: boolean;
211
212
  sparse: boolean;
212
213
  expireAfterSeconds?: number;
214
+ /** The filter of a partial index exactly as the backend lists it; absent otherwise. */
215
+ partialFilterExpression?: Record<string, unknown>;
213
216
  }
214
217
  interface ISmartdataDecoratorMetadata {
215
218
  globalSaveableProperties?: string[];
@@ -2814,6 +2817,9 @@ export class SmartdataCollection<T> {
2814
2817
  typeof indexArg.expireAfterSeconds === 'number'
2815
2818
  ? indexArg.expireAfterSeconds
2816
2819
  : undefined,
2820
+ ...(isPlainIndexDocument(indexArg.partialFilterExpression)
2821
+ ? { partialFilterExpression: indexArg.partialFilterExpression }
2822
+ : {}),
2817
2823
  };
2818
2824
  });
2819
2825
  }
@@ -33,6 +33,13 @@ export interface ISmartdataCollectionTopologyIndex {
33
33
  readonly unique: boolean;
34
34
  readonly sparse: boolean;
35
35
  readonly expireAfterSeconds: number | null;
36
+ /**
37
+ * The filter of a partial index exactly as the backend lists it, present
38
+ * only on a partial index. Plain documents and arrays inside it are frozen
39
+ * copies; BSON values keep the type the driver decoded. A model never
40
+ * declares one, so an expected index never carries it.
41
+ */
42
+ readonly partialFilterExpression?: Readonly<Record<string, unknown>>;
36
43
  }
37
44
 
38
45
  export interface ISmartdataExpectedCollectionTopology {
@@ -110,6 +117,7 @@ const ordinaryActualIndexKeys = new Set([
110
117
  'unique',
111
118
  'sparse',
112
119
  'expireAfterSeconds',
120
+ 'partialFilterExpression',
113
121
  ]);
114
122
  const textActualIndexKeys = new Set([
115
123
  ...ordinaryActualIndexKeys,
@@ -141,6 +149,37 @@ export const compareSmartdataTopologyStrings = (
141
149
  return leftCodePoints.length < rightCodePoints.length ? -1 : 1;
142
150
  };
143
151
 
152
+ /** @internal Whether a listed index property is a plain BSON document. */
153
+ export const isPlainIndexDocument = (
154
+ valueArg: unknown,
155
+ ): valueArg is Record<string, unknown> => {
156
+ if (typeof valueArg !== 'object' || valueArg === null || Array.isArray(valueArg)) {
157
+ return false;
158
+ }
159
+ const prototype = Object.getPrototypeOf(valueArg);
160
+ return prototype === Object.prototype || prototype === null;
161
+ };
162
+
163
+ /**
164
+ * Copies and freezes the documents and arrays of a listed index filter, so the
165
+ * reported filter is exactly what the backend listed and nobody can edit it
166
+ * afterwards. BSON values — ObjectIds, dates, decimals — are returned as the
167
+ * driver decoded them.
168
+ */
169
+ const snapshotIndexFilterValue = (valueArg: unknown): unknown => {
170
+ if (Array.isArray(valueArg)) {
171
+ return Object.freeze(valueArg.map((entryArg) => snapshotIndexFilterValue(entryArg)));
172
+ }
173
+ if (isPlainIndexDocument(valueArg)) {
174
+ const copy: Record<string, unknown> = {};
175
+ for (const [key, entry] of Object.entries(valueArg)) {
176
+ copy[key] = snapshotIndexFilterValue(entry);
177
+ }
178
+ return Object.freeze(copy);
179
+ }
180
+ return valueArg;
181
+ };
182
+
144
183
  function freezeIndex(
145
184
  indexArg: {
146
185
  name: string;
@@ -152,6 +191,7 @@ function freezeIndex(
152
191
  unique: boolean;
153
192
  sparse: boolean;
154
193
  expireAfterSeconds: number | null;
194
+ partialFilterExpression?: Readonly<Record<string, unknown>>;
155
195
  },
156
196
  ): ISmartdataCollectionTopologyIndex {
157
197
  return Object.freeze({
@@ -162,6 +202,15 @@ function freezeIndex(
162
202
  unique: indexArg.unique,
163
203
  sparse: indexArg.sparse,
164
204
  expireAfterSeconds: indexArg.expireAfterSeconds,
205
+ // Absent rather than null on an ordinary index, so every index that is
206
+ // not partial reads exactly as it did before filters were reported.
207
+ ...(indexArg.partialFilterExpression === undefined
208
+ ? {}
209
+ : {
210
+ partialFilterExpression: snapshotIndexFilterValue(
211
+ indexArg.partialFilterExpression,
212
+ ) as Readonly<Record<string, unknown>>,
213
+ }),
165
214
  });
166
215
  }
167
216
 
@@ -390,6 +439,13 @@ const normalizeActualIndex = (
390
439
  if (rawIndex.sparse !== undefined && typeof rawIndex.sparse !== 'boolean') {
391
440
  supported = false;
392
441
  }
442
+ const partialFilterExpression = rawIndex.partialFilterExpression;
443
+ if (
444
+ partialFilterExpression !== undefined &&
445
+ !isPlainIndexDocument(partialFilterExpression)
446
+ ) {
447
+ supported = false;
448
+ }
393
449
  const expireAfterSeconds = rawIndex.expireAfterSeconds;
394
450
  if (
395
451
  expireAfterSeconds !== undefined &&
@@ -495,6 +551,9 @@ const normalizeActualIndex = (
495
551
  expireAfterSeconds >= 0
496
552
  ? expireAfterSeconds
497
553
  : null,
554
+ ...(isPlainIndexDocument(partialFilterExpression)
555
+ ? { partialFilterExpression }
556
+ : {}),
498
557
  });
499
558
  if (
500
559
  name === '_id_' &&
@@ -504,7 +563,8 @@ const normalizeActualIndex = (
504
563
  normalized.keys[0].weight === null &&
505
564
  (rawIndex.unique === undefined || rawIndex.unique === true) &&
506
565
  normalized.sparse === false &&
507
- normalized.expireAfterSeconds === null
566
+ normalized.expireAfterSeconds === null &&
567
+ normalized.partialFilterExpression === undefined
508
568
  ) {
509
569
  normalized = fixedIdIndex;
510
570
  }
@@ -37,6 +37,35 @@ import type { ICapabilities } from '../core/interfaces.js';
37
37
  */
38
38
  export type TConnectionStatus = 'initial' | 'disconnected' | 'connected' | 'failed';
39
39
 
40
+ /**
41
+ * How a `SmartdataDb` connects. The first four fields locate and authenticate
42
+ * the database — the same shape as `IMongoDescriptor`, so every descriptor is
43
+ * valid options — and the pool fields tune the driver's connection pool.
44
+ */
45
+ export interface ISmartdataDbOptions {
46
+ /**
47
+ * The connection URL. `<USERNAME>`/`<USER>`, `<PASSWORD>` and `<DBNAME>`
48
+ * (upper or lower case) are replaced with the URL-encoded `mongoDbUser`,
49
+ * `mongoDbPass` and `mongoDbName`.
50
+ */
51
+ mongoDbUrl: string;
52
+ /** The database to use. */
53
+ mongoDbName?: string;
54
+ mongoDbUser?: string;
55
+ mongoDbPass?: string;
56
+ /** Upper bound of pooled connections per server. Default 100. */
57
+ maxPoolSize?: number;
58
+ /** Milliseconds an idle pooled connection is kept before it is closed. Default 300000. */
59
+ maxIdleTimeMS?: number;
60
+ /**
61
+ * Milliseconds to wait for a suitable server. Default 30000. A bounded
62
+ * `init({ timeoutMs })` replaces it with the remaining init budget.
63
+ */
64
+ serverSelectionTimeoutMS?: number;
65
+ /** Milliseconds a socket may stay inactive before it is closed. Default 30000. */
66
+ socketTimeoutMS?: number;
67
+ }
68
+
40
69
  export interface ISmartdataReadinessOptions {
41
70
  timeoutMs?: number;
42
71
  signal?: AbortSignal;
@@ -138,7 +167,7 @@ const assertExactDataObject = (
138
167
  };
139
168
 
140
169
  export class SmartdataDb {
141
- smartdataOptions: plugins.tsclass.database.IMongoDescriptor;
170
+ smartdataOptions: ISmartdataDbOptions;
142
171
  mongoDbClient!: plugins.mongodb.MongoClient;
143
172
  mongoDb!: plugins.mongodb.Db;
144
173
  status: TConnectionStatus;
@@ -151,7 +180,7 @@ export class SmartdataDb {
151
180
  private engineIdentityState: ISmartdataEngineIdentity = unidentifiedSmartdataEngine;
152
181
  private capabilitiesState: ICapabilities | undefined;
153
182
 
154
- constructor(smartdataOptions: plugins.tsclass.database.IMongoDescriptor) {
183
+ constructor(smartdataOptions: ISmartdataDbOptions) {
155
184
  this.smartdataOptions = smartdataOptions;
156
185
  this.status = 'initial';
157
186
  void this.statusConnectedDeferred.promise.catch(() => {});
@@ -219,9 +248,7 @@ export class SmartdataDb {
219
248
  .replace('<DBNAME>', this.smartdataOptions.mongoDbName || '')
220
249
  .replace('<dbname>', this.smartdataOptions.mongoDbName || '');
221
250
 
222
- const descriptor = this.smartdataOptions as plugins.tsclass.database.IMongoDescriptor &
223
- Pick<plugins.mongodb.MongoClientOptions, 'maxPoolSize' | 'maxIdleTimeMS' |
224
- 'serverSelectionTimeoutMS' | 'socketTimeoutMS'>;
251
+ const descriptor = this.smartdataOptions;
225
252
  const clientOptions: plugins.mongodb.MongoClientOptions = {
226
253
  maxPoolSize: descriptor.maxPoolSize ?? 100,
227
254
  maxIdleTimeMS: descriptor.maxIdleTimeMS ?? 300000,
@@ -5774,9 +5774,11 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
5774
5774
  }
5775
5775
 
5776
5776
  /**
5777
- * saves this instance (optionally within a transaction)
5777
+ * Saves this instance, optionally within a transaction. `session` takes the
5778
+ * handle from `db.createSession()` — leased for the one write, like every
5779
+ * other model API — or a raw driver session.
5778
5780
  */
5779
- public async save(opts?: { session?: plugins.mongodb.ClientSession }) {
5781
+ public async save(opts?: { session?: TSmartdataOrdinarySession }) {
5780
5782
  if (getOrdinaryPersistencePolicy(this.constructor)) {
5781
5783
  throw new SmartdataPersistenceError('unsupported_operation',
5782
5784
  'Validated ordinary models use insert() or transactional postimage updates.');
@@ -5980,9 +5982,11 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
5980
5982
  }
5981
5983
 
5982
5984
  /**
5983
- * deletes a document from the database (optionally within a transaction)
5985
+ * Deletes this instance's document, optionally within a transaction.
5986
+ * `session` takes the handle from `db.createSession()` — leased for the one
5987
+ * write, like every other model API — or a raw driver session.
5984
5988
  */
5985
- public async delete(opts?: { session?: plugins.mongodb.ClientSession }) {
5989
+ public async delete(opts?: { session?: TSmartdataOrdinarySession }) {
5986
5990
  const exactPersistencePolicy = (this.constructor as any)[
5987
5991
  exactPersistencePolicySymbol
5988
5992
  ] as { forbiddenDelete?: () => Promise<never> } | undefined;