Skip to content

Commit 4e10044

Browse files
authored
Merge pull request #93 from monarch-orm/immutable-collection
Add immutable query builders, options types, and aggregate improvements
2 parents 351cc07 + 29d7e0c commit 4e10044

24 files changed

Lines changed: 634 additions & 166 deletions

.changeset/afraid-cars-reply.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"monarch-orm": minor
3+
---
4+
5+
Make query builder methods immutable — each method returns a new instance instead of mutating.
6+

README.md

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,8 @@ const userSchema = createSchema("users", {
3737

3838
const schemas = defineSchemas({ userSchema });
3939

40-
// Create and connect the MongoDB client.
40+
// Create a MongoDB client.
4141
const client = createClient(process.env.MONGODB_URI!);
42-
await client.connect();
4342

4443
// Create a database instance.
4544
const db = createDatabase(client.db("app"), schemas);
@@ -469,8 +468,6 @@ Deletes one document by `_id` and returns it.
469468
const deleted = await db.collections.users.findByIdAndDelete("67f0123456789abcdef0123");
470469
```
471470

472-
### Other Collection Methods
473-
474471
### `distinct(key, filter?)`
475472

476473
Returns a query for the distinct values of a field.
@@ -518,13 +515,19 @@ Returns MongoDB's estimated document count for the collection.
518515
const totalCount = await db.collections.users.estimatedDocumentCount();
519516
```
520517

521-
### `aggregate()`
518+
### `aggregate(pipeline?)`
522519

523-
Builds an aggregation pipeline.
520+
Builds an aggregation pipeline. Accepts an optional pipeline, and additional stages can be appended with `addStage()`.
524521

525522
```ts
526523
const result = await db.collections.users
527-
.aggregate()
524+
.aggregate<{ count: number }>([
525+
{ $match: { isVerified: true } },
526+
{ $group: { _id: "$isVerified", count: { $sum: 1 } } },
527+
]);
528+
529+
const result = await db.collections.users
530+
.aggregate<{ count: number }>()
528531
.addStage({ $match: { isVerified: true } })
529532
.addStage({ $group: { _id: "$isVerified", count: { $sum: 1 } } });
530533
```
@@ -537,19 +540,20 @@ Returns the underlying MongoDB collection.
537540
const rawUsers = await db.collections.users.raw().find().toArray();
538541
```
539542

540-
Queries are lazy, so you can build and reuse them before execution. They run only when you `await` them or call a promise method like `.then()`, `.catch()`, or `.finally()`.
543+
Queries are lazy and immutable — each builder method returns a new query instance, leaving the original unchanged. Queries run only when you `await` them or call a promise method like `.then()`, `.catch()`, or `.finally()`.
541544

542545
```ts
543-
let verifiedUsersQuery = db.collections.users
546+
// Each builder method returns a new query — the original is never modified.
547+
const base = db.collections.users
544548
.find({ isVerified: true })
545-
.omit({ age: true })
546-
.sort({ email: "asc" });
549+
.omit({ age: true });
547550

548-
if (limitResults) {
549-
verifiedUsersQuery = verifiedUsersQuery.limit(10);
550-
}
551+
const sorted = base.sort({ email: "asc" }); // new instance
552+
const limited = base.limit(10); // new instance from base, no sort
551553

552-
const verifiedUsers = await verifiedUsersQuery;
554+
const sortedUsers = await sorted; // sorted, no limit
555+
const limitedUsers = await limited; // no sort, limited to 10
556+
const allVerified = await base; // unchanged
553557
```
554558

555559
### Schema features

src/collection/collection.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
type Abortable,
23
type AnyBulkWriteOperation,
34
type CountDocumentsOptions,
45
type Db,
@@ -30,6 +31,7 @@ import { InsertOneQuery } from "./query/insert-one";
3031
import { ReplaceOneQuery } from "./query/replace-one";
3132
import { UpdateManyQuery } from "./query/update-many";
3233
import { UpdateOneQuery } from "./query/update-one";
34+
import type { PipelineStage } from "./types/pipeline-stage";
3335

3436
/**
3537
* Collection interface for MongoDB operations.
@@ -119,7 +121,7 @@ export class Collection<TSchema extends AnySchema, TRelations extends Record<str
119121
* @param update - Update operations
120122
* @returns FindOneAndUpdateQuery instance
121123
*/
122-
public findByIdAndUpdate(id: Index<InferSchemaInput<TSchema>, "_id">, update: UpdateFilter<TSchema> | Document[]) {
124+
public findByIdAndUpdate(id: Index<InferSchemaInput<TSchema>, "_id">, update: UpdateFilter<TSchema>) {
123125
const _idType = Schema.types(this.schema)._id;
124126
const isObjectIdType = MonarchType.isInstanceOf(_idType, MonarchObjectId);
125127

@@ -180,7 +182,7 @@ export class Collection<TSchema extends AnySchema, TRelations extends Record<str
180182
* @param update - Update operations
181183
* @returns FindOneAndUpdateQuery instance
182184
*/
183-
public findOneAndUpdate(filter: Filter<TSchema>, update: UpdateFilter<TSchema> | Document[]) {
185+
public findOneAndUpdate(filter: Filter<TSchema>, update: UpdateFilter<TSchema>) {
184186
return new FindOneAndUpdateQuery(this.schema, this.collection, this.readyPromise, filter, update);
185187
}
186188

@@ -242,7 +244,7 @@ export class Collection<TSchema extends AnySchema, TRelations extends Record<str
242244
* @param update - Update operations
243245
* @returns UpdateOneQuery instance
244246
*/
245-
public updateOne(filter: Filter<TSchema>, update: UpdateFilter<TSchema> | Document[]) {
247+
public updateOne(filter: Filter<TSchema>, update: UpdateFilter<TSchema>) {
246248
return new UpdateOneQuery(this.schema, this.collection, this.readyPromise, filter, update);
247249
}
248250

@@ -253,7 +255,7 @@ export class Collection<TSchema extends AnySchema, TRelations extends Record<str
253255
* @param update - Update operations
254256
* @returns UpdateManyQuery instance
255257
*/
256-
public updateMany(filter: Filter<TSchema>, update: UpdateFilter<TSchema> | Document[]) {
258+
public updateMany(filter: Filter<TSchema>, update: UpdateFilter<TSchema>) {
257259
return new UpdateManyQuery(this.schema, this.collection, this.readyPromise, filter, update);
258260
}
259261

@@ -282,8 +284,8 @@ export class Collection<TSchema extends AnySchema, TRelations extends Record<str
282284
*
283285
* @returns AggregationPipeline instance
284286
*/
285-
public aggregate<TOutput extends any[]>() {
286-
return new AggregationPipeline<TSchema, TOutput[]>(this.schema, this.collection, this.readyPromise);
287+
public aggregate<TOutput extends Document>(pipeline: PipelineStage<InferSchemaData<TSchema>>[] = []) {
288+
return new AggregationPipeline<TSchema, TOutput>(this.schema, this.collection, this.readyPromise, {}, pipeline);
287289
}
288290

289291
/**
@@ -293,7 +295,7 @@ export class Collection<TSchema extends AnySchema, TRelations extends Record<str
293295
* @param options - Count options
294296
* @returns Promise resolving to document count
295297
*/
296-
public async countDocuments(filter: Filter<TSchema> = {}, options?: CountDocumentsOptions) {
298+
public async countDocuments(filter: Filter<TSchema> = {}, options?: CountDocumentsOptions & Abortable) {
297299
return await this.collection.countDocuments(filter as MongoFilter<InferSchemaData<TSchema>>, options);
298300
}
299301

src/collection/pipeline/aggregation.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,55 @@
1-
import type { AggregateOptions, AggregationCursor, Collection as MongoCollection } from "mongodb";
1+
import type { Abortable, AggregateOptions, AggregationCursor, Document, Collection as MongoCollection } from "mongodb";
22
import type { AnySchema } from "../../schema/schema";
33
import type { InferSchemaData } from "../../schema/type-helpers";
4+
import type { PipelineStage } from "../types/pipeline-stage";
45
import { Pipeline } from "./base";
56

7+
export type AggregationPipelineOptions = AggregateOptions & Abortable;
8+
69
/**
710
* Collection.aggregate().
811
*/
9-
export class AggregationPipeline<TSchema extends AnySchema, TOutput extends any[]> extends Pipeline<TSchema, TOutput> {
12+
export class AggregationPipeline<TSchema extends AnySchema, TOutput extends Document> extends Pipeline<
13+
TSchema,
14+
TOutput
15+
> {
1016
constructor(
1117
schema: TSchema,
1218
collection: MongoCollection<InferSchemaData<TSchema>>,
1319
readyPromise: Promise<void>,
1420
private _options: AggregateOptions = {},
21+
pipeline: PipelineStage<InferSchemaData<TSchema>>[] = [],
1522
) {
16-
super(schema, collection, readyPromise);
23+
super(schema, collection, readyPromise, pipeline);
24+
}
25+
26+
/**
27+
* Appends aggregation pipeline stage.
28+
*
29+
* @param stage - Pipeline stage
30+
* @returns AggregationPipeline instance
31+
*/
32+
public addStage(stage: PipelineStage<InferSchemaData<TSchema>>): this {
33+
return new AggregationPipeline(this.schema, this.collection, this.readyPromise, this._options, [
34+
...this.pipeline,
35+
stage,
36+
]) as this;
1737
}
1838

1939
/**
2040
* Adds aggregation options. Options are merged into existing options.
2141
*
22-
* @param options - AggregateOptions
42+
* @param options - AggregationPipelineOptions
2343
* @returns AggregationPipeline instance
2444
*/
25-
public options(options: AggregateOptions): this {
26-
Object.assign(this._options, options);
27-
return this;
45+
public options(options: AggregationPipelineOptions): this {
46+
return new AggregationPipeline(
47+
this.schema,
48+
this.collection,
49+
this.readyPromise,
50+
{ ...this._options, ...options },
51+
this.pipeline,
52+
) as this;
2853
}
2954

3055
/**

src/collection/pipeline/base.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
1-
import type { Collection as MongoCollection } from "mongodb";
1+
import type { Document, Collection as MongoCollection } from "mongodb";
22
import type { AnySchema } from "../../schema/schema";
33
import type { InferSchemaData } from "../../schema/type-helpers";
44
import type { PipelineStage } from "../types/pipeline-stage";
55

66
/**
77
* Base aggregation pipeline class implementing thenable interface.
88
*/
9-
export abstract class Pipeline<TSchema extends AnySchema, TOutput> {
9+
export abstract class Pipeline<TSchema extends AnySchema, TOutput extends Document> {
1010
constructor(
1111
protected schema: TSchema,
1212
protected collection: MongoCollection<InferSchemaData<TSchema>>,
1313
protected readyPromise: Promise<void>,
1414
protected pipeline: PipelineStage<InferSchemaData<TSchema>>[] = [],
1515
) {}
1616

17-
/**
18-
* Appends aggregation pipeline stage.
19-
*
20-
* @param stage - Pipeline stage
21-
* @returns Pipeline instance
22-
*/
23-
public addStage(stage: PipelineStage<InferSchemaData<TSchema>>): this {
24-
this.pipeline.push(stage);
25-
return this;
26-
}
27-
2817
protected abstract exec(): Promise<TOutput[]>;
2918

3019
public async then<TResult1 = TOutput[], TResult2 = never>(

src/collection/query/bulk-write.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@ import type { AnySchema } from "../../schema/schema";
33
import type { InferSchemaData } from "../../schema/type-helpers";
44
import { Query } from "./base";
55

6+
export type BulkWriteQueryOptions = BulkWriteOptions;
7+
68
export class BulkWriteQuery<TSchema extends AnySchema> extends Query<TSchema, BulkWriteResult> {
79
constructor(
810
schema: TSchema,
911
collection: MongoCollection<InferSchemaData<TSchema>>,
1012
readyPromise: Promise<void>,
1113
private _data: AnyBulkWriteOperation<InferSchemaData<TSchema>>[],
12-
private _options: BulkWriteOptions = {},
14+
private _options: BulkWriteQueryOptions = {},
1315
) {
1416
super(schema, collection, readyPromise);
1517
}
1618

17-
public options(options: BulkWriteOptions): this {
18-
Object.assign(this._options, options);
19-
return this;
19+
public options(options: BulkWriteQueryOptions): this {
20+
return new BulkWriteQuery(this.schema, this.collection, this.readyPromise, this._data, {
21+
...this._options,
22+
...options,
23+
}) as this;
2024
}
2125

2226
protected async exec(): Promise<BulkWriteResult> {

src/collection/query/delete-many.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { AnySchema } from "../../schema/schema";
33
import type { Filter, InferSchemaData } from "../../schema/type-helpers";
44
import { Query } from "./base";
55

6+
export type DeleteManyQueryOptions = DeleteOptions;
7+
68
/**
79
* Collection.deleteMany().
810
*/
@@ -12,20 +14,22 @@ export class DeleteManyQuery<TSchema extends AnySchema> extends Query<TSchema, D
1214
collection: MongoCollection<InferSchemaData<TSchema>>,
1315
readyPromise: Promise<void>,
1416
private _filter: Filter<TSchema>,
15-
private _options: DeleteOptions = {},
17+
private _options: DeleteManyQueryOptions = {},
1618
) {
1719
super(schema, collection, readyPromise);
1820
}
1921

2022
/**
2123
* Adds delete options. Options are merged into existing options.
2224
*
23-
* @param options - DeleteOptions
25+
* @param options - DeleteManyQueryOptions
2426
* @returns DeleteManyQuery instance
2527
*/
26-
public options(options: DeleteOptions): this {
27-
Object.assign(this._options, options);
28-
return this;
28+
public options(options: DeleteManyQueryOptions): this {
29+
return new DeleteManyQuery(this.schema, this.collection, this.readyPromise, this._filter, {
30+
...this._options,
31+
...options,
32+
}) as this;
2933
}
3034

3135
protected async exec(): Promise<DeleteResult> {

src/collection/query/delete-one.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { AnySchema } from "../../schema/schema";
33
import type { Filter, InferSchemaData } from "../../schema/type-helpers";
44
import { Query } from "./base";
55

6+
export type DeleteOneQueryOptions = DeleteOptions;
7+
68
/**
79
* Collection.deleteOne().
810
*/
@@ -12,20 +14,22 @@ export class DeleteOneQuery<TSchema extends AnySchema> extends Query<TSchema, De
1214
collection: MongoCollection<InferSchemaData<TSchema>>,
1315
readyPromise: Promise<void>,
1416
private _filter: Filter<TSchema>,
15-
private _options: DeleteOptions = {},
17+
private _options: DeleteOneQueryOptions = {},
1618
) {
1719
super(schema, collection, readyPromise);
1820
}
1921

2022
/**
2123
* Adds delete options. Options are merged into existing options.
2224
*
23-
* @param options - DeleteOptions
25+
* @param options - DeleteOneQueryOptions
2426
* @returns DeleteOneQuery instance
2527
*/
26-
public options(options: DeleteOptions): this {
27-
Object.assign(this._options, options);
28-
return this;
28+
public options(options: DeleteOneQueryOptions): this {
29+
return new DeleteOneQuery(this.schema, this.collection, this.readyPromise, this._filter, {
30+
...this._options,
31+
...options,
32+
}) as this;
2933
}
3034

3135
protected async exec(): Promise<DeleteResult> {

src/collection/query/distinct.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { AnySchema } from "../../schema/schema";
33
import type { DistinctFilter, Filter, InferSchemaData } from "../../schema/type-helpers";
44
import { Query } from "./base";
55

6+
export type DistinctQueryOptions = DistinctOptions;
7+
68
export class DistinctQuery<
79
TSchema extends AnySchema,
810
Key extends keyof DistinctFilter<TSchema>,
@@ -14,14 +16,16 @@ export class DistinctQuery<
1416
readyPromise: Promise<void>,
1517
private _filter: Filter<TSchema>,
1618
private _key: Key,
17-
private _options: DistinctOptions = {},
19+
private _options: DistinctQueryOptions = {},
1820
) {
1921
super(schema, collection, readyPromise);
2022
}
2123

22-
public options(options: DistinctOptions): this {
23-
Object.assign(this._options, options);
24-
return this;
24+
public options(options: DistinctQueryOptions): this {
25+
return new DistinctQuery(this.schema, this.collection, this.readyPromise, this._filter, this._key, {
26+
...this._options,
27+
...options,
28+
}) as this;
2529
}
2630

2731
protected async exec(): Promise<TOutput> {

0 commit comments

Comments
 (0)