Thin MongoDB ODM built for Standard Schema
mongodb zod deno

add indexing

knotbin.com 3888b2b5 722faea3

verified
+33 -2
README.md
···
connect,
disconnect,
InferModel,
-
InsertType,
+
Input,
Model,
} from "@nozzle/nozzle";
import { userSchema } from "./schemas/user";
import { ObjectId } from "mongodb"; // v6+ driver recommended
type User = InferModel<typeof userSchema>;
-
type UserInsert = InsertType<typeof userSchema>;
+
type UserInsert = Input<typeof userSchema>;
async function main() {
// Use the latest connection string format and options
···
```ts
// Insert one
+
// Note: createdAt has a default, so it's optional in the input type
const newUser: UserInsert = {
name: "John Doe",
email: "john.doe@example.com",
age: 30,
+
// createdAt is optional because of z.date().default(() => new Date())
};
const insertResult = await UserModel.insertOne(newUser);
···
{ age: { $gte: 18 } },
{ skip: 0, limit: 10, sort: { age: -1 } },
);
+
+
// Index Management
+
// Create a unique index
+
await UserModel.createIndex({ email: 1 }, { unique: true });
+
+
// Create a compound index
+
await UserModel.createIndex({ name: 1, age: -1 });
+
+
// Create multiple indexes at once
+
await UserModel.createIndexes([
+
{ key: { email: 1 }, name: "email_idx", unique: true },
+
{ key: { name: 1, age: -1 }, name: "name_age_idx" },
+
]);
+
+
// List all indexes
+
const indexes = await UserModel.listIndexes();
+
console.log("Indexes:", indexes);
+
+
// Check if index exists
+
const exists = await UserModel.indexExists("email_idx");
+
+
// Drop an index
+
await UserModel.dropIndex("email_idx");
+
+
// Sync indexes (useful for migrations - creates missing, updates changed)
+
await UserModel.syncIndexes([
+
{ key: { email: 1 }, name: "email_idx", unique: true },
+
{ key: { createdAt: 1 }, name: "created_at_idx" },
+
]);
```
---
+2 -2
examples/user.ts
···
connect,
disconnect,
type InferModel,
-
type InsertType,
+
type Input,
Model,
} from "../mod.ts";
···
// Infer the TypeScript type from the Zod schema
type User = InferModel<typeof userSchema>;
-
type UserInsert = InsertType<typeof userSchema>;
+
type UserInsert = Input<typeof userSchema>;
async function runExample() {
try {
+1 -1
mod.ts
···
-
export { type InferModel, type InsertType } from "./schema.ts";
+
export { type InferModel, type Input } from "./schema.ts";
export { connect, disconnect } from "./client.ts";
export { Model } from "./model.ts";
+138
model.ts
···
import type { z } from "@zod/zod";
import type {
Collection,
+
CreateIndexesOptions,
DeleteResult,
Document,
+
DropIndexesOptions,
Filter,
+
IndexDescription,
+
IndexSpecification,
InsertManyResult,
InsertOneResult,
+
ListIndexesOptions,
OptionalUnlessRequiredId,
UpdateResult,
WithId,
···
.limit(options.limit ?? 10)
.sort(options.sort ?? {})
.toArray();
+
}
+
+
// Index Management Methods
+
+
/**
+
* Create a single index on the collection
+
* @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
+
* @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
+
* @returns The name of the created index
+
*/
+
async createIndex(
+
keys: IndexSpecification,
+
options?: CreateIndexesOptions,
+
): Promise<string> {
+
return await this.collection.createIndex(keys, options);
+
}
+
+
/**
+
* Create multiple indexes on the collection
+
* @param indexes - Array of index descriptions
+
* @param options - Index creation options
+
* @returns Array of index names created
+
*/
+
async createIndexes(
+
indexes: IndexDescription[],
+
options?: CreateIndexesOptions,
+
): Promise<string[]> {
+
return await this.collection.createIndexes(indexes, options);
+
}
+
+
/**
+
* Drop a single index from the collection
+
* @param index - Index name or specification
+
* @param options - Drop index options
+
*/
+
async dropIndex(
+
index: string | IndexSpecification,
+
options?: DropIndexesOptions,
+
): Promise<void> {
+
// MongoDB driver accepts string or IndexSpecification
+
await this.collection.dropIndex(index as string, options);
+
}
+
+
/**
+
* Drop all indexes from the collection (except _id index)
+
* @param options - Drop index options
+
*/
+
async dropIndexes(options?: DropIndexesOptions): Promise<void> {
+
await this.collection.dropIndexes(options);
+
}
+
+
/**
+
* List all indexes on the collection
+
* @param options - List indexes options
+
* @returns Array of index information
+
*/
+
async listIndexes(
+
options?: ListIndexesOptions,
+
): Promise<IndexDescription[]> {
+
const indexes = await this.collection.listIndexes(options).toArray();
+
return indexes as IndexDescription[];
+
}
+
+
/**
+
* Get index information by name
+
* @param indexName - Name of the index
+
* @returns Index description or null if not found
+
*/
+
async getIndex(indexName: string): Promise<IndexDescription | null> {
+
const indexes = await this.listIndexes();
+
return indexes.find((idx) => idx.name === indexName) || null;
+
}
+
+
/**
+
* Check if an index exists
+
* @param indexName - Name of the index
+
* @returns True if index exists, false otherwise
+
*/
+
async indexExists(indexName: string): Promise<boolean> {
+
const index = await this.getIndex(indexName);
+
return index !== null;
+
}
+
+
/**
+
* Synchronize indexes - create indexes if they don't exist, update if they differ
+
* This is useful for ensuring indexes match your schema definition
+
* @param indexes - Array of index descriptions to synchronize
+
* @param options - Options for index creation
+
*/
+
async syncIndexes(
+
indexes: IndexDescription[],
+
options?: CreateIndexesOptions,
+
): Promise<string[]> {
+
const existingIndexes = await this.listIndexes();
+
+
const indexesToCreate: IndexDescription[] = [];
+
+
for (const index of indexes) {
+
const indexName = index.name || this._generateIndexName(index.key);
+
const existingIndex = existingIndexes.find(
+
(idx) => idx.name === indexName,
+
);
+
+
if (!existingIndex) {
+
indexesToCreate.push(index);
+
} else if (
+
JSON.stringify(existingIndex.key) !== JSON.stringify(index.key)
+
) {
+
// Index exists but keys differ - drop and recreate
+
await this.dropIndex(indexName);
+
indexesToCreate.push(index);
+
}
+
// If index exists and matches, skip it
+
}
+
+
const created: string[] = [];
+
if (indexesToCreate.length > 0) {
+
const names = await this.createIndexes(indexesToCreate, options);
+
created.push(...names);
+
}
+
+
return created;
+
}
+
+
/**
+
* Helper method to generate index name from key specification
+
*/
+
private _generateIndexName(keys: IndexSpecification): string {
+
if (typeof keys === "string") {
+
return keys;
+
}
+
const entries = Object.entries(keys as Record<string, number | string>);
+
return entries.map(([field, direction]) => `${field}_${direction}`).join("_");
}
}
+1 -3
schema.ts
···
_id?: ObjectId;
};
-
export type InsertType<T extends Schema> = Omit<Infer<T>, "createdAt"> & {
-
createdAt?: Date;
-
};
+
export type Input<T extends Schema> = z.input<T>;
+2 -2
tests/crud_test.ts
···
-
import { assertEquals, assertExists } from "@std/assert";
+
import { assert, assertEquals, assertExists } from "@std/assert";
import { ObjectId } from "mongodb";
import {
cleanupCollection,
···
// Find all users with age >= 25
const foundUsers = await UserModel.find({ age: { $gte: 25 } });
-
assertEquals(foundUsers.length >= 2, true);
+
assert(foundUsers.length >= 2);
},
sanitizeResources: false,
sanitizeOps: false,
+2 -2
tests/features_test.ts
···
-
import { assertEquals, assertExists } from "@std/assert";
+
import { assert, assertExists } from "@std/assert";
import { ObjectId } from "mongodb";
import {
cleanupCollection,
···
assertExists(foundUser);
assertExists(foundUser.createdAt);
-
assertEquals(foundUser.createdAt instanceof Date, true);
+
assert(foundUser.createdAt instanceof Date);
},
sanitizeResources: false,
sanitizeOps: false,
+165
tests/index_test.ts
···
+
import { assert, assertEquals, assertExists, assertFalse } from "@std/assert";
+
import type { IndexDescription } from "mongodb";
+
import {
+
cleanupCollection,
+
createUserModel,
+
setupTestDb,
+
teardownTestDb,
+
} from "./utils.ts";
+
import type { Model } from "../mod.ts";
+
+
let UserModel: Model<typeof import("./utils.ts").userSchema>;
+
+
Deno.test.beforeAll(async () => {
+
await setupTestDb();
+
UserModel = createUserModel();
+
});
+
+
Deno.test.beforeEach(async () => {
+
await cleanupCollection(UserModel);
+
// Drop all indexes except _id
+
try {
+
await UserModel.dropIndexes();
+
} catch {
+
// Ignore if no indexes exist
+
}
+
});
+
+
Deno.test.afterAll(async () => {
+
await teardownTestDb();
+
});
+
+
Deno.test({
+
name: "Index: Create - should create a simple index",
+
async fn() {
+
const indexName = await UserModel.createIndex({ email: 1 });
+
assertExists(indexName);
+
assertEquals(typeof indexName, "string");
+
+
const indexExists = await UserModel.indexExists(indexName);
+
assert(indexExists);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Create Unique - should create a unique index",
+
async fn() {
+
const indexName = await UserModel.createIndex(
+
{ email: 1 },
+
{ unique: true, name: "email_unique_test" },
+
);
+
assertExists(indexName);
+
assertEquals(indexName, "email_unique_test");
+
+
const index = await UserModel.getIndex(indexName);
+
assertExists(index);
+
assert(index.unique);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Create Compound - should create a compound index",
+
async fn() {
+
const indexName = await UserModel.createIndex({ name: 1, age: -1 });
+
assertExists(indexName);
+
+
const index = await UserModel.getIndex(indexName);
+
assertExists(index);
+
assertEquals(Object.keys(index.key || {}).length, 2);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: List - should list all indexes",
+
async fn() {
+
// Create a few indexes
+
await UserModel.createIndex({ email: 1 });
+
await UserModel.createIndex({ name: 1, age: -1 });
+
+
const indexes = await UserModel.listIndexes();
+
// Should have at least _id index + the 2 we created
+
assert(indexes.length >= 3);
+
+
const indexNames = indexes.map((idx) => idx.name);
+
assert(indexNames.includes("_id_"));
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Drop - should drop an index",
+
async fn() {
+
const indexName = await UserModel.createIndex({ email: 1 });
+
assertExists(indexName);
+
+
let indexExists = await UserModel.indexExists(indexName);
+
assert(indexExists);
+
+
await UserModel.dropIndex(indexName);
+
+
indexExists = await UserModel.indexExists(indexName);
+
assertFalse(indexExists);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Create Multiple - should create multiple indexes",
+
async fn() {
+
const indexNames = await UserModel.createIndexes([
+
{ key: { email: 1 }, name: "email_multiple_test" },
+
{ key: { name: 1, age: -1 }, name: "name_age_multiple_test" },
+
]);
+
+
assertEquals(indexNames.length, 2);
+
assertEquals(indexNames.includes("email_multiple_test"), true);
+
assertEquals(indexNames.includes("name_age_multiple_test"), true);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Sync - should create missing indexes",
+
async fn() {
+
const indexesToSync: IndexDescription[] = [
+
{ key: { email: 1 }, name: "email_idx" },
+
{ key: { name: 1 }, name: "name_idx" },
+
];
+
+
const created = await UserModel.syncIndexes(indexesToSync);
+
assertEquals(created.length, 2);
+
+
// Running again should not create duplicates
+
const createdAgain = await UserModel.syncIndexes(indexesToSync);
+
assertEquals(createdAgain.length, 0);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Get - should get index by name",
+
async fn() {
+
await UserModel.createIndex(
+
{ email: 1 },
+
{ unique: true, name: "email_unique_idx" },
+
);
+
+
const index = await UserModel.getIndex("email_unique_idx");
+
assertExists(index);
+
assertEquals(index.name, "email_unique_idx");
+
assert(index.unique);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+2 -2
tests/utils.ts
···
import { z } from "@zod/zod";
-
import { connect, disconnect, type InsertType, Model } from "../mod.ts";
+
import { connect, disconnect, type Input, Model } from "../mod.ts";
import { MongoMemoryServer } from "mongodb-memory-server-core";
export const userSchema = z.object({
···
createdAt: z.date().default(() => new Date()),
});
-
export type UserInsert = InsertType<typeof userSchema>;
+
export type UserInsert = Input<typeof userSchema>;
let mongoServer: MongoMemoryServer | null = null;
let isSetup = false;