Thin MongoDB ODM built for Standard Schema
mongodb zod deno

add indexing

knotbin.com 3888b2b5 722faea3

verified
+346 -14
+33 -2
README.md
··· 55 55 connect, 56 56 disconnect, 57 57 InferModel, 58 - InsertType, 58 + Input, 59 59 Model, 60 60 } from "@nozzle/nozzle"; 61 61 import { userSchema } from "./schemas/user"; 62 62 import { ObjectId } from "mongodb"; // v6+ driver recommended 63 63 64 64 type User = InferModel<typeof userSchema>; 65 - type UserInsert = InsertType<typeof userSchema>; 65 + type UserInsert = Input<typeof userSchema>; 66 66 67 67 async function main() { 68 68 // Use the latest connection string format and options ··· 83 83 84 84 ```ts 85 85 // Insert one 86 + // Note: createdAt has a default, so it's optional in the input type 86 87 const newUser: UserInsert = { 87 88 name: "John Doe", 88 89 email: "john.doe@example.com", 89 90 age: 30, 91 + // createdAt is optional because of z.date().default(() => new Date()) 90 92 }; 91 93 const insertResult = await UserModel.insertOne(newUser); 92 94 ··· 140 142 { age: { $gte: 18 } }, 141 143 { skip: 0, limit: 10, sort: { age: -1 } }, 142 144 ); 145 + 146 + // Index Management 147 + // Create a unique index 148 + await UserModel.createIndex({ email: 1 }, { unique: true }); 149 + 150 + // Create a compound index 151 + await UserModel.createIndex({ name: 1, age: -1 }); 152 + 153 + // Create multiple indexes at once 154 + await UserModel.createIndexes([ 155 + { key: { email: 1 }, name: "email_idx", unique: true }, 156 + { key: { name: 1, age: -1 }, name: "name_age_idx" }, 157 + ]); 158 + 159 + // List all indexes 160 + const indexes = await UserModel.listIndexes(); 161 + console.log("Indexes:", indexes); 162 + 163 + // Check if index exists 164 + const exists = await UserModel.indexExists("email_idx"); 165 + 166 + // Drop an index 167 + await UserModel.dropIndex("email_idx"); 168 + 169 + // Sync indexes (useful for migrations - creates missing, updates changed) 170 + await UserModel.syncIndexes([ 171 + { key: { email: 1 }, name: "email_idx", unique: true }, 172 + { key: { createdAt: 1 }, name: "created_at_idx" }, 173 + ]); 143 174 ``` 144 175 145 176 ---
+2 -2
examples/user.ts
··· 4 4 connect, 5 5 disconnect, 6 6 type InferModel, 7 - type InsertType, 7 + type Input, 8 8 Model, 9 9 } from "../mod.ts"; 10 10 ··· 18 18 19 19 // Infer the TypeScript type from the Zod schema 20 20 type User = InferModel<typeof userSchema>; 21 - type UserInsert = InsertType<typeof userSchema>; 21 + type UserInsert = Input<typeof userSchema>; 22 22 23 23 async function runExample() { 24 24 try {
+1 -1
mod.ts
··· 1 - export { type InferModel, type InsertType } from "./schema.ts"; 1 + export { type InferModel, type Input } from "./schema.ts"; 2 2 export { connect, disconnect } from "./client.ts"; 3 3 export { Model } from "./model.ts";
+138
model.ts
··· 1 1 import type { z } from "@zod/zod"; 2 2 import type { 3 3 Collection, 4 + CreateIndexesOptions, 4 5 DeleteResult, 5 6 Document, 7 + DropIndexesOptions, 6 8 Filter, 9 + IndexDescription, 10 + IndexSpecification, 7 11 InsertManyResult, 8 12 InsertOneResult, 13 + ListIndexesOptions, 9 14 OptionalUnlessRequiredId, 10 15 UpdateResult, 11 16 WithId, ··· 131 136 .limit(options.limit ?? 10) 132 137 .sort(options.sort ?? {}) 133 138 .toArray(); 139 + } 140 + 141 + // Index Management Methods 142 + 143 + /** 144 + * Create a single index on the collection 145 + * @param keys - Index specification (e.g., { email: 1 } or { name: "text" }) 146 + * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.) 147 + * @returns The name of the created index 148 + */ 149 + async createIndex( 150 + keys: IndexSpecification, 151 + options?: CreateIndexesOptions, 152 + ): Promise<string> { 153 + return await this.collection.createIndex(keys, options); 154 + } 155 + 156 + /** 157 + * Create multiple indexes on the collection 158 + * @param indexes - Array of index descriptions 159 + * @param options - Index creation options 160 + * @returns Array of index names created 161 + */ 162 + async createIndexes( 163 + indexes: IndexDescription[], 164 + options?: CreateIndexesOptions, 165 + ): Promise<string[]> { 166 + return await this.collection.createIndexes(indexes, options); 167 + } 168 + 169 + /** 170 + * Drop a single index from the collection 171 + * @param index - Index name or specification 172 + * @param options - Drop index options 173 + */ 174 + async dropIndex( 175 + index: string | IndexSpecification, 176 + options?: DropIndexesOptions, 177 + ): Promise<void> { 178 + // MongoDB driver accepts string or IndexSpecification 179 + await this.collection.dropIndex(index as string, options); 180 + } 181 + 182 + /** 183 + * Drop all indexes from the collection (except _id index) 184 + * @param options - Drop index options 185 + */ 186 + async dropIndexes(options?: DropIndexesOptions): Promise<void> { 187 + await this.collection.dropIndexes(options); 188 + } 189 + 190 + /** 191 + * List all indexes on the collection 192 + * @param options - List indexes options 193 + * @returns Array of index information 194 + */ 195 + async listIndexes( 196 + options?: ListIndexesOptions, 197 + ): Promise<IndexDescription[]> { 198 + const indexes = await this.collection.listIndexes(options).toArray(); 199 + return indexes as IndexDescription[]; 200 + } 201 + 202 + /** 203 + * Get index information by name 204 + * @param indexName - Name of the index 205 + * @returns Index description or null if not found 206 + */ 207 + async getIndex(indexName: string): Promise<IndexDescription | null> { 208 + const indexes = await this.listIndexes(); 209 + return indexes.find((idx) => idx.name === indexName) || null; 210 + } 211 + 212 + /** 213 + * Check if an index exists 214 + * @param indexName - Name of the index 215 + * @returns True if index exists, false otherwise 216 + */ 217 + async indexExists(indexName: string): Promise<boolean> { 218 + const index = await this.getIndex(indexName); 219 + return index !== null; 220 + } 221 + 222 + /** 223 + * Synchronize indexes - create indexes if they don't exist, update if they differ 224 + * This is useful for ensuring indexes match your schema definition 225 + * @param indexes - Array of index descriptions to synchronize 226 + * @param options - Options for index creation 227 + */ 228 + async syncIndexes( 229 + indexes: IndexDescription[], 230 + options?: CreateIndexesOptions, 231 + ): Promise<string[]> { 232 + const existingIndexes = await this.listIndexes(); 233 + 234 + const indexesToCreate: IndexDescription[] = []; 235 + 236 + for (const index of indexes) { 237 + const indexName = index.name || this._generateIndexName(index.key); 238 + const existingIndex = existingIndexes.find( 239 + (idx) => idx.name === indexName, 240 + ); 241 + 242 + if (!existingIndex) { 243 + indexesToCreate.push(index); 244 + } else if ( 245 + JSON.stringify(existingIndex.key) !== JSON.stringify(index.key) 246 + ) { 247 + // Index exists but keys differ - drop and recreate 248 + await this.dropIndex(indexName); 249 + indexesToCreate.push(index); 250 + } 251 + // If index exists and matches, skip it 252 + } 253 + 254 + const created: string[] = []; 255 + if (indexesToCreate.length > 0) { 256 + const names = await this.createIndexes(indexesToCreate, options); 257 + created.push(...names); 258 + } 259 + 260 + return created; 261 + } 262 + 263 + /** 264 + * Helper method to generate index name from key specification 265 + */ 266 + private _generateIndexName(keys: IndexSpecification): string { 267 + if (typeof keys === "string") { 268 + return keys; 269 + } 270 + const entries = Object.entries(keys as Record<string, number | string>); 271 + return entries.map(([field, direction]) => `${field}_${direction}`).join("_"); 134 272 } 135 273 }
+1 -3
schema.ts
··· 8 8 _id?: ObjectId; 9 9 }; 10 10 11 - export type InsertType<T extends Schema> = Omit<Infer<T>, "createdAt"> & { 12 - createdAt?: Date; 13 - }; 11 + export type Input<T extends Schema> = z.input<T>;
+2 -2
tests/crud_test.ts
··· 1 - import { assertEquals, assertExists } from "@std/assert"; 1 + import { assert, assertEquals, assertExists } from "@std/assert"; 2 2 import { ObjectId } from "mongodb"; 3 3 import { 4 4 cleanupCollection, ··· 152 152 // Find all users with age >= 25 153 153 const foundUsers = await UserModel.find({ age: { $gte: 25 } }); 154 154 155 - assertEquals(foundUsers.length >= 2, true); 155 + assert(foundUsers.length >= 2); 156 156 }, 157 157 sanitizeResources: false, 158 158 sanitizeOps: false,
+2 -2
tests/features_test.ts
··· 1 - import { assertEquals, assertExists } from "@std/assert"; 1 + import { assert, assertExists } from "@std/assert"; 2 2 import { ObjectId } from "mongodb"; 3 3 import { 4 4 cleanupCollection, ··· 44 44 45 45 assertExists(foundUser); 46 46 assertExists(foundUser.createdAt); 47 - assertEquals(foundUser.createdAt instanceof Date, true); 47 + assert(foundUser.createdAt instanceof Date); 48 48 }, 49 49 sanitizeResources: false, 50 50 sanitizeOps: false,
+165
tests/index_test.ts
··· 1 + import { assert, assertEquals, assertExists, assertFalse } from "@std/assert"; 2 + import type { IndexDescription } from "mongodb"; 3 + import { 4 + cleanupCollection, 5 + createUserModel, 6 + setupTestDb, 7 + teardownTestDb, 8 + } from "./utils.ts"; 9 + import type { Model } from "../mod.ts"; 10 + 11 + let UserModel: Model<typeof import("./utils.ts").userSchema>; 12 + 13 + Deno.test.beforeAll(async () => { 14 + await setupTestDb(); 15 + UserModel = createUserModel(); 16 + }); 17 + 18 + Deno.test.beforeEach(async () => { 19 + await cleanupCollection(UserModel); 20 + // Drop all indexes except _id 21 + try { 22 + await UserModel.dropIndexes(); 23 + } catch { 24 + // Ignore if no indexes exist 25 + } 26 + }); 27 + 28 + Deno.test.afterAll(async () => { 29 + await teardownTestDb(); 30 + }); 31 + 32 + Deno.test({ 33 + name: "Index: Create - should create a simple index", 34 + async fn() { 35 + const indexName = await UserModel.createIndex({ email: 1 }); 36 + assertExists(indexName); 37 + assertEquals(typeof indexName, "string"); 38 + 39 + const indexExists = await UserModel.indexExists(indexName); 40 + assert(indexExists); 41 + }, 42 + sanitizeResources: false, 43 + sanitizeOps: false, 44 + }); 45 + 46 + Deno.test({ 47 + name: "Index: Create Unique - should create a unique index", 48 + async fn() { 49 + const indexName = await UserModel.createIndex( 50 + { email: 1 }, 51 + { unique: true, name: "email_unique_test" }, 52 + ); 53 + assertExists(indexName); 54 + assertEquals(indexName, "email_unique_test"); 55 + 56 + const index = await UserModel.getIndex(indexName); 57 + assertExists(index); 58 + assert(index.unique); 59 + }, 60 + sanitizeResources: false, 61 + sanitizeOps: false, 62 + }); 63 + 64 + Deno.test({ 65 + name: "Index: Create Compound - should create a compound index", 66 + async fn() { 67 + const indexName = await UserModel.createIndex({ name: 1, age: -1 }); 68 + assertExists(indexName); 69 + 70 + const index = await UserModel.getIndex(indexName); 71 + assertExists(index); 72 + assertEquals(Object.keys(index.key || {}).length, 2); 73 + }, 74 + sanitizeResources: false, 75 + sanitizeOps: false, 76 + }); 77 + 78 + Deno.test({ 79 + name: "Index: List - should list all indexes", 80 + async fn() { 81 + // Create a few indexes 82 + await UserModel.createIndex({ email: 1 }); 83 + await UserModel.createIndex({ name: 1, age: -1 }); 84 + 85 + const indexes = await UserModel.listIndexes(); 86 + // Should have at least _id index + the 2 we created 87 + assert(indexes.length >= 3); 88 + 89 + const indexNames = indexes.map((idx) => idx.name); 90 + assert(indexNames.includes("_id_")); 91 + }, 92 + sanitizeResources: false, 93 + sanitizeOps: false, 94 + }); 95 + 96 + Deno.test({ 97 + name: "Index: Drop - should drop an index", 98 + async fn() { 99 + const indexName = await UserModel.createIndex({ email: 1 }); 100 + assertExists(indexName); 101 + 102 + let indexExists = await UserModel.indexExists(indexName); 103 + assert(indexExists); 104 + 105 + await UserModel.dropIndex(indexName); 106 + 107 + indexExists = await UserModel.indexExists(indexName); 108 + assertFalse(indexExists); 109 + }, 110 + sanitizeResources: false, 111 + sanitizeOps: false, 112 + }); 113 + 114 + Deno.test({ 115 + name: "Index: Create Multiple - should create multiple indexes", 116 + async fn() { 117 + const indexNames = await UserModel.createIndexes([ 118 + { key: { email: 1 }, name: "email_multiple_test" }, 119 + { key: { name: 1, age: -1 }, name: "name_age_multiple_test" }, 120 + ]); 121 + 122 + assertEquals(indexNames.length, 2); 123 + assertEquals(indexNames.includes("email_multiple_test"), true); 124 + assertEquals(indexNames.includes("name_age_multiple_test"), true); 125 + }, 126 + sanitizeResources: false, 127 + sanitizeOps: false, 128 + }); 129 + 130 + Deno.test({ 131 + name: "Index: Sync - should create missing indexes", 132 + async fn() { 133 + const indexesToSync: IndexDescription[] = [ 134 + { key: { email: 1 }, name: "email_idx" }, 135 + { key: { name: 1 }, name: "name_idx" }, 136 + ]; 137 + 138 + const created = await UserModel.syncIndexes(indexesToSync); 139 + assertEquals(created.length, 2); 140 + 141 + // Running again should not create duplicates 142 + const createdAgain = await UserModel.syncIndexes(indexesToSync); 143 + assertEquals(createdAgain.length, 0); 144 + }, 145 + sanitizeResources: false, 146 + sanitizeOps: false, 147 + }); 148 + 149 + Deno.test({ 150 + name: "Index: Get - should get index by name", 151 + async fn() { 152 + await UserModel.createIndex( 153 + { email: 1 }, 154 + { unique: true, name: "email_unique_idx" }, 155 + ); 156 + 157 + const index = await UserModel.getIndex("email_unique_idx"); 158 + assertExists(index); 159 + assertEquals(index.name, "email_unique_idx"); 160 + assert(index.unique); 161 + }, 162 + sanitizeResources: false, 163 + sanitizeOps: false, 164 + }); 165 +
+2 -2
tests/utils.ts
··· 1 1 import { z } from "@zod/zod"; 2 - import { connect, disconnect, type InsertType, Model } from "../mod.ts"; 2 + import { connect, disconnect, type Input, Model } from "../mod.ts"; 3 3 import { MongoMemoryServer } from "mongodb-memory-server-core"; 4 4 5 5 export const userSchema = z.object({ ··· 9 9 createdAt: z.date().default(() => new Date()), 10 10 }); 11 11 12 - export type UserInsert = InsertType<typeof userSchema>; 12 + export type UserInsert = Input<typeof userSchema>; 13 13 14 14 let mongoServer: MongoMemoryServer | null = null; 15 15 let isSetup = false;