@abejarano/ts-mongodb-criteria 1.11.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,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?, 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: { name: 1 } }
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,26 @@ class MongoRepository {
25
25
  id: result._id.toString(),
26
26
  });
27
27
  }
28
- async many(filter, transaction) {
28
+ async many(filter, options) {
29
29
  const collection = await this.collection();
30
- const session = MongoTransaction_1.MongoTransaction.sessionFor(transaction);
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;
31
45
  const documents = await collection
32
- .find(filter, session === undefined ? undefined : { session })
46
+ .find(filter, findOptions)
47
+ .sort(options?.sort ? options?.sort : { _id: -1 })
33
48
  .toArray();
34
49
  return documents.map((document) => this.aggregateRootClass.fromPrimitives({
35
50
  ...document,
@@ -38,7 +53,18 @@ class MongoRepository {
38
53
  }
39
54
  /** Upserts an aggregate by delegating to persist with its id. */
40
55
  async upsert(entity, transaction) {
41
- 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);
42
68
  }
43
69
  /** Lists entities by criteria and returns a paginated response. */
44
70
  async list(criteria, fieldsToExclude = [], transaction) {
@@ -79,21 +105,6 @@ class MongoRepository {
79
105
  .db()
80
106
  .collection(this.collectionName());
81
107
  }
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
108
  async searchByCriteria(criteria, fieldsToExclude = [], transaction) {
98
109
  this.criteria = criteria;
99
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,11 +22,26 @@ export class MongoRepository {
22
22
  id: result._id.toString(),
23
23
  });
24
24
  }
25
- async many(filter, transaction) {
25
+ async many(filter, options) {
26
26
  const collection = await this.collection();
27
- const session = MongoTransaction.sessionFor(transaction);
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;
28
42
  const documents = await collection
29
- .find(filter, session === undefined ? undefined : { session })
43
+ .find(filter, findOptions)
44
+ .sort(options?.sort ? options?.sort : { _id: -1 })
30
45
  .toArray();
31
46
  return documents.map((document) => this.aggregateRootClass.fromPrimitives({
32
47
  ...document,
@@ -35,7 +50,18 @@ export class MongoRepository {
35
50
  }
36
51
  /** Upserts an aggregate by delegating to persist with its id. */
37
52
  async upsert(entity, transaction) {
38
- 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);
39
65
  }
40
66
  /** Lists entities by criteria and returns a paginated response. */
41
67
  async list(criteria, fieldsToExclude = [], transaction) {
@@ -76,21 +102,6 @@ export class MongoRepository {
76
102
  .db()
77
103
  .collection(this.collectionName());
78
104
  }
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
105
  async searchByCriteria(criteria, fieldsToExclude = [], transaction) {
95
106
  this.criteria = criteria;
96
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,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 { MongoSort } from "../types";
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
+ fields?: string[];
10
+ sort?: MongoSort;
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 {};
@@ -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,7 +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>;
15
- many(filter: object, transaction?: MongoTransaction): Promise<T[]>;
16
+ many(filter: object, options?: {
17
+ transaction?: MongoTransaction;
18
+ fields?: string[];
19
+ sort?: MongoSort;
20
+ }): Promise<T[]>;
16
21
  /** Upserts an aggregate by delegating to persist with its id. */
17
22
  upsert(entity: T, transaction?: MongoTransaction): Promise<void>;
18
23
  /** Lists entities by criteria and returns a paginated response. */
@@ -30,7 +35,6 @@ export declare abstract class MongoRepository<T extends AggregateRoot> {
30
35
  protected collection<U extends Document>(): Promise<Collection<U>>;
31
36
  protected updateOne(filter: object, update: Document[] | UpdateFilter<any>, transaction?: MongoTransaction): Promise<void>;
32
37
  private collectionRaw;
33
- private persist;
34
38
  private searchByCriteria;
35
39
  private paginate;
36
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.11.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",