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/base.mjs +1 -1
- package/dist/base.mjs.map +1 -1
- package/dist/db.mjs.map +1 -1
- package/dist/mongo.d.mts +38 -3
- package/dist/mongo.d.mts.map +1 -1
- package/dist/mongo.mjs +310 -117
- package/dist/mongo.mjs.map +1 -1
- package/dist/repo.d.mts +6 -3
- package/dist/repo.d.mts.map +1 -1
- package/dist/repo.mjs +6 -5
- package/dist/repo.mjs.map +1 -1
- package/dist/types.d.mts +1 -0
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs +1 -0
- package/dist/types.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
438
|
+
return {};
|
|
427
439
|
const dbName = this.db;
|
|
428
440
|
fjLog.debug('findByIdsInManyCollections called', dbName, request);
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
1243
|
-
|
|
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
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1606
|
-
|
|
1607
|
-
if (
|
|
1608
|
-
await this.
|
|
1609
|
-
|
|
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
|
-
|
|
1632
|
-
|
|
1633
|
-
const
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
|
1857
|
+
_shouldAuditCollection(db, col) {
|
|
1681
1858
|
if (!this.auditing)
|
|
1682
1859
|
return false;
|
|
1683
1860
|
const fullName = ((db ? db + "." : "") + (col || "")).toLowerCase();
|
|
1684
|
-
return
|
|
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
|
-
|
|
1879
|
-
|
|
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(
|
|
1923
|
-
|
|
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(
|
|
1929
|
-
|
|
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;
|