cry-db 2.4.18 → 2.4.21
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/README.md +299 -0
- package/dist/base.mjs +1 -1
- package/dist/base.mjs.map +1 -1
- package/dist/bic.d.mts +6 -0
- package/dist/bic.d.mts.map +1 -0
- package/dist/bic.mjs +48 -0
- package/dist/bic.mjs.map +1 -0
- package/dist/db.mjs.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/dist/mongo.d.mts +75 -4
- package/dist/mongo.d.mts.map +1 -1
- package/dist/mongo.mjs +508 -122
- package/dist/mongo.mjs.map +1 -1
- package/dist/repo.d.mts +14 -3
- package/dist/repo.d.mts.map +1 -1
- package/dist/repo.mjs +30 -5
- package/dist/repo.mjs.map +1 -1
- package/dist/types.d.mts +11 -3
- 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) {
|
|
@@ -70,16 +70,19 @@ export class Mongo extends Db {
|
|
|
70
70
|
super(db, url);
|
|
71
71
|
this.revisions = false;
|
|
72
72
|
this.softdelete = false;
|
|
73
|
+
this.softarchive = false;
|
|
73
74
|
this.syncSupport = false;
|
|
74
75
|
this.session = undefined;
|
|
75
76
|
this.emittingPublishEvents = false;
|
|
76
77
|
this.emittingPublishRevEvents = false;
|
|
77
78
|
this.auditing = false;
|
|
78
79
|
this.auditCollectionName = "dblog";
|
|
80
|
+
this.auditedCollectionsLower = [];
|
|
79
81
|
this.auditedCollections = this.auditCollections(process.env.AUDIT_COLLECTIONS || []);
|
|
80
82
|
this.emitter = new TypedEmitter();
|
|
81
83
|
this.user = undefined;
|
|
82
84
|
this.audit = undefined;
|
|
85
|
+
this._sequencesIndexEnsured = false;
|
|
83
86
|
fjLog.debug('new Mongo:', this.url, this.db);
|
|
84
87
|
}
|
|
85
88
|
on(evt, listener) {
|
|
@@ -112,6 +115,17 @@ export class Mongo extends Db {
|
|
|
112
115
|
usesSoftDelete() {
|
|
113
116
|
return this.softdelete;
|
|
114
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Enable or disable archive filtering.
|
|
120
|
+
* When enabled, query operations exclude records with `_archived` set,
|
|
121
|
+
* unless `returnArchived: true` is passed in opts.
|
|
122
|
+
*/
|
|
123
|
+
useArchive(enabled) {
|
|
124
|
+
return this.softarchive = !!enabled;
|
|
125
|
+
}
|
|
126
|
+
usesArchive() {
|
|
127
|
+
return this.softarchive;
|
|
128
|
+
}
|
|
115
129
|
useAuditing(enabled) {
|
|
116
130
|
return this.auditing = enabled;
|
|
117
131
|
}
|
|
@@ -137,9 +151,11 @@ export class Mongo extends Db {
|
|
|
137
151
|
if (this.auditedCollections.includes(coll))
|
|
138
152
|
return;
|
|
139
153
|
this.auditedCollections.push(coll);
|
|
154
|
+
this.auditedCollectionsLower.push(coll);
|
|
140
155
|
}
|
|
141
156
|
else {
|
|
142
157
|
this.auditedCollections = this.auditedCollections.filter((c) => c != coll);
|
|
158
|
+
this.auditedCollectionsLower = this.auditedCollectionsLower.filter((c) => c != coll);
|
|
143
159
|
}
|
|
144
160
|
}
|
|
145
161
|
auditCollections(arr) {
|
|
@@ -149,6 +165,7 @@ export class Mongo extends Db {
|
|
|
149
165
|
this.auditedCollections = arr.toString().split(',').map(a => a.trim().toLowerCase()).filter(a => !!a);
|
|
150
166
|
if (arr instanceof Array)
|
|
151
167
|
this.auditedCollections = arr.map(a => a.trim().toLowerCase()).filter(a => !!a);
|
|
168
|
+
this.auditedCollectionsLower = this.auditedCollections.map(a => a.toLowerCase());
|
|
152
169
|
return this.auditedCollections;
|
|
153
170
|
}
|
|
154
171
|
auditingCollection(coll, db = this.db) {
|
|
@@ -172,11 +189,19 @@ export class Mongo extends Db {
|
|
|
172
189
|
emitsPublishRevEvents() {
|
|
173
190
|
return this.emittingPublishRevEvents;
|
|
174
191
|
}
|
|
175
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Returns distinct values for a field. Results are sorted.
|
|
194
|
+
* @param opts - Use `{ collation: { locale: "en", strength: 2 } }` for case-insensitive
|
|
195
|
+
*/
|
|
196
|
+
async distinct(collection, field, opts = {}) {
|
|
197
|
+
assert(collection);
|
|
198
|
+
assert(field);
|
|
176
199
|
fjLog.debug('distinct called', collection, field);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
const dbName = this.db;
|
|
201
|
+
let client = await this.connect();
|
|
202
|
+
let conn = client.db(dbName).collection(collection);
|
|
203
|
+
let ret = await conn.distinct(field, {}, opts);
|
|
204
|
+
ret.sort();
|
|
180
205
|
fjLog.debug('distinct returns', ret);
|
|
181
206
|
return ret;
|
|
182
207
|
}
|
|
@@ -186,6 +211,10 @@ export class Mongo extends Db {
|
|
|
186
211
|
if (!query._deleted)
|
|
187
212
|
query._deleted = { $exists: false };
|
|
188
213
|
}
|
|
214
|
+
if (this.softarchive && !opts.returnArchived) {
|
|
215
|
+
if (!query._archived)
|
|
216
|
+
query._archived = { $exists: false };
|
|
217
|
+
}
|
|
189
218
|
fjLog.debug('count called', collection, query, opts);
|
|
190
219
|
let ret = await this.executeTransactionally(collection, async (conn) => {
|
|
191
220
|
return await conn.countDocuments(query, opts);
|
|
@@ -200,6 +229,10 @@ export class Mongo extends Db {
|
|
|
200
229
|
if (!query._deleted)
|
|
201
230
|
query._deleted = { $exists: false };
|
|
202
231
|
}
|
|
232
|
+
if (this.softarchive && !opts.returnArchived) {
|
|
233
|
+
if (!query._archived)
|
|
234
|
+
query._archived = { $exists: false };
|
|
235
|
+
}
|
|
203
236
|
fjLog.debug('find called', collection, query, opts);
|
|
204
237
|
let ret = await this.executeTransactionally(collection, async (conn) => {
|
|
205
238
|
let r = this._applyQueryOpts(conn.find(query, this._buildFindOptions(opts)), opts);
|
|
@@ -224,6 +257,10 @@ export class Mongo extends Db {
|
|
|
224
257
|
if (!query._deleted)
|
|
225
258
|
query._deleted = { $exists: false };
|
|
226
259
|
}
|
|
260
|
+
if (this.softarchive && !opts.returnArchived) {
|
|
261
|
+
if (!query._archived)
|
|
262
|
+
query._archived = { $exists: false };
|
|
263
|
+
}
|
|
227
264
|
fjLog.debug('findNewer called', collection, timestamp, query, opts);
|
|
228
265
|
let ret = await this.executeTransactionally(collection, async (conn) => {
|
|
229
266
|
let r = this._applyQueryOpts(conn.find(query, this._buildFindOptions(opts)).sort({ _ts: 1 }), opts);
|
|
@@ -232,18 +269,22 @@ export class Mongo extends Db {
|
|
|
232
269
|
fjLog.debug('findNewer returns', ret);
|
|
233
270
|
return ret;
|
|
234
271
|
}
|
|
235
|
-
async findNewerMany(spec = []) {
|
|
272
|
+
async findNewerMany(spec = [], defaultOpts = {}) {
|
|
236
273
|
var _a;
|
|
237
274
|
fjLog.debug('findNewerMany called', spec);
|
|
238
275
|
const dbName = this.db; // Capture db at operation start
|
|
239
276
|
let conn = await this.connect();
|
|
240
277
|
const getOneColl = async (coll) => {
|
|
241
278
|
let query = this._createQueryForNewer(coll.timestamp, coll.query || {});
|
|
242
|
-
const opts = coll.opts
|
|
279
|
+
const opts = { ...defaultOpts, ...coll.opts };
|
|
243
280
|
if (this.softdelete && !opts.returnDeleted) {
|
|
244
281
|
if (!query._deleted)
|
|
245
282
|
query._deleted = { $exists: false };
|
|
246
283
|
}
|
|
284
|
+
if (this.softarchive && !opts.returnArchived) {
|
|
285
|
+
if (!query._archived)
|
|
286
|
+
query._archived = { $exists: false };
|
|
287
|
+
}
|
|
247
288
|
if (process.env.MONGO_DEBUG_FINDNEWERMANY) {
|
|
248
289
|
fjLog.debug("findNewerMany <-", coll.collection, coll.timestamp, coll.query, " -> ", JSON.stringify(query));
|
|
249
290
|
}
|
|
@@ -393,6 +434,10 @@ export class Mongo extends Db {
|
|
|
393
434
|
if (!query._deleted)
|
|
394
435
|
query._deleted = { $exists: false };
|
|
395
436
|
}
|
|
437
|
+
if (this.softarchive && !opts.returnArchived) {
|
|
438
|
+
if (!query._archived)
|
|
439
|
+
query._archived = { $exists: false };
|
|
440
|
+
}
|
|
396
441
|
// if (!query._blocked) query._blocked = { $exists: false }; // intentionally - blocked records are returned
|
|
397
442
|
fjLog.debug('findOne called', collection, query, projection);
|
|
398
443
|
let ret = await this.executeTransactionally(collection, async (conn) => await conn.findOne(query, { ...(projection ? { projection } : {}), ...this._sessionOpt() }), false, { operation: "findOne", collection, query, projection });
|
|
@@ -407,30 +452,32 @@ export class Mongo extends Db {
|
|
|
407
452
|
const dbName = this.db; // Capture db at operation start
|
|
408
453
|
let query = {
|
|
409
454
|
_id: Mongo._toId(id),
|
|
410
|
-
// _deleted: { $exists: false }
|
|
411
455
|
};
|
|
456
|
+
if (this.softdelete && !opts.returnDeleted) {
|
|
457
|
+
query._deleted = { $exists: false };
|
|
458
|
+
}
|
|
459
|
+
if (this.softarchive && !opts.returnArchived) {
|
|
460
|
+
query._archived = { $exists: false };
|
|
461
|
+
}
|
|
412
462
|
fjLog.debug('findById called', dbName, collection, id, projection);
|
|
413
463
|
fjLog.trace('findById executing with query', collection, query, projection);
|
|
414
464
|
let ret = await this.executeTransactionally(collection, async (conn) => {
|
|
415
465
|
let r = await conn.findOne(query, { ...(projection ? { projection } : {}), ...this._sessionOpt() });
|
|
416
466
|
return r;
|
|
417
467
|
}, false, { operation: "findById", collection, id, projection });
|
|
418
|
-
if ((ret === null || ret === void 0 ? void 0 : ret._deleted) && this.softdelete && !opts.returnDeleted)
|
|
419
|
-
ret = null;
|
|
420
468
|
fjLog.debug('findById returns', ret);
|
|
421
469
|
return this._processReturnedObject(ret);
|
|
422
470
|
}
|
|
423
471
|
async findByIdsInManyCollections(request, opts = {}) {
|
|
424
|
-
const result = {};
|
|
425
472
|
if (!request || request.length === 0)
|
|
426
|
-
return
|
|
473
|
+
return {};
|
|
427
474
|
const dbName = this.db;
|
|
428
475
|
fjLog.debug('findByIdsInManyCollections called', dbName, request);
|
|
429
|
-
|
|
476
|
+
// Process all collections in parallel
|
|
477
|
+
const fetchOne = async (req) => {
|
|
430
478
|
const { collection, ids, projection } = req;
|
|
431
479
|
if (!collection || !ids || ids.length === 0) {
|
|
432
|
-
|
|
433
|
-
continue;
|
|
480
|
+
return { collection, entities: [] };
|
|
434
481
|
}
|
|
435
482
|
const objectIds = ids.map(id => Mongo._toId(id));
|
|
436
483
|
const entities = await this.executeTransactionally(collection, async (conn) => {
|
|
@@ -438,10 +485,18 @@ export class Mongo extends Db {
|
|
|
438
485
|
if (this.softdelete && !opts.returnDeleted) {
|
|
439
486
|
query._deleted = { $exists: false };
|
|
440
487
|
}
|
|
488
|
+
if (this.softarchive && !opts.returnArchived) {
|
|
489
|
+
query._archived = { $exists: false };
|
|
490
|
+
}
|
|
441
491
|
let r = conn.find(query, { ...(projection ? { projection } : {}), ...this._sessionOpt() });
|
|
442
492
|
return await r.toArray();
|
|
443
493
|
}, false, { operation: "findByIdsInManyCollections", collection, ids, projection });
|
|
444
|
-
|
|
494
|
+
return { collection, entities: this._processReturnedObject(entities) };
|
|
495
|
+
};
|
|
496
|
+
const results = await Promise.all(request.map(fetchOne));
|
|
497
|
+
const result = {};
|
|
498
|
+
for (const { collection, entities } of results) {
|
|
499
|
+
result[collection] = entities;
|
|
445
500
|
}
|
|
446
501
|
fjLog.debug('findByIdsInManyCollections returns', result);
|
|
447
502
|
return result;
|
|
@@ -459,6 +514,9 @@ export class Mongo extends Db {
|
|
|
459
514
|
if (this.softdelete && !opts.returnDeleted) {
|
|
460
515
|
query._deleted = { $exists: false };
|
|
461
516
|
}
|
|
517
|
+
if (this.softarchive && !opts.returnArchived) {
|
|
518
|
+
query._archived = { $exists: false };
|
|
519
|
+
}
|
|
462
520
|
let r = conn.find(query, { ...(projection ? { projection } : {}), ...this._sessionOpt() });
|
|
463
521
|
return await r.toArray();
|
|
464
522
|
}, false, { operation: "findByIds", collection, ids, projection });
|
|
@@ -477,7 +535,9 @@ export class Mongo extends Db {
|
|
|
477
535
|
returnDocument: "after",
|
|
478
536
|
...this._sessionOpt()
|
|
479
537
|
};
|
|
480
|
-
|
|
538
|
+
if (this._hasHashedKeys(update))
|
|
539
|
+
await this._processHashedKeys(update);
|
|
540
|
+
update = this._processUpdateObject(update);
|
|
481
541
|
fjLog.debug('updateOne called', collection, query, update);
|
|
482
542
|
let seqKeys = this._findSequenceKeys(update.$set);
|
|
483
543
|
let obj = await this.executeTransactionally(collection, async (conn, client) => {
|
|
@@ -507,7 +567,9 @@ export class Mongo extends Db {
|
|
|
507
567
|
...this._sessionOpt()
|
|
508
568
|
};
|
|
509
569
|
let _id = Mongo.toId(id || update._id) || Mongo.newid();
|
|
510
|
-
|
|
570
|
+
if (this._hasHashedKeys(update))
|
|
571
|
+
await this._processHashedKeys(update);
|
|
572
|
+
update = this._processUpdateObject(update);
|
|
511
573
|
fjLog.debug('save called', collection, id, update);
|
|
512
574
|
let seqKeys = this._findSequenceKeys(update.$set);
|
|
513
575
|
let obj = await this.executeTransactionally(collection, async (conn, client) => {
|
|
@@ -541,7 +603,9 @@ export class Mongo extends Db {
|
|
|
541
603
|
returnDocument: "after",
|
|
542
604
|
...this._sessionOpt()
|
|
543
605
|
};
|
|
544
|
-
|
|
606
|
+
if (this._hasHashedKeys(update))
|
|
607
|
+
await this._processHashedKeys(update);
|
|
608
|
+
update = this._processUpdateObject(update);
|
|
545
609
|
fjLog.debug('update called', collection, query, update);
|
|
546
610
|
let seqKeys = this._findSequenceKeys(update.$set);
|
|
547
611
|
let obj = await this.executeTransactionally(collection, async (conn, client) => {
|
|
@@ -578,7 +642,9 @@ export class Mongo extends Db {
|
|
|
578
642
|
...this._sessionOpt()
|
|
579
643
|
};
|
|
580
644
|
fjLog.debug('upsert called', collection, query, update);
|
|
581
|
-
|
|
645
|
+
if (this._hasHashedKeys(update))
|
|
646
|
+
await this._processHashedKeys(update);
|
|
647
|
+
update = this._processUpdateObject(update);
|
|
582
648
|
let seqKeys = this._findSequenceKeys(update.$set);
|
|
583
649
|
fjLog.debug('upsert processed', collection, query, update);
|
|
584
650
|
if (Object.keys(query).length === 0)
|
|
@@ -612,6 +678,7 @@ export class Mongo extends Db {
|
|
|
612
678
|
fjLog.debug('insert called', collection, insert);
|
|
613
679
|
const dbName = this.db; // Capture db at operation start
|
|
614
680
|
insert = this.replaceIds(insert);
|
|
681
|
+
this._stripDoubleUnderscoreKeys(insert);
|
|
615
682
|
if (this.revisions) {
|
|
616
683
|
insert._rev = 1;
|
|
617
684
|
insert._ts = Base.timestamp();
|
|
@@ -660,13 +727,22 @@ export class Mongo extends Db {
|
|
|
660
727
|
};
|
|
661
728
|
const batchData = [];
|
|
662
729
|
const errors = [];
|
|
730
|
+
// Handle sequences in updates before processing
|
|
731
|
+
if (updates === null || updates === void 0 ? void 0 : updates.length) {
|
|
732
|
+
const updatesWithSeq = updates.filter(u => this._hasSequenceFields(u.update));
|
|
733
|
+
if (updatesWithSeq.length > 0) {
|
|
734
|
+
await this._processSequenceFieldForMany(conn, dbName, collection, updatesWithSeq.map(u => u.update));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
663
737
|
// Process updates (with upsert) in parallel
|
|
664
738
|
if (updates === null || updates === void 0 ? void 0 : updates.length) {
|
|
665
739
|
const updatePromises = updates.map(async ({ _id, update }) => {
|
|
666
740
|
var _a;
|
|
667
741
|
try {
|
|
668
742
|
const objectId = Mongo._toId(_id);
|
|
669
|
-
|
|
743
|
+
if (this._hasHashedKeys(update))
|
|
744
|
+
await this._processHashedKeys(update);
|
|
745
|
+
const processedUpdate = this._processUpdateObject({ ...update });
|
|
670
746
|
const opts = {
|
|
671
747
|
upsert: true,
|
|
672
748
|
returnDocument: "after",
|
|
@@ -821,48 +897,56 @@ export class Mongo extends Db {
|
|
|
821
897
|
batch = this.replaceIds(batch);
|
|
822
898
|
await Promise.all(batch.map(item => this._processHashedKeys(item === null || item === void 0 ? void 0 : item.update)));
|
|
823
899
|
let ret = await this.executeTransactionally(collection, async (conn, client) => {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
900
|
+
// Only process sequences if any batch item has SEQ_NEXT or SEQ_LAST
|
|
901
|
+
const updatesWithSeq = batch.filter(b => this._hasSequenceFields(b.update));
|
|
902
|
+
if (updatesWithSeq.length > 0) {
|
|
903
|
+
await this._processSequenceFieldForMany(client, dbName, collection, updatesWithSeq.map(b => b.update));
|
|
904
|
+
}
|
|
905
|
+
// Process all updates in parallel
|
|
906
|
+
const upsertPromises = batch.map(async (part) => {
|
|
907
|
+
var _a, _b;
|
|
908
|
+
const { query, update, opts } = part;
|
|
909
|
+
const options = {
|
|
831
910
|
...(opts || {}),
|
|
832
911
|
upsert: true,
|
|
833
912
|
returnDocument: "after",
|
|
834
913
|
includeResultMetadata: true,
|
|
835
914
|
...this._sessionOpt()
|
|
836
915
|
};
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
916
|
+
if (this._hasHashedKeys(update))
|
|
917
|
+
await this._processHashedKeys(update);
|
|
918
|
+
const processedUpdate = this._processUpdateObject({ ...update });
|
|
919
|
+
const result = await conn.findOneAndUpdate(query, processedUpdate, options);
|
|
920
|
+
if (!((_a = result === null || result === void 0 ? void 0 : result.value) === null || _a === void 0 ? void 0 : _a._id))
|
|
921
|
+
return null;
|
|
922
|
+
const ret = result.value;
|
|
923
|
+
// Determine operation based on result fields
|
|
924
|
+
let oper;
|
|
925
|
+
if (ret._deleted !== undefined) {
|
|
926
|
+
oper = "delete";
|
|
927
|
+
}
|
|
928
|
+
else if ((_b = result.lastErrorObject) === null || _b === void 0 ? void 0 : _b.updatedExisting) {
|
|
929
|
+
oper = "update";
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
oper = "insert";
|
|
933
|
+
}
|
|
934
|
+
const retObj = oper === "insert" ? ret : this._removeUnchanged(ret, processedUpdate, !!(opts === null || opts === void 0 ? void 0 : opts.returnFullObject));
|
|
935
|
+
this._processReturnedObject(retObj);
|
|
936
|
+
return { operation: oper, data: retObj };
|
|
937
|
+
});
|
|
938
|
+
const results = await Promise.all(upsertPromises);
|
|
939
|
+
// Filter out nulls and separate batchData and changes
|
|
940
|
+
const batchData = [];
|
|
941
|
+
const changes = [];
|
|
942
|
+
for (const result of results) {
|
|
943
|
+
if (result) {
|
|
944
|
+
batchData.push(result);
|
|
945
|
+
changes.push(result.data);
|
|
861
946
|
}
|
|
862
|
-
;
|
|
863
947
|
}
|
|
864
948
|
if (this.emittingPublishEvents || this.auditing) {
|
|
865
|
-
|
|
949
|
+
this.emit("publish", {
|
|
866
950
|
channel: `db/${dbName}/${collection}`,
|
|
867
951
|
payload: {
|
|
868
952
|
operation: "batch",
|
|
@@ -883,7 +967,7 @@ export class Mongo extends Db {
|
|
|
883
967
|
_ts: item.data._ts,
|
|
884
968
|
}))
|
|
885
969
|
};
|
|
886
|
-
|
|
970
|
+
this.emit("publishRev", {
|
|
887
971
|
channel: `dbrev/${dbName}/${collection}`,
|
|
888
972
|
payload,
|
|
889
973
|
});
|
|
@@ -900,11 +984,16 @@ export class Mongo extends Db {
|
|
|
900
984
|
fjLog.debug('insertMany called', collection, insert);
|
|
901
985
|
const dbName = this.db; // Capture db at operation start
|
|
902
986
|
insert = this.replaceIds(insert);
|
|
987
|
+
insert.forEach(item => this._stripDoubleUnderscoreKeys(item));
|
|
903
988
|
await Promise.all(insert.map(item => this._processHashedKeys(item)));
|
|
904
989
|
if (this.revisions)
|
|
905
990
|
insert.forEach(ins => { ins._rev = 1; ins._ts = Base.timestamp(); });
|
|
906
991
|
let ret = await this.executeTransactionally(collection, async (conn, client) => {
|
|
907
|
-
|
|
992
|
+
// Only process sequences if any insert has SEQ_NEXT or SEQ_LAST
|
|
993
|
+
const insertsWithSeq = insert.filter(item => this._hasSequenceFields(item));
|
|
994
|
+
if (insertsWithSeq.length > 0) {
|
|
995
|
+
await this._processSequenceFieldForMany(client, dbName, collection, insert);
|
|
996
|
+
}
|
|
908
997
|
let obj = await conn.insertMany(insert, this._sessionOpt());
|
|
909
998
|
let ret = [];
|
|
910
999
|
for (let ns of Object.keys(obj.insertedIds)) {
|
|
@@ -1019,6 +1108,158 @@ export class Mongo extends Db {
|
|
|
1019
1108
|
fjLog.debug('unblockOne returns', ret);
|
|
1020
1109
|
return ret;
|
|
1021
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Archives a single document by setting `_archived` to the current date.
|
|
1113
|
+
* Increments `_rev` and updates `_ts`.
|
|
1114
|
+
*/
|
|
1115
|
+
async archiveOne(collection, query) {
|
|
1116
|
+
let opts = {
|
|
1117
|
+
upsert: false,
|
|
1118
|
+
returnDocument: "after",
|
|
1119
|
+
...this._sessionOpt()
|
|
1120
|
+
};
|
|
1121
|
+
const dbName = this.db;
|
|
1122
|
+
query = this.replaceIds(query);
|
|
1123
|
+
fjLog.debug('archiveOne called', collection, query);
|
|
1124
|
+
let ret = await this.executeTransactionally(collection, async (conn) => {
|
|
1125
|
+
query._archived = { $exists: false };
|
|
1126
|
+
let update = {
|
|
1127
|
+
$set: { _archived: new Date(), },
|
|
1128
|
+
$inc: { _rev: 1, },
|
|
1129
|
+
$currentDate: { _ts: { $type: 'timestamp' } }
|
|
1130
|
+
};
|
|
1131
|
+
let obj = await conn.findOneAndUpdate(query, update, opts);
|
|
1132
|
+
if (!obj || !(obj === null || obj === void 0 ? void 0 : obj._id))
|
|
1133
|
+
return {
|
|
1134
|
+
ok: false
|
|
1135
|
+
};
|
|
1136
|
+
await this._publishAndAudit('update', dbName, collection, obj);
|
|
1137
|
+
return obj;
|
|
1138
|
+
}, false, { operation: "archiveOne", collection, query });
|
|
1139
|
+
fjLog.debug('archiveOne returns', ret);
|
|
1140
|
+
return ret;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Unarchives a single document by removing the `_archived` field.
|
|
1144
|
+
* Increments `_rev` and updates `_ts`.
|
|
1145
|
+
*/
|
|
1146
|
+
async unarchiveOne(collection, query) {
|
|
1147
|
+
let opts = {
|
|
1148
|
+
upsert: false,
|
|
1149
|
+
returnDocument: "after",
|
|
1150
|
+
...this._sessionOpt()
|
|
1151
|
+
};
|
|
1152
|
+
const dbName = this.db;
|
|
1153
|
+
query = this.replaceIds(query);
|
|
1154
|
+
fjLog.debug('unarchiveOne called', collection, query);
|
|
1155
|
+
let ret = await this.executeTransactionally(collection, async (conn) => {
|
|
1156
|
+
query._archived = { $exists: true };
|
|
1157
|
+
let update = {
|
|
1158
|
+
$unset: { _archived: 1 },
|
|
1159
|
+
$inc: { _rev: 1, },
|
|
1160
|
+
$currentDate: { _ts: { $type: 'timestamp' } }
|
|
1161
|
+
};
|
|
1162
|
+
let obj = await conn.findOneAndUpdate(query, update, opts);
|
|
1163
|
+
if (!obj || !(obj === null || obj === void 0 ? void 0 : obj._id))
|
|
1164
|
+
return {
|
|
1165
|
+
ok: false
|
|
1166
|
+
};
|
|
1167
|
+
await this._publishAndAudit('update', dbName, collection, obj);
|
|
1168
|
+
return obj;
|
|
1169
|
+
}, false, { operation: "unarchiveOne", collection, query });
|
|
1170
|
+
fjLog.debug('unarchiveOne returns', ret);
|
|
1171
|
+
return ret;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Archives a single document found by its `_id`.
|
|
1175
|
+
*/
|
|
1176
|
+
async archiveOneById(collection, id) {
|
|
1177
|
+
assert(collection);
|
|
1178
|
+
assert(id);
|
|
1179
|
+
return await this.archiveOne(collection, { _id: Mongo._toId(id) });
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Archives multiple documents found by their `_id`s.
|
|
1183
|
+
* Returns the array of archived documents.
|
|
1184
|
+
*/
|
|
1185
|
+
async archiveManyByIds(collection, ids) {
|
|
1186
|
+
assert(collection);
|
|
1187
|
+
assert(ids);
|
|
1188
|
+
if (!ids || ids.length === 0)
|
|
1189
|
+
return [];
|
|
1190
|
+
const opts = {
|
|
1191
|
+
upsert: false,
|
|
1192
|
+
returnDocument: "after",
|
|
1193
|
+
...this._sessionOpt()
|
|
1194
|
+
};
|
|
1195
|
+
const dbName = this.db;
|
|
1196
|
+
fjLog.debug('archiveManyByIds called', collection, ids);
|
|
1197
|
+
let ret = [];
|
|
1198
|
+
for (const id of ids) {
|
|
1199
|
+
let r = await this.executeTransactionally(collection, async (conn) => {
|
|
1200
|
+
let query = { _id: Mongo._toId(id), _archived: { $exists: false } };
|
|
1201
|
+
let update = {
|
|
1202
|
+
$set: { _archived: new Date() },
|
|
1203
|
+
$inc: { _rev: 1 },
|
|
1204
|
+
$currentDate: { _ts: { $type: 'timestamp' } }
|
|
1205
|
+
};
|
|
1206
|
+
let obj = await conn.findOneAndUpdate(query, update, opts);
|
|
1207
|
+
if (obj === null || obj === void 0 ? void 0 : obj._id) {
|
|
1208
|
+
await this._publishAndAudit('update', dbName, collection, obj);
|
|
1209
|
+
}
|
|
1210
|
+
return obj;
|
|
1211
|
+
}, false, { operation: "archiveManyByIds", collection, id });
|
|
1212
|
+
if (r === null || r === void 0 ? void 0 : r._id)
|
|
1213
|
+
ret.push(r);
|
|
1214
|
+
}
|
|
1215
|
+
fjLog.debug('archiveManyByIds returns', ret);
|
|
1216
|
+
return ret;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Unarchives a single document found by its `_id`.
|
|
1220
|
+
*/
|
|
1221
|
+
async unarchiveOneById(collection, id) {
|
|
1222
|
+
assert(collection);
|
|
1223
|
+
assert(id);
|
|
1224
|
+
return await this.unarchiveOne(collection, { _id: Mongo._toId(id) });
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Unarchives multiple documents found by their `_id`s.
|
|
1228
|
+
* Returns the array of unarchived documents.
|
|
1229
|
+
*/
|
|
1230
|
+
async unarchiveManyByIds(collection, ids) {
|
|
1231
|
+
assert(collection);
|
|
1232
|
+
assert(ids);
|
|
1233
|
+
if (!ids || ids.length === 0)
|
|
1234
|
+
return [];
|
|
1235
|
+
const opts = {
|
|
1236
|
+
upsert: false,
|
|
1237
|
+
returnDocument: "after",
|
|
1238
|
+
...this._sessionOpt()
|
|
1239
|
+
};
|
|
1240
|
+
const dbName = this.db;
|
|
1241
|
+
fjLog.debug('unarchiveManyByIds called', collection, ids);
|
|
1242
|
+
let ret = [];
|
|
1243
|
+
for (const id of ids) {
|
|
1244
|
+
let r = await this.executeTransactionally(collection, async (conn) => {
|
|
1245
|
+
let query = { _id: Mongo._toId(id), _archived: { $exists: true } };
|
|
1246
|
+
let update = {
|
|
1247
|
+
$unset: { _archived: 1 },
|
|
1248
|
+
$inc: { _rev: 1 },
|
|
1249
|
+
$currentDate: { _ts: { $type: 'timestamp' } }
|
|
1250
|
+
};
|
|
1251
|
+
let obj = await conn.findOneAndUpdate(query, update, opts);
|
|
1252
|
+
if (obj === null || obj === void 0 ? void 0 : obj._id) {
|
|
1253
|
+
await this._publishAndAudit('update', dbName, collection, obj);
|
|
1254
|
+
}
|
|
1255
|
+
return obj;
|
|
1256
|
+
}, false, { operation: "unarchiveManyByIds", collection, id });
|
|
1257
|
+
if (r === null || r === void 0 ? void 0 : r._id)
|
|
1258
|
+
ret.push(r);
|
|
1259
|
+
}
|
|
1260
|
+
fjLog.debug('unarchiveManyByIds returns', ret);
|
|
1261
|
+
return ret;
|
|
1262
|
+
}
|
|
1022
1263
|
async hardDeleteOne(collection, query) {
|
|
1023
1264
|
assert(collection);
|
|
1024
1265
|
assert(query);
|
|
@@ -1175,35 +1416,14 @@ export class Mongo extends Db {
|
|
|
1175
1416
|
fjLog.debug('isUnique returns', ret);
|
|
1176
1417
|
return ret;
|
|
1177
1418
|
}
|
|
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
1419
|
async dropCollection(collection) {
|
|
1201
1420
|
assert(collection);
|
|
1202
1421
|
fjLog.debug('dropCollection called', this.auditCollections);
|
|
1203
1422
|
const dbName = this.db; // Capture db at operation start
|
|
1204
1423
|
let client = await this.connect();
|
|
1205
1424
|
let existing = await client.db(dbName).collections();
|
|
1206
|
-
|
|
1425
|
+
const existingNames = new Set(existing.map((c) => c.collectionName));
|
|
1426
|
+
if (existingNames.has(collection)) {
|
|
1207
1427
|
await client.db(dbName).dropCollection(collection);
|
|
1208
1428
|
}
|
|
1209
1429
|
fjLog.debug('dropCollection returns');
|
|
@@ -1225,8 +1445,9 @@ export class Mongo extends Db {
|
|
|
1225
1445
|
const dbName = this.db; // Capture db at operation start
|
|
1226
1446
|
let client = await this.connect();
|
|
1227
1447
|
let existing = await client.db(dbName).collections();
|
|
1228
|
-
|
|
1229
|
-
|
|
1448
|
+
const existingNames = new Set(existing.map((c) => c.collectionName));
|
|
1449
|
+
for (const collection of collections) {
|
|
1450
|
+
if (existingNames.has(collection)) {
|
|
1230
1451
|
await client.db(dbName).dropCollection(collection);
|
|
1231
1452
|
}
|
|
1232
1453
|
}
|
|
@@ -1239,8 +1460,9 @@ export class Mongo extends Db {
|
|
|
1239
1460
|
const dbName = this.db; // Capture db at operation start
|
|
1240
1461
|
let client = await this.connect();
|
|
1241
1462
|
let existing = await this.getCollections();
|
|
1242
|
-
|
|
1243
|
-
|
|
1463
|
+
const existingSet = new Set(existing);
|
|
1464
|
+
for (const collection of collections) {
|
|
1465
|
+
if (!existingSet.has(collection)) {
|
|
1244
1466
|
await client.db(dbName).createCollection(collection);
|
|
1245
1467
|
}
|
|
1246
1468
|
}
|
|
@@ -1388,6 +1610,9 @@ export class Mongo extends Db {
|
|
|
1388
1610
|
}
|
|
1389
1611
|
// 4. Clean up state
|
|
1390
1612
|
this.session = undefined;
|
|
1613
|
+
this.auditedCollections.length = 0;
|
|
1614
|
+
this.auditedCollectionsLower.length = 0;
|
|
1615
|
+
this._sequencesIndexEnsured = false;
|
|
1391
1616
|
}
|
|
1392
1617
|
async inTransaction() {
|
|
1393
1618
|
if (!await this.isOnReplicaSet())
|
|
@@ -1575,6 +1800,139 @@ export class Mongo extends Db {
|
|
|
1575
1800
|
return undefined;
|
|
1576
1801
|
return parseInt((_a = maxfld === null || maxfld === void 0 ? void 0 : maxfld[0]) === null || _a === void 0 ? void 0 : _a[key]) || 0;
|
|
1577
1802
|
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Ensures the unique index on _sequences collection exists.
|
|
1805
|
+
* Called lazily on first sequence operation.
|
|
1806
|
+
*/
|
|
1807
|
+
async _ensureSequencesIndex(dbConnection) {
|
|
1808
|
+
if (this._sequencesIndexEnsured)
|
|
1809
|
+
return;
|
|
1810
|
+
try {
|
|
1811
|
+
await dbConnection
|
|
1812
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1813
|
+
.createIndex({ collection: 1, field: 1 }, { unique: true });
|
|
1814
|
+
this._sequencesIndexEnsured = true;
|
|
1815
|
+
}
|
|
1816
|
+
catch (err) {
|
|
1817
|
+
// Index may already exist, which is fine
|
|
1818
|
+
if (err.code === 85 || err.code === 86) {
|
|
1819
|
+
this._sequencesIndexEnsured = true;
|
|
1820
|
+
}
|
|
1821
|
+
else {
|
|
1822
|
+
throw err;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Atomically gets the next sequence value for a field in a collection.
|
|
1828
|
+
* On first use, auto-seeds from existing data in the collection.
|
|
1829
|
+
*/
|
|
1830
|
+
async _getNextSequenceValue(dbConnection, collection, field) {
|
|
1831
|
+
// Check current max in the actual collection
|
|
1832
|
+
const existingMax = await this._findLastSequenceForKey(dbConnection.collection(collection), field);
|
|
1833
|
+
// Check if we have a sequence document
|
|
1834
|
+
const existingSeq = await dbConnection
|
|
1835
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1836
|
+
.findOne({ collection, field }, this._sessionOpt());
|
|
1837
|
+
if (!existingSeq) {
|
|
1838
|
+
// First time for this collection/field - ensure index exists
|
|
1839
|
+
await this._ensureSequencesIndex(dbConnection);
|
|
1840
|
+
const seedValue = (existingMax !== undefined && existingMax >= 1) ? existingMax : 0;
|
|
1841
|
+
// Use $max to handle concurrent seeding - only sets if greater than current value
|
|
1842
|
+
// This ensures the highest seed value wins in case of concurrent operations
|
|
1843
|
+
await dbConnection
|
|
1844
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1845
|
+
.findOneAndUpdate({ collection, field }, {
|
|
1846
|
+
$max: { lastSeqNum: seedValue },
|
|
1847
|
+
$setOnInsert: { collection, field }
|
|
1848
|
+
}, { upsert: true, returnDocument: 'after', ...this._sessionOpt() });
|
|
1849
|
+
// Now increment atomically to get next value
|
|
1850
|
+
const updated = await dbConnection
|
|
1851
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1852
|
+
.findOneAndUpdate({ collection, field }, { $inc: { lastSeqNum: 1 } }, { returnDocument: 'after', ...this._sessionOpt() });
|
|
1853
|
+
return updated.lastSeqNum;
|
|
1854
|
+
}
|
|
1855
|
+
// Sequence exists - check if collection was cleared (reset scenario)
|
|
1856
|
+
if (existingMax === undefined && existingSeq.lastSeqNum > 0) {
|
|
1857
|
+
// Collection is empty but we have a non-zero sequence - reset to 0 then increment to 1
|
|
1858
|
+
await dbConnection
|
|
1859
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1860
|
+
.updateOne({ collection, field }, { $set: { lastSeqNum: 0 } }, this._sessionOpt());
|
|
1861
|
+
}
|
|
1862
|
+
// Increment and return
|
|
1863
|
+
const result = await dbConnection
|
|
1864
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1865
|
+
.findOneAndUpdate({ collection, field }, { $inc: { lastSeqNum: 1 } }, { returnDocument: 'after', ...this._sessionOpt() });
|
|
1866
|
+
return result.lastSeqNum;
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Gets the current (last) sequence value without incrementing.
|
|
1870
|
+
* On first use, auto-seeds from existing data in the collection.
|
|
1871
|
+
* If collection becomes empty, resets the sequence to 0.
|
|
1872
|
+
*/
|
|
1873
|
+
async _getLastSequenceValue(dbConnection, collection, field) {
|
|
1874
|
+
// Check current max in the actual collection
|
|
1875
|
+
const existingMax = await this._findLastSequenceForKey(dbConnection.collection(collection), field);
|
|
1876
|
+
// Try to get existing sequence value
|
|
1877
|
+
const existing = await dbConnection
|
|
1878
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1879
|
+
.findOne({ collection, field }, this._sessionOpt());
|
|
1880
|
+
if (existing) {
|
|
1881
|
+
// If collection is empty (existingMax undefined), reset sequence to 0
|
|
1882
|
+
if (existingMax === undefined && existing.lastSeqNum > 0) {
|
|
1883
|
+
await dbConnection
|
|
1884
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1885
|
+
.updateOne({ collection, field }, { $set: { lastSeqNum: 0 } }, this._sessionOpt());
|
|
1886
|
+
return 0;
|
|
1887
|
+
}
|
|
1888
|
+
return existing.lastSeqNum;
|
|
1889
|
+
}
|
|
1890
|
+
if (existingMax !== undefined && existingMax >= 0) {
|
|
1891
|
+
// First time with existing data - ensure index exists and seed
|
|
1892
|
+
await this._ensureSequencesIndex(dbConnection);
|
|
1893
|
+
await dbConnection
|
|
1894
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1895
|
+
.updateOne({ collection, field }, { $setOnInsert: { lastSeqNum: existingMax } }, { upsert: true, ...this._sessionOpt() });
|
|
1896
|
+
return existingMax;
|
|
1897
|
+
}
|
|
1898
|
+
return 0;
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Atomically reserves a range of sequence numbers for batch operations.
|
|
1902
|
+
* Returns { start, end } where start is the first reserved value and end is the last.
|
|
1903
|
+
*/
|
|
1904
|
+
async _reserveSequenceRange(dbConnection, collection, field, count) {
|
|
1905
|
+
// Check current max in the actual collection
|
|
1906
|
+
const existingMax = await this._findLastSequenceForKey(dbConnection.collection(collection), field);
|
|
1907
|
+
// Check if we have a sequence document
|
|
1908
|
+
const existingSeq = await dbConnection
|
|
1909
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1910
|
+
.findOne({ collection, field }, this._sessionOpt());
|
|
1911
|
+
if (!existingSeq) {
|
|
1912
|
+
// First time for this collection/field - ensure index exists
|
|
1913
|
+
await this._ensureSequencesIndex(dbConnection);
|
|
1914
|
+
const seedValue = (existingMax !== undefined && existingMax >= 1) ? existingMax : 0;
|
|
1915
|
+
// Use $max to handle concurrent seeding
|
|
1916
|
+
await dbConnection
|
|
1917
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1918
|
+
.findOneAndUpdate({ collection, field }, {
|
|
1919
|
+
$max: { lastSeqNum: seedValue },
|
|
1920
|
+
$setOnInsert: { collection, field }
|
|
1921
|
+
}, { upsert: true, returnDocument: 'after', ...this._sessionOpt() });
|
|
1922
|
+
}
|
|
1923
|
+
else if (existingMax === undefined && existingSeq.lastSeqNum > 0) {
|
|
1924
|
+
// Collection is empty but we have a non-zero sequence - reset to 0
|
|
1925
|
+
await dbConnection
|
|
1926
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1927
|
+
.updateOne({ collection, field }, { $set: { lastSeqNum: 0 } }, this._sessionOpt());
|
|
1928
|
+
}
|
|
1929
|
+
// Now atomically reserve the range
|
|
1930
|
+
const result = await dbConnection
|
|
1931
|
+
.collection(FIELD_SEQUENCES_COLLECTION)
|
|
1932
|
+
.findOneAndUpdate({ collection, field }, { $inc: { lastSeqNum: count } }, { returnDocument: 'after', ...this._sessionOpt() });
|
|
1933
|
+
const end = result.lastSeqNum;
|
|
1934
|
+
return { start: end - count + 1, end };
|
|
1935
|
+
}
|
|
1578
1936
|
// private async _getNextCollectionUpdateSeqNo(collection: string, conn: MongoClient) {
|
|
1579
1937
|
// let opts : FindOneAndUpdateOptions = {
|
|
1580
1938
|
// upsert: true,
|
|
@@ -1594,22 +1952,24 @@ export class Mongo extends Db {
|
|
|
1594
1952
|
_findSequenceKeys(object) {
|
|
1595
1953
|
if (!object)
|
|
1596
1954
|
return;
|
|
1597
|
-
|
|
1598
|
-
|
|
1955
|
+
if (!this._hasSequenceFields(object) && !this.syncSupport)
|
|
1956
|
+
return;
|
|
1957
|
+
const seqKeys = Object.keys(object).filter(key => object[key] === 'SEQ_NEXT' || object[key] === 'SEQ_LAST');
|
|
1958
|
+
return { seqKeys };
|
|
1599
1959
|
}
|
|
1600
1960
|
async _processSequenceField(client, dbName, collection, insert, seqKeys) {
|
|
1601
1961
|
assert(this.client);
|
|
1602
1962
|
// if (this.syncSupport) {
|
|
1603
1963
|
// insert._csq = (await this._getNextCollectionUpdateSeqNo(collection, client));
|
|
1604
1964
|
// }
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
if (
|
|
1608
|
-
await this.
|
|
1609
|
-
|
|
1965
|
+
const db = client.db(dbName);
|
|
1966
|
+
for (const seqKey of (seqKeys === null || seqKeys === void 0 ? void 0 : seqKeys.seqKeys) || []) {
|
|
1967
|
+
if (insert[seqKey] === 'SEQ_LAST') {
|
|
1968
|
+
insert[seqKey] = await this._getLastSequenceValue(db, collection, seqKey);
|
|
1969
|
+
}
|
|
1970
|
+
else {
|
|
1971
|
+
insert[seqKey] = await this._getNextSequenceValue(db, collection, seqKey);
|
|
1610
1972
|
}
|
|
1611
|
-
let next = insert[seqKey] === 'SEQ_LAST' ? last : last + 1;
|
|
1612
|
-
insert[seqKey] = next;
|
|
1613
1973
|
}
|
|
1614
1974
|
return insert;
|
|
1615
1975
|
}
|
|
@@ -1618,35 +1978,45 @@ export class Mongo extends Db {
|
|
|
1618
1978
|
assert(connection);
|
|
1619
1979
|
if (!(inserts === null || inserts === void 0 ? void 0 : inserts.length))
|
|
1620
1980
|
return;
|
|
1981
|
+
// Collect all unique sequence keys across all inserts
|
|
1621
1982
|
let seqKeysSet = new Set();
|
|
1622
1983
|
for (let insert of inserts) {
|
|
1623
1984
|
let spec = this._findSequenceKeys(insert);
|
|
1624
1985
|
spec === null || spec === void 0 ? void 0 : spec.seqKeys.forEach(key => seqKeysSet.add(key));
|
|
1625
1986
|
}
|
|
1626
1987
|
let seqKeys = Array.from(seqKeysSet);
|
|
1627
|
-
// let seq: number = 0;
|
|
1628
|
-
// if (this.syncSupport) seq = await this._getNextCollectionUpdateSeqNo(collection, connection);
|
|
1629
1988
|
if (!seqKeys.length)
|
|
1630
1989
|
return inserts;
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
const
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1990
|
+
const db = connection.db(dbName);
|
|
1991
|
+
// Process each sequence key
|
|
1992
|
+
for (const seqKey of seqKeys) {
|
|
1993
|
+
// Count SEQ_NEXT occurrences to reserve the range
|
|
1994
|
+
let seqNextCount = 0;
|
|
1995
|
+
for (const insert of inserts) {
|
|
1996
|
+
if (insert[seqKey] === 'SEQ_NEXT')
|
|
1997
|
+
seqNextCount++;
|
|
1998
|
+
}
|
|
1999
|
+
// Get current value and reserve range for SEQ_NEXT if needed
|
|
2000
|
+
let currentValue;
|
|
2001
|
+
if (seqNextCount > 0) {
|
|
2002
|
+
const range = await this._reserveSequenceRange(db, collection, seqKey, seqNextCount);
|
|
2003
|
+
// currentValue starts at one before the range start (since SEQ_LAST gets current, SEQ_NEXT gets next)
|
|
2004
|
+
currentValue = range.start - 1;
|
|
1638
2005
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
2006
|
+
else {
|
|
2007
|
+
// Only SEQ_LAST values, just get the current value
|
|
2008
|
+
currentValue = await this._getLastSequenceValue(db, collection, seqKey);
|
|
2009
|
+
}
|
|
2010
|
+
// Process inserts in order - SEQ_LAST gets current, SEQ_NEXT increments and gets new value
|
|
2011
|
+
for (const insert of inserts) {
|
|
2012
|
+
const val = insert[seqKey];
|
|
2013
|
+
if (val === 'SEQ_LAST') {
|
|
2014
|
+
insert[seqKey] = currentValue;
|
|
2015
|
+
}
|
|
2016
|
+
else if (val === 'SEQ_NEXT') {
|
|
2017
|
+
currentValue++;
|
|
2018
|
+
insert[seqKey] = currentValue;
|
|
2019
|
+
}
|
|
1650
2020
|
}
|
|
1651
2021
|
}
|
|
1652
2022
|
return inserts;
|
|
@@ -1677,11 +2047,11 @@ export class Mongo extends Db {
|
|
|
1677
2047
|
a._id = wholerecord._id;
|
|
1678
2048
|
return a;
|
|
1679
2049
|
}
|
|
1680
|
-
_shouldAuditCollection(db, col
|
|
2050
|
+
_shouldAuditCollection(db, col) {
|
|
1681
2051
|
if (!this.auditing)
|
|
1682
2052
|
return false;
|
|
1683
2053
|
const fullName = ((db ? db + "." : "") + (col || "")).toLowerCase();
|
|
1684
|
-
return
|
|
2054
|
+
return this.auditedCollectionsLower.some(m => fullName.includes(m));
|
|
1685
2055
|
}
|
|
1686
2056
|
async _publishAndAudit(operation, db, collection, dataToPublish, noEmit) {
|
|
1687
2057
|
if (!dataToPublish._id && !["deleteMany", "updateMany"].includes(operation))
|
|
@@ -1875,8 +2245,13 @@ export class Mongo extends Db {
|
|
|
1875
2245
|
r = r.collation(opts.collation);
|
|
1876
2246
|
return r;
|
|
1877
2247
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
2248
|
+
_hasHashedKeys(obj) {
|
|
2249
|
+
return Object.keys(obj).some(startsWithHashedPrefix);
|
|
2250
|
+
}
|
|
2251
|
+
_hasSequenceFields(obj) {
|
|
2252
|
+
return Object.values(obj).some(v => v === 'SEQ_NEXT' || v === 'SEQ_LAST');
|
|
2253
|
+
}
|
|
2254
|
+
_processUpdateObject(update) {
|
|
1880
2255
|
for (let k in update) {
|
|
1881
2256
|
let key = k;
|
|
1882
2257
|
const keyStr = String(key);
|
|
@@ -1919,16 +2294,27 @@ export class Mongo extends Db {
|
|
|
1919
2294
|
}
|
|
1920
2295
|
return update;
|
|
1921
2296
|
}
|
|
1922
|
-
async _processHashedKeys(
|
|
1923
|
-
|
|
1924
|
-
const hashedKeys = Object.keys(update).filter(startsWithHashedPrefix);
|
|
1925
|
-
// Process all hashed keys in parallel
|
|
2297
|
+
async _processHashedKeys(obj) {
|
|
2298
|
+
const hashedKeys = Object.keys(obj).filter(startsWithHashedPrefix);
|
|
1926
2299
|
await Promise.all(hashedKeys.map(async (key) => {
|
|
1927
2300
|
const salt = await bcrypt.genSalt(saltRounds);
|
|
1928
|
-
const hash = await bcrypt.hash(
|
|
1929
|
-
|
|
2301
|
+
const hash = await bcrypt.hash(obj[key], salt);
|
|
2302
|
+
obj[key] = { salt, hash };
|
|
1930
2303
|
}));
|
|
1931
2304
|
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Removes fields starting with `__` from an object (mutates in place).
|
|
2307
|
+
* Used to sanitize input on insert/update operations, preventing clients
|
|
2308
|
+
* from writing internal/reserved fields directly to the database.
|
|
2309
|
+
* Fields with `__hashed__` prefix are preserved (used for password hashing).
|
|
2310
|
+
*/
|
|
2311
|
+
_stripDoubleUnderscoreKeys(obj) {
|
|
2312
|
+
for (const key in obj) {
|
|
2313
|
+
if (startsWithDoubleUnderscore(key) && !startsWithHashedPrefix(key)) {
|
|
2314
|
+
delete obj[key];
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
1932
2318
|
_processReturnedObject(ret) {
|
|
1933
2319
|
if (!ret)
|
|
1934
2320
|
return ret;
|