@abejarano/ts-mongodb-criteria 1.11.1 → 1.11.3

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
@@ -181,8 +181,8 @@ Use the `collection` argument inside `ensureIndexes` to avoid recursion.
181
181
 
182
182
  MongoRepository provides ready-to-use public methods for repositories that extend it:
183
183
 
184
- - `list(criteria, fieldsToExclude?)` for paginated queries
185
- - `many(filter, options?)` to fetch multiple entities matching a filter, with optional `{ transaction?, sort }`
184
+ - `list(criteria, transaction?)` for paginated queries
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
@@ -220,7 +220,7 @@ await MongoTransaction.run(async (tx) => {
220
220
  const persistedUser = await userRepository.one({ id: user.getId() }, tx)
221
221
  const activeUsers = await userRepository.many(
222
222
  { status: "active" },
223
- { transaction: tx, sort: { name: 1 } }
223
+ { transaction: tx, sort: Order.asc("name") }
224
224
  )
225
225
  })
226
226
  ```
@@ -241,7 +241,7 @@ async deactivateUser(id: string, tx: MongoTransaction): Promise<void> {
241
241
  MongoDB transactions require a replica set or a sharded cluster; standalone
242
242
  MongoDB instances do not support them. This initial API applies the transaction
243
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`.
244
+ Pass `tx` as the second argument to `list`, and inside the options object (`{ transaction: tx }`) to `many`.
245
245
 
246
246
  **Your First Query in 30 Seconds:**
247
247
 
@@ -388,7 +388,7 @@ const criteria = new Criteria(Filters.fromValues(filters), Order.none())
388
388
 
389
389
  ## 🧪 Testing
390
390
 
391
- The library includes comprehensive test coverage (46/46 tests passing).
391
+ The library includes comprehensive test coverage (45/45 tests passing).
392
392
 
393
393
  ```bash
394
394
  # Run all tests
@@ -2,9 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AggregateRoot = void 0;
4
4
  class AggregateRoot {
5
- constructor(id) {
6
- this.aggregateId = id;
7
- }
8
5
  getId() {
9
6
  return this.aggregateId;
10
7
  }
@@ -2,16 +2,18 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Criteria = void 0;
4
4
  class Criteria {
5
- constructor(filters, order, limit, offset) {
5
+ constructor(filters, order, limit = 10, currentPage = 1) {
6
+ if (!Number.isInteger(limit) || limit <= 0) {
7
+ throw new Error("CRITERIA_LIMIT_MUST_BE_A_POSITIVE_INTEGER");
8
+ }
9
+ if (!Number.isInteger(currentPage) || currentPage <= 0) {
10
+ throw new Error("CRITERIA_PAGE_MUST_BE_A_POSITIVE_INTEGER");
11
+ }
6
12
  this.filters = filters;
7
13
  this.order = order;
8
14
  this.limit = limit;
9
- this.currentPage = offset;
10
- if (offset && limit) {
11
- this.offset = offset > 0 ? (offset - 1) * limit : 0;
12
- }
13
- else
14
- this.offset = undefined;
15
+ this.currentPage = currentPage;
16
+ this.offset = (currentPage - 1) * limit;
15
17
  }
16
18
  hasFilters() {
17
19
  return this.filters.filters.length > 0;
@@ -27,8 +27,8 @@ class MongoCriteriaConverter {
27
27
  sort: criteria.order.hasOrder()
28
28
  ? this.generateSort(criteria.order)
29
29
  : { _id: -1 },
30
- skip: criteria.offset || 0,
31
- limit: criteria.limit || 0,
30
+ skip: criteria.offset,
31
+ limit: criteria.limit,
32
32
  };
33
33
  }
34
34
  generateFilter(filters) {
@@ -20,36 +20,39 @@ class MongoRepository {
20
20
  if (!result) {
21
21
  return null;
22
22
  }
23
- return this.aggregateRootClass.fromPrimitives({
24
- ...result,
25
- id: result._id.toString(),
26
- });
23
+ const { _id, ...primitives } = result;
24
+ const entity = this.aggregateRootClass.fromPrimitives(primitives);
25
+ entity.assignId(_id.toString());
26
+ return entity;
27
27
  }
28
+ /**
29
+ *
30
+ * @param filter
31
+ * @param options
32
+ */
28
33
  async many(filter, options) {
29
34
  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
- });
35
+ let order = { _id: -1 };
36
+ if (options?.sort?.hasOrder()) {
37
+ order = {
38
+ [options?.sort.orderBy.value === "id"
39
+ ? "_id"
40
+ : options?.sort.orderBy.value]: options.sort.orderType.isAsc()
41
+ ? 1
42
+ : -1,
43
+ };
39
44
  }
40
- const findOptions = session
41
- ? { ...(projection ? { projection } : {}), session }
42
- : projection
43
- ? { projection }
44
- : undefined;
45
+ const session = MongoTransaction_1.MongoTransaction.sessionFor(options?.transaction);
45
46
  const documents = await collection
46
- .find(filter, findOptions)
47
- .sort(options?.sort ? options?.sort : { _id: -1 })
47
+ .find(filter, session ? { session } : undefined)
48
+ .sort(order)
48
49
  .toArray();
49
- return documents.map((document) => this.aggregateRootClass.fromPrimitives({
50
- ...document,
51
- id: document._id.toString(),
52
- }));
50
+ return documents.map((document) => {
51
+ const { _id, ...primitives } = document;
52
+ const entity = this.aggregateRootClass.fromPrimitives(primitives);
53
+ entity.assignId(_id.toString());
54
+ return entity;
55
+ });
53
56
  }
54
57
  /** Upserts an aggregate by delegating to persist with its id. */
55
58
  async upsert(entity, transaction) {
@@ -67,15 +70,17 @@ class MongoRepository {
67
70
  }, transaction);
68
71
  }
69
72
  /** Lists entities by criteria and returns a paginated response. */
70
- async list(criteria, fieldsToExclude = [], transaction) {
71
- const documents = await this.searchByCriteria(criteria, fieldsToExclude, transaction);
72
- return this.paginate(documents, transaction);
73
+ async list(criteria, transaction) {
74
+ const query = this.criteriaConverter.convert(criteria);
75
+ const documents = await this.searchByCriteria(query, transaction);
76
+ return this.paginate(documents, query, criteria, transaction);
73
77
  }
74
78
  /**
75
79
  * Deletes documents from the database based on a filter and optional options.
76
80
  *
77
81
  * @param {object} filter - The query to match documents that should be deleted.
78
82
  * @param {DeleteOptions} [options] - Optional parameters that modify the behavior of the delete operation.
83
+ * @param transaction
79
84
  * @return {Promise<void>} A promise that resolves when the deletion is complete, or rejects if an error occurs.
80
85
  */
81
86
  async delete(filter, options, transaction) {
@@ -105,50 +110,37 @@ class MongoRepository {
105
110
  .db()
106
111
  .collection(this.collectionName());
107
112
  }
108
- async searchByCriteria(criteria, fieldsToExclude = [], transaction) {
109
- this.criteria = criteria;
110
- this.query = this.criteriaConverter.convert(criteria);
113
+ async searchByCriteria(query, transaction) {
111
114
  const collection = await this.collection();
112
115
  const session = MongoTransaction_1.MongoTransaction.sessionFor(transaction);
113
- if (fieldsToExclude.length === 0) {
114
- const results = await collection
115
- .find(this.query.filter, session ? { session } : {})
116
- .sort(this.query.sort)
117
- .skip(this.query.skip)
118
- .limit(this.query.limit)
119
- .toArray();
120
- return results.map(({ _id, ...rest }) => rest);
121
- }
122
- const projection = {};
123
- fieldsToExclude.forEach((field) => {
124
- projection[field] = 0;
125
- });
126
116
  const results = await collection
127
- .find(this.query.filter, session ? { projection, session } : { projection })
128
- .sort(this.query.sort)
129
- .skip(this.query.skip)
130
- .limit(this.query.limit)
117
+ .find(query.filter, session ? { session } : undefined)
118
+ .sort(query.sort)
119
+ .skip(query.skip)
120
+ .limit(query.limit)
131
121
  .toArray();
132
- return results.map(({ _id, ...rest }) => rest);
122
+ return results.map(({ _id, ...rest }) => {
123
+ const entity = this.aggregateRootClass.fromPrimitives(rest);
124
+ entity.assignId(_id.toString());
125
+ return entity;
126
+ });
133
127
  }
134
- async paginate(documents, transaction) {
128
+ async paginate(documents, query, criteria, transaction) {
135
129
  const collection = await this.collection();
136
130
  const session = MongoTransaction_1.MongoTransaction.sessionFor(transaction);
137
- const count = session
138
- ? await collection.countDocuments(this.query.filter, { session })
139
- : await collection.countDocuments(this.query.filter);
140
- const limit = this.criteria?.limit || 10;
141
- const currentPage = this.criteria?.currentPage || 1;
142
- const hasNextPage = currentPage * limit < count;
131
+ const count = await collection.countDocuments(query.filter, session ? { session } : undefined);
132
+ const limit = criteria.limit;
133
+ const currentPage = criteria.currentPage;
134
+ const hasNextPage = limit > 0 && currentPage * limit < count;
143
135
  if (documents.length === 0) {
144
136
  return {
145
137
  nextPag: null,
146
- count: 0,
138
+ count,
147
139
  results: [],
148
140
  };
149
141
  }
150
142
  return {
151
- nextPag: hasNextPage ? Number(this.criteria.currentPage) + 1 : null,
143
+ nextPag: hasNextPage ? Number(criteria.currentPage) + 1 : null,
152
144
  count: count,
153
145
  results: documents,
154
146
  };
@@ -1,7 +1,4 @@
1
1
  export class AggregateRoot {
2
- constructor(id) {
3
- this.aggregateId = id;
4
- }
5
2
  getId() {
6
3
  return this.aggregateId;
7
4
  }
@@ -1,14 +1,16 @@
1
1
  export class Criteria {
2
- constructor(filters, order, limit, offset) {
2
+ constructor(filters, order, limit = 10, currentPage = 1) {
3
+ if (!Number.isInteger(limit) || limit <= 0) {
4
+ throw new Error("CRITERIA_LIMIT_MUST_BE_A_POSITIVE_INTEGER");
5
+ }
6
+ if (!Number.isInteger(currentPage) || currentPage <= 0) {
7
+ throw new Error("CRITERIA_PAGE_MUST_BE_A_POSITIVE_INTEGER");
8
+ }
3
9
  this.filters = filters;
4
10
  this.order = order;
5
11
  this.limit = limit;
6
- this.currentPage = offset;
7
- if (offset && limit) {
8
- this.offset = offset > 0 ? (offset - 1) * limit : 0;
9
- }
10
- else
11
- this.offset = undefined;
12
+ this.currentPage = currentPage;
13
+ this.offset = (currentPage - 1) * limit;
12
14
  }
13
15
  hasFilters() {
14
16
  return this.filters.filters.length > 0;
@@ -24,8 +24,8 @@ export class MongoCriteriaConverter {
24
24
  sort: criteria.order.hasOrder()
25
25
  ? this.generateSort(criteria.order)
26
26
  : { _id: -1 },
27
- skip: criteria.offset || 0,
28
- limit: criteria.limit || 0,
27
+ skip: criteria.offset,
28
+ limit: criteria.limit,
29
29
  };
30
30
  }
31
31
  generateFilter(filters) {
@@ -17,36 +17,39 @@ export class MongoRepository {
17
17
  if (!result) {
18
18
  return null;
19
19
  }
20
- return this.aggregateRootClass.fromPrimitives({
21
- ...result,
22
- id: result._id.toString(),
23
- });
20
+ const { _id, ...primitives } = result;
21
+ const entity = this.aggregateRootClass.fromPrimitives(primitives);
22
+ entity.assignId(_id.toString());
23
+ return entity;
24
24
  }
25
+ /**
26
+ *
27
+ * @param filter
28
+ * @param options
29
+ */
25
30
  async many(filter, options) {
26
31
  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
- });
32
+ let order = { _id: -1 };
33
+ if (options?.sort?.hasOrder()) {
34
+ order = {
35
+ [options?.sort.orderBy.value === "id"
36
+ ? "_id"
37
+ : options?.sort.orderBy.value]: options.sort.orderType.isAsc()
38
+ ? 1
39
+ : -1,
40
+ };
36
41
  }
37
- const findOptions = session
38
- ? { ...(projection ? { projection } : {}), session }
39
- : projection
40
- ? { projection }
41
- : undefined;
42
+ const session = MongoTransaction.sessionFor(options?.transaction);
42
43
  const documents = await collection
43
- .find(filter, findOptions)
44
- .sort(options?.sort ? options?.sort : { _id: -1 })
44
+ .find(filter, session ? { session } : undefined)
45
+ .sort(order)
45
46
  .toArray();
46
- return documents.map((document) => this.aggregateRootClass.fromPrimitives({
47
- ...document,
48
- id: document._id.toString(),
49
- }));
47
+ return documents.map((document) => {
48
+ const { _id, ...primitives } = document;
49
+ const entity = this.aggregateRootClass.fromPrimitives(primitives);
50
+ entity.assignId(_id.toString());
51
+ return entity;
52
+ });
50
53
  }
51
54
  /** Upserts an aggregate by delegating to persist with its id. */
52
55
  async upsert(entity, transaction) {
@@ -64,15 +67,17 @@ export class MongoRepository {
64
67
  }, transaction);
65
68
  }
66
69
  /** Lists entities by criteria and returns a paginated response. */
67
- async list(criteria, fieldsToExclude = [], transaction) {
68
- const documents = await this.searchByCriteria(criteria, fieldsToExclude, transaction);
69
- return this.paginate(documents, transaction);
70
+ async list(criteria, transaction) {
71
+ const query = this.criteriaConverter.convert(criteria);
72
+ const documents = await this.searchByCriteria(query, transaction);
73
+ return this.paginate(documents, query, criteria, transaction);
70
74
  }
71
75
  /**
72
76
  * Deletes documents from the database based on a filter and optional options.
73
77
  *
74
78
  * @param {object} filter - The query to match documents that should be deleted.
75
79
  * @param {DeleteOptions} [options] - Optional parameters that modify the behavior of the delete operation.
80
+ * @param transaction
76
81
  * @return {Promise<void>} A promise that resolves when the deletion is complete, or rejects if an error occurs.
77
82
  */
78
83
  async delete(filter, options, transaction) {
@@ -102,50 +107,37 @@ export class MongoRepository {
102
107
  .db()
103
108
  .collection(this.collectionName());
104
109
  }
105
- async searchByCriteria(criteria, fieldsToExclude = [], transaction) {
106
- this.criteria = criteria;
107
- this.query = this.criteriaConverter.convert(criteria);
110
+ async searchByCriteria(query, transaction) {
108
111
  const collection = await this.collection();
109
112
  const session = MongoTransaction.sessionFor(transaction);
110
- if (fieldsToExclude.length === 0) {
111
- const results = await collection
112
- .find(this.query.filter, session ? { session } : {})
113
- .sort(this.query.sort)
114
- .skip(this.query.skip)
115
- .limit(this.query.limit)
116
- .toArray();
117
- return results.map(({ _id, ...rest }) => rest);
118
- }
119
- const projection = {};
120
- fieldsToExclude.forEach((field) => {
121
- projection[field] = 0;
122
- });
123
113
  const results = await collection
124
- .find(this.query.filter, session ? { projection, session } : { projection })
125
- .sort(this.query.sort)
126
- .skip(this.query.skip)
127
- .limit(this.query.limit)
114
+ .find(query.filter, session ? { session } : undefined)
115
+ .sort(query.sort)
116
+ .skip(query.skip)
117
+ .limit(query.limit)
128
118
  .toArray();
129
- return results.map(({ _id, ...rest }) => rest);
119
+ return results.map(({ _id, ...rest }) => {
120
+ const entity = this.aggregateRootClass.fromPrimitives(rest);
121
+ entity.assignId(_id.toString());
122
+ return entity;
123
+ });
130
124
  }
131
- async paginate(documents, transaction) {
125
+ async paginate(documents, query, criteria, transaction) {
132
126
  const collection = await this.collection();
133
127
  const session = MongoTransaction.sessionFor(transaction);
134
- const count = session
135
- ? await collection.countDocuments(this.query.filter, { session })
136
- : await collection.countDocuments(this.query.filter);
137
- const limit = this.criteria?.limit || 10;
138
- const currentPage = this.criteria?.currentPage || 1;
139
- const hasNextPage = currentPage * limit < count;
128
+ const count = await collection.countDocuments(query.filter, session ? { session } : undefined);
129
+ const limit = criteria.limit;
130
+ const currentPage = criteria.currentPage;
131
+ const hasNextPage = limit > 0 && currentPage * limit < count;
140
132
  if (documents.length === 0) {
141
133
  return {
142
134
  nextPag: null,
143
- count: 0,
135
+ count,
144
136
  results: [],
145
137
  };
146
138
  }
147
139
  return {
148
- nextPag: hasNextPage ? Number(this.criteria.currentPage) + 1 : null,
140
+ nextPag: hasNextPage ? Number(criteria.currentPage) + 1 : null,
149
141
  count: count,
150
142
  results: documents,
151
143
  };
@@ -1,12 +1,9 @@
1
1
  export declare abstract class AggregateRoot {
2
2
  private aggregateId?;
3
- protected constructor(id?: string);
4
3
  getId(): string | undefined;
5
4
  assignId(mongoId: string): void;
6
5
  abstract toPrimitives(): any;
7
6
  }
8
7
  export type AggregateRootClass<T extends AggregateRoot> = {
9
- fromPrimitives(data: Record<string, unknown> & {
10
- readonly id: string;
11
- }): T;
8
+ fromPrimitives(data: Record<string, unknown>): T;
12
9
  };
@@ -3,9 +3,9 @@ import { Order } from "./Order";
3
3
  export declare class Criteria {
4
4
  readonly filters: Filters;
5
5
  readonly order: Order;
6
- readonly limit?: number;
7
- readonly offset?: number;
8
- readonly currentPage?: number;
9
- constructor(filters: Filters, order: Order, limit?: number, offset?: number);
6
+ readonly limit: number;
7
+ readonly offset: number;
8
+ readonly currentPage: number;
9
+ constructor(filters: Filters, order: Order, limit?: number, currentPage?: number);
10
10
  hasFilters(): boolean;
11
11
  }
@@ -1,4 +1,3 @@
1
1
  export * from "./criteria";
2
2
  export * from "./mongo";
3
3
  export * from "./AggregateRoot";
4
- export type { MongoSort, MongoDirection } from "./types";
@@ -1,16 +1,14 @@
1
- import { Criteria, Paginate } from "../criteria";
1
+ import { Criteria, Order, 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";
6
5
  export interface IRepository<T extends AggregateRoot> {
7
6
  many(filter: object, options?: {
8
7
  transaction?: MongoTransaction;
9
- fields?: string[];
10
- sort?: MongoSort;
8
+ sort?: Order;
11
9
  }): Promise<T[]>;
12
10
  one(filter: object, transaction?: MongoTransaction): Promise<T | null>;
13
- list<D>(criteria: Criteria, fieldsToExclude?: string[], transaction?: MongoTransaction): Promise<Paginate<D>>;
11
+ list(criteria: Criteria, transaction?: MongoTransaction): Promise<Paginate<T>>;
14
12
  upsert(entity: T, transaction?: MongoTransaction): Promise<void>;
15
13
  delete(filter: object, options?: DeleteOptions, transaction?: MongoTransaction): Promise<void>;
16
14
  }
@@ -1,32 +1,34 @@
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
- import { MongoSort } from "../types";
6
5
  export declare abstract class MongoRepository<T extends AggregateRoot> {
7
6
  private readonly aggregateRootClass;
8
7
  private static indexRegistry;
9
8
  private criteriaConverter;
10
- private query;
11
- private criteria;
12
9
  protected constructor(aggregateRootClass: AggregateRootClass<T>);
13
10
  abstract collectionName(): string;
14
11
  /** Finds a single entity and hydrates it via the aggregate's fromPrimitives. */
15
12
  one(filter: object, transaction?: MongoTransaction): Promise<T | null>;
13
+ /**
14
+ *
15
+ * @param filter
16
+ * @param options
17
+ */
16
18
  many(filter: object, options?: {
17
19
  transaction?: MongoTransaction;
18
- fields?: string[];
19
- sort?: MongoSort;
20
+ sort?: Order;
20
21
  }): Promise<T[]>;
21
22
  /** Upserts an aggregate by delegating to persist with its id. */
22
23
  upsert(entity: T, transaction?: MongoTransaction): Promise<void>;
23
24
  /** Lists entities by criteria and returns a paginated response. */
24
- list<D>(criteria: Criteria, fieldsToExclude?: string[], transaction?: MongoTransaction): Promise<Paginate<D>>;
25
+ list(criteria: Criteria, transaction?: MongoTransaction): Promise<Paginate<T>>;
25
26
  /**
26
27
  * Deletes documents from the database based on a filter and optional options.
27
28
  *
28
29
  * @param {object} filter - The query to match documents that should be deleted.
29
30
  * @param {DeleteOptions} [options] - Optional parameters that modify the behavior of the delete operation.
31
+ * @param transaction
30
32
  * @return {Promise<void>} A promise that resolves when the deletion is complete, or rejects if an error occurs.
31
33
  */
32
34
  delete(filter: object, options?: DeleteOptions, transaction?: MongoTransaction): Promise<void>;
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.1",
4
+ "version": "1.11.3",
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",