@nextage/nx-frame-be 1.0.40 → 1.0.42

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
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const coded_entity_controller_1 = require("../../models/coded-entity.controller");
4
+ const loader_1 = require("../loader");
5
+ const registry_1 = require("../registry");
6
+ /**
7
+ * `SchemaRegistry` defers `generateSchema`/`loadSchema` until `apply`, so module
8
+ * LOADING is decoupled from schema MUTATION. These tests assert the deferral,
9
+ * the single-shot application and the reset semantics used for test isolation.
10
+ */
11
+ describe('SchemaRegistry', () => {
12
+ afterEach(() => registry_1.schemaRegistry.reset());
13
+ it('starts empty', () => {
14
+ expect(registry_1.schemaRegistry.count).toBe(0);
15
+ });
16
+ it('defers registerSchema until apply', () => {
17
+ const marker = 'registryProbeField: String';
18
+ registry_1.schemaRegistry.registerSchema({ typeDefs: { query: marker } });
19
+ // registration is recorded but NOT yet folded into mainSchema
20
+ expect(registry_1.schemaRegistry.count).toBe(1);
21
+ expect(loader_1.mainSchema.typeDefs.query).not.toContain('registryProbeField');
22
+ registry_1.schemaRegistry.apply();
23
+ expect(loader_1.mainSchema.typeDefs.query).toContain('registryProbeField');
24
+ });
25
+ it('defers register (controller) until apply', () => {
26
+ const params = {
27
+ fields: { type: 'id: String', query: 'code: String', input: 'code: String' }
28
+ };
29
+ registry_1.schemaRegistry.register(coded_entity_controller_1.ceCtrl, params);
30
+ // the CRUD query name is generated by generateSchema at apply time only
31
+ expect(registry_1.schemaRegistry.count).toBe(1);
32
+ expect(loader_1.mainSchema.typeDefs.query).not.toContain('codedEntityList');
33
+ registry_1.schemaRegistry.apply();
34
+ expect(loader_1.mainSchema.typeDefs.query).toContain('codedEntityList');
35
+ });
36
+ it('reset clears every registration', () => {
37
+ registry_1.schemaRegistry.registerSchema({ typeDefs: { query: 'a: String' } });
38
+ registry_1.schemaRegistry.registerSchema({ typeDefs: { query: 'b: String' } });
39
+ expect(registry_1.schemaRegistry.count).toBe(2);
40
+ registry_1.schemaRegistry.reset();
41
+ expect(registry_1.schemaRegistry.count).toBe(0);
42
+ });
43
+ it('exposes a class for isolated instances', () => {
44
+ const local = new registry_1.SchemaRegistry();
45
+ local.registerSchema({ typeDefs: { query: 'c: String' } });
46
+ expect(local.count).toBe(1);
47
+ expect(registry_1.schemaRegistry.count).toBe(0);
48
+ });
49
+ });
@@ -1,4 +1,5 @@
1
1
  export * from './directives';
2
2
  export * from './loader';
3
3
  export * from './generator';
4
+ export * from './registry';
4
5
  export * from './utils';
@@ -17,5 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./directives"), exports);
18
18
  __exportStar(require("./loader"), exports);
19
19
  __exportStar(require("./generator"), exports);
20
+ __exportStar(require("./registry"), exports);
20
21
  __exportStar(require("./utils"), exports);
21
22
  //export * from './scalars';
@@ -0,0 +1,47 @@
1
+ import { BaseController } from '../base/base.controller';
2
+ import { NxGraphQLSchemaData, NxGraphQLSchemaExtension } from './interfaces';
3
+ /**
4
+ * Deferred GraphQL schema-assembly registry.
5
+ *
6
+ * Historically every nx-app GraphQL leaf mutated the global `mainSchema` at
7
+ * import time by calling `generateSchema`/`loadSchema` as a module side-effect.
8
+ * That coupled module LOADING (which files get imported, and in what order) with
9
+ * schema MUTATION, and left the registered set invisible and non-resettable.
10
+ *
11
+ * With this registry a leaf instead REGISTERS its module — a controller plus a
12
+ * schema extension, or a raw typeDefs/resolvers block — deferring the actual
13
+ * `generateSchema`/`loadSchema` call. The bootstrap `graphql` phase discovers and
14
+ * imports the leaves (which call {@link register}/{@link registerSchema}) and then
15
+ * calls {@link apply} ONCE to fold every module into `mainSchema`.
16
+ *
17
+ * Benefits: loading is order-independent (filesystem discovery), mutation is a
18
+ * single explicit logged step, and the registered set is inspectable ({@link count})
19
+ * and resettable ({@link reset}) for test isolation.
20
+ */
21
+ export declare class SchemaRegistry {
22
+ private deferred;
23
+ /**
24
+ * Register a controller-backed module. Mirrors {@link generateSchema}'s
25
+ * signature verbatim; the call is deferred until {@link apply}.
26
+ */
27
+ register<T extends BaseController<any, any, any>>(controller: T, schema?: NxGraphQLSchemaExtension, mergeCodedEntity?: boolean, readOnly?: boolean): void;
28
+ /**
29
+ * Register a raw typeDefs/resolvers block (no controller). Mirrors
30
+ * {@link loadSchema}; the call is deferred until {@link apply}.
31
+ */
32
+ registerSchema(data: NxGraphQLSchemaData): void;
33
+ /**
34
+ * Fold every registered module into the global `mainSchema`. Call ONCE from
35
+ * the bootstrap `graphql` phase, after all leaves have been imported.
36
+ */
37
+ apply(): void;
38
+ /** Clear all registrations (test isolation). */
39
+ reset(): void;
40
+ /** Number of modules registered so far. */
41
+ get count(): number;
42
+ }
43
+ /**
44
+ * Process-wide GraphQL schema registry singleton. nx-app leaves import this and
45
+ * call `register`/`registerSchema`; the bootstrap `graphql` phase calls `apply`.
46
+ */
47
+ export declare const schemaRegistry: SchemaRegistry;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.schemaRegistry = exports.SchemaRegistry = void 0;
4
+ const constants_1 = require("../constants");
5
+ const generator_1 = require("./generator");
6
+ const loader_1 = require("./loader");
7
+ /**
8
+ * Deferred GraphQL schema-assembly registry.
9
+ *
10
+ * Historically every nx-app GraphQL leaf mutated the global `mainSchema` at
11
+ * import time by calling `generateSchema`/`loadSchema` as a module side-effect.
12
+ * That coupled module LOADING (which files get imported, and in what order) with
13
+ * schema MUTATION, and left the registered set invisible and non-resettable.
14
+ *
15
+ * With this registry a leaf instead REGISTERS its module — a controller plus a
16
+ * schema extension, or a raw typeDefs/resolvers block — deferring the actual
17
+ * `generateSchema`/`loadSchema` call. The bootstrap `graphql` phase discovers and
18
+ * imports the leaves (which call {@link register}/{@link registerSchema}) and then
19
+ * calls {@link apply} ONCE to fold every module into `mainSchema`.
20
+ *
21
+ * Benefits: loading is order-independent (filesystem discovery), mutation is a
22
+ * single explicit logged step, and the registered set is inspectable ({@link count})
23
+ * and resettable ({@link reset}) for test isolation.
24
+ */
25
+ class SchemaRegistry {
26
+ constructor() {
27
+ this.deferred = [];
28
+ }
29
+ /**
30
+ * Register a controller-backed module. Mirrors {@link generateSchema}'s
31
+ * signature verbatim; the call is deferred until {@link apply}.
32
+ */
33
+ register(controller, schema = {}, mergeCodedEntity = false, readOnly = false) {
34
+ this.deferred.push(() => (0, generator_1.generateSchema)(controller, schema, mergeCodedEntity, readOnly));
35
+ }
36
+ /**
37
+ * Register a raw typeDefs/resolvers block (no controller). Mirrors
38
+ * {@link loadSchema}; the call is deferred until {@link apply}.
39
+ */
40
+ registerSchema(data) {
41
+ this.deferred.push(() => (0, loader_1.loadSchema)(data));
42
+ }
43
+ /**
44
+ * Fold every registered module into the global `mainSchema`. Call ONCE from
45
+ * the bootstrap `graphql` phase, after all leaves have been imported.
46
+ */
47
+ apply() {
48
+ this.deferred.forEach(run => run());
49
+ constants_1.APP.logger.info(`🧩 GraphQL registry: applied ${this.deferred.length} schema module(s)`);
50
+ }
51
+ /** Clear all registrations (test isolation). */
52
+ reset() {
53
+ this.deferred = [];
54
+ }
55
+ /** Number of modules registered so far. */
56
+ get count() {
57
+ return this.deferred.length;
58
+ }
59
+ }
60
+ exports.SchemaRegistry = SchemaRegistry;
61
+ /**
62
+ * Process-wide GraphQL schema registry singleton. nx-app leaves import this and
63
+ * call `register`/`registerSchema`; the bootstrap `graphql` phase calls `apply`.
64
+ */
65
+ exports.schemaRegistry = new SchemaRegistry();
@@ -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[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextage/nx-frame-be",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",