Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/afraid-cars-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"monarch-orm": minor
---

Make query builder methods immutable — each method returns a new instance instead of mutating.

34 changes: 19 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ const userSchema = createSchema("users", {

const schemas = defineSchemas({ userSchema });

// Create and connect the MongoDB client.
// Create a MongoDB client.
const client = createClient(process.env.MONGODB_URI!);
await client.connect();

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

### Other Collection Methods

### `distinct(key, filter?)`

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

### `aggregate()`
### `aggregate(pipeline?)`

Builds an aggregation pipeline.
Builds an aggregation pipeline. Accepts an optional pipeline, and additional stages can be appended with `addStage()`.

```ts
const result = await db.collections.users
.aggregate()
.aggregate<{ count: number }>([
{ $match: { isVerified: true } },
{ $group: { _id: "$isVerified", count: { $sum: 1 } } },
]);

const result = await db.collections.users
.aggregate<{ count: number }>()
.addStage({ $match: { isVerified: true } })
.addStage({ $group: { _id: "$isVerified", count: { $sum: 1 } } });
```
Expand All @@ -537,19 +540,20 @@ Returns the underlying MongoDB collection.
const rawUsers = await db.collections.users.raw().find().toArray();
```

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()`.
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()`.

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

if (limitResults) {
verifiedUsersQuery = verifiedUsersQuery.limit(10);
}
const sorted = base.sort({ email: "asc" }); // new instance
const limited = base.limit(10); // new instance from base, no sort

const verifiedUsers = await verifiedUsersQuery;
const sortedUsers = await sorted; // sorted, no limit
const limitedUsers = await limited; // no sort, limited to 10
const allVerified = await base; // unchanged
```

### Schema features
Expand Down
16 changes: 9 additions & 7 deletions src/collection/collection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type Abortable,
type AnyBulkWriteOperation,
type CountDocumentsOptions,
type Db,
Expand Down Expand Up @@ -30,6 +31,7 @@ import { InsertOneQuery } from "./query/insert-one";
import { ReplaceOneQuery } from "./query/replace-one";
import { UpdateManyQuery } from "./query/update-many";
import { UpdateOneQuery } from "./query/update-one";
import type { PipelineStage } from "./types/pipeline-stage";

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

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

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

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

Expand Down Expand Up @@ -282,8 +284,8 @@ export class Collection<TSchema extends AnySchema, TRelations extends Record<str
*
* @returns AggregationPipeline instance
*/
public aggregate<TOutput extends any[]>() {
return new AggregationPipeline<TSchema, TOutput[]>(this.schema, this.collection, this.readyPromise);
public aggregate<TOutput extends Document>(pipeline: PipelineStage<InferSchemaData<TSchema>>[] = []) {
return new AggregationPipeline<TSchema, TOutput>(this.schema, this.collection, this.readyPromise, {}, pipeline);
}

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

Expand Down
39 changes: 32 additions & 7 deletions src/collection/pipeline/aggregation.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,55 @@
import type { AggregateOptions, AggregationCursor, Collection as MongoCollection } from "mongodb";
import type { Abortable, AggregateOptions, AggregationCursor, Document, Collection as MongoCollection } from "mongodb";
import type { AnySchema } from "../../schema/schema";
import type { InferSchemaData } from "../../schema/type-helpers";
import type { PipelineStage } from "../types/pipeline-stage";
import { Pipeline } from "./base";

export type AggregationPipelineOptions = AggregateOptions & Abortable;

/**
* Collection.aggregate().
*/
export class AggregationPipeline<TSchema extends AnySchema, TOutput extends any[]> extends Pipeline<TSchema, TOutput> {
export class AggregationPipeline<TSchema extends AnySchema, TOutput extends Document> extends Pipeline<
TSchema,
TOutput
> {
constructor(
schema: TSchema,
collection: MongoCollection<InferSchemaData<TSchema>>,
readyPromise: Promise<void>,
private _options: AggregateOptions = {},
pipeline: PipelineStage<InferSchemaData<TSchema>>[] = [],
) {
super(schema, collection, readyPromise);
super(schema, collection, readyPromise, pipeline);
}

/**
* Appends aggregation pipeline stage.
*
* @param stage - Pipeline stage
* @returns AggregationPipeline instance
*/
public addStage(stage: PipelineStage<InferSchemaData<TSchema>>): this {
return new AggregationPipeline(this.schema, this.collection, this.readyPromise, this._options, [
...this.pipeline,
stage,
]) as this;
}

/**
* Adds aggregation options. Options are merged into existing options.
*
* @param options - AggregateOptions
* @param options - AggregationPipelineOptions
* @returns AggregationPipeline instance
*/
public options(options: AggregateOptions): this {
Object.assign(this._options, options);
return this;
public options(options: AggregationPipelineOptions): this {
return new AggregationPipeline(
this.schema,
this.collection,
this.readyPromise,
{ ...this._options, ...options },
this.pipeline,
) as this;
}

/**
Expand Down
15 changes: 2 additions & 13 deletions src/collection/pipeline/base.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
import type { Collection as MongoCollection } from "mongodb";
import type { Document, Collection as MongoCollection } from "mongodb";
import type { AnySchema } from "../../schema/schema";
import type { InferSchemaData } from "../../schema/type-helpers";
import type { PipelineStage } from "../types/pipeline-stage";

/**
* Base aggregation pipeline class implementing thenable interface.
*/
export abstract class Pipeline<TSchema extends AnySchema, TOutput> {
export abstract class Pipeline<TSchema extends AnySchema, TOutput extends Document> {
constructor(
protected schema: TSchema,
protected collection: MongoCollection<InferSchemaData<TSchema>>,
protected readyPromise: Promise<void>,
protected pipeline: PipelineStage<InferSchemaData<TSchema>>[] = [],
) {}

/**
* Appends aggregation pipeline stage.
*
* @param stage - Pipeline stage
* @returns Pipeline instance
*/
public addStage(stage: PipelineStage<InferSchemaData<TSchema>>): this {
this.pipeline.push(stage);
return this;
}

protected abstract exec(): Promise<TOutput[]>;

public async then<TResult1 = TOutput[], TResult2 = never>(
Expand Down
12 changes: 8 additions & 4 deletions src/collection/query/bulk-write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ import type { AnySchema } from "../../schema/schema";
import type { InferSchemaData } from "../../schema/type-helpers";
import { Query } from "./base";

export type BulkWriteQueryOptions = BulkWriteOptions;

export class BulkWriteQuery<TSchema extends AnySchema> extends Query<TSchema, BulkWriteResult> {
constructor(
schema: TSchema,
collection: MongoCollection<InferSchemaData<TSchema>>,
readyPromise: Promise<void>,
private _data: AnyBulkWriteOperation<InferSchemaData<TSchema>>[],
private _options: BulkWriteOptions = {},
private _options: BulkWriteQueryOptions = {},
) {
super(schema, collection, readyPromise);
}

public options(options: BulkWriteOptions): this {
Object.assign(this._options, options);
return this;
public options(options: BulkWriteQueryOptions): this {
return new BulkWriteQuery(this.schema, this.collection, this.readyPromise, this._data, {
...this._options,
...options,
}) as this;
}

protected async exec(): Promise<BulkWriteResult> {
Expand Down
14 changes: 9 additions & 5 deletions src/collection/query/delete-many.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { AnySchema } from "../../schema/schema";
import type { Filter, InferSchemaData } from "../../schema/type-helpers";
import { Query } from "./base";

export type DeleteManyQueryOptions = DeleteOptions;

/**
* Collection.deleteMany().
*/
Expand All @@ -12,20 +14,22 @@ export class DeleteManyQuery<TSchema extends AnySchema> extends Query<TSchema, D
collection: MongoCollection<InferSchemaData<TSchema>>,
readyPromise: Promise<void>,
private _filter: Filter<TSchema>,
private _options: DeleteOptions = {},
private _options: DeleteManyQueryOptions = {},
) {
super(schema, collection, readyPromise);
}

/**
* Adds delete options. Options are merged into existing options.
*
* @param options - DeleteOptions
* @param options - DeleteManyQueryOptions
* @returns DeleteManyQuery instance
*/
public options(options: DeleteOptions): this {
Object.assign(this._options, options);
return this;
public options(options: DeleteManyQueryOptions): this {
return new DeleteManyQuery(this.schema, this.collection, this.readyPromise, this._filter, {
...this._options,
...options,
}) as this;
}

protected async exec(): Promise<DeleteResult> {
Expand Down
14 changes: 9 additions & 5 deletions src/collection/query/delete-one.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { AnySchema } from "../../schema/schema";
import type { Filter, InferSchemaData } from "../../schema/type-helpers";
import { Query } from "./base";

export type DeleteOneQueryOptions = DeleteOptions;

/**
* Collection.deleteOne().
*/
Expand All @@ -12,20 +14,22 @@ export class DeleteOneQuery<TSchema extends AnySchema> extends Query<TSchema, De
collection: MongoCollection<InferSchemaData<TSchema>>,
readyPromise: Promise<void>,
private _filter: Filter<TSchema>,
private _options: DeleteOptions = {},
private _options: DeleteOneQueryOptions = {},
) {
super(schema, collection, readyPromise);
}

/**
* Adds delete options. Options are merged into existing options.
*
* @param options - DeleteOptions
* @param options - DeleteOneQueryOptions
* @returns DeleteOneQuery instance
*/
public options(options: DeleteOptions): this {
Object.assign(this._options, options);
return this;
public options(options: DeleteOneQueryOptions): this {
return new DeleteOneQuery(this.schema, this.collection, this.readyPromise, this._filter, {
...this._options,
...options,
}) as this;
}

protected async exec(): Promise<DeleteResult> {
Expand Down
12 changes: 8 additions & 4 deletions src/collection/query/distinct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { AnySchema } from "../../schema/schema";
import type { DistinctFilter, Filter, InferSchemaData } from "../../schema/type-helpers";
import { Query } from "./base";

export type DistinctQueryOptions = DistinctOptions;

export class DistinctQuery<
TSchema extends AnySchema,
Key extends keyof DistinctFilter<TSchema>,
Expand All @@ -14,14 +16,16 @@ export class DistinctQuery<
readyPromise: Promise<void>,
private _filter: Filter<TSchema>,
private _key: Key,
private _options: DistinctOptions = {},
private _options: DistinctQueryOptions = {},
) {
super(schema, collection, readyPromise);
}

public options(options: DistinctOptions): this {
Object.assign(this._options, options);
return this;
public options(options: DistinctQueryOptions): this {
return new DistinctQuery(this.schema, this.collection, this.readyPromise, this._filter, this._key, {
...this._options,
...options,
}) as this;
}

protected async exec(): Promise<TOutput> {
Expand Down
Loading
Loading