@nextage/nx-frame-be 1.0.39 → 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.
@@ -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[];
@@ -91,6 +91,12 @@ export type AppJwt = {
91
91
  secret?: string;
92
92
  algorithm?: Algorithm;
93
93
  duration?: string;
94
+ refresh?: AppJwtRefresh;
95
+ };
96
+ export type AppJwtRefresh = {
97
+ secret?: string;
98
+ algorithm?: Algorithm;
99
+ duration?: string;
94
100
  };
95
101
  export type AppEmail = {
96
102
  source?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextage/nx-frame-be",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",