@abejarano/ts-mongodb-criteria 1.11.0 → 1.11.2

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
@@ -6,7 +6,7 @@
6
6
  [![MongoDB](https://img.shields.io/badge/MongoDB-6.0+-green.svg)](https://www.mongodb.com/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
  [![Node.js](https://img.shields.io/badge/Node.js-20+-green.svg)](https://nodejs.org/)
9
- [![Tests](https://img.shields.io/badge/Tests-30%2F30%20passing-brightgreen.svg)](#testing)
9
+ [![Tests](https://img.shields.io/badge/Tests-46%2F46%20passing-brightgreen.svg)](#testing)
10
10
 
11
11
  > A robust, type-safe implementation of the **Criteria Pattern** for MongoDB queries in TypeScript. Build complex database queries dynamically with a fluent, composable API designed following Domain-Driven Design (DDD) and Clean Architecture principles.
12
12
 
@@ -182,7 +182,7 @@ Use the `collection` argument inside `ensureIndexes` to avoid recursion.
182
182
  MongoRepository provides ready-to-use public methods for repositories that extend it:
183
183
 
184
184
  - `list(criteria, fieldsToExclude?)` for paginated queries
185
- - `many(filter, transaction?)` to fetch multiple entities matching a filter
185
+ - `many(filter, options?)` to fetch multiple entities matching a filter, with optional `{ transaction?, fieldsToExclude?, sort? }`
186
186
  - `one(filter, transaction?)` to fetch a single entity
187
187
  - `upsert(entity, transaction?)` to persist an aggregate
188
188
  Internal helpers are private, so repositories should call these public methods
@@ -199,8 +199,8 @@ export interface IUserRepository extends IRepository<User> {
199
199
  }
200
200
  ```
201
201
 
202
- The goal of `IRepository` is to prevent signature drift (e.g. `upsert` returning
203
- `void` in your app while the base repository returns `ObjectId | null`).
202
+ The goal of `IRepository` is to prevent signature drift between your app's
203
+ interfaces and the library's repository return types.
204
204
 
205
205
  ## Atomic Transactions
206
206
 
@@ -218,7 +218,10 @@ await MongoTransaction.run(async (tx) => {
218
218
 
219
219
  // Reads can observe writes made earlier in this transaction.
220
220
  const persistedUser = await userRepository.one({ id: user.getId() }, tx)
221
- const activeUsers = await userRepository.many({ status: "active" }, tx)
221
+ const activeUsers = await userRepository.many(
222
+ { status: "active" },
223
+ { transaction: tx, sort: Order.asc("name") }
224
+ )
222
225
  })
223
226
  ```
224
227
 
@@ -237,8 +240,8 @@ async deactivateUser(id: string, tx: MongoTransaction): Promise<void> {
237
240
 
238
241
  MongoDB transactions require a replica set or a sharded cluster; standalone
239
242
  MongoDB instances do not support them. This initial API applies the transaction
240
- context to `upsert`, `delete`, protected `updateOne`, `one`, and `list`.
241
- Pass `tx` as the third argument to `list` after `fieldsToExclude`, and as the second argument to `many`.
243
+ context to `upsert`, `delete`, protected `updateOne`, `one`, `list`, and `many`.
244
+ Pass `tx` as the third argument to `list` after `fieldsToExclude`, and inside the options object (`{ transaction: tx }`) to `many`.
242
245
 
243
246
  **Your First Query in 30 Seconds:**
244
247
 
@@ -385,7 +388,7 @@ const criteria = new Criteria(Filters.fromValues(filters), Order.none())
385
388
 
386
389
  ## 🧪 Testing
387
390
 
388
- The library includes comprehensive test coverage (30/30 tests passing).
391
+ The library includes comprehensive test coverage (46/46 tests passing).
389
392
 
390
393
  ```bash
391
394
  # Run all tests
@@ -2,8 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AggregateRoot = void 0;
4
4
  class AggregateRoot {
5
- static fromPrimitives(_data) {
6
- throw new Error("fromPrimitives must be implemented in subclasses");
5
+ constructor(id) {
6
+ this.aggregateId = id;
7
+ }
8
+ getId() {
9
+ return this.aggregateId;
10
+ }
11
+ assignId(mongoId) {
12
+ if (this.aggregateId !== undefined) {
13
+ throw new Error("AGGREGATE_ID_ALREADY_ASSIGNED");
14
+ }
15
+ this.aggregateId = mongoId;
7
16
  }
8
17
  }
9
18
  exports.AggregateRoot = AggregateRoot;
@@ -25,11 +25,43 @@ class MongoRepository {
25
25
  id: result._id.toString(),
26
26
  });
27
27
  }
28
- async many(filter, transaction) {
28
+ /**
29
+ *
30
+ * @param filter
31
+ * @param options
32
+ */
33
+ async many(filter, options) {
29
34
  const collection = await this.collection();
30
- const session = MongoTransaction_1.MongoTransaction.sessionFor(transaction);
35
+ const session = options?.transaction
36
+ ? MongoTransaction_1.MongoTransaction.sessionFor(options?.transaction)
37
+ : undefined;
38
+ const hasFields = options?.fieldsToExclude && options.fieldsToExclude.length > 0;
39
+ const projection = hasFields
40
+ ? {}
41
+ : undefined;
42
+ if (hasFields) {
43
+ options.fieldsToExclude.forEach((field) => {
44
+ projection[field] = 0;
45
+ });
46
+ }
47
+ const findOptions = session
48
+ ? { ...(projection ? { projection } : {}), session }
49
+ : projection
50
+ ? { projection }
51
+ : undefined;
52
+ let order = { _id: -1 };
53
+ if (options?.sort?.hasOrder()) {
54
+ order = {
55
+ [options?.sort.orderBy.value === "id"
56
+ ? "_id"
57
+ : options?.sort.orderBy.value]: options.sort.orderType.isAsc()
58
+ ? 1
59
+ : -1,
60
+ };
61
+ }
31
62
  const documents = await collection
32
- .find(filter, session === undefined ? undefined : { session })
63
+ .find(filter, findOptions)
64
+ .sort(order)
33
65
  .toArray();
34
66
  return documents.map((document) => this.aggregateRootClass.fromPrimitives({
35
67
  ...document,
@@ -38,7 +70,18 @@ class MongoRepository {
38
70
  }
39
71
  /** Upserts an aggregate by delegating to persist with its id. */
40
72
  async upsert(entity, transaction) {
41
- await this.persist(entity.getId(), entity, transaction);
73
+ const primitiveResult = entity.toPrimitives();
74
+ const primitives = await Promise.resolve(primitiveResult);
75
+ const currentId = entity.getId();
76
+ const mongoId = currentId === undefined ? new mongodb_1.ObjectId() : new mongodb_1.ObjectId(currentId);
77
+ if (currentId === undefined) {
78
+ entity.assignId(mongoId.toString());
79
+ }
80
+ await this.updateOne({ _id: mongoId }, {
81
+ $set: {
82
+ ...primitives,
83
+ },
84
+ }, transaction);
42
85
  }
43
86
  /** Lists entities by criteria and returns a paginated response. */
44
87
  async list(criteria, fieldsToExclude = [], transaction) {
@@ -50,6 +93,7 @@ class MongoRepository {
50
93
  *
51
94
  * @param {object} filter - The query to match documents that should be deleted.
52
95
  * @param {DeleteOptions} [options] - Optional parameters that modify the behavior of the delete operation.
96
+ * @param transaction
53
97
  * @return {Promise<void>} A promise that resolves when the deletion is complete, or rejects if an error occurs.
54
98
  */
55
99
  async delete(filter, options, transaction) {
@@ -79,21 +123,6 @@ class MongoRepository {
79
123
  .db()
80
124
  .collection(this.collectionName());
81
125
  }
82
- async persist(id, aggregateRoot, transaction) {
83
- let primitives;
84
- if (aggregateRoot.toPrimitives() instanceof Promise) {
85
- primitives = await aggregateRoot.toPrimitives();
86
- }
87
- else {
88
- primitives = aggregateRoot.toPrimitives();
89
- }
90
- await this.updateOne({ _id: new mongodb_1.ObjectId(id) }, {
91
- $set: {
92
- ...primitives,
93
- id: id,
94
- },
95
- }, transaction);
96
- }
97
126
  async searchByCriteria(criteria, fieldsToExclude = [], transaction) {
98
127
  this.criteria = criteria;
99
128
  this.query = this.criteriaConverter.convert(criteria);
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,5 +1,14 @@
1
1
  export class AggregateRoot {
2
- static fromPrimitives(_data) {
3
- throw new Error("fromPrimitives must be implemented in subclasses");
2
+ constructor(id) {
3
+ this.aggregateId = id;
4
+ }
5
+ getId() {
6
+ return this.aggregateId;
7
+ }
8
+ assignId(mongoId) {
9
+ if (this.aggregateId !== undefined) {
10
+ throw new Error("AGGREGATE_ID_ALREADY_ASSIGNED");
11
+ }
12
+ this.aggregateId = mongoId;
4
13
  }
5
14
  }
@@ -22,11 +22,43 @@ export class MongoRepository {
22
22
  id: result._id.toString(),
23
23
  });
24
24
  }
25
- async many(filter, transaction) {
25
+ /**
26
+ *
27
+ * @param filter
28
+ * @param options
29
+ */
30
+ async many(filter, options) {
26
31
  const collection = await this.collection();
27
- const session = MongoTransaction.sessionFor(transaction);
32
+ const session = options?.transaction
33
+ ? MongoTransaction.sessionFor(options?.transaction)
34
+ : undefined;
35
+ const hasFields = options?.fieldsToExclude && options.fieldsToExclude.length > 0;
36
+ const projection = hasFields
37
+ ? {}
38
+ : undefined;
39
+ if (hasFields) {
40
+ options.fieldsToExclude.forEach((field) => {
41
+ projection[field] = 0;
42
+ });
43
+ }
44
+ const findOptions = session
45
+ ? { ...(projection ? { projection } : {}), session }
46
+ : projection
47
+ ? { projection }
48
+ : undefined;
49
+ let order = { _id: -1 };
50
+ if (options?.sort?.hasOrder()) {
51
+ order = {
52
+ [options?.sort.orderBy.value === "id"
53
+ ? "_id"
54
+ : options?.sort.orderBy.value]: options.sort.orderType.isAsc()
55
+ ? 1
56
+ : -1,
57
+ };
58
+ }
28
59
  const documents = await collection
29
- .find(filter, session === undefined ? undefined : { session })
60
+ .find(filter, findOptions)
61
+ .sort(order)
30
62
  .toArray();
31
63
  return documents.map((document) => this.aggregateRootClass.fromPrimitives({
32
64
  ...document,
@@ -35,7 +67,18 @@ export class MongoRepository {
35
67
  }
36
68
  /** Upserts an aggregate by delegating to persist with its id. */
37
69
  async upsert(entity, transaction) {
38
- await this.persist(entity.getId(), entity, transaction);
70
+ const primitiveResult = entity.toPrimitives();
71
+ const primitives = await Promise.resolve(primitiveResult);
72
+ const currentId = entity.getId();
73
+ const mongoId = currentId === undefined ? new ObjectId() : new ObjectId(currentId);
74
+ if (currentId === undefined) {
75
+ entity.assignId(mongoId.toString());
76
+ }
77
+ await this.updateOne({ _id: mongoId }, {
78
+ $set: {
79
+ ...primitives,
80
+ },
81
+ }, transaction);
39
82
  }
40
83
  /** Lists entities by criteria and returns a paginated response. */
41
84
  async list(criteria, fieldsToExclude = [], transaction) {
@@ -47,6 +90,7 @@ export class MongoRepository {
47
90
  *
48
91
  * @param {object} filter - The query to match documents that should be deleted.
49
92
  * @param {DeleteOptions} [options] - Optional parameters that modify the behavior of the delete operation.
93
+ * @param transaction
50
94
  * @return {Promise<void>} A promise that resolves when the deletion is complete, or rejects if an error occurs.
51
95
  */
52
96
  async delete(filter, options, transaction) {
@@ -76,21 +120,6 @@ export class MongoRepository {
76
120
  .db()
77
121
  .collection(this.collectionName());
78
122
  }
79
- async persist(id, aggregateRoot, transaction) {
80
- let primitives;
81
- if (aggregateRoot.toPrimitives() instanceof Promise) {
82
- primitives = await aggregateRoot.toPrimitives();
83
- }
84
- else {
85
- primitives = aggregateRoot.toPrimitives();
86
- }
87
- await this.updateOne({ _id: new ObjectId(id) }, {
88
- $set: {
89
- ...primitives,
90
- id: id,
91
- },
92
- }, transaction);
93
- }
94
123
  async searchByCriteria(criteria, fieldsToExclude = [], transaction) {
95
124
  this.criteria = criteria;
96
125
  this.query = this.criteriaConverter.convert(criteria);
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,12 @@
1
1
  export declare abstract class AggregateRoot {
2
- static fromPrimitives(_data: any): AggregateRoot;
3
- abstract getId(): string | undefined;
2
+ private aggregateId?;
3
+ protected constructor(id?: string);
4
+ getId(): string | undefined;
5
+ assignId(mongoId: string): void;
4
6
  abstract toPrimitives(): any;
5
7
  }
6
8
  export type AggregateRootClass<T extends AggregateRoot> = {
7
- fromPrimitives(data: any): T;
9
+ fromPrimitives(data: Record<string, unknown> & {
10
+ readonly id: string;
11
+ }): T;
8
12
  };
@@ -2,8 +2,13 @@ import { Criteria, Paginate } from "../criteria";
2
2
  import { AggregateRoot } from "../AggregateRoot";
3
3
  import { DeleteOptions } from "mongodb";
4
4
  import { MongoTransaction } from "./MongoTransaction";
5
+ import { Order } from "../criteria";
5
6
  export interface IRepository<T extends AggregateRoot> {
6
- many(filter: object, transaction?: MongoTransaction): Promise<T[]>;
7
+ many(filter: object, options?: {
8
+ transaction?: MongoTransaction;
9
+ fieldsToExclude?: string[];
10
+ sort?: Order;
11
+ }): Promise<T[]>;
7
12
  one(filter: object, transaction?: MongoTransaction): Promise<T | null>;
8
13
  list<D>(criteria: Criteria, fieldsToExclude?: string[], transaction?: MongoTransaction): Promise<Paginate<D>>;
9
14
  upsert(entity: T, transaction?: MongoTransaction): Promise<void>;
@@ -1,23 +1,5 @@
1
1
  import { Criteria, Filters, Order } from "../criteria";
2
- type MongoFilterOperator = "$eq" | "$ne" | "$gt" | "$lt" | "$regex" | "$lte" | "$gte" | "$or" | "$in" | "$nin";
3
- type MongoFilterValue = boolean | string | number | Date;
4
- type MongoFilterArrayValue = MongoFilterValue[];
5
- type MongoFilterOperation = {
6
- [operator in MongoFilterOperator]?: MongoFilterValue | MongoFilterArrayValue;
7
- };
8
- type MongoFilter = {
9
- [field: string]: MongoFilterOperation;
10
- } | {
11
- [field: string]: {
12
- $not: MongoFilterOperation;
13
- };
14
- } | {
15
- $or: any[];
16
- };
17
- type MongoDirection = 1 | -1;
18
- type MongoSort = {
19
- [field: string]: MongoDirection;
20
- };
2
+ import { MongoFilter, MongoSort } from "../types";
21
3
  export interface MongoQuery {
22
4
  filter: MongoFilter;
23
5
  sort: MongoSort;
@@ -43,4 +25,3 @@ export declare class MongoCriteriaConverter {
43
25
  private orFilter;
44
26
  private betweenFilter;
45
27
  }
46
- export {};
@@ -1,5 +1,5 @@
1
1
  import { MongoTransaction } from "./MongoTransaction";
2
- import { Criteria, Paginate } from "../criteria";
2
+ import { Criteria, Order, Paginate } from "../criteria";
3
3
  import { AggregateRoot, AggregateRootClass } from "../AggregateRoot";
4
4
  import { Collection, DeleteOptions, Document, UpdateFilter } from "mongodb";
5
5
  export declare abstract class MongoRepository<T extends AggregateRoot> {
@@ -12,7 +12,16 @@ export declare abstract class MongoRepository<T extends AggregateRoot> {
12
12
  abstract collectionName(): string;
13
13
  /** Finds a single entity and hydrates it via the aggregate's fromPrimitives. */
14
14
  one(filter: object, transaction?: MongoTransaction): Promise<T | null>;
15
- many(filter: object, transaction?: MongoTransaction): Promise<T[]>;
15
+ /**
16
+ *
17
+ * @param filter
18
+ * @param options
19
+ */
20
+ many(filter: object, options?: {
21
+ transaction?: MongoTransaction;
22
+ fieldsToExclude?: string[];
23
+ sort?: Order;
24
+ }): Promise<T[]>;
16
25
  /** Upserts an aggregate by delegating to persist with its id. */
17
26
  upsert(entity: T, transaction?: MongoTransaction): Promise<void>;
18
27
  /** Lists entities by criteria and returns a paginated response. */
@@ -22,6 +31,7 @@ export declare abstract class MongoRepository<T extends AggregateRoot> {
22
31
  *
23
32
  * @param {object} filter - The query to match documents that should be deleted.
24
33
  * @param {DeleteOptions} [options] - Optional parameters that modify the behavior of the delete operation.
34
+ * @param transaction
25
35
  * @return {Promise<void>} A promise that resolves when the deletion is complete, or rejects if an error occurs.
26
36
  */
27
37
  delete(filter: object, options?: DeleteOptions, transaction?: MongoTransaction): Promise<void>;
@@ -30,7 +40,6 @@ export declare abstract class MongoRepository<T extends AggregateRoot> {
30
40
  protected collection<U extends Document>(): Promise<Collection<U>>;
31
41
  protected updateOne(filter: object, update: Document[] | UpdateFilter<any>, transaction?: MongoTransaction): Promise<void>;
32
42
  private collectionRaw;
33
- private persist;
34
43
  private searchByCriteria;
35
44
  private paginate;
36
45
  }
@@ -0,0 +1,25 @@
1
+ export type MongoFilterOperator = "$eq" | "$ne" | "$gt" | "$lt" | "$regex" | "$lte" | "$gte" | "$or" | "$in" | "$nin";
2
+ export type MongoFilterBetween = {
3
+ [p: string]: {
4
+ $gte: MongoFilterValue;
5
+ $lte: MongoFilterValue;
6
+ };
7
+ };
8
+ export type MongoFilterValue = boolean | string | number | Date;
9
+ export type MongoFilterArrayValue = MongoFilterValue[];
10
+ export type MongoFilterOperation = {
11
+ [operator in MongoFilterOperator]?: MongoFilterValue | MongoFilterArrayValue;
12
+ };
13
+ export type MongoFilter = {
14
+ [field: string]: MongoFilterOperation;
15
+ } | {
16
+ [field: string]: {
17
+ $not: MongoFilterOperation;
18
+ };
19
+ } | {
20
+ $or: any[];
21
+ };
22
+ export type MongoDirection = 1 | -1;
23
+ export type MongoSort = {
24
+ [field: string]: MongoDirection;
25
+ };
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.11.0",
4
+ "version": "1.11.2",
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",