@abejarano/ts-mongodb-criteria 1.2.1 → 1.4.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.
Files changed (70) hide show
  1. package/README.md +10 -1
  2. package/dist/{AggregateRoot.js → cjs/AggregateRoot.js} +3 -0
  3. package/dist/{criteria → cjs/criteria}/FilterOperator.js +2 -0
  4. package/dist/{criteria → cjs/criteria}/FilterValue.js +9 -0
  5. package/dist/{mongo → cjs/mongo}/MongoCriteriaConverter.js +14 -0
  6. package/dist/{mongo → cjs/mongo}/MongoRepository.js +44 -36
  7. package/dist/esm/AggregateRoot.js +5 -0
  8. package/dist/esm/criteria/Criteria.js +15 -0
  9. package/dist/esm/criteria/Filter.js +20 -0
  10. package/dist/esm/criteria/FilterField.js +6 -0
  11. package/dist/esm/criteria/FilterOperator.js +34 -0
  12. package/dist/esm/criteria/FilterValue.js +79 -0
  13. package/dist/esm/criteria/Filters.js +12 -0
  14. package/dist/esm/criteria/Order.js +26 -0
  15. package/dist/esm/criteria/OrderBy.js +13 -0
  16. package/dist/esm/criteria/OrderType.js +30 -0
  17. package/dist/esm/criteria/Paginate.js +1 -0
  18. package/dist/esm/exceptions/InvalidArgumentError.exception.js +2 -0
  19. package/dist/esm/mongo/MongoClientFactory.js +45 -0
  20. package/dist/esm/mongo/MongoCriteriaConverter.js +125 -0
  21. package/dist/esm/mongo/MongoRepository.js +99 -0
  22. package/dist/esm/valueObject/EnumValueObject.js +12 -0
  23. package/dist/esm/valueObject/StringValueObject.js +20 -0
  24. package/dist/esm/valueObject/ValueObject.js +19 -0
  25. package/dist/types/AggregateRoot.d.ts +8 -0
  26. package/dist/{criteria → types/criteria}/FilterOperator.d.ts +3 -1
  27. package/dist/{criteria → types/criteria}/FilterValue.d.ts +2 -0
  28. package/dist/types/criteria/index.d.ts +10 -0
  29. package/dist/types/index.d.ts +3 -0
  30. package/dist/{mongo → types/mongo}/MongoCriteriaConverter.d.ts +5 -2
  31. package/dist/types/mongo/MongoRepository.d.ts +22 -0
  32. package/dist/types/mongo/index.d.ts +3 -0
  33. package/dist/types/valueObject/index.d.ts +3 -0
  34. package/package.json +18 -5
  35. package/dist/AggregateRoot.d.ts +0 -4
  36. package/dist/mongo/MongoRepository.d.ts +0 -15
  37. /package/dist/{criteria → cjs/criteria}/Criteria.js +0 -0
  38. /package/dist/{criteria → cjs/criteria}/Filter.js +0 -0
  39. /package/dist/{criteria → cjs/criteria}/FilterField.js +0 -0
  40. /package/dist/{criteria → cjs/criteria}/Filters.js +0 -0
  41. /package/dist/{criteria → cjs/criteria}/Order.js +0 -0
  42. /package/dist/{criteria → cjs/criteria}/OrderBy.js +0 -0
  43. /package/dist/{criteria → cjs/criteria}/OrderType.js +0 -0
  44. /package/dist/{criteria → cjs/criteria}/Paginate.js +0 -0
  45. /package/dist/{criteria → cjs/criteria}/index.js +0 -0
  46. /package/dist/{exceptions → cjs/exceptions}/InvalidArgumentError.exception.js +0 -0
  47. /package/dist/{index.js → cjs/index.js} +0 -0
  48. /package/dist/{mongo → cjs/mongo}/MongoClientFactory.js +0 -0
  49. /package/dist/{mongo → cjs/mongo}/index.js +0 -0
  50. /package/dist/{valueObject → cjs/valueObject}/EnumValueObject.js +0 -0
  51. /package/dist/{valueObject → cjs/valueObject}/StringValueObject.js +0 -0
  52. /package/dist/{valueObject → cjs/valueObject}/ValueObject.js +0 -0
  53. /package/dist/{valueObject → cjs/valueObject}/index.js +0 -0
  54. /package/dist/{criteria/index.d.ts → esm/criteria/index.js} +0 -0
  55. /package/dist/{index.d.ts → esm/index.js} +0 -0
  56. /package/dist/{mongo/index.d.ts → esm/mongo/index.js} +0 -0
  57. /package/dist/{valueObject/index.d.ts → esm/valueObject/index.js} +0 -0
  58. /package/dist/{criteria → types/criteria}/Criteria.d.ts +0 -0
  59. /package/dist/{criteria → types/criteria}/Filter.d.ts +0 -0
  60. /package/dist/{criteria → types/criteria}/FilterField.d.ts +0 -0
  61. /package/dist/{criteria → types/criteria}/Filters.d.ts +0 -0
  62. /package/dist/{criteria → types/criteria}/Order.d.ts +0 -0
  63. /package/dist/{criteria → types/criteria}/OrderBy.d.ts +0 -0
  64. /package/dist/{criteria → types/criteria}/OrderType.d.ts +0 -0
  65. /package/dist/{criteria → types/criteria}/Paginate.d.ts +0 -0
  66. /package/dist/{exceptions → types/exceptions}/InvalidArgumentError.exception.d.ts +0 -0
  67. /package/dist/{mongo → types/mongo}/MongoClientFactory.d.ts +0 -0
  68. /package/dist/{valueObject → types/valueObject}/EnumValueObject.d.ts +0 -0
  69. /package/dist/{valueObject → types/valueObject}/StringValueObject.d.ts +0 -0
  70. /package/dist/{valueObject → types/valueObject}/ValueObject.d.ts +0 -0
package/README.md CHANGED
@@ -156,6 +156,11 @@ const userRepo = new UserRepository()
156
156
  const users = await userRepo.searchByCriteria(criteria)
157
157
  ```
158
158
 
159
+ MongoRepository provides ready-to-use public methods for repositories that extend it:
160
+ - `list(criteria, fieldsToExclude?)` for paginated queries
161
+ - `one(filter)` to fetch a single entity
162
+ - `upsert(entity)` to persist an aggregate
163
+
159
164
  **Your First Query in 30 Seconds:**
160
165
 
161
166
  ```typescript
@@ -178,7 +183,7 @@ const activeAdultUsers = new Criteria(
178
183
  1 // First page
179
184
  )
180
185
 
181
- const results = await repository.searchByCriteria(activeAdultUsers)
186
+ const results = await repository.list(activeAdultUsers)
182
187
  ```
183
188
 
184
189
  ## 📖 Documentation
@@ -219,6 +224,10 @@ const order = Order.desc("createdAt") // or Order.asc("name")
219
224
  - `CONTAINS`, `NOT_CONTAINS` - Text search
220
225
  - `OR` - Logical OR combinations
221
226
 
227
+ ## ✅ Runtime Compatibility
228
+
229
+ This library ships dual builds (CJS + ESM) and works in both Node.js and Bun.
230
+
222
231
  ### 🆕 OR Operator Example
223
232
 
224
233
  ```typescript
@@ -2,5 +2,8 @@
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");
7
+ }
5
8
  }
6
9
  exports.AggregateRoot = AggregateRoot;
@@ -14,6 +14,8 @@ var Operator;
14
14
  Operator["LTE"] = "<=";
15
15
  Operator["BETWEEN"] = "BETWEEN";
16
16
  Operator["OR"] = "OR";
17
+ Operator["IN"] = "IN";
18
+ Operator["NOT_IN"] = "NOT_IN";
17
19
  })(Operator || (exports.Operator = Operator = {}));
18
20
  //export class FilterOperator extends EnumValueObject<Operator> {
19
21
  class FilterOperator {
@@ -70,5 +70,14 @@ class FilterValue extends valueObject_1.StringValueObject {
70
70
  }
71
71
  throw new Error("Value is not a BETWEEN structure");
72
72
  }
73
+ get isPrimitiveArray() {
74
+ return Array.isArray(this._originalValue) && !this.isOrConditions;
75
+ }
76
+ get asPrimitiveArray() {
77
+ if (this.isPrimitiveArray) {
78
+ return this._originalValue;
79
+ }
80
+ throw new Error("Value is not an array of primitive values");
81
+ }
73
82
  }
74
83
  exports.FilterValue = FilterValue;
@@ -15,6 +15,8 @@ class MongoCriteriaConverter {
15
15
  [criteria_1.Operator.LTE, this.lowerThanOrEqualFilter],
16
16
  [criteria_1.Operator.BETWEEN, this.betweenFilter],
17
17
  [criteria_1.Operator.OR, this.orFilter],
18
+ [criteria_1.Operator.IN, this.inFilter],
19
+ [criteria_1.Operator.NOT_IN, this.notInFilter],
18
20
  ]);
19
21
  }
20
22
  convert(criteria) {
@@ -70,6 +72,18 @@ class MongoCriteriaConverter {
70
72
  [filter.field.value]: { $not: { $regex: filter.value.value } },
71
73
  };
72
74
  }
75
+ inFilter(filter) {
76
+ if (!filter.value.isPrimitiveArray) {
77
+ throw new Error("IN operator requires an array of primitive values");
78
+ }
79
+ return { [filter.field.value]: { $in: filter.value.asPrimitiveArray } };
80
+ }
81
+ notInFilter(filter) {
82
+ if (!filter.value.isPrimitiveArray) {
83
+ throw new Error("NOT_IN operator requires an array of primitive values");
84
+ }
85
+ return { [filter.field.value]: { $nin: filter.value.asPrimitiveArray } };
86
+ }
73
87
  orFilter(filter) {
74
88
  if (!filter.value.isOrConditions) {
75
89
  throw new Error("OR operator requires an array of OrCondition objects");
@@ -5,45 +5,41 @@ const MongoCriteriaConverter_1 = require("./MongoCriteriaConverter");
5
5
  const MongoClientFactory_1 = require("./MongoClientFactory");
6
6
  const mongodb_1 = require("mongodb");
7
7
  class MongoRepository {
8
- constructor() {
8
+ constructor(aggregateRootClass) {
9
+ this.aggregateRootClass = aggregateRootClass;
9
10
  this.criteriaConverter = new MongoCriteriaConverter_1.MongoCriteriaConverter();
10
11
  }
11
- // transformationToUpsertInSubDocuments(
12
- // subDocumentField: string,
13
- // primitiveData: any,
14
- // ): {} {
15
- // const response = {};
16
- //
17
- // for (const key in primitiveData) {
18
- // response[`${subDocumentField}.$.${key}`] = primitiveData[key];
19
- // }
20
- //
21
- // return response;
22
- // }
23
- async paginate(documents) {
12
+ /** Finds a single entity and hydrates it via the aggregate's fromPrimitives. */
13
+ async one(filter) {
24
14
  const collection = await this.collection();
25
- const count = await collection.countDocuments(this.query.filter);
26
- const limit = this.criteria?.limit || 10;
27
- const currentPage = this.criteria?.currentPage || 1;
28
- const hasNextPage = currentPage * limit < count;
29
- if (documents.length === 0) {
30
- return {
31
- nextPag: null,
32
- count: 0,
33
- results: [],
34
- };
15
+ const result = await collection.findOne(filter);
16
+ if (!result) {
17
+ return null;
35
18
  }
36
- return {
37
- nextPag: hasNextPage ? Number(this.criteria.currentPage) + 1 : null,
38
- count: count,
39
- results: documents,
40
- };
19
+ const { _id, ...rest } = result;
20
+ return this.aggregateRootClass.fromPrimitives(rest);
21
+ }
22
+ /** Upserts an aggregate by delegating to persist with its id. */
23
+ async upsert(entity) {
24
+ return this.persist(entity.getId(), entity);
25
+ }
26
+ /** Lists entities by criteria and returns a paginated response. */
27
+ async list(criteria, fieldsToExclude = []) {
28
+ const documents = await this.searchByCriteria(criteria, fieldsToExclude);
29
+ return this.paginate(documents);
41
30
  }
42
31
  async collection() {
43
32
  return (await MongoClientFactory_1.MongoClientFactory.createClient())
44
33
  .db()
45
34
  .collection(this.collectionName());
46
35
  }
36
+ async updateOne(filter, update) {
37
+ const collection = await this.collection();
38
+ const result = await collection.updateOne(filter, update, {
39
+ upsert: true,
40
+ });
41
+ return result.upsertedId;
42
+ }
47
43
  async persist(id, aggregateRoot) {
48
44
  let primitives;
49
45
  if (aggregateRoot.toPrimitives() instanceof Promise) {
@@ -59,13 +55,6 @@ class MongoRepository {
59
55
  },
60
56
  });
61
57
  }
62
- async updateOne(filter, update) {
63
- const collection = await this.collection();
64
- const result = await collection.updateOne(filter, update, {
65
- upsert: true,
66
- });
67
- return result.upsertedId;
68
- }
69
58
  async searchByCriteria(criteria, fieldsToExclude = []) {
70
59
  this.criteria = criteria;
71
60
  this.query = this.criteriaConverter.convert(criteria);
@@ -91,5 +80,24 @@ class MongoRepository {
91
80
  .toArray();
92
81
  return results.map(({ _id, ...rest }) => rest);
93
82
  }
83
+ async paginate(documents) {
84
+ const collection = await this.collection();
85
+ const count = await collection.countDocuments(this.query.filter);
86
+ const limit = this.criteria?.limit || 10;
87
+ const currentPage = this.criteria?.currentPage || 1;
88
+ const hasNextPage = currentPage * limit < count;
89
+ if (documents.length === 0) {
90
+ return {
91
+ nextPag: null,
92
+ count: 0,
93
+ results: [],
94
+ };
95
+ }
96
+ return {
97
+ nextPag: hasNextPage ? Number(this.criteria.currentPage) + 1 : null,
98
+ count: count,
99
+ results: documents,
100
+ };
101
+ }
94
102
  }
95
103
  exports.MongoRepository = MongoRepository;
@@ -0,0 +1,5 @@
1
+ export class AggregateRoot {
2
+ static fromPrimitives(_data) {
3
+ throw new Error("fromPrimitives must be implemented in subclasses");
4
+ }
5
+ }
@@ -0,0 +1,15 @@
1
+ export class Criteria {
2
+ constructor(filters, order, limit, offset) {
3
+ this.filters = filters;
4
+ this.order = order;
5
+ this.limit = limit;
6
+ this.currentPage = offset;
7
+ this.offset =
8
+ offset !== undefined && limit !== undefined
9
+ ? (offset - 1) * limit
10
+ : undefined;
11
+ }
12
+ hasFilters() {
13
+ return this.filters.filters.length > 0;
14
+ }
15
+ }
@@ -0,0 +1,20 @@
1
+ import { FilterField } from "./FilterField";
2
+ import { FilterOperator } from "./FilterOperator";
3
+ import { FilterValue } from "./FilterValue";
4
+ import { InvalidArgumentError } from "../exceptions/InvalidArgumentError.exception";
5
+ export class Filter {
6
+ constructor(field, operator, value) {
7
+ this.field = field;
8
+ this.operator = operator;
9
+ this.value = value;
10
+ }
11
+ static fromValues(values) {
12
+ const field = values.get("field");
13
+ const operator = values.get("operator");
14
+ const value = values.get("value");
15
+ if (!field || !operator || value === undefined) {
16
+ throw new InvalidArgumentError(`The filter is invalid`);
17
+ }
18
+ return new Filter(new FilterField(field), FilterOperator.fromValue(operator), new FilterValue(value));
19
+ }
20
+ }
@@ -0,0 +1,6 @@
1
+ import { StringValueObject } from "../valueObject";
2
+ export class FilterField extends StringValueObject {
3
+ constructor(value) {
4
+ super(value);
5
+ }
6
+ }
@@ -0,0 +1,34 @@
1
+ import { InvalidArgumentError } from "../exceptions/InvalidArgumentError.exception";
2
+ export var Operator;
3
+ (function (Operator) {
4
+ Operator["EQUAL"] = "=";
5
+ Operator["NOT_EQUAL"] = "!=";
6
+ Operator["GT"] = ">";
7
+ Operator["LT"] = "<";
8
+ Operator["CONTAINS"] = "CONTAINS";
9
+ Operator["NOT_CONTAINS"] = "NOT_CONTAINS";
10
+ Operator["GTE"] = ">=";
11
+ Operator["LTE"] = "<=";
12
+ Operator["BETWEEN"] = "BETWEEN";
13
+ Operator["OR"] = "OR";
14
+ Operator["IN"] = "IN";
15
+ Operator["NOT_IN"] = "NOT_IN";
16
+ })(Operator || (Operator = {}));
17
+ //export class FilterOperator extends EnumValueObject<Operator> {
18
+ export class FilterOperator {
19
+ constructor(value) {
20
+ this.value = value;
21
+ //super(value, Object.values(Operator));
22
+ }
23
+ static fromValue(value) {
24
+ for (const operatorValue of Object.values(Operator)) {
25
+ if (value === operatorValue.toString()) {
26
+ return new FilterOperator(operatorValue);
27
+ }
28
+ }
29
+ throw new InvalidArgumentError(`The filter operator ${value} is invalid`);
30
+ }
31
+ throwErrorForInvalidValue(value) {
32
+ throw new InvalidArgumentError(`The filter operator ${value} is invalid`);
33
+ }
34
+ }
@@ -0,0 +1,79 @@
1
+ import { StringValueObject } from "../valueObject";
2
+ const isOrConditionArray = (value) => {
3
+ if (!Array.isArray(value) || value.length === 0) {
4
+ return false;
5
+ }
6
+ const candidate = value[0];
7
+ return (typeof candidate === "object" &&
8
+ candidate !== null &&
9
+ "field" in candidate &&
10
+ "operator" in candidate &&
11
+ "value" in candidate);
12
+ };
13
+ const isBetweenObject = (value) => {
14
+ if (value === null || typeof value !== "object") {
15
+ return false;
16
+ }
17
+ const candidate = value;
18
+ const hasStartEnd = "start" in candidate && "end" in candidate;
19
+ const hasStartDateEndDate = "startDate" in candidate && "endDate" in candidate;
20
+ const hasFromTo = "from" in candidate && "to" in candidate;
21
+ return hasStartEnd || hasStartDateEndDate || hasFromTo;
22
+ };
23
+ const normalizeBetweenValue = (value) => {
24
+ if ("start" in value && "end" in value) {
25
+ return { start: value.start, end: value.end };
26
+ }
27
+ if ("startDate" in value && "endDate" in value) {
28
+ return { start: value.startDate, end: value.endDate };
29
+ }
30
+ return { start: value.from, end: value.to };
31
+ };
32
+ export class FilterValue extends StringValueObject {
33
+ constructor(value) {
34
+ if (Array.isArray(value) && isOrConditionArray(value)) {
35
+ // Handle OrCondition[]
36
+ super(JSON.stringify(value));
37
+ }
38
+ else if (Array.isArray(value)) {
39
+ // Handle primitive arrays (kept for backward compatibility)
40
+ super(value.join(","));
41
+ }
42
+ else if (isBetweenObject(value)) {
43
+ // Store serialized version but keep original reference
44
+ super(JSON.stringify(value));
45
+ }
46
+ else {
47
+ // Handle primitive values (string, number, boolean, Date)
48
+ super(value);
49
+ }
50
+ this._originalValue = value;
51
+ }
52
+ get isOrConditions() {
53
+ return isOrConditionArray(this._originalValue);
54
+ }
55
+ get asOrConditions() {
56
+ if (this.isOrConditions) {
57
+ return this._originalValue;
58
+ }
59
+ throw new Error("Value is not an OrCondition array");
60
+ }
61
+ get isBetween() {
62
+ return isBetweenObject(this._originalValue);
63
+ }
64
+ get asBetween() {
65
+ if (this.isBetween) {
66
+ return normalizeBetweenValue(this._originalValue);
67
+ }
68
+ throw new Error("Value is not a BETWEEN structure");
69
+ }
70
+ get isPrimitiveArray() {
71
+ return Array.isArray(this._originalValue) && !this.isOrConditions;
72
+ }
73
+ get asPrimitiveArray() {
74
+ if (this.isPrimitiveArray) {
75
+ return this._originalValue;
76
+ }
77
+ throw new Error("Value is not an array of primitive values");
78
+ }
79
+ }
@@ -0,0 +1,12 @@
1
+ import { Filter } from "./Filter";
2
+ export class Filters {
3
+ constructor(filters) {
4
+ this.filters = filters;
5
+ }
6
+ static fromValues(filters) {
7
+ return new Filters(filters.map((values) => Filter.fromValues(values)));
8
+ }
9
+ static none() {
10
+ return new Filters([]);
11
+ }
12
+ }
@@ -0,0 +1,26 @@
1
+ import { OrderBy } from "./OrderBy";
2
+ import { OrderType, OrderTypes } from "./OrderType";
3
+ export class Order {
4
+ constructor(orderBy, orderType) {
5
+ this.orderBy = orderBy;
6
+ this.orderType = orderType;
7
+ }
8
+ static fromValues(orderBy, orderType) {
9
+ if (!orderBy) {
10
+ return Order.none();
11
+ }
12
+ return new Order(new OrderBy(orderBy), OrderType.fromValue(orderType || OrderTypes.ASC));
13
+ }
14
+ static none() {
15
+ return new Order(new OrderBy(""), new OrderType(OrderTypes.NONE));
16
+ }
17
+ static desc(orderBy) {
18
+ return new Order(new OrderBy(orderBy), new OrderType(OrderTypes.DESC));
19
+ }
20
+ static asc(orderBy) {
21
+ return new Order(new OrderBy(orderBy), new OrderType(OrderTypes.ASC));
22
+ }
23
+ hasOrder() {
24
+ return !this.orderType.isNone();
25
+ }
26
+ }
@@ -0,0 +1,13 @@
1
+ import { ValueObject } from "../valueObject/ValueObject";
2
+ export class OrderBy extends ValueObject {
3
+ constructor(value) {
4
+ super(value);
5
+ this.value = value;
6
+ }
7
+ static create(value) {
8
+ return new OrderBy(value);
9
+ }
10
+ getValue() {
11
+ return this.value;
12
+ }
13
+ }
@@ -0,0 +1,30 @@
1
+ import { EnumValueObject } from "../valueObject";
2
+ import { InvalidArgumentError } from "../exceptions/InvalidArgumentError.exception";
3
+ export var OrderTypes;
4
+ (function (OrderTypes) {
5
+ OrderTypes["ASC"] = "asc";
6
+ OrderTypes["DESC"] = "desc";
7
+ OrderTypes["NONE"] = "none";
8
+ })(OrderTypes || (OrderTypes = {}));
9
+ export class OrderType extends EnumValueObject {
10
+ constructor(value) {
11
+ super(value, Object.values(OrderTypes));
12
+ }
13
+ static fromValue(value) {
14
+ for (const orderTypeValue of Object.values(OrderTypes)) {
15
+ if (value === orderTypeValue.toString()) {
16
+ return new OrderType(orderTypeValue);
17
+ }
18
+ }
19
+ throw new InvalidArgumentError(`The order type ${value} is invalid`);
20
+ }
21
+ isNone() {
22
+ return this.value === OrderTypes.NONE;
23
+ }
24
+ isAsc() {
25
+ return this.value === OrderTypes.ASC;
26
+ }
27
+ throwErrorForInvalidValue(value) {
28
+ throw new InvalidArgumentError(`The order type ${value} is invalid`);
29
+ }
30
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export class InvalidArgumentError extends Error {
2
+ }
@@ -0,0 +1,45 @@
1
+ import { MongoClient } from "mongodb";
2
+ export class MongoClientFactory {
3
+ /**
4
+ * Obtiene o crea una instancia de MongoClient.
5
+ * Reutiliza la conexión si ya existe.
6
+ */
7
+ static async createClient() {
8
+ if (!MongoClientFactory.client) {
9
+ MongoClientFactory.client =
10
+ await MongoClientFactory.createAndConnectClient();
11
+ }
12
+ return MongoClientFactory.client;
13
+ }
14
+ /**
15
+ * Cierra la conexión a MongoDB.
16
+ */
17
+ static async closeClient() {
18
+ if (MongoClientFactory.client) {
19
+ await MongoClientFactory.client.close();
20
+ MongoClientFactory.client = null; // Limpia la instancia
21
+ }
22
+ }
23
+ /**
24
+ * Crea y conecta una nueva instancia de MongoClient.
25
+ */
26
+ static async createAndConnectClient() {
27
+ const MONGO_PASS = process.env.MONGO_PASS;
28
+ const MONGO_USER = process.env.MONGO_USER;
29
+ const MONGO_DB = process.env.MONGO_DB;
30
+ const MONGO_SERVER = process.env.MONGO_SERVER;
31
+ if (!MONGO_PASS || !MONGO_USER || !MONGO_DB || !MONGO_SERVER) {
32
+ throw new Error("Missing MongoDB environment variables.");
33
+ }
34
+ const uri = `mongodb+srv://${MONGO_USER}:${MONGO_PASS}@${MONGO_SERVER}/${MONGO_DB}?retryWrites=true&w=majority`;
35
+ const client = new MongoClient(uri, { ignoreUndefined: true });
36
+ try {
37
+ await client.connect();
38
+ return client;
39
+ }
40
+ catch (error) {
41
+ throw error;
42
+ }
43
+ }
44
+ }
45
+ MongoClientFactory.client = null;
@@ -0,0 +1,125 @@
1
+ import { Operator } from "../criteria";
2
+ export class MongoCriteriaConverter {
3
+ constructor() {
4
+ this.filterTransformers = new Map([
5
+ [Operator.EQUAL, this.equalFilter],
6
+ [Operator.NOT_EQUAL, this.notEqualFilter],
7
+ [Operator.GT, this.greaterThanFilter],
8
+ [Operator.LT, this.lowerThanFilter],
9
+ [Operator.CONTAINS, this.containsFilter],
10
+ [Operator.NOT_CONTAINS, this.notContainsFilter],
11
+ [Operator.GTE, this.greaterThanOrEqualFilter],
12
+ [Operator.LTE, this.lowerThanOrEqualFilter],
13
+ [Operator.BETWEEN, this.betweenFilter],
14
+ [Operator.OR, this.orFilter],
15
+ [Operator.IN, this.inFilter],
16
+ [Operator.NOT_IN, this.notInFilter],
17
+ ]);
18
+ }
19
+ convert(criteria) {
20
+ return {
21
+ filter: criteria.hasFilters()
22
+ ? this.generateFilter(criteria.filters)
23
+ : {},
24
+ sort: criteria.order.hasOrder()
25
+ ? this.generateSort(criteria.order)
26
+ : { _id: -1 },
27
+ skip: criteria.offset || 0,
28
+ limit: criteria.limit || 0,
29
+ };
30
+ }
31
+ generateFilter(filters) {
32
+ const filter = filters.filters.map((filter) => {
33
+ const transformer = this.filterTransformers.get(filter.operator.value);
34
+ if (!transformer) {
35
+ throw Error(`Unexpected operator value ${filter.operator.value}`);
36
+ }
37
+ return transformer(filter);
38
+ });
39
+ return Object.assign({}, ...filter);
40
+ }
41
+ generateSort(order) {
42
+ return {
43
+ [order.orderBy.value === "id" ? "_id" : order.orderBy.value]: order.orderType.isAsc() ? 1 : -1,
44
+ };
45
+ }
46
+ equalFilter(filter) {
47
+ return { [filter.field.value]: { $eq: filter.value.value } };
48
+ }
49
+ notEqualFilter(filter) {
50
+ return { [filter.field.value]: { $ne: filter.value.value } };
51
+ }
52
+ greaterThanFilter(filter) {
53
+ return { [filter.field.value]: { $gt: filter.value.value } };
54
+ }
55
+ greaterThanOrEqualFilter(filter) {
56
+ return { [filter.field.value]: { $gte: filter.value.value } };
57
+ }
58
+ lowerThanOrEqualFilter(filter) {
59
+ return { [filter.field.value]: { $lte: filter.value.value } };
60
+ }
61
+ lowerThanFilter(filter) {
62
+ return { [filter.field.value]: { $lt: filter.value.value } };
63
+ }
64
+ containsFilter(filter) {
65
+ return { [filter.field.value]: { $regex: filter.value.value } };
66
+ }
67
+ notContainsFilter(filter) {
68
+ return {
69
+ [filter.field.value]: { $not: { $regex: filter.value.value } },
70
+ };
71
+ }
72
+ inFilter(filter) {
73
+ if (!filter.value.isPrimitiveArray) {
74
+ throw new Error("IN operator requires an array of primitive values");
75
+ }
76
+ return { [filter.field.value]: { $in: filter.value.asPrimitiveArray } };
77
+ }
78
+ notInFilter(filter) {
79
+ if (!filter.value.isPrimitiveArray) {
80
+ throw new Error("NOT_IN operator requires an array of primitive values");
81
+ }
82
+ return { [filter.field.value]: { $nin: filter.value.asPrimitiveArray } };
83
+ }
84
+ orFilter(filter) {
85
+ if (!filter.value.isOrConditions) {
86
+ throw new Error("OR operator requires an array of OrCondition objects");
87
+ }
88
+ const conditions = filter.value.asOrConditions;
89
+ const orConditions = conditions.map((condition) => {
90
+ switch (condition.operator) {
91
+ case Operator.CONTAINS:
92
+ return { [condition.field]: { $regex: condition.value } };
93
+ case Operator.EQUAL:
94
+ return { [condition.field]: { $eq: condition.value } };
95
+ case Operator.NOT_EQUAL:
96
+ return { [condition.field]: { $ne: condition.value } };
97
+ case Operator.GT:
98
+ return { [condition.field]: { $gt: condition.value } };
99
+ case Operator.LT:
100
+ return { [condition.field]: { $lt: condition.value } };
101
+ case Operator.GTE:
102
+ return { [condition.field]: { $gte: condition.value } };
103
+ case Operator.LTE:
104
+ return { [condition.field]: { $lte: condition.value } };
105
+ case Operator.NOT_CONTAINS:
106
+ return { [condition.field]: { $not: { $regex: condition.value } } };
107
+ default:
108
+ throw new Error(`Unsupported operator in OR condition: ${condition.operator}`);
109
+ }
110
+ });
111
+ return { $or: orConditions };
112
+ }
113
+ betweenFilter(filter) {
114
+ if (!filter.value.isBetween) {
115
+ throw new Error("BETWEEN operator requires an object with start and end values.");
116
+ }
117
+ const { start, end } = filter.value.asBetween;
118
+ return {
119
+ [filter.field.value]: {
120
+ $gte: start,
121
+ $lte: end,
122
+ },
123
+ };
124
+ }
125
+ }
@@ -0,0 +1,99 @@
1
+ import { MongoCriteriaConverter } from "./MongoCriteriaConverter";
2
+ import { MongoClientFactory } from "./MongoClientFactory";
3
+ import { ObjectId } from "mongodb";
4
+ export class MongoRepository {
5
+ constructor(aggregateRootClass) {
6
+ this.aggregateRootClass = aggregateRootClass;
7
+ this.criteriaConverter = new MongoCriteriaConverter();
8
+ }
9
+ /** Finds a single entity and hydrates it via the aggregate's fromPrimitives. */
10
+ async one(filter) {
11
+ const collection = await this.collection();
12
+ const result = await collection.findOne(filter);
13
+ if (!result) {
14
+ return null;
15
+ }
16
+ const { _id, ...rest } = result;
17
+ return this.aggregateRootClass.fromPrimitives(rest);
18
+ }
19
+ /** Upserts an aggregate by delegating to persist with its id. */
20
+ async upsert(entity) {
21
+ return this.persist(entity.getId(), entity);
22
+ }
23
+ /** Lists entities by criteria and returns a paginated response. */
24
+ async list(criteria, fieldsToExclude = []) {
25
+ const documents = await this.searchByCriteria(criteria, fieldsToExclude);
26
+ return this.paginate(documents);
27
+ }
28
+ async collection() {
29
+ return (await MongoClientFactory.createClient())
30
+ .db()
31
+ .collection(this.collectionName());
32
+ }
33
+ async updateOne(filter, update) {
34
+ const collection = await this.collection();
35
+ const result = await collection.updateOne(filter, update, {
36
+ upsert: true,
37
+ });
38
+ return result.upsertedId;
39
+ }
40
+ async persist(id, aggregateRoot) {
41
+ let primitives;
42
+ if (aggregateRoot.toPrimitives() instanceof Promise) {
43
+ primitives = await aggregateRoot.toPrimitives();
44
+ }
45
+ else {
46
+ primitives = aggregateRoot.toPrimitives();
47
+ }
48
+ return await this.updateOne({ _id: new ObjectId(id) }, {
49
+ $set: {
50
+ ...primitives,
51
+ id: id,
52
+ },
53
+ });
54
+ }
55
+ async searchByCriteria(criteria, fieldsToExclude = []) {
56
+ this.criteria = criteria;
57
+ this.query = this.criteriaConverter.convert(criteria);
58
+ const collection = await this.collection();
59
+ if (fieldsToExclude.length === 0) {
60
+ const results = await collection
61
+ .find(this.query.filter, {})
62
+ .sort(this.query.sort)
63
+ .skip(this.query.skip)
64
+ .limit(this.query.limit)
65
+ .toArray();
66
+ return results.map(({ _id, ...rest }) => rest);
67
+ }
68
+ const projection = {};
69
+ fieldsToExclude.forEach((field) => {
70
+ projection[field] = 0;
71
+ });
72
+ const results = await collection
73
+ .find(this.query.filter, { projection })
74
+ .sort(this.query.sort)
75
+ .skip(this.query.skip)
76
+ .limit(this.query.limit)
77
+ .toArray();
78
+ return results.map(({ _id, ...rest }) => rest);
79
+ }
80
+ async paginate(documents) {
81
+ const collection = await this.collection();
82
+ const count = await collection.countDocuments(this.query.filter);
83
+ const limit = this.criteria?.limit || 10;
84
+ const currentPage = this.criteria?.currentPage || 1;
85
+ const hasNextPage = currentPage * limit < count;
86
+ if (documents.length === 0) {
87
+ return {
88
+ nextPag: null,
89
+ count: 0,
90
+ results: [],
91
+ };
92
+ }
93
+ return {
94
+ nextPag: hasNextPage ? Number(this.criteria.currentPage) + 1 : null,
95
+ count: count,
96
+ results: documents,
97
+ };
98
+ }
99
+ }
@@ -0,0 +1,12 @@
1
+ export class EnumValueObject {
2
+ constructor(value, validValues) {
3
+ this.validValues = validValues;
4
+ this.value = value;
5
+ this.checkValueIsValid(value);
6
+ }
7
+ checkValueIsValid(value) {
8
+ if (!this.validValues.includes(value)) {
9
+ this.throwErrorForInvalidValue(value);
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,20 @@
1
+ import { ValueObject } from "./ValueObject";
2
+ import { InvalidArgumentError } from "../exceptions/InvalidArgumentError.exception";
3
+ export class StringValueObject extends ValueObject {
4
+ constructor(value) {
5
+ super(value);
6
+ this.value = value;
7
+ this.ensureStringIsNotEmpty();
8
+ }
9
+ static create(value) {
10
+ return new StringValueObject(value);
11
+ }
12
+ getValue() {
13
+ return String(this.value);
14
+ }
15
+ ensureStringIsNotEmpty() {
16
+ if (typeof this.value !== "object" && this.value.length < 1) {
17
+ throw new InvalidArgumentError("String should have a length");
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,19 @@
1
+ import { InvalidArgumentError } from "../exceptions/InvalidArgumentError.exception";
2
+ export class ValueObject {
3
+ constructor(value) {
4
+ this.value = value;
5
+ this.ensureValueIsDefined(value);
6
+ }
7
+ equals(other) {
8
+ return (other.constructor.name === this.constructor.name &&
9
+ other.value === this.value);
10
+ }
11
+ toString() {
12
+ return this.value.toString();
13
+ }
14
+ ensureValueIsDefined(value) {
15
+ if (value === undefined) {
16
+ throw new InvalidArgumentError("Value must be defined");
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ export declare abstract class AggregateRoot {
2
+ abstract getId(): string;
3
+ abstract toPrimitives(): any;
4
+ static fromPrimitives(_data: any): AggregateRoot;
5
+ }
6
+ export type AggregateRootClass<T extends AggregateRoot> = {
7
+ fromPrimitives(data: any): T;
8
+ };
@@ -8,7 +8,9 @@ export declare enum Operator {
8
8
  GTE = ">=",
9
9
  LTE = "<=",
10
10
  BETWEEN = "BETWEEN",
11
- OR = "OR"
11
+ OR = "OR",
12
+ IN = "IN",
13
+ NOT_IN = "NOT_IN"
12
14
  }
13
15
  export declare class FilterOperator {
14
16
  value: Operator;
@@ -27,4 +27,6 @@ export declare class FilterValue extends StringValueObject {
27
27
  start: FilterPrimitive;
28
28
  end: FilterPrimitive;
29
29
  };
30
+ get isPrimitiveArray(): boolean;
31
+ get asPrimitiveArray(): FilterPrimitive[];
30
32
  }
@@ -0,0 +1,10 @@
1
+ export * from "./FilterField";
2
+ export * from "./FilterOperator";
3
+ export * from "./FilterValue";
4
+ export * from "./OrderBy";
5
+ export * from "./Filter";
6
+ export * from "./Criteria";
7
+ export * from "./Filters";
8
+ export * from "./Order";
9
+ export * from "./Paginate";
10
+ export * from "./OrderType";
@@ -0,0 +1,3 @@
1
+ export * from "./criteria";
2
+ export * from "./mongo";
3
+ export * from "./AggregateRoot";
@@ -1,8 +1,9 @@
1
1
  import { Criteria, Filters, Order } from "../criteria";
2
- type MongoFilterOperator = "$eq" | "$ne" | "$gt" | "$lt" | "$regex" | "$lte" | "$gte" | "$or";
2
+ type MongoFilterOperator = "$eq" | "$ne" | "$gt" | "$lt" | "$regex" | "$lte" | "$gte" | "$or" | "$in" | "$nin";
3
3
  type MongoFilterValue = boolean | string | number | Date;
4
+ type MongoFilterArrayValue = MongoFilterValue[];
4
5
  type MongoFilterOperation = {
5
- [operator in MongoFilterOperator]?: MongoFilterValue;
6
+ [operator in MongoFilterOperator]?: MongoFilterValue | MongoFilterArrayValue;
6
7
  };
7
8
  type MongoFilter = {
8
9
  [field: string]: MongoFilterOperation;
@@ -37,6 +38,8 @@ export declare class MongoCriteriaConverter {
37
38
  private lowerThanFilter;
38
39
  private containsFilter;
39
40
  private notContainsFilter;
41
+ private inFilter;
42
+ private notInFilter;
40
43
  private orFilter;
41
44
  private betweenFilter;
42
45
  }
@@ -0,0 +1,22 @@
1
+ import { Criteria, Paginate } from "../criteria";
2
+ import { AggregateRoot, AggregateRootClass } from "../AggregateRoot";
3
+ import { Collection, ObjectId, UpdateFilter } from "mongodb";
4
+ export declare abstract class MongoRepository<T extends AggregateRoot> {
5
+ private readonly aggregateRootClass;
6
+ private criteriaConverter;
7
+ private query;
8
+ private criteria;
9
+ protected constructor(aggregateRootClass: AggregateRootClass<T>);
10
+ abstract collectionName(): string;
11
+ /** Finds a single entity and hydrates it via the aggregate's fromPrimitives. */
12
+ one(filter: object): Promise<T | null>;
13
+ /** Upserts an aggregate by delegating to persist with its id. */
14
+ upsert(entity: T): Promise<ObjectId | null>;
15
+ /** Lists entities by criteria and returns a paginated response. */
16
+ list<D>(criteria: Criteria, fieldsToExclude?: string[]): Promise<Paginate<D>>;
17
+ protected collection<T extends Document>(): Promise<Collection<T>>;
18
+ protected updateOne(filter: object, update: Document[] | UpdateFilter<any>): Promise<ObjectId | null>;
19
+ private persist;
20
+ private searchByCriteria;
21
+ private paginate;
22
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./MongoCriteriaConverter";
2
+ export * from "./MongoRepository";
3
+ export * from "./MongoClientFactory";
@@ -0,0 +1,3 @@
1
+ export * from "./EnumValueObject";
2
+ export * from "./StringValueObject";
3
+ export * from "./ValueObject";
package/package.json CHANGED
@@ -1,15 +1,27 @@
1
1
  {
2
2
  "name": "@abejarano/ts-mongodb-criteria",
3
3
  "author": "angel bejarano / angel.bejarano@jaspesoft.com",
4
- "version": "1.2.1",
4
+ "version": "1.4.0",
5
5
  "description": "Patrón Criteria para consultas MongoDB en TypeScript",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
6
+ "main": "dist/cjs/index.js",
7
+ "module": "dist/esm/index.js",
8
+ "types": "dist/types/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "import": "./dist/esm/index.js",
13
+ "require": "./dist/cjs/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
8
17
  "files": [
9
18
  "dist"
10
19
  ],
11
20
  "scripts": {
12
- "build": "tsc -p tsconfig.json",
21
+ "build": "yarn build:cjs && yarn build:esm && yarn build:types",
22
+ "build:cjs": "tsc -p tsconfig.cjs.json",
23
+ "build:esm": "tsc -p tsconfig.esm.json",
24
+ "build:types": "tsc -p tsconfig.types.json",
13
25
  "clean": "rimraf dist",
14
26
  "prepare": "yarn build",
15
27
  "prebuild": "yarn clean",
@@ -22,7 +34,8 @@
22
34
  "version:major": "npm version major",
23
35
  "test": "jest",
24
36
  "test:watch": "jest --watch",
25
- "test:coverage": "jest --coverage"
37
+ "test:coverage": "jest --coverage",
38
+ "test:bun": "bun test tests/bun"
26
39
  },
27
40
  "keywords": [
28
41
  "mongodb",
@@ -1,4 +0,0 @@
1
- export declare abstract class AggregateRoot {
2
- abstract getId(): string;
3
- abstract toPrimitives(): any;
4
- }
@@ -1,15 +0,0 @@
1
- import { Criteria, Paginate } from "../criteria";
2
- import { AggregateRoot } from "../AggregateRoot";
3
- import { Collection, ObjectId, UpdateFilter } from "mongodb";
4
- export declare abstract class MongoRepository<T extends AggregateRoot> {
5
- private criteriaConverter;
6
- private query;
7
- private criteria;
8
- protected constructor();
9
- abstract collectionName(): string;
10
- paginate<T>(documents: T[]): Promise<Paginate<T>>;
11
- protected collection<T extends Document>(): Promise<Collection<T>>;
12
- protected persist(id: string, aggregateRoot: T): Promise<ObjectId | null>;
13
- protected updateOne(filter: object, update: Document[] | UpdateFilter<any>): Promise<ObjectId | null>;
14
- protected searchByCriteria<D>(criteria: Criteria, fieldsToExclude?: string[]): Promise<D[]>;
15
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes