Thin MongoDB ODM built for Standard Schema
mongodb zod deno

readme

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