Thin MongoDB ODM built for Standard Schema
mongodb
zod
deno
1import type { StandardSchemaV1 } from "@standard-schema/spec";
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";
15
16export class Model<T extends StandardSchemaV1<unknown, Document>> {
17 private collection: Collection<StandardSchemaV1.InferOutput<T>>;
18 private schema: T;
19
20 constructor(collectionName: string, schema: T) {
21 this.collection = getDb().collection<
22 StandardSchemaV1.InferOutput<T> & Document
23 >(
24 collectionName,
25 );
26 this.schema = schema;
27 }
28
29 async insertOne(
30 data: StandardSchemaV1.InferInput<T>,
31 ): Promise<InsertOneResult<StandardSchemaV1.InferOutput<T>>> {
32 const result = this.schema["~standard"].validate(data);
33 if (result instanceof Promise) {
34 throw new Error("Async validation not supported");
35 }
36 if (result.issues) {
37 throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`);
38 }
39 return await this.collection.insertOne(
40 result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>,
41 );
42 }
43
44 async insertMany(
45 data: StandardSchemaV1.InferInput<T>[],
46 ): Promise<InsertManyResult<StandardSchemaV1.InferOutput<T>>> {
47 const validatedData = data.map((item) => {
48 const result = this.schema["~standard"].validate(item);
49 if (result instanceof Promise) {
50 throw new Error("Async validation not supported");
51 }
52 if (result.issues) {
53 throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`);
54 }
55 return result.value;
56 });
57 return await this.collection.insertMany(
58 validatedData as OptionalUnlessRequiredId<
59 StandardSchemaV1.InferOutput<T>
60 >[],
61 );
62 }
63
64 async find(
65 query: Filter<StandardSchemaV1.InferOutput<T>>,
66 ): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> {
67 return await this.collection.find(query).toArray();
68 }
69
70 async findOne(
71 query: Filter<StandardSchemaV1.InferOutput<T>>,
72 ): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> {
73 return await this.collection.findOne(query);
74 }
75
76 async findById(
77 id: string | ObjectId,
78 ): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> {
79 const objectId = typeof id === "string" ? new ObjectId(id) : id;
80 return await this.findOne(
81 { _id: objectId } as Filter<StandardSchemaV1.InferOutput<T>>,
82 );
83 }
84
85 async update(
86 query: Filter<StandardSchemaV1.InferOutput<T>>,
87 data: Partial<StandardSchemaV1.InferOutput<T>>,
88 ): Promise<UpdateResult> {
89 return await this.collection.updateMany(query, { $set: data });
90 }
91
92 async updateOne(
93 query: Filter<StandardSchemaV1.InferOutput<T>>,
94 data: Partial<StandardSchemaV1.InferOutput<T>>,
95 ): Promise<UpdateResult> {
96 return await this.collection.updateOne(query, { $set: data });
97 }
98
99 async replaceOne(
100 query: Filter<StandardSchemaV1.InferOutput<T>>,
101 data: StandardSchemaV1.InferInput<T>,
102 ): Promise<UpdateResult> {
103 const result = this.schema["~standard"].validate(data);
104 if (result instanceof Promise) {
105 throw new Error("Async validation not supported");
106 }
107 if (result.issues) {
108 throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`);
109 }
110 return await this.collection.replaceOne(
111 query,
112 result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>,
113 );
114 }
115
116 async delete(
117 query: Filter<StandardSchemaV1.InferOutput<T>>,
118 ): Promise<DeleteResult> {
119 return await this.collection.deleteMany(query);
120 }
121
122 async deleteOne(
123 query: Filter<StandardSchemaV1.InferOutput<T>>,
124 ): Promise<DeleteResult> {
125 return await this.collection.deleteOne(query);
126 }
127
128 async count(query: Filter<StandardSchemaV1.InferOutput<T>>): Promise<number> {
129 return await this.collection.countDocuments(query);
130 }
131
132 async aggregate(pipeline: Document[]): Promise<Document[]> {
133 return await this.collection.aggregate(pipeline).toArray();
134 }
135
136 // Pagination support for find
137 async findPaginated(
138 query: Filter<StandardSchemaV1.InferOutput<T>>,
139 options: { skip?: number; limit?: number; sort?: Document } = {},
140 ): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> {
141 return await this.collection
142 .find(query)
143 .skip(options.skip ?? 0)
144 .limit(options.limit ?? 10)
145 .sort(options.sort ?? {})
146 .toArray();
147 }
148}