Thin MongoDB ODM built for Standard Schema
mongodb
zod
deno
1import type {
2 Collection,
3 CreateIndexesOptions,
4 DropIndexesOptions,
5 IndexDescription,
6 IndexSpecification,
7 ListIndexesOptions,
8} from "mongodb";
9import type { Infer, Schema } from "../types.ts";
10
11/**
12 * Index management operations for the Model class
13 *
14 * This module contains all index-related operations including creation,
15 * deletion, listing, and synchronization of indexes.
16 */
17
18/**
19 * Create a single index on the collection
20 *
21 * @param collection - MongoDB collection
22 * @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
23 * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
24 * @returns The name of the created index
25 */
26export async function createIndex<T extends Schema>(
27 collection: Collection<Infer<T>>,
28 keys: IndexSpecification,
29 options?: CreateIndexesOptions,
30): Promise<string> {
31 return await collection.createIndex(keys, options);
32}
33
34/**
35 * Create multiple indexes on the collection
36 *
37 * @param collection - MongoDB collection
38 * @param indexes - Array of index descriptions
39 * @param options - Index creation options
40 * @returns Array of index names created
41 */
42export async function createIndexes<T extends Schema>(
43 collection: Collection<Infer<T>>,
44 indexes: IndexDescription[],
45 options?: CreateIndexesOptions,
46): Promise<string[]> {
47 return await collection.createIndexes(indexes, options);
48}
49
50/**
51 * Drop a single index from the collection
52 *
53 * @param collection - MongoDB collection
54 * @param index - Index name or specification
55 * @param options - Drop index options
56 */
57export async function dropIndex<T extends Schema>(
58 collection: Collection<Infer<T>>,
59 index: string | IndexSpecification,
60 options?: DropIndexesOptions,
61): Promise<void> {
62 await collection.dropIndex(index as string, options);
63}
64
65/**
66 * Drop all indexes from the collection (except _id index)
67 *
68 * @param collection - MongoDB collection
69 * @param options - Drop index options
70 */
71export async function dropIndexes<T extends Schema>(
72 collection: Collection<Infer<T>>,
73 options?: DropIndexesOptions,
74): Promise<void> {
75 await collection.dropIndexes(options);
76}
77
78/**
79 * List all indexes on the collection
80 *
81 * @param collection - MongoDB collection
82 * @param options - List indexes options
83 * @returns Array of index information
84 */
85export async function listIndexes<T extends Schema>(
86 collection: Collection<Infer<T>>,
87 options?: ListIndexesOptions,
88): Promise<IndexDescription[]> {
89 const indexes = await collection.listIndexes(options).toArray();
90 return indexes as IndexDescription[];
91}
92
93/**
94 * Get index information by name
95 *
96 * @param collection - MongoDB collection
97 * @param indexName - Name of the index
98 * @returns Index description or null if not found
99 */
100export async function getIndex<T extends Schema>(
101 collection: Collection<Infer<T>>,
102 indexName: string,
103): Promise<IndexDescription | null> {
104 const indexes = await listIndexes(collection);
105 return indexes.find((idx) => idx.name === indexName) || null;
106}
107
108/**
109 * Check if an index exists
110 *
111 * @param collection - MongoDB collection
112 * @param indexName - Name of the index
113 * @returns True if index exists, false otherwise
114 */
115export async function indexExists<T extends Schema>(
116 collection: Collection<Infer<T>>,
117 indexName: string,
118): Promise<boolean> {
119 const index = await getIndex(collection, indexName);
120 return index !== null;
121}
122
123/**
124 * Synchronize indexes - create indexes if they don't exist, update if they differ
125 *
126 * This is useful for ensuring indexes match your schema definition
127 *
128 * @param collection - MongoDB collection
129 * @param indexes - Array of index descriptions to synchronize
130 * @param options - Options for index creation
131 * @returns Array of index names that were created
132 */
133export async function syncIndexes<T extends Schema>(
134 collection: Collection<Infer<T>>,
135 indexes: IndexDescription[],
136 options?: CreateIndexesOptions,
137): Promise<string[]> {
138 const existingIndexes = await listIndexes(collection);
139 const indexesToCreate: IndexDescription[] = [];
140
141 for (const index of indexes) {
142 const indexName = index.name || generateIndexName(index.key);
143 const existingIndex = existingIndexes.find(
144 (idx) => idx.name === indexName,
145 );
146
147 if (!existingIndex) {
148 indexesToCreate.push(index);
149 } else if (
150 JSON.stringify(existingIndex.key) !== JSON.stringify(index.key)
151 ) {
152 // Index exists but keys differ - drop and recreate
153 await dropIndex(collection, indexName);
154 indexesToCreate.push(index);
155 }
156 // If index exists and matches, skip it
157 }
158
159 const created: string[] = [];
160 if (indexesToCreate.length > 0) {
161 const names = await createIndexes(collection, indexesToCreate, options);
162 created.push(...names);
163 }
164
165 return created;
166}
167
168/**
169 * Generate index name from key specification
170 *
171 * @param keys - Index specification
172 * @returns Generated index name
173 */
174export function generateIndexName(keys: IndexSpecification): string {
175 if (typeof keys === "string") {
176 return keys;
177 }
178 const entries = Object.entries(keys as Record<string, number | string>);
179 return entries.map(([field, direction]) => `${field}_${direction}`).join("_");
180}