@abejarano/ts-mongodb-criteria 1.10.0 → 1.11.1

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,8 +182,9 @@ 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
- - `one(filter)` to fetch a single entity
186
- - `upsert(entity)` to persist an aggregate
185
+ - `many(filter, options?)` to fetch multiple entities matching a filter, with optional `{ transaction?, sort }`
186
+ - `one(filter, transaction?)` to fetch a single entity
187
+ - `upsert(entity, transaction?)` to persist an aggregate
187
188
  Internal helpers are private, so repositories should call these public methods
188
189
  directly.
189
190
 
@@ -198,8 +199,8 @@ export interface IUserRepository extends IRepository<User> {
198
199
  }
199
200
  ```
200
201
 
201
- The goal of `IRepository` is to prevent signature drift (e.g. `upsert` returning
202
- `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.
203
204
 
204
205
  ## Atomic Transactions
205
206
 
@@ -217,6 +218,10 @@ await MongoTransaction.run(async (tx) => {
217
218
 
218
219
  // Reads can observe writes made earlier in this transaction.
219
220
  const persistedUser = await userRepository.one({ id: user.getId() }, tx)
221
+ const activeUsers = await userRepository.many(
222
+ { status: "active" },
223
+ { transaction: tx, sort: { name: 1 } }
224
+ )
220
225
  })
221
226
  ```
222
227
 
@@ -235,8 +240,8 @@ async deactivateUser(id: string, tx: MongoTransaction): Promise<void> {
235
240
 
236
241
  MongoDB transactions require a replica set or a sharded cluster; standalone
237
242
  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`.
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`.
240
245
 
241
246
  **Your First Query in 30 Seconds:**
242
247
 
@@ -383,7 +388,7 @@ const criteria = new Criteria(Filters.fromValues(filters), Order.none())
383
388
 
384
389
  ## 🧪 Testing
385
390
 
386
- The library includes comprehensive test coverage (30/30 tests passing).
391
+ The library includes comprehensive test coverage (46/46 tests passing).
387
392
 
388
393
  ```bash
389
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,9 +25,46 @@ class MongoRepository {
25
25
  id: result._id.toString(),
26
26
  });
27
27
  }
28
+ async many(filter, options) {
29
+ const collection = await this.collection();
30
+ const session = options?.transaction
31
+ ? MongoTransaction_1.MongoTransaction.sessionFor(options?.transaction)
32
+ : undefined;
33
+ const hasFields = options?.fields && options.fields.length > 0;
34
+ const projection = hasFields ? {} : undefined;
35
+ if (hasFields) {
36
+ options.fields.forEach((field) => {
37
+ projection[field] = 1;
38
+ });
39
+ }
40
+ const findOptions = session
41
+ ? { ...(projection ? { projection } : {}), session }
42
+ : projection
43
+ ? { projection }
44
+ : undefined;
45
+ const documents = await collection
46
+ .find(filter, findOptions)
47
+ .sort(options?.sort ? options?.sort : { _id: -1 })
48
+ .toArray();
49
+ return documents.map((document) => this.aggregateRootClass.fromPrimitives({
50
+ ...document,
51
+ id: document._id.toString(),
52
+ }));
53
+ }
28
54
  /** Upserts an aggregate by delegating to persist with its id. */
29
55
  async upsert(entity, transaction) {
30
- await this.persist(entity.getId(), entity, transaction);
56
+ const primitiveResult = entity.toPrimitives();
57
+ const primitives = await Promise.resolve(primitiveResult);
58
+ const currentId = entity.getId();
59
+ const mongoId = currentId === undefined ? new mongodb_1.ObjectId() : new mongodb_1.ObjectId(currentId);
60
+ if (currentId === undefined) {
61
+ entity.assignId(mongoId.toString());
62
+ }
63
+ await this.updateOne({ _id: mongoId }, {
64
+ $set: {
65
+ ...primitives,
66
+ },
67
+ }, transaction);
31
68
  }
32
69
  /** Lists entities by criteria and returns a paginated response. */
33
70
  async list(criteria, fieldsToExclude = [], transaction) {
@@ -68,21 +105,6 @@ class MongoRepository {
68
105
  .db()
69
106
  .collection(this.collectionName());
70
107
  }
71
- async persist(id, aggregateRoot, transaction) {
72
- let primitives;
73
- if (aggregateRoot.toPrimitives() instanceof Promise) {
74
- primitives = await aggregateRoot.toPrimitives();
75
- }
76
- else {
77
- primitives = aggregateRoot.toPrimitives();
78
- }
79
- await this.updateOne({ _id: new mongodb_1.ObjectId(id) }, {
80
- $set: {
81
- ...primitives,
82
- id: id,
83
- },
84
- }, transaction);
85
- }
86
108
  async searchByCriteria(criteria, fieldsToExclude = [], transaction) {
87
109
  this.criteria = criteria;
88
110
  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,9 +22,46 @@ export class MongoRepository {
22
22
  id: result._id.toString(),
23
23
  });
24
24
  }
25
+ async many(filter, options) {
26
+ const collection = await this.collection();
27
+ const session = options?.transaction
28
+ ? MongoTransaction.sessionFor(options?.transaction)
29
+ : undefined;
30
+ const hasFields = options?.fields && options.fields.length > 0;
31
+ const projection = hasFields ? {} : undefined;
32
+ if (hasFields) {
33
+ options.fields.forEach((field) => {
34
+ projection[field] = 1;
35
+ });
36
+ }
37
+ const findOptions = session
38
+ ? { ...(projection ? { projection } : {}), session }
39
+ : projection
40
+ ? { projection }
41
+ : undefined;
42
+ const documents = await collection
43
+ .find(filter, findOptions)
44
+ .sort(options?.sort ? options?.sort : { _id: -1 })
45
+ .toArray();
46
+ return documents.map((document) => this.aggregateRootClass.fromPrimitives({
47
+ ...document,
48
+ id: document._id.toString(),
49
+ }));
50
+ }
25
51
  /** Upserts an aggregate by delegating to persist with its id. */
26
52
  async upsert(entity, transaction) {
27
- await this.persist(entity.getId(), entity, transaction);
53
+ const primitiveResult = entity.toPrimitives();
54
+ const primitives = await Promise.resolve(primitiveResult);
55
+ const currentId = entity.getId();
56
+ const mongoId = currentId === undefined ? new ObjectId() : new ObjectId(currentId);
57
+ if (currentId === undefined) {
58
+ entity.assignId(mongoId.toString());
59
+ }
60
+ await this.updateOne({ _id: mongoId }, {
61
+ $set: {
62
+ ...primitives,
63
+ },
64
+ }, transaction);
28
65
  }
29
66
  /** Lists entities by criteria and returns a paginated response. */
30
67
  async list(criteria, fieldsToExclude = [], transaction) {
@@ -65,21 +102,6 @@ export class MongoRepository {
65
102
  .db()
66
103
  .collection(this.collectionName());
67
104
  }
68
- async persist(id, aggregateRoot, transaction) {
69
- let primitives;
70
- if (aggregateRoot.toPrimitives() instanceof Promise) {
71
- primitives = await aggregateRoot.toPrimitives();
72
- }
73
- else {
74
- primitives = aggregateRoot.toPrimitives();
75
- }
76
- await this.updateOne({ _id: new ObjectId(id) }, {
77
- $set: {
78
- ...primitives,
79
- id: id,
80
- },
81
- }, transaction);
82
- }
83
105
  async searchByCriteria(criteria, fieldsToExclude = [], transaction) {
84
106
  this.criteria = criteria;
85
107
  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
  };
@@ -1,3 +1,4 @@
1
1
  export * from "./criteria";
2
2
  export * from "./mongo";
3
3
  export * from "./AggregateRoot";
4
+ export type { MongoSort, MongoDirection } from "./types";
@@ -2,7 +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 { MongoSort } from "../types";
5
6
  export interface IRepository<T extends AggregateRoot> {
7
+ many(filter: object, options?: {
8
+ transaction?: MongoTransaction;
9
+ fields?: string[];
10
+ sort?: MongoSort;
11
+ }): Promise<T[]>;
6
12
  one(filter: object, transaction?: MongoTransaction): Promise<T | null>;
7
13
  list<D>(criteria: Criteria, fieldsToExclude?: string[], transaction?: MongoTransaction): Promise<Paginate<D>>;
8
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 {};
@@ -2,6 +2,7 @@ import { MongoTransaction } from "./MongoTransaction";
2
2
  import { Criteria, Paginate } from "../criteria";
3
3
  import { AggregateRoot, AggregateRootClass } from "../AggregateRoot";
4
4
  import { Collection, DeleteOptions, Document, UpdateFilter } from "mongodb";
5
+ import { MongoSort } from "../types";
5
6
  export declare abstract class MongoRepository<T extends AggregateRoot> {
6
7
  private readonly aggregateRootClass;
7
8
  private static indexRegistry;
@@ -12,6 +13,11 @@ export declare abstract class MongoRepository<T extends AggregateRoot> {
12
13
  abstract collectionName(): string;
13
14
  /** Finds a single entity and hydrates it via the aggregate's fromPrimitives. */
14
15
  one(filter: object, transaction?: MongoTransaction): Promise<T | null>;
16
+ many(filter: object, options?: {
17
+ transaction?: MongoTransaction;
18
+ fields?: string[];
19
+ sort?: MongoSort;
20
+ }): Promise<T[]>;
15
21
  /** Upserts an aggregate by delegating to persist with its id. */
16
22
  upsert(entity: T, transaction?: MongoTransaction): Promise<void>;
17
23
  /** Lists entities by criteria and returns a paginated response. */
@@ -29,7 +35,6 @@ export declare abstract class MongoRepository<T extends AggregateRoot> {
29
35
  protected collection<U extends Document>(): Promise<Collection<U>>;
30
36
  protected updateOne(filter: object, update: Document[] | UpdateFilter<any>, transaction?: MongoTransaction): Promise<void>;
31
37
  private collectionRaw;
32
- private persist;
33
38
  private searchByCriteria;
34
39
  private paginate;
35
40
  }
@@ -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.10.0",
4
+ "version": "1.11.1",
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",