Thin MongoDB ODM built for Standard Schema
mongodb
zod
deno
1import type { z } from "zod";
2import type {
3 Collection,
4 DeleteResult,
5 Document,
6 Filter,
7 InsertManyResult,
8 InsertOneResult,
9 OptionalUnlessRequiredId,
10 UpdateResult,
11 WithId,
12} from "mongodb";
13import { ObjectId } from "mongodb";
14import { getDb } from "./client.ts";
15import type { InsertType } from "./schema.ts";
16
17export class Model<T extends z.ZodObject> {
18 private collection: Collection<z.infer<T>>;
19 private schema: T;
20
21 constructor(collectionName: string, schema: T) {
22 this.collection = getDb().collection<z.infer<T>>(collectionName);
23 this.schema = schema;
24 }
25
26 async insertOne(data: InsertType<T>): Promise<InsertOneResult<z.infer<T>>> {
27 const validatedData = this.schema.parse(data);
28 return await this.collection.insertOne(
29 validatedData as OptionalUnlessRequiredId<z.infer<T>>,
30 );
31 }
32
33 async insertMany(
34 data: InsertType<T>[],
35 ): Promise<InsertManyResult<z.infer<T>>> {
36 const validatedData = data.map((item) => this.schema.parse(item));
37 return await this.collection.insertMany(
38 validatedData as OptionalUnlessRequiredId<z.infer<T>>[],
39 );
40 }
41
42 async find(query: Filter<z.infer<T>>): Promise<(WithId<z.infer<T>>)[]> {
43 return await this.collection.find(query).toArray();
44 }
45
46 async findOne(query: Filter<z.infer<T>>): Promise<WithId<z.infer<T>> | null> {
47 return await this.collection.findOne(query);
48 }
49
50 async findById(id: string | ObjectId): Promise<WithId<z.infer<T>> | null> {
51 const objectId = typeof id === "string" ? new ObjectId(id) : id;
52 return await this.findOne({ _id: objectId } as Filter<z.infer<T>>);
53 }
54
55 async update(
56 query: Filter<z.infer<T>>,
57 data: Partial<z.infer<T>>,
58 ): Promise<UpdateResult> {
59 return await this.collection.updateMany(query, { $set: data });
60 }
61
62 async updateOne(
63 query: Filter<z.infer<T>>,
64 data: Partial<z.infer<T>>,
65 ): Promise<UpdateResult> {
66 return await this.collection.updateOne(query, { $set: data });
67 }
68
69 async replaceOne(
70 query: Filter<z.infer<T>>,
71 data: InsertType<T>,
72 ): Promise<UpdateResult> {
73 const validatedData = this.schema.parse(data);
74 return await this.collection.replaceOne(
75 query,
76 validatedData as OptionalUnlessRequiredId<z.infer<T>>,
77 );
78 }
79
80 async delete(query: Filter<z.infer<T>>): Promise<DeleteResult> {
81 return await this.collection.deleteMany(query);
82 }
83
84 async deleteOne(query: Filter<z.infer<T>>): Promise<DeleteResult> {
85 return await this.collection.deleteOne(query);
86 }
87
88 async count(query: Filter<z.infer<T>>): Promise<number> {
89 return await this.collection.countDocuments(query);
90 }
91
92 async aggregate(pipeline: Document[]): Promise<Document[]> {
93 return await this.collection.aggregate(pipeline).toArray();
94 }
95
96 // Pagination support for find
97 async findPaginated(
98 query: Filter<z.infer<T>>,
99 options: { skip?: number; limit?: number; sort?: Document } = {},
100 ): Promise<(WithId<z.infer<T>>)[]> {
101 return await this.collection
102 .find(query)
103 .skip(options.skip ?? 0)
104 .limit(options.limit ?? 10)
105 .sort(options.sort ?? {})
106 .toArray();
107 }
108}