@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 +13 -8
- package/dist/cjs/AggregateRoot.js +11 -2
- package/dist/cjs/mongo/MongoRepository.js +38 -16
- package/dist/cjs/types/index.js +2 -0
- package/dist/esm/AggregateRoot.js +11 -2
- package/dist/esm/mongo/MongoRepository.js +38 -16
- package/dist/esm/types/index.js +1 -0
- package/dist/types/AggregateRoot.d.ts +7 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mongo/IRepository.d.ts +6 -0
- package/dist/types/mongo/MongoCriteriaConverter.d.ts +1 -20
- package/dist/types/mongo/MongoRepository.d.ts +6 -1
- package/dist/types/types/index.d.ts +25 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://www.mongodb.com/)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
[](https://nodejs.org/)
|
|
9
|
-
[](#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
|
-
- `
|
|
186
|
-
- `
|
|
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
|
|
202
|
-
|
|
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 `
|
|
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 (
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export class AggregateRoot {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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:
|
|
9
|
+
fromPrimitives(data: Record<string, unknown> & {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
}): T;
|
|
8
12
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|