Thin MongoDB ODM built for Standard Schema
mongodb zod deno

zod to standard schema

+1
deno.json
···
"test:watch": "deno run -A scripts/test.ts --mock --watch"
},
"imports": {
"zod": "jsr:@zod/zod@^4.0.17",
"mongodb": "npm:mongodb@^6.18.0"
}
···
"test:watch": "deno run -A scripts/test.ts --mock --watch"
},
"imports": {
+
"@standard-schema/spec": "jsr:@standard-schema/spec@^1.0.0",
"zod": "jsr:@zod/zod@^4.0.17",
"mongodb": "npm:mongodb@^6.18.0"
}
+5
deno.lock
···
{
"version": "5",
"specifiers": {
"jsr:@std/assert@*": "1.0.13",
"jsr:@std/assert@^1.0.13": "1.0.13",
"jsr:@std/internal@^1.0.10": "1.0.10",
···
"npm:mongodb@^6.18.0": "6.18.0"
},
"jsr": {
"@std/assert@1.0.13": {
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
"dependencies": [
···
},
"workspace": {
"dependencies": [
"jsr:@zod/zod@^4.0.17",
"npm:mongodb@^6.18.0"
]
···
{
"version": "5",
"specifiers": {
+
"jsr:@standard-schema/spec@1": "1.0.0",
"jsr:@std/assert@*": "1.0.13",
"jsr:@std/assert@^1.0.13": "1.0.13",
"jsr:@std/internal@^1.0.10": "1.0.10",
···
"npm:mongodb@^6.18.0": "6.18.0"
},
"jsr": {
+
"@standard-schema/spec@1.0.0": {
+
"integrity": "4f20bbcf34e92b92f8c01589b958abc7c87385fa9a96170cecdc643d4d5737c0"
+
},
"@std/assert@1.0.13": {
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
"dependencies": [
···
},
"workspace": {
"dependencies": [
+
"jsr:@standard-schema/spec@1",
"jsr:@zod/zod@^4.0.17",
"npm:mongodb@^6.18.0"
]
+69 -29
model.ts
···
-
import type { z } from "zod";
import type {
Collection,
DeleteResult,
···
} from "mongodb";
import { ObjectId } from "mongodb";
import { getDb } from "./client.ts";
-
import type { InsertType } from "./schema.ts";
-
export class Model<T extends z.ZodObject> {
-
private collection: Collection<z.infer<T>>;
private schema: T;
constructor(collectionName: string, schema: T) {
-
this.collection = getDb().collection<z.infer<T>>(collectionName);
this.schema = schema;
}
-
async insertOne(data: InsertType<T>): Promise<InsertOneResult<z.infer<T>>> {
-
const validatedData = this.schema.parse(data);
return await this.collection.insertOne(
-
validatedData as OptionalUnlessRequiredId<z.infer<T>>,
);
}
async insertMany(
-
data: InsertType<T>[],
-
): Promise<InsertManyResult<z.infer<T>>> {
-
const validatedData = data.map((item) => this.schema.parse(item));
return await this.collection.insertMany(
-
validatedData as OptionalUnlessRequiredId<z.infer<T>>[],
);
}
-
async find(query: Filter<z.infer<T>>): Promise<(WithId<z.infer<T>>)[]> {
return await this.collection.find(query).toArray();
}
-
async findOne(query: Filter<z.infer<T>>): Promise<WithId<z.infer<T>> | null> {
return await this.collection.findOne(query);
}
-
async findById(id: string | ObjectId): Promise<WithId<z.infer<T>> | null> {
const objectId = typeof id === "string" ? new ObjectId(id) : id;
-
return await this.findOne({ _id: objectId } as Filter<z.infer<T>>);
}
async update(
-
query: Filter<z.infer<T>>,
-
data: Partial<z.infer<T>>,
): Promise<UpdateResult> {
return await this.collection.updateMany(query, { $set: data });
}
async updateOne(
-
query: Filter<z.infer<T>>,
-
data: Partial<z.infer<T>>,
): Promise<UpdateResult> {
return await this.collection.updateOne(query, { $set: data });
}
async replaceOne(
-
query: Filter<z.infer<T>>,
-
data: InsertType<T>,
): Promise<UpdateResult> {
-
const validatedData = this.schema.parse(data);
return await this.collection.replaceOne(
query,
-
validatedData as OptionalUnlessRequiredId<z.infer<T>>,
);
}
-
async delete(query: Filter<z.infer<T>>): Promise<DeleteResult> {
return await this.collection.deleteMany(query);
}
-
async deleteOne(query: Filter<z.infer<T>>): Promise<DeleteResult> {
return await this.collection.deleteOne(query);
}
-
async count(query: Filter<z.infer<T>>): Promise<number> {
return await this.collection.countDocuments(query);
}
···
// Pagination support for find
async findPaginated(
-
query: Filter<z.infer<T>>,
options: { skip?: number; limit?: number; sort?: Document } = {},
-
): Promise<(WithId<z.infer<T>>)[]> {
return await this.collection
.find(query)
.skip(options.skip ?? 0)
···
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
import type {
Collection,
DeleteResult,
···
} from "mongodb";
import { ObjectId } from "mongodb";
import { getDb } from "./client.ts";
+
export class Model<T extends StandardSchemaV1<unknown, Document>> {
+
private collection: Collection<StandardSchemaV1.InferOutput<T>>;
private schema: T;
constructor(collectionName: string, schema: T) {
+
this.collection = getDb().collection<
+
StandardSchemaV1.InferOutput<T> & Document
+
>(
+
collectionName,
+
);
this.schema = schema;
}
+
async insertOne(
+
data: StandardSchemaV1.InferInput<T>,
+
): Promise<InsertOneResult<StandardSchemaV1.InferOutput<T>>> {
+
const result = this.schema["~standard"].validate(data);
+
if (result instanceof Promise) {
+
throw new Error("Async validation not supported");
+
}
+
if (result.issues) {
+
throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`);
+
}
return await this.collection.insertOne(
+
result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>,
);
}
async insertMany(
+
data: StandardSchemaV1.InferInput<T>[],
+
): Promise<InsertManyResult<StandardSchemaV1.InferOutput<T>>> {
+
const validatedData = data.map((item) => {
+
const result = this.schema["~standard"].validate(item);
+
if (result instanceof Promise) {
+
throw new Error("Async validation not supported");
+
}
+
if (result.issues) {
+
throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`);
+
}
+
return result.value;
+
});
return await this.collection.insertMany(
+
validatedData as OptionalUnlessRequiredId<
+
StandardSchemaV1.InferOutput<T>
+
>[],
);
}
+
async find(
+
query: Filter<StandardSchemaV1.InferOutput<T>>,
+
): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> {
return await this.collection.find(query).toArray();
}
+
async findOne(
+
query: Filter<StandardSchemaV1.InferOutput<T>>,
+
): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> {
return await this.collection.findOne(query);
}
+
async findById(
+
id: string | ObjectId,
+
): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> {
const objectId = typeof id === "string" ? new ObjectId(id) : id;
+
return await this.findOne(
+
{ _id: objectId } as Filter<StandardSchemaV1.InferOutput<T>>,
+
);
}
async update(
+
query: Filter<StandardSchemaV1.InferOutput<T>>,
+
data: Partial<StandardSchemaV1.InferOutput<T>>,
): Promise<UpdateResult> {
return await this.collection.updateMany(query, { $set: data });
}
async updateOne(
+
query: Filter<StandardSchemaV1.InferOutput<T>>,
+
data: Partial<StandardSchemaV1.InferOutput<T>>,
): Promise<UpdateResult> {
return await this.collection.updateOne(query, { $set: data });
}
async replaceOne(
+
query: Filter<StandardSchemaV1.InferOutput<T>>,
+
data: StandardSchemaV1.InferInput<T>,
): Promise<UpdateResult> {
+
const result = this.schema["~standard"].validate(data);
+
if (result instanceof Promise) {
+
throw new Error("Async validation not supported");
+
}
+
if (result.issues) {
+
throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`);
+
}
return await this.collection.replaceOne(
query,
+
result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>,
);
}
+
async delete(
+
query: Filter<StandardSchemaV1.InferOutput<T>>,
+
): Promise<DeleteResult> {
return await this.collection.deleteMany(query);
}
+
async deleteOne(
+
query: Filter<StandardSchemaV1.InferOutput<T>>,
+
): Promise<DeleteResult> {
return await this.collection.deleteOne(query);
}
+
async count(query: Filter<StandardSchemaV1.InferOutput<T>>): Promise<number> {
return await this.collection.countDocuments(query);
}
···
// Pagination support for find
async findPaginated(
+
query: Filter<StandardSchemaV1.InferOutput<T>>,
options: { skip?: number; limit?: number; sort?: Document } = {},
+
): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> {
return await this.collection
.find(query)
.skip(options.skip ?? 0)
+12 -6
schema.ts
···
-
import type { z } from "zod";
import type { ObjectId } from "mongodb";
-
export type InferModel<T extends z.ZodObject> = z.infer<T> & {
-
_id?: ObjectId;
-
};
-
export type InsertType<T extends z.ZodObject> =
-
& Omit<z.infer<T>, "createdAt">
& { createdAt?: Date };
···
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { ObjectId } from "mongodb";
+
export type InferModel<
+
T extends StandardSchemaV1<unknown, Record<string, unknown>>,
+
> =
+
& StandardSchemaV1.InferOutput<T>
+
& {
+
_id?: ObjectId;
+
};
+
export type InsertType<
+
T extends StandardSchemaV1<unknown, Record<string, unknown>>,
+
> =
+
& Omit<StandardSchemaV1.InferOutput<T>, "createdAt">
& { createdAt?: Date };