@nextage/nx-frame-be 1.0.37 → 1.0.39
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/build/common/base/__test__/base.model.test.js +25 -0
- package/build/common/base/base.model.d.ts +27 -0
- package/build/common/base/base.model.js +86 -12
- package/build/common/base/mongo-utils.d.ts +4 -2
- package/build/common/base/mongo-utils.js +25 -5
- package/build/common/base/mongo.types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -289,3 +289,28 @@ it('runs aggregation', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
289
289
|
expect(data.length).toEqual(1);
|
|
290
290
|
expect(data[0].count).toEqual(count);
|
|
291
291
|
}));
|
|
292
|
+
it('bulkWriteVersioned injects version handling on native bulk ops', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
293
|
+
const code = (0, utils_1.uuid)();
|
|
294
|
+
// insertOne without an explicit version -> baseline 0 (same as save())
|
|
295
|
+
yield coded_entity_model_1.ceModel.bulkWriteVersioned([
|
|
296
|
+
{ insertOne: { document: { code, name: 'BWV insert', data: { items: [] } } } }
|
|
297
|
+
]);
|
|
298
|
+
const inserted = yield coded_entity_model_1.ceModel.findOne({ code });
|
|
299
|
+
expect(inserted.version).toEqual(0);
|
|
300
|
+
// updateOne without $inc -> auto +1 (native bulk keeps the query-hook guarantee)
|
|
301
|
+
yield coded_entity_model_1.ceModel.bulkWriteVersioned([
|
|
302
|
+
{ updateOne: { filter: { code }, update: { $set: { name: 'BWV update' } } } }
|
|
303
|
+
]);
|
|
304
|
+
const updated = yield coded_entity_model_1.ceModel.findOne({ code });
|
|
305
|
+
expect(updated.name).toEqual('BWV update');
|
|
306
|
+
expect(updated.version).toEqual(1);
|
|
307
|
+
}));
|
|
308
|
+
it('keeps TDoc-specialised subclasses assignable to BaseModel<any, any, any> (regression: TS2322 from bulkWriteVersioned<TDoc>)', () => {
|
|
309
|
+
// Compile-time guard. `CodedEntity extends BaseModel<..., CodedEntityDoc, ...>` specialises
|
|
310
|
+
// TDoc; if bulkWriteVersioned is typed on AnyBulkWriteOperation<TDoc> (invariant via
|
|
311
|
+
// ReplaceOneModel<TDoc>.filter), the subclass method stops being compatible with
|
|
312
|
+
// BaseModel<any, any, any> and `this` can no longer be passed to a BaseModel<any, any, any>
|
|
313
|
+
// sink -> this is exactly the auth-srv boot failure (user.model.ts KeyOptions.model).
|
|
314
|
+
const sink = (m) => m;
|
|
315
|
+
expect(sink(coded_entity_model_1.ceModel)).toBe(coded_entity_model_1.ceModel);
|
|
316
|
+
});
|
|
@@ -243,6 +243,33 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
|
|
|
243
243
|
* @param {*} options
|
|
244
244
|
*/
|
|
245
245
|
updateManyNative(filter: FilterParams, update: mongoose.UpdateQuery<TMongoModel>, options?: mongoose.mongo.UpdateOptions): Promise<mongoose.mongo.UpdateResult<Document>>;
|
|
246
|
+
/**
|
|
247
|
+
* Execute bulkWrite injecting version handling on each operation, so native bulk
|
|
248
|
+
* paths keep the same versioning guarantees as the Mongoose query hooks:
|
|
249
|
+
* - insertOne -> set versionKey to 0 when absent (matches save() baseline)
|
|
250
|
+
* - updateOne/updateMany -> add $inc:{version:1} unless the update already touches version
|
|
251
|
+
*
|
|
252
|
+
* Upsert ops that do not touch version fall back to $inc (an upsert insert lands at
|
|
253
|
+
* version 1). Other op kinds (replaceOne, deleteOne, deleteMany) pass through unchanged.
|
|
254
|
+
*
|
|
255
|
+
* NB: the parameter is intentionally typed on `AnyBulkWriteOperation<any>` (not `<TDoc>`):
|
|
256
|
+
* `AnyBulkWriteOperation<TDoc>` is invariant (via `ReplaceOneModel<TDoc>.filter`), so a
|
|
257
|
+
* `<TDoc>`-typed signature makes each subclass method incompatible with
|
|
258
|
+
* `BaseModel<any, any, any>` and breaks `this`-assignability (e.g. passing `this` to a
|
|
259
|
+
* `BaseModel<any, any, any>` sink). Ops are validated/cast internally, so the loose
|
|
260
|
+
* element type costs nothing.
|
|
261
|
+
*
|
|
262
|
+
* @param {*} bulkOps
|
|
263
|
+
* @param {*} options
|
|
264
|
+
*/
|
|
265
|
+
bulkWriteVersioned(bulkOps: mongoose.AnyBulkWriteOperation<any>[], options?: mongoose.mongo.BulkWriteOptions): Promise<mongoose.mongo.BulkWriteResult>;
|
|
266
|
+
/**
|
|
267
|
+
* Inject the version key into a single bulkWrite operation (see bulkWriteVersioned).
|
|
268
|
+
*
|
|
269
|
+
* @param {*} op
|
|
270
|
+
* @param {*} versionKey
|
|
271
|
+
*/
|
|
272
|
+
private applyVersionToBulkOp;
|
|
246
273
|
/**
|
|
247
274
|
* Update document using Mongoose wrapper method
|
|
248
275
|
*
|
|
@@ -19,8 +19,12 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
19
19
|
}
|
|
20
20
|
return t;
|
|
21
21
|
};
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
22
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
26
|
exports.BaseModel = void 0;
|
|
27
|
+
const mongoose_1 = __importDefault(require("mongoose"));
|
|
24
28
|
const events_1 = require("events");
|
|
25
29
|
const utils_1 = require("../utils");
|
|
26
30
|
const constants_1 = require("../constants");
|
|
@@ -456,19 +460,35 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
456
460
|
item = yield this.beforeUpdate(item, context);
|
|
457
461
|
//const res = await this.mgModel.findByIdAndUpdate(id, { $set: item }, { new: true });
|
|
458
462
|
//const itemDoc = this.mgModel.build(Object.assign({ id, item }));
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
463
|
+
// read-modify-write with optimistic concurrency: on a VersionError (a concurrent
|
|
464
|
+
// writer bumped the version between get() and save()) re-read the fresh document,
|
|
465
|
+
// re-apply the update payload and retry a bounded number of times.
|
|
466
|
+
const maxAttempts = 3;
|
|
467
|
+
let res;
|
|
468
|
+
for (let attempt = 1;; attempt++) {
|
|
469
|
+
const itemDoc = yield this.get(id, context);
|
|
470
|
+
if (!itemDoc)
|
|
471
|
+
throw new errors_1.AppError('messages.notFound');
|
|
472
|
+
let toApply = item;
|
|
473
|
+
// complete merge with the freshly read document
|
|
474
|
+
if (mergeItem) {
|
|
475
|
+
// item = merge (itemDoc.toObject(), item); // Merge with existing doc
|
|
476
|
+
toApply = (0, utils_1.merge)(itemDoc.toObject(), item, (objValue, srcValue) => {
|
|
477
|
+
if (Array.isArray(objValue))
|
|
478
|
+
return srcValue; // sovrascrivi invece di mergiare
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
itemDoc.set(toApply);
|
|
482
|
+
try {
|
|
483
|
+
res = yield itemDoc.save();
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
if (err instanceof mongoose_1.default.Error.VersionError && attempt < maxAttempts)
|
|
488
|
+
continue;
|
|
489
|
+
throw err;
|
|
490
|
+
}
|
|
469
491
|
}
|
|
470
|
-
itemDoc === null || itemDoc === void 0 ? void 0 : itemDoc.set(item);
|
|
471
|
-
const res = yield itemDoc.save();
|
|
472
492
|
if (context)
|
|
473
493
|
context.evtItem = res;
|
|
474
494
|
this.publishCRUDEvent({ type: enums_1.ModelCrudEventType.update, data: { uid: id, model: this.name } }, context);
|
|
@@ -506,6 +526,60 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
506
526
|
updateManyNative(filter, update, options) {
|
|
507
527
|
return this.mgModel.collection.updateMany(filter, update, options);
|
|
508
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Execute bulkWrite injecting version handling on each operation, so native bulk
|
|
531
|
+
* paths keep the same versioning guarantees as the Mongoose query hooks:
|
|
532
|
+
* - insertOne -> set versionKey to 0 when absent (matches save() baseline)
|
|
533
|
+
* - updateOne/updateMany -> add $inc:{version:1} unless the update already touches version
|
|
534
|
+
*
|
|
535
|
+
* Upsert ops that do not touch version fall back to $inc (an upsert insert lands at
|
|
536
|
+
* version 1). Other op kinds (replaceOne, deleteOne, deleteMany) pass through unchanged.
|
|
537
|
+
*
|
|
538
|
+
* NB: the parameter is intentionally typed on `AnyBulkWriteOperation<any>` (not `<TDoc>`):
|
|
539
|
+
* `AnyBulkWriteOperation<TDoc>` is invariant (via `ReplaceOneModel<TDoc>.filter`), so a
|
|
540
|
+
* `<TDoc>`-typed signature makes each subclass method incompatible with
|
|
541
|
+
* `BaseModel<any, any, any>` and breaks `this`-assignability (e.g. passing `this` to a
|
|
542
|
+
* `BaseModel<any, any, any>` sink). Ops are validated/cast internally, so the loose
|
|
543
|
+
* element type costs nothing.
|
|
544
|
+
*
|
|
545
|
+
* @param {*} bulkOps
|
|
546
|
+
* @param {*} options
|
|
547
|
+
*/
|
|
548
|
+
bulkWriteVersioned(bulkOps, options) {
|
|
549
|
+
const versionKey = this.mgModel.schema.get('versionKey') || 'version';
|
|
550
|
+
const versioned = bulkOps.map(op => this.applyVersionToBulkOp(op, versionKey));
|
|
551
|
+
return this.mgModel.bulkWrite(versioned, options !== null && options !== void 0 ? options : {});
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Inject the version key into a single bulkWrite operation (see bulkWriteVersioned).
|
|
555
|
+
*
|
|
556
|
+
* @param {*} op
|
|
557
|
+
* @param {*} versionKey
|
|
558
|
+
*/
|
|
559
|
+
applyVersionToBulkOp(op, versionKey) {
|
|
560
|
+
var _a, _b;
|
|
561
|
+
const anyOp = op;
|
|
562
|
+
if ((_a = anyOp.insertOne) === null || _a === void 0 ? void 0 : _a.document) {
|
|
563
|
+
const doc = anyOp.insertOne.document;
|
|
564
|
+
if (doc[versionKey] === undefined)
|
|
565
|
+
doc[versionKey] = 0;
|
|
566
|
+
return op;
|
|
567
|
+
}
|
|
568
|
+
const writeOp = (_b = anyOp.updateOne) !== null && _b !== void 0 ? _b : anyOp.updateMany;
|
|
569
|
+
if (!writeOp)
|
|
570
|
+
return op;
|
|
571
|
+
const update = writeOp.update;
|
|
572
|
+
// update pipelines (arrays) are left untouched
|
|
573
|
+
if (!update || Array.isArray(update))
|
|
574
|
+
return op;
|
|
575
|
+
const nextUpdate = update;
|
|
576
|
+
const versionOperators = [nextUpdate, nextUpdate.$set, nextUpdate.$unset, nextUpdate.$inc];
|
|
577
|
+
const touchesVersion = versionOperators.some(operator => !!operator && Object.keys(operator).includes(versionKey));
|
|
578
|
+
if (touchesVersion)
|
|
579
|
+
return op;
|
|
580
|
+
nextUpdate.$inc = nextUpdate.$inc ? Object.assign(Object.assign({}, nextUpdate.$inc), { [versionKey]: 1 }) : { [versionKey]: 1 };
|
|
581
|
+
return op;
|
|
582
|
+
}
|
|
509
583
|
/**
|
|
510
584
|
* Update document using Mongoose wrapper method
|
|
511
585
|
*
|
|
@@ -54,14 +54,16 @@ export declare function loadModel(model: BaseModel<any, any, any>): void;
|
|
|
54
54
|
* @param { name, modelName, schema, collection, mergeSchema } param
|
|
55
55
|
* @param load
|
|
56
56
|
*/
|
|
57
|
-
export declare function createModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel extends BaseMongoModel<TAttrs, TDoc>, TNxModel>({ name, classDef, modelName, modelParams, collection, schema, mergeSchema }: ModelDef<TDoc, TMongoModel, TNxModel>, load: boolean, logger?: any): TNxModel;
|
|
57
|
+
export declare function createModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel extends BaseMongoModel<TAttrs, TDoc>, TNxModel>({ name, classDef, modelName, modelParams, collection, schema, mergeSchema, incrementOnSave }: ModelDef<TDoc, TMongoModel, TNxModel>, load: boolean, logger?: any): TNxModel;
|
|
58
58
|
/**
|
|
59
59
|
*
|
|
60
60
|
* @param {*} modelName
|
|
61
61
|
* @param {*} schema
|
|
62
62
|
* @param {*} collection
|
|
63
|
+
* @param {*} version enable optimistic concurrency + query version increment
|
|
64
|
+
* @param {*} incrementOnSave opt-in version bump on save() (Mixed in-place mutations)
|
|
63
65
|
*/
|
|
64
|
-
export declare function loadMongoModel<TAttrs extends BaseDBItem, TDoc extends BaseMongoDoc, TMongoModel extends BaseMongoModel<TAttrs, TDoc>>(modelName: string, schema: any, collection: string, version?: boolean): TMongoModel;
|
|
66
|
+
export declare function loadMongoModel<TAttrs extends BaseDBItem, TDoc extends BaseMongoDoc, TMongoModel extends BaseMongoModel<TAttrs, TDoc>>(modelName: string, schema: any, collection: string, version?: boolean, incrementOnSave?: boolean): TMongoModel;
|
|
65
67
|
/**
|
|
66
68
|
* transform string or string array to ObjectId or ObjectId array
|
|
67
69
|
*
|
|
@@ -58,7 +58,7 @@ function loadModel(model) {
|
|
|
58
58
|
* @param { name, modelName, schema, collection, mergeSchema } param
|
|
59
59
|
* @param load
|
|
60
60
|
*/
|
|
61
|
-
function createModel({ name, classDef, modelName, modelParams, collection, schema, mergeSchema }, load, logger = constants_1.APP.logger) {
|
|
61
|
+
function createModel({ name, classDef, modelName, modelParams, collection, schema, mergeSchema, incrementOnSave }, load, logger = constants_1.APP.logger) {
|
|
62
62
|
let entitySchema;
|
|
63
63
|
if (!name)
|
|
64
64
|
throw new Error('Missing param to create model');
|
|
@@ -68,7 +68,7 @@ function createModel({ name, classDef, modelName, modelParams, collection, schem
|
|
|
68
68
|
entitySchema = exports.codedEntitySchema;
|
|
69
69
|
else
|
|
70
70
|
entitySchema = mergeSchema ? mergeDBSchema(exports.codedEntitySchema, schema) : schema;
|
|
71
|
-
const mgModel = loadMongoModel(modelName, entitySchema, collection);
|
|
71
|
+
const mgModel = loadMongoModel(modelName, entitySchema, collection, true, !!incrementOnSave);
|
|
72
72
|
const model = classDef ? new classDef({ name, model: mgModel, params: modelParams, logger }) : new base_model_1.BaseModel({ name, model: mgModel, params: modelParams, logger });
|
|
73
73
|
if (load)
|
|
74
74
|
loadModel(model);
|
|
@@ -76,7 +76,7 @@ function createModel({ name, classDef, modelName, modelParams, collection, schem
|
|
|
76
76
|
//console.log(`created model ${modelName} (load: ${!!load})`);
|
|
77
77
|
return model;
|
|
78
78
|
}
|
|
79
|
-
function enableQueryVersioning(mgSchema, version) {
|
|
79
|
+
function enableQueryVersioning(mgSchema, version, incrementOnSave = false) {
|
|
80
80
|
if (!version)
|
|
81
81
|
return;
|
|
82
82
|
const versionKey = mgSchema.get('versionKey');
|
|
@@ -97,6 +97,24 @@ function enableQueryVersioning(mgSchema, version) {
|
|
|
97
97
|
mgSchema.pre('findOneAndUpdate', addVersionIncrement);
|
|
98
98
|
mgSchema.pre('updateOne', addVersionIncrement);
|
|
99
99
|
mgSchema.pre('updateMany', addVersionIncrement);
|
|
100
|
+
if (!incrementOnSave)
|
|
101
|
+
return;
|
|
102
|
+
// Opt-in (per-model) version bump on save().
|
|
103
|
+
//
|
|
104
|
+
// With optimisticConcurrency mongoose already increments the version when a
|
|
105
|
+
// tracked path is modified. This hook covers the case of Mixed fields mutated
|
|
106
|
+
// in-place (doc.data.x = y) WITHOUT markModified(): mongoose does not see them
|
|
107
|
+
// as modified, so no increment would happen and the published event would carry
|
|
108
|
+
// a stale version. Enable it only on event-source entities that expose Mixed
|
|
109
|
+
// fields (e.g. companyAsset). increment() shares the same internal version flag
|
|
110
|
+
// as optimisticConcurrency, so the net effect is at most +1 per save().
|
|
111
|
+
mgSchema.pre('save', function () {
|
|
112
|
+
if (this.isNew) // creation: versionKey default acts as v0
|
|
113
|
+
return;
|
|
114
|
+
if (this.isModified(versionKey)) // version explicitly set: respect it
|
|
115
|
+
return;
|
|
116
|
+
this.increment();
|
|
117
|
+
});
|
|
100
118
|
}
|
|
101
119
|
;
|
|
102
120
|
/**
|
|
@@ -104,8 +122,10 @@ function enableQueryVersioning(mgSchema, version) {
|
|
|
104
122
|
* @param {*} modelName
|
|
105
123
|
* @param {*} schema
|
|
106
124
|
* @param {*} collection
|
|
125
|
+
* @param {*} version enable optimistic concurrency + query version increment
|
|
126
|
+
* @param {*} incrementOnSave opt-in version bump on save() (Mixed in-place mutations)
|
|
107
127
|
*/
|
|
108
|
-
function loadMongoModel(modelName, schema, collection, version = true) {
|
|
128
|
+
function loadMongoModel(modelName, schema, collection, version = true, incrementOnSave = false) {
|
|
109
129
|
const mgSchema = new mongoose_1.default.Schema(schema, {
|
|
110
130
|
optimisticConcurrency: version,
|
|
111
131
|
versionKey: 'version',
|
|
@@ -120,7 +140,7 @@ function loadMongoModel(modelName, schema, collection, version = true) {
|
|
|
120
140
|
});
|
|
121
141
|
// change documents' version key from __v to version
|
|
122
142
|
mgSchema.set('versionKey', 'version');
|
|
123
|
-
enableQueryVersioning(mgSchema, version);
|
|
143
|
+
enableQueryVersioning(mgSchema, version, incrementOnSave);
|
|
124
144
|
mgSchema.statics.build = (attrs) => {
|
|
125
145
|
const item = { _id: attrs.id };
|
|
126
146
|
delete attrs.id;
|