cry-db 2.4.18 → 2.4.19

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/mongo.mjs CHANGED
@@ -4,7 +4,7 @@ import cloneDeep from "lodash.clonedeep";
4
4
  import { ObjectId, ReadConcern, ReadPreference, Timestamp, WriteConcern } from 'mongodb';
5
5
  import { TypedEmitter } from "tiny-typed-emitter";
6
6
  import { Db, log } from './db.mjs';
7
- import { SEQUENCES_COLLECTION } from './types.mjs';
7
+ import { FIELD_SEQUENCES_COLLECTION, SEQUENCES_COLLECTION } from './types.mjs';
8
8
  import { Base } from "./base.mjs";
9
9
  const assert = (cond, msg) => {
10
10
  if (!cond) {
@@ -76,10 +76,12 @@ export class Mongo extends Db {
76
76
  this.emittingPublishRevEvents = false;
77
77
  this.auditing = false;
78
78
  this.auditCollectionName = "dblog";
79
+ this.auditedCollectionsLower = [];
79
80
  this.auditedCollections = this.auditCollections(process.env.AUDIT_COLLECTIONS || []);
80
81
  this.emitter = new TypedEmitter();
81
82
  this.user = undefined;
82
83
  this.audit = undefined;
84
+ this._sequencesIndexEnsured = false;
83
85
  fjLog.debug('new Mongo:', this.url, this.db);
84
86
  }
85
87
  on(evt, listener) {
@@ -137,9 +139,11 @@ export class Mongo extends Db {
137
139
  if (this.auditedCollections.includes(coll))
138
140
  return;
139
141
  this.auditedCollections.push(coll);
142
+ this.auditedCollectionsLower.push(coll);
140
143
  }
141
144
  else {
142
145
  this.auditedCollections = this.auditedCollections.filter((c) => c != coll);
146
+ this.auditedCollectionsLower = this.auditedCollectionsLower.filter((c) => c != coll);
143
147
  }
144
148
  }
145
149
  auditCollections(arr) {
@@ -149,6 +153,7 @@ export class Mongo extends Db {
149
153
  this.auditedCollections = arr.toString().split(',').map(a => a.trim().toLowerCase()).filter(a => !!a);
150
154
  if (arr instanceof Array)
151
155
  this.auditedCollections = arr.map(a => a.trim().toLowerCase()).filter(a => !!a);
156
+ this.auditedCollectionsLower = this.auditedCollections.map(a => a.toLowerCase());
152
157
  return this.auditedCollections;
153
158
  }
154
159
  auditingCollection(coll, db = this.db) {
@@ -172,11 +177,19 @@ export class Mongo extends Db {
172
177
  emitsPublishRevEvents() {
173
178
  return this.emittingPublishRevEvents;
174
179
  }
175
- async distinct(collection, field) {
180
+ /**
181
+ * Returns distinct values for a field. Results are sorted.
182
+ * @param opts - Use `{ collation: { locale: "en", strength: 2 } }` for case-insensitive
183
+ */
184
+ async distinct(collection, field, opts = {}) {
185
+ assert(collection);
186
+ assert(field);
176
187
  fjLog.debug('distinct called', collection, field);
177
- let ret = await this.executeTransactionally(collection, async (conn) => {
178
- return await conn.distinct(field);
179
- }, false, { operation: "distinct", collection, field });
188
+ const dbName = this.db;
189
+ let client = await this.connect();
190
+ let conn = client.db(dbName).collection(collection);
191
+ let ret = await conn.distinct(field, {}, opts);
192
+ ret.sort();
180
193
  fjLog.debug('distinct returns', ret);
181
194
  return ret;
182
195
  }
@@ -421,16 +434,15 @@ export class Mongo extends Db {
421
434
  return this._processReturnedObject(ret);
422
435
  }
423
436
  async findByIdsInManyCollections(request, opts = {}) {
424
- const result = {};
425
437
  if (!request || request.length === 0)
426
- return result;
438
+ return {};
427
439
  const dbName = this.db;
428
440
  fjLog.debug('findByIdsInManyCollections called', dbName, request);
429
- for (const req of request) {
441
+ // Process all collections in parallel
442
+ const fetchOne = async (req) => {
430
443
  const { collection, ids, projection } = req;
431
444
  if (!collection || !ids || ids.length === 0) {
432
- result[collection] = [];
433
- continue;
445
+ return { collection, entities: [] };
434
446
  }
435
447
  const objectIds = ids.map(id => Mongo._toId(id));
436
448
  const entities = await this.executeTransactionally(collection, async (conn) => {
@@ -441,7 +453,12 @@ export class Mongo extends Db {
441
453
  let r = conn.find(query, { ...(projection ? { projection } : {}), ...this._sessionOpt() });
442
454
  return await r.toArray();
443
455
  }, false, { operation: "findByIdsInManyCollections", collection, ids, projection });
444
- result[collection] = this._processReturnedObject(entities);
456
+ return { collection, entities: this._processReturnedObject(entities) };
457
+ };
458
+ const results = await Promise.all(request.map(fetchOne));
459
+ const result = {};
460
+ for (const { collection, entities } of results) {
461
+ result[collection] = entities;
445
462
  }
446
463
  fjLog.debug('findByIdsInManyCollections returns', result);
447
464
  return result;
@@ -477,7 +494,9 @@ export class Mongo extends Db {
477
494
  returnDocument: "after",
478
495
  ...this._sessionOpt()
479
496
  };
480
- update = await this._processUpdateObject(update);
497
+ if (this._hasHashedKeys(update))
498
+ await this._processHashedKeys(update);
499
+ update = this._processUpdateObject(update);
481
500
  fjLog.debug('updateOne called', collection, query, update);
482
501
  let seqKeys = this._findSequenceKeys(update.$set);
483
502
  let obj = await this.executeTransactionally(collection, async (conn, client) => {
@@ -507,7 +526,9 @@ export class Mongo extends Db {
507
526
  ...this._sessionOpt()
508
527
  };
509
528
  let _id = Mongo.toId(id || update._id) || Mongo.newid();
510
- update = await this._processUpdateObject(update);
529
+ if (this._hasHashedKeys(update))
530
+ await this._processHashedKeys(update);
531
+ update = this._processUpdateObject(update);
511
532
  fjLog.debug('save called', collection, id, update);
512
533
  let seqKeys = this._findSequenceKeys(update.$set);
513
534
  let obj = await this.executeTransactionally(collection, async (conn, client) => {
@@ -541,7 +562,9 @@ export class Mongo extends Db {
541
562
  returnDocument: "after",
542
563
  ...this._sessionOpt()
543
564
  };
544
- update = await this._processUpdateObject(update);
565
+ if (this._hasHashedKeys(update))
566
+ await this._processHashedKeys(update);
567
+ update = this._processUpdateObject(update);
545
568
  fjLog.debug('update called', collection, query, update);
546
569
  let seqKeys = this._findSequenceKeys(update.$set);
547
570
  let obj = await this.executeTransactionally(collection, async (conn, client) => {
@@ -578,7 +601,9 @@ export class Mongo extends Db {
578
601
  ...this._sessionOpt()
579
602
  };
580
603
  fjLog.debug('upsert called', collection, query, update);
581
- update = await this._processUpdateObject(update);
604
+ if (this._hasHashedKeys(update))
605
+ await this._processHashedKeys(update);
606
+ update = this._processUpdateObject(update);
582
607
  let seqKeys = this._findSequenceKeys(update.$set);
583
608
  fjLog.debug('upsert processed', collection, query, update);
584
609
  if (Object.keys(query).length === 0)
@@ -612,6 +637,7 @@ export class Mongo extends Db {
612
637
  fjLog.debug('insert called', collection, insert);
613
638
  const dbName = this.db; // Capture db at operation start
614
639
  insert = this.replaceIds(insert);
640
+ this._stripDoubleUnderscoreKeys(insert);
615
641
  if (this.revisions) {
616
642
  insert._rev = 1;
617
643
  insert._ts = Base.timestamp();
@@ -660,13 +686,22 @@ export class Mongo extends Db {
660
686
  };
661
687
  const batchData = [];
662
688
  const errors = [];
689
+ // Handle sequences in updates before processing
690
+ if (updates === null || updates === void 0 ? void 0 : updates.length) {
691
+ const updatesWithSeq = updates.filter(u => this._hasSequenceFields(u.update));
692
+ if (updatesWithSeq.length > 0) {
693
+ await this._processSequenceFieldForMany(conn, dbName, collection, updatesWithSeq.map(u => u.update));
694
+ }
695
+ }
663
696
  // Process updates (with upsert) in parallel
664
697
  if (updates === null || updates === void 0 ? void 0 : updates.length) {
665
698
  const updatePromises = updates.map(async ({ _id, update }) => {
666
699
  var _a;
667
700
  try {
668
701
  const objectId = Mongo._toId(_id);
669
- const processedUpdate = await this._processUpdateObject({ ...update });
702
+ if (this._hasHashedKeys(update))
703
+ await this._processHashedKeys(update);
704
+ const processedUpdate = this._processUpdateObject({ ...update });
670
705
  const opts = {
671
706
  upsert: true,
672
707
  returnDocument: "after",
@@ -821,48 +856,56 @@ export class Mongo extends Db {
821
856
  batch = this.replaceIds(batch);
822
857
  await Promise.all(batch.map(item => this._processHashedKeys(item === null || item === void 0 ? void 0 : item.update)));
823
858
  let ret = await this.executeTransactionally(collection, async (conn, client) => {
824
- var _a;
825
- await this._processSequenceFieldForMany(client, dbName, collection, batch.map(b => b.update));
826
- let batchData = [];
827
- let changes = [];
828
- for await (let part of batch) {
829
- let { query, update, opts } = part;
830
- let options = {
859
+ // Only process sequences if any batch item has SEQ_NEXT or SEQ_LAST
860
+ const updatesWithSeq = batch.filter(b => this._hasSequenceFields(b.update));
861
+ if (updatesWithSeq.length > 0) {
862
+ await this._processSequenceFieldForMany(client, dbName, collection, updatesWithSeq.map(b => b.update));
863
+ }
864
+ // Process all updates in parallel
865
+ const upsertPromises = batch.map(async (part) => {
866
+ var _a, _b;
867
+ const { query, update, opts } = part;
868
+ const options = {
831
869
  ...(opts || {}),
832
870
  upsert: true,
833
871
  returnDocument: "after",
834
872
  includeResultMetadata: true,
835
873
  ...this._sessionOpt()
836
874
  };
837
- update = await this._processUpdateObject(update);
838
- let result = await conn.findOneAndUpdate(query, update, options);
839
- if (!result)
840
- continue;
841
- let ret = result.value;
842
- if (ret === null || ret === void 0 ? void 0 : ret._id) {
843
- // Determine operation based on result fields
844
- let oper;
845
- if (ret._deleted !== undefined) {
846
- oper = "delete";
847
- }
848
- else if ((_a = result.lastErrorObject) === null || _a === void 0 ? void 0 : _a.updatedExisting) {
849
- oper = "update";
850
- }
851
- else {
852
- oper = "insert";
853
- }
854
- let retObj = oper === "insert" ? ret : this._removeUnchanged(ret, update, !!(opts === null || opts === void 0 ? void 0 : opts.returnFullObject));
855
- this._processReturnedObject(retObj);
856
- batchData.push({
857
- operation: oper,
858
- data: retObj
859
- });
860
- changes.push(retObj);
875
+ if (this._hasHashedKeys(update))
876
+ await this._processHashedKeys(update);
877
+ const processedUpdate = this._processUpdateObject({ ...update });
878
+ const result = await conn.findOneAndUpdate(query, processedUpdate, options);
879
+ if (!((_a = result === null || result === void 0 ? void 0 : result.value) === null || _a === void 0 ? void 0 : _a._id))
880
+ return null;
881
+ const ret = result.value;
882
+ // Determine operation based on result fields
883
+ let oper;
884
+ if (ret._deleted !== undefined) {
885
+ oper = "delete";
886
+ }
887
+ else if ((_b = result.lastErrorObject) === null || _b === void 0 ? void 0 : _b.updatedExisting) {
888
+ oper = "update";
889
+ }
890
+ else {
891
+ oper = "insert";
892
+ }
893
+ const retObj = oper === "insert" ? ret : this._removeUnchanged(ret, processedUpdate, !!(opts === null || opts === void 0 ? void 0 : opts.returnFullObject));
894
+ this._processReturnedObject(retObj);
895
+ return { operation: oper, data: retObj };
896
+ });
897
+ const results = await Promise.all(upsertPromises);
898
+ // Filter out nulls and separate batchData and changes
899
+ const batchData = [];
900
+ const changes = [];
901
+ for (const result of results) {
902
+ if (result) {
903
+ batchData.push(result);
904
+ changes.push(result.data);
861
905
  }
862
- ;
863
906
  }
864
907
  if (this.emittingPublishEvents || this.auditing) {
865
- await this.emit("publish", {
908
+ this.emit("publish", {
866
909
  channel: `db/${dbName}/${collection}`,
867
910
  payload: {
868
911
  operation: "batch",
@@ -883,7 +926,7 @@ export class Mongo extends Db {
883
926
  _ts: item.data._ts,
884
927
  }))
885
928
  };
886
- await this.emit("publishRev", {
929
+ this.emit("publishRev", {
887
930
  channel: `dbrev/${dbName}/${collection}`,
888
931
  payload,
889
932
  });
@@ -900,11 +943,16 @@ export class Mongo extends Db {
900
943
  fjLog.debug('insertMany called', collection, insert);
901
944
  const dbName = this.db; // Capture db at operation start
902
945
  insert = this.replaceIds(insert);
946
+ insert.forEach(item => this._stripDoubleUnderscoreKeys(item));
903
947
  await Promise.all(insert.map(item => this._processHashedKeys(item)));
904
948
  if (this.revisions)
905
949
  insert.forEach(ins => { ins._rev = 1; ins._ts = Base.timestamp(); });
906
950
  let ret = await this.executeTransactionally(collection, async (conn, client) => {
907
- await this._processSequenceFieldForMany(client, dbName, collection, insert);
951
+ // Only process sequences if any insert has SEQ_NEXT or SEQ_LAST
952
+ const insertsWithSeq = insert.filter(item => this._hasSequenceFields(item));
953
+ if (insertsWithSeq.length > 0) {
954
+ await this._processSequenceFieldForMany(client, dbName, collection, insert);
955
+ }
908
956
  let obj = await conn.insertMany(insert, this._sessionOpt());
909
957
  let ret = [];
910
958
  for (let ns of Object.keys(obj.insertedIds)) {
@@ -1175,35 +1223,14 @@ export class Mongo extends Db {
1175
1223
  fjLog.debug('isUnique returns', ret);
1176
1224
  return ret;
1177
1225
  }
1178
- async collectFieldValues(collection, field, inArray = false, opts) {
1179
- assert(collection);
1180
- assert(field);
1181
- fjLog.debug('collectFieldValues called', collection, field);
1182
- let pipeline = [
1183
- { $group: { _id: '$' + field } },
1184
- { $sort: { _id: 1 } }
1185
- ];
1186
- if (inArray)
1187
- pipeline = [
1188
- { $unwind: '$' + field },
1189
- ...pipeline
1190
- ];
1191
- let res = await this.executeTransactionally(collection, async (conn) => {
1192
- let agg = await conn.aggregate(pipeline, opts);
1193
- let res = await agg.toArray();
1194
- return res;
1195
- }, false, { operation: "collectFieldValues", collection, field, inArray, pipeline, opts });
1196
- let ret = res === null || res === void 0 ? void 0 : res.map((v) => v._id);
1197
- fjLog.debug('collectFieldValues returns', ret);
1198
- return ret;
1199
- }
1200
1226
  async dropCollection(collection) {
1201
1227
  assert(collection);
1202
1228
  fjLog.debug('dropCollection called', this.auditCollections);
1203
1229
  const dbName = this.db; // Capture db at operation start
1204
1230
  let client = await this.connect();
1205
1231
  let existing = await client.db(dbName).collections();
1206
- if (existing.map((c) => c.collectionName).includes(collection)) {
1232
+ const existingNames = new Set(existing.map((c) => c.collectionName));
1233
+ if (existingNames.has(collection)) {
1207
1234
  await client.db(dbName).dropCollection(collection);
1208
1235
  }
1209
1236
  fjLog.debug('dropCollection returns');
@@ -1225,8 +1252,9 @@ export class Mongo extends Db {
1225
1252
  const dbName = this.db; // Capture db at operation start
1226
1253
  let client = await this.connect();
1227
1254
  let existing = await client.db(dbName).collections();
1228
- for await (let collection of collections) {
1229
- if (existing.map((c) => c.collectionName).includes(collection)) {
1255
+ const existingNames = new Set(existing.map((c) => c.collectionName));
1256
+ for (const collection of collections) {
1257
+ if (existingNames.has(collection)) {
1230
1258
  await client.db(dbName).dropCollection(collection);
1231
1259
  }
1232
1260
  }
@@ -1239,8 +1267,9 @@ export class Mongo extends Db {
1239
1267
  const dbName = this.db; // Capture db at operation start
1240
1268
  let client = await this.connect();
1241
1269
  let existing = await this.getCollections();
1242
- for await (let collection of collections) {
1243
- if (!existing.includes(collection)) {
1270
+ const existingSet = new Set(existing);
1271
+ for (const collection of collections) {
1272
+ if (!existingSet.has(collection)) {
1244
1273
  await client.db(dbName).createCollection(collection);
1245
1274
  }
1246
1275
  }
@@ -1388,6 +1417,9 @@ export class Mongo extends Db {
1388
1417
  }
1389
1418
  // 4. Clean up state
1390
1419
  this.session = undefined;
1420
+ this.auditedCollections.length = 0;
1421
+ this.auditedCollectionsLower.length = 0;
1422
+ this._sequencesIndexEnsured = false;
1391
1423
  }
1392
1424
  async inTransaction() {
1393
1425
  if (!await this.isOnReplicaSet())
@@ -1575,6 +1607,139 @@ export class Mongo extends Db {
1575
1607
  return undefined;
1576
1608
  return parseInt((_a = maxfld === null || maxfld === void 0 ? void 0 : maxfld[0]) === null || _a === void 0 ? void 0 : _a[key]) || 0;
1577
1609
  }
1610
+ /**
1611
+ * Ensures the unique index on _sequences collection exists.
1612
+ * Called lazily on first sequence operation.
1613
+ */
1614
+ async _ensureSequencesIndex(dbConnection) {
1615
+ if (this._sequencesIndexEnsured)
1616
+ return;
1617
+ try {
1618
+ await dbConnection
1619
+ .collection(FIELD_SEQUENCES_COLLECTION)
1620
+ .createIndex({ collection: 1, field: 1 }, { unique: true });
1621
+ this._sequencesIndexEnsured = true;
1622
+ }
1623
+ catch (err) {
1624
+ // Index may already exist, which is fine
1625
+ if (err.code === 85 || err.code === 86) {
1626
+ this._sequencesIndexEnsured = true;
1627
+ }
1628
+ else {
1629
+ throw err;
1630
+ }
1631
+ }
1632
+ }
1633
+ /**
1634
+ * Atomically gets the next sequence value for a field in a collection.
1635
+ * On first use, auto-seeds from existing data in the collection.
1636
+ */
1637
+ async _getNextSequenceValue(dbConnection, collection, field) {
1638
+ // Check current max in the actual collection
1639
+ const existingMax = await this._findLastSequenceForKey(dbConnection.collection(collection), field);
1640
+ // Check if we have a sequence document
1641
+ const existingSeq = await dbConnection
1642
+ .collection(FIELD_SEQUENCES_COLLECTION)
1643
+ .findOne({ collection, field }, this._sessionOpt());
1644
+ if (!existingSeq) {
1645
+ // First time for this collection/field - ensure index exists
1646
+ await this._ensureSequencesIndex(dbConnection);
1647
+ const seedValue = (existingMax !== undefined && existingMax >= 1) ? existingMax : 0;
1648
+ // Use $max to handle concurrent seeding - only sets if greater than current value
1649
+ // This ensures the highest seed value wins in case of concurrent operations
1650
+ await dbConnection
1651
+ .collection(FIELD_SEQUENCES_COLLECTION)
1652
+ .findOneAndUpdate({ collection, field }, {
1653
+ $max: { lastSeqNum: seedValue },
1654
+ $setOnInsert: { collection, field }
1655
+ }, { upsert: true, returnDocument: 'after', ...this._sessionOpt() });
1656
+ // Now increment atomically to get next value
1657
+ const updated = await dbConnection
1658
+ .collection(FIELD_SEQUENCES_COLLECTION)
1659
+ .findOneAndUpdate({ collection, field }, { $inc: { lastSeqNum: 1 } }, { returnDocument: 'after', ...this._sessionOpt() });
1660
+ return updated.lastSeqNum;
1661
+ }
1662
+ // Sequence exists - check if collection was cleared (reset scenario)
1663
+ if (existingMax === undefined && existingSeq.lastSeqNum > 0) {
1664
+ // Collection is empty but we have a non-zero sequence - reset to 0 then increment to 1
1665
+ await dbConnection
1666
+ .collection(FIELD_SEQUENCES_COLLECTION)
1667
+ .updateOne({ collection, field }, { $set: { lastSeqNum: 0 } }, this._sessionOpt());
1668
+ }
1669
+ // Increment and return
1670
+ const result = await dbConnection
1671
+ .collection(FIELD_SEQUENCES_COLLECTION)
1672
+ .findOneAndUpdate({ collection, field }, { $inc: { lastSeqNum: 1 } }, { returnDocument: 'after', ...this._sessionOpt() });
1673
+ return result.lastSeqNum;
1674
+ }
1675
+ /**
1676
+ * Gets the current (last) sequence value without incrementing.
1677
+ * On first use, auto-seeds from existing data in the collection.
1678
+ * If collection becomes empty, resets the sequence to 0.
1679
+ */
1680
+ async _getLastSequenceValue(dbConnection, collection, field) {
1681
+ // Check current max in the actual collection
1682
+ const existingMax = await this._findLastSequenceForKey(dbConnection.collection(collection), field);
1683
+ // Try to get existing sequence value
1684
+ const existing = await dbConnection
1685
+ .collection(FIELD_SEQUENCES_COLLECTION)
1686
+ .findOne({ collection, field }, this._sessionOpt());
1687
+ if (existing) {
1688
+ // If collection is empty (existingMax undefined), reset sequence to 0
1689
+ if (existingMax === undefined && existing.lastSeqNum > 0) {
1690
+ await dbConnection
1691
+ .collection(FIELD_SEQUENCES_COLLECTION)
1692
+ .updateOne({ collection, field }, { $set: { lastSeqNum: 0 } }, this._sessionOpt());
1693
+ return 0;
1694
+ }
1695
+ return existing.lastSeqNum;
1696
+ }
1697
+ if (existingMax !== undefined && existingMax >= 0) {
1698
+ // First time with existing data - ensure index exists and seed
1699
+ await this._ensureSequencesIndex(dbConnection);
1700
+ await dbConnection
1701
+ .collection(FIELD_SEQUENCES_COLLECTION)
1702
+ .updateOne({ collection, field }, { $setOnInsert: { lastSeqNum: existingMax } }, { upsert: true, ...this._sessionOpt() });
1703
+ return existingMax;
1704
+ }
1705
+ return 0;
1706
+ }
1707
+ /**
1708
+ * Atomically reserves a range of sequence numbers for batch operations.
1709
+ * Returns { start, end } where start is the first reserved value and end is the last.
1710
+ */
1711
+ async _reserveSequenceRange(dbConnection, collection, field, count) {
1712
+ // Check current max in the actual collection
1713
+ const existingMax = await this._findLastSequenceForKey(dbConnection.collection(collection), field);
1714
+ // Check if we have a sequence document
1715
+ const existingSeq = await dbConnection
1716
+ .collection(FIELD_SEQUENCES_COLLECTION)
1717
+ .findOne({ collection, field }, this._sessionOpt());
1718
+ if (!existingSeq) {
1719
+ // First time for this collection/field - ensure index exists
1720
+ await this._ensureSequencesIndex(dbConnection);
1721
+ const seedValue = (existingMax !== undefined && existingMax >= 1) ? existingMax : 0;
1722
+ // Use $max to handle concurrent seeding
1723
+ await dbConnection
1724
+ .collection(FIELD_SEQUENCES_COLLECTION)
1725
+ .findOneAndUpdate({ collection, field }, {
1726
+ $max: { lastSeqNum: seedValue },
1727
+ $setOnInsert: { collection, field }
1728
+ }, { upsert: true, returnDocument: 'after', ...this._sessionOpt() });
1729
+ }
1730
+ else if (existingMax === undefined && existingSeq.lastSeqNum > 0) {
1731
+ // Collection is empty but we have a non-zero sequence - reset to 0
1732
+ await dbConnection
1733
+ .collection(FIELD_SEQUENCES_COLLECTION)
1734
+ .updateOne({ collection, field }, { $set: { lastSeqNum: 0 } }, this._sessionOpt());
1735
+ }
1736
+ // Now atomically reserve the range
1737
+ const result = await dbConnection
1738
+ .collection(FIELD_SEQUENCES_COLLECTION)
1739
+ .findOneAndUpdate({ collection, field }, { $inc: { lastSeqNum: count } }, { returnDocument: 'after', ...this._sessionOpt() });
1740
+ const end = result.lastSeqNum;
1741
+ return { start: end - count + 1, end };
1742
+ }
1578
1743
  // private async _getNextCollectionUpdateSeqNo(collection: string, conn: MongoClient) {
1579
1744
  // let opts : FindOneAndUpdateOptions = {
1580
1745
  // upsert: true,
@@ -1594,22 +1759,24 @@ export class Mongo extends Db {
1594
1759
  _findSequenceKeys(object) {
1595
1760
  if (!object)
1596
1761
  return;
1597
- let seqKeys = Object.keys(object).filter(key => object[key] === 'SEQ_NEXT' || object[key] === 'SEQ_LAST');
1598
- return ((seqKeys === null || seqKeys === void 0 ? void 0 : seqKeys.length) > 0 || this.syncSupport) ? { seqKeys } : undefined;
1762
+ if (!this._hasSequenceFields(object) && !this.syncSupport)
1763
+ return;
1764
+ const seqKeys = Object.keys(object).filter(key => object[key] === 'SEQ_NEXT' || object[key] === 'SEQ_LAST');
1765
+ return { seqKeys };
1599
1766
  }
1600
1767
  async _processSequenceField(client, dbName, collection, insert, seqKeys) {
1601
1768
  assert(this.client);
1602
1769
  // if (this.syncSupport) {
1603
1770
  // insert._csq = (await this._getNextCollectionUpdateSeqNo(collection, client));
1604
1771
  // }
1605
- for await (let seqKey of (seqKeys === null || seqKeys === void 0 ? void 0 : seqKeys.seqKeys) || []) {
1606
- let last = await this._findLastSequenceForKey(client.db(dbName).collection(collection), seqKey);
1607
- if (last === undefined) {
1608
- await this.createCollection(collection);
1609
- last = 0;
1772
+ const db = client.db(dbName);
1773
+ for (const seqKey of (seqKeys === null || seqKeys === void 0 ? void 0 : seqKeys.seqKeys) || []) {
1774
+ if (insert[seqKey] === 'SEQ_LAST') {
1775
+ insert[seqKey] = await this._getLastSequenceValue(db, collection, seqKey);
1776
+ }
1777
+ else {
1778
+ insert[seqKey] = await this._getNextSequenceValue(db, collection, seqKey);
1610
1779
  }
1611
- let next = insert[seqKey] === 'SEQ_LAST' ? last : last + 1;
1612
- insert[seqKey] = next;
1613
1780
  }
1614
1781
  return insert;
1615
1782
  }
@@ -1618,35 +1785,45 @@ export class Mongo extends Db {
1618
1785
  assert(connection);
1619
1786
  if (!(inserts === null || inserts === void 0 ? void 0 : inserts.length))
1620
1787
  return;
1788
+ // Collect all unique sequence keys across all inserts
1621
1789
  let seqKeysSet = new Set();
1622
1790
  for (let insert of inserts) {
1623
1791
  let spec = this._findSequenceKeys(insert);
1624
1792
  spec === null || spec === void 0 ? void 0 : spec.seqKeys.forEach(key => seqKeysSet.add(key));
1625
1793
  }
1626
1794
  let seqKeys = Array.from(seqKeysSet);
1627
- // let seq: number = 0;
1628
- // if (this.syncSupport) seq = await this._getNextCollectionUpdateSeqNo(collection, connection);
1629
1795
  if (!seqKeys.length)
1630
1796
  return inserts;
1631
- // Fetch all sequence last values in parallel
1632
- const coll = connection.db(dbName).collection(collection);
1633
- const lastValues = await Promise.all(seqKeys.map(seqKey => this._findLastSequenceForKey(coll, seqKey)));
1634
- // Check if collection needs to be created (any undefined value)
1635
- if (lastValues.some(v => v === undefined)) {
1636
- try {
1637
- await this.createCollection(collection);
1797
+ const db = connection.db(dbName);
1798
+ // Process each sequence key
1799
+ for (const seqKey of seqKeys) {
1800
+ // Count SEQ_NEXT occurrences to reserve the range
1801
+ let seqNextCount = 0;
1802
+ for (const insert of inserts) {
1803
+ if (insert[seqKey] === 'SEQ_NEXT')
1804
+ seqNextCount++;
1805
+ }
1806
+ // Get current value and reserve range for SEQ_NEXT if needed
1807
+ let currentValue;
1808
+ if (seqNextCount > 0) {
1809
+ const range = await this._reserveSequenceRange(db, collection, seqKey, seqNextCount);
1810
+ // currentValue starts at one before the range start (since SEQ_LAST gets current, SEQ_NEXT gets next)
1811
+ currentValue = range.start - 1;
1812
+ }
1813
+ else {
1814
+ // Only SEQ_LAST values, just get the current value
1815
+ currentValue = await this._getLastSequenceValue(db, collection, seqKey);
1638
1816
  }
1639
- catch { /* collection may already exist */ }
1640
- }
1641
- // Build a map of seqKey -> last value
1642
- const seqKeyToLast = new Map(seqKeys.map((key, idx) => { var _a; return [key, (_a = lastValues[idx]) !== null && _a !== void 0 ? _a : 0]; }));
1643
- // Process inserts for all sequence keys
1644
- for (const insert of inserts) {
1645
- for (const seqKey of seqKeys) {
1646
- const last = seqKeyToLast.get(seqKey);
1647
- const next = insert[seqKey] === 'SEQ_LAST' ? last : last + 1;
1648
- insert[seqKey] = next;
1649
- seqKeyToLast.set(seqKey, next);
1817
+ // Process inserts in order - SEQ_LAST gets current, SEQ_NEXT increments and gets new value
1818
+ for (const insert of inserts) {
1819
+ const val = insert[seqKey];
1820
+ if (val === 'SEQ_LAST') {
1821
+ insert[seqKey] = currentValue;
1822
+ }
1823
+ else if (val === 'SEQ_NEXT') {
1824
+ currentValue++;
1825
+ insert[seqKey] = currentValue;
1826
+ }
1650
1827
  }
1651
1828
  }
1652
1829
  return inserts;
@@ -1677,11 +1854,11 @@ export class Mongo extends Db {
1677
1854
  a._id = wholerecord._id;
1678
1855
  return a;
1679
1856
  }
1680
- _shouldAuditCollection(db, col, audited = this.auditedCollections) {
1857
+ _shouldAuditCollection(db, col) {
1681
1858
  if (!this.auditing)
1682
1859
  return false;
1683
1860
  const fullName = ((db ? db + "." : "") + (col || "")).toLowerCase();
1684
- return audited.some(m => fullName.includes(m.toLowerCase()));
1861
+ return this.auditedCollectionsLower.some(m => fullName.includes(m));
1685
1862
  }
1686
1863
  async _publishAndAudit(operation, db, collection, dataToPublish, noEmit) {
1687
1864
  if (!dataToPublish._id && !["deleteMany", "updateMany"].includes(operation))
@@ -1875,8 +2052,13 @@ export class Mongo extends Db {
1875
2052
  r = r.collation(opts.collation);
1876
2053
  return r;
1877
2054
  }
1878
- async _processUpdateObject(update) {
1879
- await this._processHashedKeys(update);
2055
+ _hasHashedKeys(obj) {
2056
+ return Object.keys(obj).some(startsWithHashedPrefix);
2057
+ }
2058
+ _hasSequenceFields(obj) {
2059
+ return Object.values(obj).some(v => v === 'SEQ_NEXT' || v === 'SEQ_LAST');
2060
+ }
2061
+ _processUpdateObject(update) {
1880
2062
  for (let k in update) {
1881
2063
  let key = k;
1882
2064
  const keyStr = String(key);
@@ -1919,16 +2101,27 @@ export class Mongo extends Db {
1919
2101
  }
1920
2102
  return update;
1921
2103
  }
1922
- async _processHashedKeys(update) {
1923
- // Pre-filter hashed keys before async processing
1924
- const hashedKeys = Object.keys(update).filter(startsWithHashedPrefix);
1925
- // Process all hashed keys in parallel
2104
+ async _processHashedKeys(obj) {
2105
+ const hashedKeys = Object.keys(obj).filter(startsWithHashedPrefix);
1926
2106
  await Promise.all(hashedKeys.map(async (key) => {
1927
2107
  const salt = await bcrypt.genSalt(saltRounds);
1928
- const hash = await bcrypt.hash(update[key], salt);
1929
- update[key] = { salt, hash };
2108
+ const hash = await bcrypt.hash(obj[key], salt);
2109
+ obj[key] = { salt, hash };
1930
2110
  }));
1931
2111
  }
2112
+ /**
2113
+ * Removes fields starting with `__` from an object (mutates in place).
2114
+ * Used to sanitize input on insert/update operations, preventing clients
2115
+ * from writing internal/reserved fields directly to the database.
2116
+ * Fields with `__hashed__` prefix are preserved (used for password hashing).
2117
+ */
2118
+ _stripDoubleUnderscoreKeys(obj) {
2119
+ for (const key in obj) {
2120
+ if (startsWithDoubleUnderscore(key) && !startsWithHashedPrefix(key)) {
2121
+ delete obj[key];
2122
+ }
2123
+ }
2124
+ }
1932
2125
  _processReturnedObject(ret) {
1933
2126
  if (!ret)
1934
2127
  return ret;