@abejarano/ts-mongodb-criteria 1.8.6 → 1.10.0

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 CHANGED
@@ -201,6 +201,43 @@ export interface IUserRepository extends IRepository<User> {
201
201
  The goal of `IRepository` is to prevent signature drift (e.g. `upsert` returning
202
202
  `void` in your app while the base repository returns `ObjectId | null`).
203
203
 
204
+ ## Atomic Transactions
205
+
206
+ `MongoTransaction.run` groups writes from one or more repositories into a
207
+ single MongoDB transaction. Pass the `tx` context to every write that must be
208
+ committed or rolled back together:
209
+
210
+ ```typescript
211
+ import { MongoTransaction } from "@abejarano/ts-mongodb-criteria"
212
+
213
+ await MongoTransaction.run(async (tx) => {
214
+ await userRepository.upsert(user, tx)
215
+ await auditRepository.upsert(auditEntry, tx)
216
+ await sessionRepository.delete({ userId: user.getId() }, undefined, tx)
217
+
218
+ // Reads can observe writes made earlier in this transaction.
219
+ const persistedUser = await userRepository.one({ id: user.getId() }, tx)
220
+ })
221
+ ```
222
+
223
+ If any operation in the callback fails, MongoDB rolls back all the writes and
224
+ the error is propagated to the caller. The MongoDB driver handles retryable
225
+ transaction errors through `withTransaction`.
226
+
227
+ Custom repositories can include their protected atomic updates in the same
228
+ transaction by accepting and forwarding `MongoTransaction`:
229
+
230
+ ```typescript
231
+ async deactivateUser(id: string, tx: MongoTransaction): Promise<void> {
232
+ await this.updateOne({ id }, { $set: { active: false } }, tx)
233
+ }
234
+ ```
235
+
236
+ MongoDB transactions require a replica set or a sharded cluster; standalone
237
+ MongoDB instances do not support them. This initial API applies the transaction
238
+ context to `upsert`, `delete`, protected `updateOne`, `one`, and `list`.
239
+ Pass `tx` as the third argument to `list` after `fieldsToExclude`.
240
+
204
241
  **Your First Query in 30 Seconds:**
205
242
 
206
243
  ```typescript
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MongoRepository = void 0;
4
4
  const MongoCriteriaConverter_1 = require("./MongoCriteriaConverter");
5
5
  const MongoClientFactory_1 = require("./MongoClientFactory");
6
+ const MongoTransaction_1 = require("./MongoTransaction");
6
7
  const mongodb_1 = require("mongodb");
7
8
  class MongoRepository {
8
9
  constructor(aggregateRootClass) {
@@ -10,9 +11,12 @@ class MongoRepository {
10
11
  this.criteriaConverter = new MongoCriteriaConverter_1.MongoCriteriaConverter();
11
12
  }
12
13
  /** Finds a single entity and hydrates it via the aggregate's fromPrimitives. */
13
- async one(filter) {
14
+ async one(filter, transaction) {
14
15
  const collection = await this.collection();
15
- const result = await collection.findOne(filter);
16
+ const session = MongoTransaction_1.MongoTransaction.sessionFor(transaction);
17
+ const result = session
18
+ ? await collection.findOne(filter, { session })
19
+ : await collection.findOne(filter);
16
20
  if (!result) {
17
21
  return null;
18
22
  }
@@ -22,13 +26,13 @@ class MongoRepository {
22
26
  });
23
27
  }
24
28
  /** Upserts an aggregate by delegating to persist with its id. */
25
- async upsert(entity) {
26
- await this.persist(entity.getId(), entity);
29
+ async upsert(entity, transaction) {
30
+ await this.persist(entity.getId(), entity, transaction);
27
31
  }
28
32
  /** Lists entities by criteria and returns a paginated response. */
29
- async list(criteria, fieldsToExclude = []) {
30
- const documents = await this.searchByCriteria(criteria, fieldsToExclude);
31
- return this.paginate(documents);
33
+ async list(criteria, fieldsToExclude = [], transaction) {
34
+ const documents = await this.searchByCriteria(criteria, fieldsToExclude, transaction);
35
+ return this.paginate(documents, transaction);
32
36
  }
33
37
  /**
34
38
  * Deletes documents from the database based on a filter and optional options.
@@ -37,9 +41,10 @@ class MongoRepository {
37
41
  * @param {DeleteOptions} [options] - Optional parameters that modify the behavior of the delete operation.
38
42
  * @return {Promise<void>} A promise that resolves when the deletion is complete, or rejects if an error occurs.
39
43
  */
40
- async delete(filter, options) {
44
+ async delete(filter, options, transaction) {
41
45
  const collection = await this.collection();
42
- await collection.deleteMany(filter, options);
46
+ const session = MongoTransaction_1.MongoTransaction.sessionFor(transaction);
47
+ await collection.deleteMany(filter, session ? { ...options, session } : options);
43
48
  }
44
49
  async ensureIndexesOnce() {
45
50
  const key = this.collectionName();
@@ -53,18 +58,17 @@ class MongoRepository {
53
58
  await this.ensureIndexesOnce();
54
59
  return this.collectionRaw();
55
60
  }
56
- async updateOne(filter, update) {
61
+ async updateOne(filter, update, transaction) {
57
62
  const collection = await this.collection();
58
- await collection.updateOne(filter, update, {
59
- upsert: true,
60
- });
63
+ const session = MongoTransaction_1.MongoTransaction.sessionFor(transaction);
64
+ await collection.updateOne(filter, update, session ? { upsert: true, session } : { upsert: true });
61
65
  }
62
66
  async collectionRaw() {
63
67
  return (await MongoClientFactory_1.MongoClientFactory.createClient())
64
68
  .db()
65
69
  .collection(this.collectionName());
66
70
  }
67
- async persist(id, aggregateRoot) {
71
+ async persist(id, aggregateRoot, transaction) {
68
72
  let primitives;
69
73
  if (aggregateRoot.toPrimitives() instanceof Promise) {
70
74
  primitives = await aggregateRoot.toPrimitives();
@@ -77,15 +81,16 @@ class MongoRepository {
77
81
  ...primitives,
78
82
  id: id,
79
83
  },
80
- });
84
+ }, transaction);
81
85
  }
82
- async searchByCriteria(criteria, fieldsToExclude = []) {
86
+ async searchByCriteria(criteria, fieldsToExclude = [], transaction) {
83
87
  this.criteria = criteria;
84
88
  this.query = this.criteriaConverter.convert(criteria);
85
89
  const collection = await this.collection();
90
+ const session = MongoTransaction_1.MongoTransaction.sessionFor(transaction);
86
91
  if (fieldsToExclude.length === 0) {
87
92
  const results = await collection
88
- .find(this.query.filter, {})
93
+ .find(this.query.filter, session ? { session } : {})
89
94
  .sort(this.query.sort)
90
95
  .skip(this.query.skip)
91
96
  .limit(this.query.limit)
@@ -97,16 +102,19 @@ class MongoRepository {
97
102
  projection[field] = 0;
98
103
  });
99
104
  const results = await collection
100
- .find(this.query.filter, { projection })
105
+ .find(this.query.filter, session ? { projection, session } : { projection })
101
106
  .sort(this.query.sort)
102
107
  .skip(this.query.skip)
103
108
  .limit(this.query.limit)
104
109
  .toArray();
105
110
  return results.map(({ _id, ...rest }) => rest);
106
111
  }
107
- async paginate(documents) {
112
+ async paginate(documents, transaction) {
108
113
  const collection = await this.collection();
109
- const count = await collection.countDocuments(this.query.filter);
114
+ const session = MongoTransaction_1.MongoTransaction.sessionFor(transaction);
115
+ const count = session
116
+ ? await collection.countDocuments(this.query.filter, { session })
117
+ : await collection.countDocuments(this.query.filter);
110
118
  const limit = this.criteria?.limit || 10;
111
119
  const currentPage = this.criteria?.currentPage || 1;
112
120
  const hasNextPage = currentPage * limit < count;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MongoTransaction = void 0;
4
+ const MongoClientFactory_1 = require("./MongoClientFactory");
5
+ const sessions = new WeakMap();
6
+ /**
7
+ * Context passed to repository writes that belong to the same MongoDB transaction.
8
+ */
9
+ class MongoTransaction {
10
+ constructor(session) {
11
+ sessions.set(this, session);
12
+ }
13
+ /**
14
+ * Runs the callback in a MongoDB transaction and releases its session afterwards.
15
+ */
16
+ static async run(callback) {
17
+ const client = await MongoClientFactory_1.MongoClientFactory.createClient();
18
+ const session = client.startSession();
19
+ const transaction = new MongoTransaction(session);
20
+ try {
21
+ return await session.withTransaction(() => callback(transaction));
22
+ }
23
+ finally {
24
+ await session.endSession();
25
+ }
26
+ }
27
+ /** @internal Retrieves the driver session used by repository implementations. */
28
+ static sessionFor(transaction) {
29
+ return transaction ? sessions.get(transaction) : undefined;
30
+ }
31
+ }
32
+ exports.MongoTransaction = MongoTransaction;
@@ -18,3 +18,4 @@ __exportStar(require("./MongoCriteriaConverter"), exports);
18
18
  __exportStar(require("./MongoRepository"), exports);
19
19
  __exportStar(require("./IRepository"), exports);
20
20
  __exportStar(require("./MongoClientFactory"), exports);
21
+ __exportStar(require("./MongoTransaction"), exports);
@@ -1,5 +1,6 @@
1
1
  import { MongoCriteriaConverter } from "./MongoCriteriaConverter";
2
2
  import { MongoClientFactory } from "./MongoClientFactory";
3
+ import { MongoTransaction } from "./MongoTransaction";
3
4
  import { ObjectId, } from "mongodb";
4
5
  export class MongoRepository {
5
6
  constructor(aggregateRootClass) {
@@ -7,9 +8,12 @@ export class MongoRepository {
7
8
  this.criteriaConverter = new MongoCriteriaConverter();
8
9
  }
9
10
  /** Finds a single entity and hydrates it via the aggregate's fromPrimitives. */
10
- async one(filter) {
11
+ async one(filter, transaction) {
11
12
  const collection = await this.collection();
12
- const result = await collection.findOne(filter);
13
+ const session = MongoTransaction.sessionFor(transaction);
14
+ const result = session
15
+ ? await collection.findOne(filter, { session })
16
+ : await collection.findOne(filter);
13
17
  if (!result) {
14
18
  return null;
15
19
  }
@@ -19,13 +23,13 @@ export class MongoRepository {
19
23
  });
20
24
  }
21
25
  /** Upserts an aggregate by delegating to persist with its id. */
22
- async upsert(entity) {
23
- await this.persist(entity.getId(), entity);
26
+ async upsert(entity, transaction) {
27
+ await this.persist(entity.getId(), entity, transaction);
24
28
  }
25
29
  /** Lists entities by criteria and returns a paginated response. */
26
- async list(criteria, fieldsToExclude = []) {
27
- const documents = await this.searchByCriteria(criteria, fieldsToExclude);
28
- return this.paginate(documents);
30
+ async list(criteria, fieldsToExclude = [], transaction) {
31
+ const documents = await this.searchByCriteria(criteria, fieldsToExclude, transaction);
32
+ return this.paginate(documents, transaction);
29
33
  }
30
34
  /**
31
35
  * Deletes documents from the database based on a filter and optional options.
@@ -34,9 +38,10 @@ export class MongoRepository {
34
38
  * @param {DeleteOptions} [options] - Optional parameters that modify the behavior of the delete operation.
35
39
  * @return {Promise<void>} A promise that resolves when the deletion is complete, or rejects if an error occurs.
36
40
  */
37
- async delete(filter, options) {
41
+ async delete(filter, options, transaction) {
38
42
  const collection = await this.collection();
39
- await collection.deleteMany(filter, options);
43
+ const session = MongoTransaction.sessionFor(transaction);
44
+ await collection.deleteMany(filter, session ? { ...options, session } : options);
40
45
  }
41
46
  async ensureIndexesOnce() {
42
47
  const key = this.collectionName();
@@ -50,18 +55,17 @@ export class MongoRepository {
50
55
  await this.ensureIndexesOnce();
51
56
  return this.collectionRaw();
52
57
  }
53
- async updateOne(filter, update) {
58
+ async updateOne(filter, update, transaction) {
54
59
  const collection = await this.collection();
55
- await collection.updateOne(filter, update, {
56
- upsert: true,
57
- });
60
+ const session = MongoTransaction.sessionFor(transaction);
61
+ await collection.updateOne(filter, update, session ? { upsert: true, session } : { upsert: true });
58
62
  }
59
63
  async collectionRaw() {
60
64
  return (await MongoClientFactory.createClient())
61
65
  .db()
62
66
  .collection(this.collectionName());
63
67
  }
64
- async persist(id, aggregateRoot) {
68
+ async persist(id, aggregateRoot, transaction) {
65
69
  let primitives;
66
70
  if (aggregateRoot.toPrimitives() instanceof Promise) {
67
71
  primitives = await aggregateRoot.toPrimitives();
@@ -74,15 +78,16 @@ export class MongoRepository {
74
78
  ...primitives,
75
79
  id: id,
76
80
  },
77
- });
81
+ }, transaction);
78
82
  }
79
- async searchByCriteria(criteria, fieldsToExclude = []) {
83
+ async searchByCriteria(criteria, fieldsToExclude = [], transaction) {
80
84
  this.criteria = criteria;
81
85
  this.query = this.criteriaConverter.convert(criteria);
82
86
  const collection = await this.collection();
87
+ const session = MongoTransaction.sessionFor(transaction);
83
88
  if (fieldsToExclude.length === 0) {
84
89
  const results = await collection
85
- .find(this.query.filter, {})
90
+ .find(this.query.filter, session ? { session } : {})
86
91
  .sort(this.query.sort)
87
92
  .skip(this.query.skip)
88
93
  .limit(this.query.limit)
@@ -94,16 +99,19 @@ export class MongoRepository {
94
99
  projection[field] = 0;
95
100
  });
96
101
  const results = await collection
97
- .find(this.query.filter, { projection })
102
+ .find(this.query.filter, session ? { projection, session } : { projection })
98
103
  .sort(this.query.sort)
99
104
  .skip(this.query.skip)
100
105
  .limit(this.query.limit)
101
106
  .toArray();
102
107
  return results.map(({ _id, ...rest }) => rest);
103
108
  }
104
- async paginate(documents) {
109
+ async paginate(documents, transaction) {
105
110
  const collection = await this.collection();
106
- const count = await collection.countDocuments(this.query.filter);
111
+ const session = MongoTransaction.sessionFor(transaction);
112
+ const count = session
113
+ ? await collection.countDocuments(this.query.filter, { session })
114
+ : await collection.countDocuments(this.query.filter);
107
115
  const limit = this.criteria?.limit || 10;
108
116
  const currentPage = this.criteria?.currentPage || 1;
109
117
  const hasNextPage = currentPage * limit < count;
@@ -0,0 +1,28 @@
1
+ import { MongoClientFactory } from "./MongoClientFactory";
2
+ const sessions = new WeakMap();
3
+ /**
4
+ * Context passed to repository writes that belong to the same MongoDB transaction.
5
+ */
6
+ export class MongoTransaction {
7
+ constructor(session) {
8
+ sessions.set(this, session);
9
+ }
10
+ /**
11
+ * Runs the callback in a MongoDB transaction and releases its session afterwards.
12
+ */
13
+ static async run(callback) {
14
+ const client = await MongoClientFactory.createClient();
15
+ const session = client.startSession();
16
+ const transaction = new MongoTransaction(session);
17
+ try {
18
+ return await session.withTransaction(() => callback(transaction));
19
+ }
20
+ finally {
21
+ await session.endSession();
22
+ }
23
+ }
24
+ /** @internal Retrieves the driver session used by repository implementations. */
25
+ static sessionFor(transaction) {
26
+ return transaction ? sessions.get(transaction) : undefined;
27
+ }
28
+ }
@@ -2,3 +2,4 @@ export * from "./MongoCriteriaConverter";
2
2
  export * from "./MongoRepository";
3
3
  export * from "./IRepository";
4
4
  export * from "./MongoClientFactory";
5
+ export * from "./MongoTransaction";
@@ -1,9 +1,10 @@
1
1
  import { Criteria, Paginate } from "../criteria";
2
2
  import { AggregateRoot } from "../AggregateRoot";
3
3
  import { DeleteOptions } from "mongodb";
4
+ import { MongoTransaction } from "./MongoTransaction";
4
5
  export interface IRepository<T extends AggregateRoot> {
5
- one(filter: object): Promise<T | null>;
6
- list<D>(criteria: Criteria, fieldsToExclude?: string[]): Promise<Paginate<D>>;
7
- upsert(entity: T): Promise<void>;
8
- delete(filter: object, options?: DeleteOptions): Promise<void>;
6
+ one(filter: object, transaction?: MongoTransaction): Promise<T | null>;
7
+ list<D>(criteria: Criteria, fieldsToExclude?: string[], transaction?: MongoTransaction): Promise<Paginate<D>>;
8
+ upsert(entity: T, transaction?: MongoTransaction): Promise<void>;
9
+ delete(filter: object, options?: DeleteOptions, transaction?: MongoTransaction): Promise<void>;
9
10
  }
@@ -1,3 +1,4 @@
1
+ import { MongoTransaction } from "./MongoTransaction";
1
2
  import { Criteria, Paginate } from "../criteria";
2
3
  import { AggregateRoot, AggregateRootClass } from "../AggregateRoot";
3
4
  import { Collection, DeleteOptions, Document, UpdateFilter } from "mongodb";
@@ -10,11 +11,11 @@ export declare abstract class MongoRepository<T extends AggregateRoot> {
10
11
  protected constructor(aggregateRootClass: AggregateRootClass<T>);
11
12
  abstract collectionName(): string;
12
13
  /** Finds a single entity and hydrates it via the aggregate's fromPrimitives. */
13
- one(filter: object): Promise<T | null>;
14
+ one(filter: object, transaction?: MongoTransaction): Promise<T | null>;
14
15
  /** Upserts an aggregate by delegating to persist with its id. */
15
- upsert(entity: T): Promise<void>;
16
+ upsert(entity: T, transaction?: MongoTransaction): Promise<void>;
16
17
  /** Lists entities by criteria and returns a paginated response. */
17
- list<D>(criteria: Criteria, fieldsToExclude?: string[]): Promise<Paginate<D>>;
18
+ list<D>(criteria: Criteria, fieldsToExclude?: string[], transaction?: MongoTransaction): Promise<Paginate<D>>;
18
19
  /**
19
20
  * Deletes documents from the database based on a filter and optional options.
20
21
  *
@@ -22,11 +23,11 @@ export declare abstract class MongoRepository<T extends AggregateRoot> {
22
23
  * @param {DeleteOptions} [options] - Optional parameters that modify the behavior of the delete operation.
23
24
  * @return {Promise<void>} A promise that resolves when the deletion is complete, or rejects if an error occurs.
24
25
  */
25
- delete(filter: object, options?: DeleteOptions): Promise<void>;
26
+ delete(filter: object, options?: DeleteOptions, transaction?: MongoTransaction): Promise<void>;
26
27
  protected abstract ensureIndexes(collection: Collection): Promise<void>;
27
28
  protected ensureIndexesOnce(): Promise<void>;
28
29
  protected collection<U extends Document>(): Promise<Collection<U>>;
29
- protected updateOne(filter: object, update: Document[] | UpdateFilter<any>): Promise<void>;
30
+ protected updateOne(filter: object, update: Document[] | UpdateFilter<any>, transaction?: MongoTransaction): Promise<void>;
30
31
  private collectionRaw;
31
32
  private persist;
32
33
  private searchByCriteria;
@@ -0,0 +1,13 @@
1
+ import { ClientSession } from "mongodb";
2
+ /**
3
+ * Context passed to repository writes that belong to the same MongoDB transaction.
4
+ */
5
+ export declare class MongoTransaction {
6
+ private constructor();
7
+ /**
8
+ * Runs the callback in a MongoDB transaction and releases its session afterwards.
9
+ */
10
+ static run<T>(callback: (transaction: MongoTransaction) => Promise<T>): Promise<T>;
11
+ /** @internal Retrieves the driver session used by repository implementations. */
12
+ static sessionFor(transaction?: MongoTransaction): ClientSession | undefined;
13
+ }
@@ -2,3 +2,4 @@ export * from "./MongoCriteriaConverter";
2
2
  export * from "./MongoRepository";
3
3
  export * from "./IRepository";
4
4
  export * from "./MongoClientFactory";
5
+ export * from "./MongoTransaction";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@abejarano/ts-mongodb-criteria",
3
3
  "author": "angel bejarano / angel.bejarano@jaspesoft.com",
4
- "version": "1.8.6",
4
+ "version": "1.10.0",
5
5
  "description": "Patrón Criteria para consultas MongoDB en TypeScript",
6
6
  "main": "dist/cjs/index.js",
7
7
  "module": "dist/esm/index.js",
@@ -65,7 +65,7 @@
65
65
  "mongodb": "^7.0.0",
66
66
  "prettier": "^3.8.3",
67
67
  "rimraf": "^6.1.2",
68
- "semantic-release": "^24.2.9",
68
+ "semantic-release": "^25.0.6",
69
69
  "ts-jest": "^29.1.2",
70
70
  "typescript": "^5.9.3"
71
71
  },
@@ -81,7 +81,7 @@
81
81
  "url": "git+https://github.com/abejarano/ts-mongo-criteria.git"
82
82
  },
83
83
  "publishConfig": {
84
- "registry": "https://registry.npmjs.org",
84
+ "registry": "https://registry.npmjs.org/",
85
85
  "access": "public"
86
86
  }
87
87
  }