@lossless.org/client 1.1.0 → 1.3.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.
Files changed (34) hide show
  1. package/.smartconfig.json +1 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/nosqldb/classes.atomicdelete.js +3 -3
  4. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +20 -0
  5. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +45 -2
  6. package/dist_ts/nosqldb/classes.atomicupdate.js +4 -4
  7. package/dist_ts/nosqldb/classes.collection.d.ts +37 -1
  8. package/dist_ts/nosqldb/classes.collection.js +235 -30
  9. package/dist_ts/nosqldb/classes.collectiontopology.d.ts +19 -0
  10. package/dist_ts/nosqldb/classes.collectiontopology.js +29 -3
  11. package/dist_ts/nosqldb/classes.cursor.d.ts +12 -1
  12. package/dist_ts/nosqldb/classes.cursor.js +20 -3
  13. package/dist_ts/nosqldb/classes.doc.d.ts +138 -11
  14. package/dist_ts/nosqldb/classes.doc.js +490 -91
  15. package/dist_ts/nosqldb/classes.exactpersistence.js +10 -2
  16. package/dist_ts/nosqldb/classes.namespaceinspection.js +2 -2
  17. package/dist_ts/nosqldb/classes.persistence.d.ts +6 -0
  18. package/dist_ts/nosqldb/classes.persistence.js +13 -1
  19. package/dist_ts/nosqldb/classes.session.d.ts +15 -1
  20. package/dist_ts/nosqldb/classes.session.js +32 -6
  21. package/package.json +4 -1
  22. package/readme.md +38 -2
  23. package/ts/00_commitinfo_data.ts +1 -1
  24. package/ts/nosqldb/classes.atomicdelete.ts +2 -4
  25. package/ts/nosqldb/classes.atomicfindoneandupdate.ts +69 -2
  26. package/ts/nosqldb/classes.atomicupdate.ts +3 -6
  27. package/ts/nosqldb/classes.collection.ts +372 -35
  28. package/ts/nosqldb/classes.collectiontopology.ts +56 -1
  29. package/ts/nosqldb/classes.cursor.ts +22 -2
  30. package/ts/nosqldb/classes.doc.ts +819 -132
  31. package/ts/nosqldb/classes.exactpersistence.ts +9 -1
  32. package/ts/nosqldb/classes.namespaceinspection.ts +1 -1
  33. package/ts/nosqldb/classes.persistence.ts +17 -0
  34. package/ts/nosqldb/classes.session.ts +52 -13
@@ -4,6 +4,7 @@ import { assertBsonWithoutMutation } from './classes.bsonassertion.js';
4
4
  import { getOrdinaryPersistencePolicy } from './classes.ordinarypersistence.js';
5
5
 
6
6
  import type { SmartdataCollection } from './classes.collection.js';
7
+ import { getIdentityIndexName } from './classes.collectiontopology.js';
7
8
  import { registerCollectionReconnectHandler } from './classes.collectionlifecycle.js';
8
9
  import type { SmartdataSession } from './classes.session.js';
9
10
  import { acquireSmartdataSession } from './classes.session.js';
@@ -1024,10 +1025,17 @@ export class SmartdataExactCollection<
1024
1025
  const missingUniqueIndexes = this.uniqueIndexes.filter(
1025
1026
  (uniqueIndexArg) => !this.collection.uniqueIndexes.includes(uniqueIndexArg),
1026
1027
  );
1028
+ const boundSchema = this.collection.getBoundModelSchema();
1027
1029
  for (const uniqueIndex of missingUniqueIndexes) {
1028
1030
  await targetCollectionArg.createIndex(
1029
1031
  { [uniqueIndex]: 1 },
1030
- { unique: true, name: `${uniqueIndex}_1` },
1032
+ {
1033
+ unique: true,
1034
+ // A model may back its identity with a differently named index;
1035
+ // creating `<field>_1` would duplicate that key.
1036
+ name: getIdentityIndexName(boundSchema, uniqueIndex)
1037
+ ?? `${uniqueIndex}_1`,
1038
+ },
1031
1039
  );
1032
1040
  }
1033
1041
 
@@ -173,7 +173,7 @@ export const inspectSmartdataNamespace = async (
173
173
  });
174
174
  }
175
175
  const topology = inspectCollectionTopologySnapshot({
176
- collectionName: namespace.name, indexes: [],
176
+ collectionName: namespace.name, indexes: [], toleratedIndexNames: [],
177
177
  }, { namespace: namespace as unknown as Record<string, unknown>, indexes });
178
178
  collections.push(Object.freeze({
179
179
  collectionName: namespace.name, namespaceType, documentCount,
@@ -43,6 +43,23 @@ export const isMongoDuplicateKeyError = (
43
43
  );
44
44
  };
45
45
 
46
+ /**
47
+ * MongoDB refuses an index whose key or name collides with an existing one:
48
+ * `IndexOptionsConflict` when an equivalent index already carries another
49
+ * name, `IndexKeySpecsConflict` when the name is taken by another key.
50
+ */
51
+ export const isMongoIndexConflictError = (
52
+ errorArg: unknown,
53
+ ): errorArg is plugins.mongodb.MongoServerError => {
54
+ return (
55
+ errorArg instanceof plugins.mongodb.MongoServerError &&
56
+ (errorArg.code === 85 ||
57
+ errorArg.code === 86 ||
58
+ errorArg.codeName === 'IndexOptionsConflict' ||
59
+ errorArg.codeName === 'IndexKeySpecsConflict')
60
+ );
61
+ };
62
+
46
63
  export const normalizeOrdinaryPersistenceError = (
47
64
  errorArg: unknown,
48
65
  messageArg: string,
@@ -45,7 +45,8 @@ interface ISmartdataSessionAcquireOptions {
45
45
  interface IOrdinarySessionOperationOptions {
46
46
  ordinaryWrite?: boolean;
47
47
  prepared?: boolean;
48
- preparationMessage?: string;
48
+ /** Names the collection whose preparation a refusal must ask for. */
49
+ collectionName?: string;
49
50
  }
50
51
 
51
52
  const transactionContext = new plugins.nodeAsyncHooks.AsyncLocalStorage<
@@ -310,27 +311,65 @@ export const isOrdinarySessionInTransaction = (
310
311
  return sessionArg.inTransaction();
311
312
  };
312
313
 
313
- export const runWithOrdinarySmartdataSession = async <TResult>(
314
+ /**
315
+ * A SmartData model initializes its collection lazily, and that initialization
316
+ * issues index DDL outside any session. Under an open transaction on the same
317
+ * collection the DDL waits for the transaction while the transaction waits for
318
+ * this operation, so the caller would only learn about it when the client
319
+ * deadline expires. Naming the missing preparation is the whole remedy.
320
+ */
321
+ const refuseUnpreparedCollection = (
322
+ optionsArg: IOrdinarySessionOperationOptions,
323
+ reasonArg: 'with an owned SmartData session' | 'inside a transaction',
324
+ ): never => {
325
+ throw new SmartdataPersistenceError(
326
+ 'unsupported_operation',
327
+ optionsArg.collectionName
328
+ ? `Initialize collection "${optionsArg.collectionName}" before using it ${reasonArg}.`
329
+ : `Initialize every participating SmartData model before using it ${reasonArg}.`,
330
+ );
331
+ };
332
+
333
+ /**
334
+ * Takes the same lease `runWithOrdinarySmartdataSession` takes, but hands it to
335
+ * the caller instead of releasing it at the end of one operation. Resources
336
+ * that outlive their creating call — a cursor the caller iterates itself — hold
337
+ * the owned session until they are closed, so a second operation on that
338
+ * session is refused while the cursor is open, exactly as the driver requires.
339
+ * A raw `ClientSession` or no session keeps its pass-through behavior and gets
340
+ * an inert release. `release()` is idempotent.
341
+ */
342
+ export const leaseOrdinarySmartdataSession = (
314
343
  sessionArg: TSmartdataOrdinarySession | undefined,
315
344
  ownerArg: SmartdataDb,
316
345
  optionsArg: IOrdinarySessionOperationOptions,
317
- operationArg: (
318
- rawSessionArg: plugins.mongodb.ClientSession | undefined,
319
- ) => Promise<TResult>,
320
- ): Promise<TResult> => {
346
+ ): {
347
+ rawSession: plugins.mongodb.ClientSession | undefined;
348
+ release: () => void;
349
+ } => {
321
350
  if (!isSmartdataSession(sessionArg)) {
322
- return operationArg(sessionArg);
351
+ if (!optionsArg.prepared && sessionArg?.inTransaction()) {
352
+ refuseUnpreparedCollection(optionsArg, 'inside a transaction');
353
+ }
354
+ return { rawSession: sessionArg, release: () => {} };
323
355
  }
324
356
  if (!optionsArg.prepared) {
325
- throw new SmartdataPersistenceError(
326
- 'unsupported_operation',
327
- optionsArg.preparationMessage ||
328
- 'Initialize every participating SmartData model before using an owned session.',
329
- );
357
+ refuseUnpreparedCollection(optionsArg, 'with an owned SmartData session');
330
358
  }
331
- const lease = acquireSmartdataSession(sessionArg, ownerArg, {
359
+ return acquireSmartdataSession(sessionArg, ownerArg, {
332
360
  ordinaryWrite: optionsArg.ordinaryWrite,
333
361
  });
362
+ };
363
+
364
+ export const runWithOrdinarySmartdataSession = async <TResult>(
365
+ sessionArg: TSmartdataOrdinarySession | undefined,
366
+ ownerArg: SmartdataDb,
367
+ optionsArg: IOrdinarySessionOperationOptions,
368
+ operationArg: (
369
+ rawSessionArg: plugins.mongodb.ClientSession | undefined,
370
+ ) => Promise<TResult>,
371
+ ): Promise<TResult> => {
372
+ const lease = leaseOrdinarySmartdataSession(sessionArg, ownerArg, optionsArg);
334
373
  try {
335
374
  return await operationArg(lease.rawSession);
336
375
  } finally {