@nextage/nx-frame-be 1.0.40 → 1.0.41
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.controller.test.d.ts +1 -0
- package/build/common/base/__test__/base.controller.test.js +63 -0
- package/build/common/base/base.controller.d.ts +25 -0
- package/build/common/base/base.controller.js +58 -0
- package/build/common/base/base.model.js +4 -4
- package/build/common/interfaces.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const base_controller_1 = require("../base.controller");
|
|
13
|
+
const coded_entity_model_1 = require("../../models/coded-entity.model");
|
|
14
|
+
/**
|
|
15
|
+
* SEAM TEST — BaseController.withTransaction (3.4.1)
|
|
16
|
+
*
|
|
17
|
+
* The test harness runs a STANDALONE mongodb-memory-server, which does NOT support
|
|
18
|
+
* multi-document transactions. This locks the graceful-degradation contract of the
|
|
19
|
+
* session seam: detection returns false, the callback runs WITHOUT a session, an
|
|
20
|
+
* already-present session is reused (nested), and the session-aware model ops keep
|
|
21
|
+
* working unchanged when no session is supplied (parity with pre-3.4.1 behaviour).
|
|
22
|
+
*/
|
|
23
|
+
// keep publishCRUDEvent inert (CRUDEvents is enabled on ceModel)
|
|
24
|
+
jest.spyOn(coded_entity_model_1.CodedEntity.prototype, 'publishCRUDEvent').mockImplementation(() => { });
|
|
25
|
+
class CeController extends base_controller_1.BaseController {
|
|
26
|
+
}
|
|
27
|
+
const ctrl = new CeController({ model: coded_entity_model_1.ceModel });
|
|
28
|
+
describe('BaseController.withTransaction — session seam (standalone fallback)', () => {
|
|
29
|
+
it('reports no transaction support on a standalone deployment', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
+
const supported = yield ctrl.supportsTransactions();
|
|
31
|
+
expect(supported).toBe(false);
|
|
32
|
+
}));
|
|
33
|
+
it('runs the callback WITHOUT a session when transactions are unsupported', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
|
+
const seen = [];
|
|
35
|
+
const result = yield ctrl.withTransaction({ i18n: 'en' }, (ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
36
|
+
seen.push(ctx);
|
|
37
|
+
return 'done';
|
|
38
|
+
}));
|
|
39
|
+
expect(result).toBe('done');
|
|
40
|
+
expect(seen).toHaveLength(1);
|
|
41
|
+
expect(seen[0].session).toBeUndefined(); // fallback injects no session
|
|
42
|
+
expect(seen[0].i18n).toBe('en'); // original context preserved
|
|
43
|
+
}));
|
|
44
|
+
it('reuses an existing session (nested call) without opening a new one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
+
const outerSession = { id: 'outer' };
|
|
46
|
+
const seen = [];
|
|
47
|
+
yield ctrl.withTransaction({ session: outerSession }, (ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
48
|
+
seen.push(ctx);
|
|
49
|
+
}));
|
|
50
|
+
expect(seen[0].session).toBe(outerSession); // same session, not replaced
|
|
51
|
+
}));
|
|
52
|
+
it('create / update / remove still work when the context carries no session', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
53
|
+
const ctx = {};
|
|
54
|
+
const created = yield coded_entity_model_1.ceModel.create({ code: 'C1', name: 'n1', data: { items: [] } }, ctx);
|
|
55
|
+
expect(created.id).toBeDefined();
|
|
56
|
+
const updated = yield coded_entity_model_1.ceModel.update(created.id, { name: 'n2' }, false, ctx);
|
|
57
|
+
expect(updated.name).toBe('n2');
|
|
58
|
+
const removed = yield coded_entity_model_1.ceModel.remove(created.id, ctx);
|
|
59
|
+
expect(removed).toBeTruthy();
|
|
60
|
+
const after = yield coded_entity_model_1.ceModel.get(created.id, ctx);
|
|
61
|
+
expect(after).toBeNull();
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
@@ -6,6 +6,8 @@ import { FilterParams, QueryPagination } from './base.interfaces';
|
|
|
6
6
|
export declare abstract class BaseController<TAttrs, TDoc extends BaseMongoDoc, T extends BaseModel<TAttrs, TDoc, any>> {
|
|
7
7
|
model: T;
|
|
8
8
|
mergeUpdate: boolean;
|
|
9
|
+
/** Memoised transaction-capability of the deployment (see `supportsTransactions`). */
|
|
10
|
+
private static txnSupport?;
|
|
9
11
|
constructor({ model, mergeUpdate }: {
|
|
10
12
|
model: T;
|
|
11
13
|
mergeUpdate?: boolean;
|
|
@@ -118,4 +120,27 @@ export declare abstract class BaseController<TAttrs, TDoc extends BaseMongoDoc,
|
|
|
118
120
|
* @returns
|
|
119
121
|
*/
|
|
120
122
|
removeArrayItem(id: string, params: ArrayItemParams, options: mongoose.QueryOptions<Document> | undefined, context: NxContext): Promise<TDoc | null>;
|
|
123
|
+
/**
|
|
124
|
+
* Whether the current deployment supports multi-document transactions (replica set
|
|
125
|
+
* or sharded cluster). A standalone mongod (e.g. local dev, in-memory tests) does
|
|
126
|
+
* NOT. Detected once per process via the `hello` admin command and memoised, so
|
|
127
|
+
* `withTransaction` can degrade gracefully instead of throwing at runtime.
|
|
128
|
+
*/
|
|
129
|
+
protected supportsTransactions(): Promise<boolean>;
|
|
130
|
+
/**
|
|
131
|
+
* Run `fn` inside a Mongo transaction when the deployment supports it, injecting
|
|
132
|
+
* the session into a CHILD context so every session-aware model op (create /
|
|
133
|
+
* update / remove) joins the same atomic unit.
|
|
134
|
+
*
|
|
135
|
+
* Degradation & safety:
|
|
136
|
+
* - if `context.session` is already set, the call is NESTED → reuse it (no new txn);
|
|
137
|
+
* - if transactions are unsupported (standalone) → run `fn(context)` WITHOUT a
|
|
138
|
+
* session, i.e. exactly the current non-atomic behaviour (graceful fallback);
|
|
139
|
+
* - `session.withTransaction` may re-run `fn` on transient errors, so `fn` MUST
|
|
140
|
+
* keep non-DB side effects (e.g. publishing) OUTSIDE the transaction.
|
|
141
|
+
*
|
|
142
|
+
* @param context caller context (may already carry a session)
|
|
143
|
+
* @param fn work to run; receives the context to pass down to model ops
|
|
144
|
+
*/
|
|
145
|
+
withTransaction<R>(context: NxContext, fn: (ctx: NxContext) => Promise<R>): Promise<R>;
|
|
121
146
|
}
|
|
@@ -170,5 +170,63 @@ class BaseController {
|
|
|
170
170
|
removeArrayItem(id, params, options = { new: true }, context) {
|
|
171
171
|
return this.model.removeArrayItem(id, params, options, context);
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether the current deployment supports multi-document transactions (replica set
|
|
175
|
+
* or sharded cluster). A standalone mongod (e.g. local dev, in-memory tests) does
|
|
176
|
+
* NOT. Detected once per process via the `hello` admin command and memoised, so
|
|
177
|
+
* `withTransaction` can degrade gracefully instead of throwing at runtime.
|
|
178
|
+
*/
|
|
179
|
+
supportsTransactions() {
|
|
180
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
181
|
+
if (BaseController.txnSupport !== undefined)
|
|
182
|
+
return BaseController.txnSupport;
|
|
183
|
+
try {
|
|
184
|
+
const conn = this.model.mgModel.db;
|
|
185
|
+
const info = yield conn.db.admin().command({ hello: 1 });
|
|
186
|
+
// `setName` => replica set member; `msg: 'isdbgrid'` => mongos (sharded).
|
|
187
|
+
BaseController.txnSupport = !!info.setName || info.msg === 'isdbgrid';
|
|
188
|
+
}
|
|
189
|
+
catch (_a) {
|
|
190
|
+
BaseController.txnSupport = false;
|
|
191
|
+
}
|
|
192
|
+
return BaseController.txnSupport;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Run `fn` inside a Mongo transaction when the deployment supports it, injecting
|
|
197
|
+
* the session into a CHILD context so every session-aware model op (create /
|
|
198
|
+
* update / remove) joins the same atomic unit.
|
|
199
|
+
*
|
|
200
|
+
* Degradation & safety:
|
|
201
|
+
* - if `context.session` is already set, the call is NESTED → reuse it (no new txn);
|
|
202
|
+
* - if transactions are unsupported (standalone) → run `fn(context)` WITHOUT a
|
|
203
|
+
* session, i.e. exactly the current non-atomic behaviour (graceful fallback);
|
|
204
|
+
* - `session.withTransaction` may re-run `fn` on transient errors, so `fn` MUST
|
|
205
|
+
* keep non-DB side effects (e.g. publishing) OUTSIDE the transaction.
|
|
206
|
+
*
|
|
207
|
+
* @param context caller context (may already carry a session)
|
|
208
|
+
* @param fn work to run; receives the context to pass down to model ops
|
|
209
|
+
*/
|
|
210
|
+
withTransaction(context, fn) {
|
|
211
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
212
|
+
// already inside a transaction → reuse it (nested call, no new session)
|
|
213
|
+
if (context === null || context === void 0 ? void 0 : context.session)
|
|
214
|
+
return fn(context);
|
|
215
|
+
// standalone deployment → no atomicity available, run as today
|
|
216
|
+
if (!(yield this.supportsTransactions()))
|
|
217
|
+
return fn(context);
|
|
218
|
+
const session = yield this.model.mgModel.startSession();
|
|
219
|
+
try {
|
|
220
|
+
let result;
|
|
221
|
+
yield session.withTransaction(() => __awaiter(this, void 0, void 0, function* () {
|
|
222
|
+
result = yield fn(Object.assign(Object.assign({}, context), { session }));
|
|
223
|
+
}));
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
yield session.endSession();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
173
231
|
}
|
|
174
232
|
exports.BaseController = BaseController;
|
|
@@ -215,7 +215,7 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
215
215
|
return null;
|
|
216
216
|
const filter = { _id: (0, mongo_utils_1.toObjectId)(id) };
|
|
217
217
|
yield this.beforeGet(filter, context);
|
|
218
|
-
return this.mgModel.findOne(filter);
|
|
218
|
+
return this.mgModel.findOne(filter, null, { session: context === null || context === void 0 ? void 0 : context.session });
|
|
219
219
|
});
|
|
220
220
|
}
|
|
221
221
|
/**
|
|
@@ -403,7 +403,7 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
403
403
|
return __awaiter(this, void 0, void 0, function* () {
|
|
404
404
|
item = yield this.beforeCreate(item, context);
|
|
405
405
|
const itemDoc = this.mgModel.build(item);
|
|
406
|
-
const res = yield itemDoc.save(); //await this.mgModel.create(item);
|
|
406
|
+
const res = yield itemDoc.save({ session: context === null || context === void 0 ? void 0 : context.session }); //await this.mgModel.create(item);
|
|
407
407
|
if (context)
|
|
408
408
|
context.evtItem = res;
|
|
409
409
|
this.publishCRUDEvent({ type: enums_1.ModelCrudEventType.create, data: { uid: res.id, model: this.name } }, context);
|
|
@@ -480,7 +480,7 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
480
480
|
}
|
|
481
481
|
itemDoc.set(toApply);
|
|
482
482
|
try {
|
|
483
|
-
res = yield itemDoc.save();
|
|
483
|
+
res = yield itemDoc.save({ session: context === null || context === void 0 ? void 0 : context.session });
|
|
484
484
|
break;
|
|
485
485
|
}
|
|
486
486
|
catch (err) {
|
|
@@ -737,7 +737,7 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
737
737
|
if (!id)
|
|
738
738
|
throw new Error('missing id');
|
|
739
739
|
id = yield this.beforeRemove(id, context);
|
|
740
|
-
const res = yield this.mgModel.findByIdAndDelete(id);
|
|
740
|
+
const res = yield this.mgModel.findByIdAndDelete(id, { session: context === null || context === void 0 ? void 0 : context.session });
|
|
741
741
|
this.publishCRUDEvent({ type: enums_1.ModelCrudEventType.remove, data: { uid: id, model: this.name } }, context);
|
|
742
742
|
return this.afterRemove(res, context);
|
|
743
743
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Response } from 'express';
|
|
2
|
+
import type { ClientSession } from 'mongoose';
|
|
2
3
|
import { ModelCrudEventType, AuthIssuer } from './enums';
|
|
3
4
|
export interface NxContext {
|
|
4
5
|
access?: string;
|
|
@@ -10,6 +11,7 @@ export interface NxContext {
|
|
|
10
11
|
remoteAddress?: string;
|
|
11
12
|
res?: Response;
|
|
12
13
|
i18n?: string;
|
|
14
|
+
session?: ClientSession;
|
|
13
15
|
}
|
|
14
16
|
export interface NxJwtData extends NxObject {
|
|
15
17
|
rls?: string[];
|