Thin MongoDB ODM built for Standard Schema
mongodb zod deno

readme

+64 -101
+9 -10
README.md
··· 1 1 # **Nozzle** 2 2 3 - A lightweight, type-safe ODM for MongoDB in TypeScript — inspired by 4 - [Drizzle ORM](https://orm.drizzle.team/) and built for developers who value 5 - simplicity, transparency, and strong typings. 3 + A lightweight, type-safe ODM for MongoDB in TypeScript 6 4 7 - > **Note:** Nozzle DB requires MongoDB **4.2 or newer** and works best with the 5 + > **Note:** Nozzle requires MongoDB **4.2 or newer** and works best with the 8 6 > latest stable MongoDB server (6.x or newer) and the official 9 7 > [mongodb](https://www.npmjs.com/package/mongodb) Node.js driver (v6+). 10 8 11 9 ## ✨ Features 12 10 13 - - **Schema-first:** Define and validate collections using 14 - [Zod](https://zod.dev/). 11 + - **Schema-first:** Define and validate collections using any schema validator 12 + that supports [Standard Schema](https://standardschema.dev). 15 13 - **Type-safe operations:** Auto-complete and strict typings for `insert`, 16 14 `find`, `update`, and `delete`. 17 15 - **Minimal & modular:** No decorators or magic. Just clean, composable APIs. 18 - - **Developer-friendly DX:** Great TypeScript support and IDE integration. 19 16 - **Built on MongoDB native driver:** Zero overhead with full control. 20 17 21 18 --- ··· 33 30 34 31 ## 🚀 Quick Start 35 32 33 + Examples below use Zod but any schema validator that supports 34 + [Standard Schema](https://standardschema.dev) will work. 35 + 36 36 ### 1. Define a schema 37 37 38 38 ```ts 39 39 // src/schemas/user.ts 40 40 import { z } from "zod"; 41 - import { defineModel } from "@nozzle/nozzle"; 42 41 43 42 export const userSchema = z.object({ 44 43 name: z.string(), ··· 61 60 disconnect, 62 61 InferModel, 63 62 InsertType, 64 - MongoModel, 63 + Model, 65 64 } from "@nozzle/nozzle"; 66 65 import { userSchema } from "./schemas/user"; 67 66 import { ObjectId } from "mongodb"; // v6+ driver recommended ··· 72 71 async function main() { 73 72 // Use the latest connection string format and options 74 73 await connect("mongodb://localhost:27017", "your_database_name"); 75 - const UserModel = new MongoModel("users", userSchema); 74 + const UserModel = new Model("users", userSchema); 76 75 77 76 // Your operations go here 78 77
-2
deno.json
··· 4 4 "exports": "./mod.ts", 5 5 "license": "MIT", 6 6 "tasks": { 7 - "build": "tsc", 8 7 "test:mock": "deno test tests/mock_test.ts", 9 8 "test:watch": "deno run -A scripts/test.ts --mock --watch" 10 9 }, 11 10 "imports": { 12 11 "@standard-schema/spec": "jsr:@standard-schema/spec@^1.0.0", 13 - "zod": "jsr:@zod/zod@^4.0.17", 14 12 "mongodb": "npm:mongodb@^6.18.0" 15 13 } 16 14 }
-5
deno.lock
··· 7 7 "jsr:@std/internal@^1.0.10": "1.0.10", 8 8 "jsr:@std/internal@^1.0.6": "1.0.10", 9 9 "jsr:@std/testing@*": "1.0.15", 10 - "jsr:@zod/zod@^4.0.17": "4.0.17", 11 10 "npm:@types/node@*": "22.15.15", 12 11 "npm:mongodb@^6.18.0": "6.18.0" 13 12 }, ··· 30 29 "jsr:@std/assert@^1.0.13", 31 30 "jsr:@std/internal@^1.0.10" 32 31 ] 33 - }, 34 - "@zod/zod@4.0.17": { 35 - "integrity": "4d9be90a1a3c16e09dad7ce25986379d7ab8ed5f5f843288509af6bf8def525f" 36 32 } 37 33 }, 38 34 "npm": { ··· 110 106 "workspace": { 111 107 "dependencies": [ 112 108 "jsr:@standard-schema/spec@1", 113 - "jsr:@zod/zod@^4.0.17", 114 109 "npm:mongodb@^6.18.0" 115 110 ] 116 111 }
+1 -1
examples/user.ts
··· 1 - import { z } from "zod"; 1 + import { z } from "jsr:@zod/zod"; 2 2 import { ObjectId } from "mongodb"; 3 3 import { 4 4 connect,
+43 -69
model.ts
··· 13 13 import { ObjectId } from "mongodb"; 14 14 import { getDb } from "./client.ts"; 15 15 16 - export class Model<T extends StandardSchemaV1<unknown, Document>> { 17 - private collection: Collection<StandardSchemaV1.InferOutput<T>>; 16 + // Type alias for cleaner code 17 + type Schema = StandardSchemaV1<unknown, Document>; 18 + type Infer<T extends Schema> = StandardSchemaV1.InferOutput<T>; 19 + type Input<T extends Schema> = StandardSchemaV1.InferInput<T>; 20 + 21 + // Helper function to make StandardSchemaV1 validation as simple as Zod's parse() 22 + function parse<T extends Schema>(schema: T, data: unknown): Infer<T> { 23 + const result = schema["~standard"].validate(data); 24 + if (result instanceof Promise) { 25 + throw new Error("Async validation not supported"); 26 + } 27 + if (result.issues) { 28 + throw new Error(`Validation failed: ${JSON.stringify(result.issues)}`); 29 + } 30 + return result.value; 31 + } 32 + 33 + export class Model<T extends Schema> { 34 + private collection: Collection<Infer<T>>; 18 35 private schema: T; 19 36 20 37 constructor(collectionName: string, schema: T) { 21 - this.collection = getDb().collection< 22 - StandardSchemaV1.InferOutput<T> & Document 23 - >( 24 - collectionName, 25 - ); 38 + this.collection = getDb().collection<Infer<T> & Document>(collectionName); 26 39 this.schema = schema; 27 40 } 28 41 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 - } 42 + async insertOne(data: Input<T>): Promise<InsertOneResult<Infer<T>>> { 43 + const validatedData = parse(this.schema, data); 39 44 return await this.collection.insertOne( 40 - result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>, 45 + validatedData as OptionalUnlessRequiredId<Infer<T>>, 41 46 ); 42 47 } 43 48 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 - }); 49 + async insertMany(data: Input<T>[]): Promise<InsertManyResult<Infer<T>>> { 50 + const validatedData = data.map((item) => parse(this.schema, item)); 57 51 return await this.collection.insertMany( 58 - validatedData as OptionalUnlessRequiredId< 59 - StandardSchemaV1.InferOutput<T> 60 - >[], 52 + validatedData as OptionalUnlessRequiredId<Infer<T>>[], 61 53 ); 62 54 } 63 55 64 - async find( 65 - query: Filter<StandardSchemaV1.InferOutput<T>>, 66 - ): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> { 56 + async find(query: Filter<Infer<T>>): Promise<(WithId<Infer<T>>)[]> { 67 57 return await this.collection.find(query).toArray(); 68 58 } 69 59 70 - async findOne( 71 - query: Filter<StandardSchemaV1.InferOutput<T>>, 72 - ): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> { 60 + async findOne(query: Filter<Infer<T>>): Promise<WithId<Infer<T>> | null> { 73 61 return await this.collection.findOne(query); 74 62 } 75 63 76 - async findById( 77 - id: string | ObjectId, 78 - ): Promise<WithId<StandardSchemaV1.InferOutput<T>> | null> { 64 + async findById(id: string | ObjectId): Promise<WithId<Infer<T>> | null> { 79 65 const objectId = typeof id === "string" ? new ObjectId(id) : id; 80 - return await this.findOne( 81 - { _id: objectId } as Filter<StandardSchemaV1.InferOutput<T>>, 82 - ); 66 + return await this.findOne({ _id: objectId } as Filter<Infer<T>>); 83 67 } 84 68 85 69 async update( 86 - query: Filter<StandardSchemaV1.InferOutput<T>>, 87 - data: Partial<StandardSchemaV1.InferOutput<T>>, 70 + query: Filter<Infer<T>>, 71 + data: Partial<Infer<T>>, 88 72 ): Promise<UpdateResult> { 89 73 return await this.collection.updateMany(query, { $set: data }); 90 74 } 91 75 92 76 async updateOne( 93 - query: Filter<StandardSchemaV1.InferOutput<T>>, 94 - data: Partial<StandardSchemaV1.InferOutput<T>>, 77 + query: Filter<Infer<T>>, 78 + data: Partial<Infer<T>>, 95 79 ): Promise<UpdateResult> { 96 80 return await this.collection.updateOne(query, { $set: data }); 97 81 } 98 82 99 83 async replaceOne( 100 - query: Filter<StandardSchemaV1.InferOutput<T>>, 101 - data: StandardSchemaV1.InferInput<T>, 84 + query: Filter<Infer<T>>, 85 + data: Input<T>, 102 86 ): 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 - } 87 + const validatedData = parse(this.schema, data); 110 88 return await this.collection.replaceOne( 111 89 query, 112 - result.value as OptionalUnlessRequiredId<StandardSchemaV1.InferOutput<T>>, 90 + validatedData as OptionalUnlessRequiredId<Infer<T>>, 113 91 ); 114 92 } 115 93 116 - async delete( 117 - query: Filter<StandardSchemaV1.InferOutput<T>>, 118 - ): Promise<DeleteResult> { 94 + async delete(query: Filter<Infer<T>>): Promise<DeleteResult> { 119 95 return await this.collection.deleteMany(query); 120 96 } 121 97 122 - async deleteOne( 123 - query: Filter<StandardSchemaV1.InferOutput<T>>, 124 - ): Promise<DeleteResult> { 98 + async deleteOne(query: Filter<Infer<T>>): Promise<DeleteResult> { 125 99 return await this.collection.deleteOne(query); 126 100 } 127 101 128 - async count(query: Filter<StandardSchemaV1.InferOutput<T>>): Promise<number> { 102 + async count(query: Filter<Infer<T>>): Promise<number> { 129 103 return await this.collection.countDocuments(query); 130 104 } 131 105 ··· 135 109 136 110 // Pagination support for find 137 111 async findPaginated( 138 - query: Filter<StandardSchemaV1.InferOutput<T>>, 112 + query: Filter<Infer<T>>, 139 113 options: { skip?: number; limit?: number; sort?: Document } = {}, 140 - ): Promise<(WithId<StandardSchemaV1.InferOutput<T>>)[]> { 114 + ): Promise<(WithId<Infer<T>>)[]> { 141 115 return await this.collection 142 116 .find(query) 143 117 .skip(options.skip ?? 0)
+9 -12
schema.ts
··· 1 1 import type { StandardSchemaV1 } from "@standard-schema/spec"; 2 2 import type { ObjectId } from "mongodb"; 3 3 4 - export type InferModel< 5 - T extends StandardSchemaV1<unknown, Record<string, unknown>>, 6 - > = 7 - & StandardSchemaV1.InferOutput<T> 8 - & { 9 - _id?: ObjectId; 10 - }; 4 + type Schema = StandardSchemaV1<unknown, Record<string, unknown>>; 5 + type Infer<T extends Schema> = StandardSchemaV1.InferOutput<T>; 6 + 7 + export type InferModel<T extends Schema> = Infer<T> & { 8 + _id?: ObjectId; 9 + }; 11 10 12 - export type InsertType< 13 - T extends StandardSchemaV1<unknown, Record<string, unknown>>, 14 - > = 15 - & Omit<StandardSchemaV1.InferOutput<T>, "createdAt"> 16 - & { createdAt?: Date }; 11 + export type InsertType<T extends Schema> = Omit<Infer<T>, "createdAt"> & { 12 + createdAt?: Date; 13 + };
+1 -1
tests/main_test.ts
··· 1 1 import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert"; 2 - import { z } from "zod"; 2 + import { z } from "jsr:@zod/zod"; 3 3 import { connect, disconnect, type InsertType, Model } from "../mod.ts"; 4 4 import { ObjectId } from "mongodb"; 5 5
+1 -1
tests/mock_test.ts
··· 1 1 import { afterEach, beforeEach, describe, it } from "jsr:@std/testing/bdd"; 2 2 import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert"; 3 - import { z } from "zod"; 3 + import { z } from "jsr:@zod/zod"; 4 4 5 5 // Mock implementation for demonstration 6 6 class MockModel<T> {