Create MongoDB repository implementation for production database. Use when implementing repository interface with MongoDB, setting up document mapping, creating indexes, or connecting to real database. Triggers on "mongodb repository", "mongo repository", "production repository", "database repository".
Creates a MongoDB repository implementation using the native MongoDB driver (not Mongoose). Includes document mapping, Zod validation, index creation, and proper ObjectId handling.
Location: src/repositories/mongodb/{entity-name}.mongodb.repository.ts
Naming: {entity-name}.mongodb.repository.ts (e.g., note.mongodb.repository.ts)
src/config/mongodb.setup.tssrc/schemas/{entity-name}.schema.tssrc/repositories/{entity-name}.repository.tsCreate src/repositories/mongodb/{entity-name}.mongodb.repository.ts
import { Collection, Db, ObjectId } from "mongodb";
import type { WithId, Filter, Sort } from "mongodb";
import type {
{Entity}Type,
Create{Entity}Type,
Update{Entity}Type,
{Entity}QueryParamsType,
{Entity}IdType,
} from "@/schemas/{entity-name}.schema";
import { {entity}Schema } from "@/schemas/{entity-name}.schema";
import {
DEFAULT_LIMIT,
DEFAULT_PAGE,
type PaginatedResultType,
} from "@/schemas/shared.schema";
import type { I{Entity}Repository } from "@/repositories/{entity-name}.repository";
import type { UserIdType } from "@/schemas/user.schemas";
import { getDatabase } from "@/config/mongodb.setup";
// MongoDB document interface (internal to repository)
// Maps domain model to MongoDB structure
interface Mongo{Entity}Document
extends Omit<{Entity}Type, "id" | "createdAt" | "updatedAt"> {
_id?: ObjectId;
createdAt: Date;
updatedAt: Date;
}
export class MongoDb{Entity}Repository implements I{Entity}Repository {
private collection: Collection<Mongo{Entity}Document> | null = null;
// Lazy load collection on first use
private async getCollection(): Promise<Collection<Mongo{Entity}Document>> {
if (!this.collection) {
const db: Db = await getDatabase();
this.collection = db.collection<Mongo{Entity}Document>("{entities}");
await this.createIndexes(this.collection);
console.log("📚 {Entities} collection initialized");
}
return this.collection;
}
// Index creation (idempotent)
private async createIndexes(
collection: Collection<Mongo{Entity}Document>,
): Promise<void> {
console.log("Creating indexes for {entities} collection...");
await Promise.all([
collection.createIndex({ createdBy: 1 }, { name: "{entities}_createdBy" }),
collection.createIndex({ createdAt: -1 }, { name: "{entities}_createdAt_desc" }),
// Add text index for searchable fields
collection.createIndex({ {searchableField}: "text" }, { name: "{entities}_{searchableField}_text" }),
]);
console.log("✅ {Entities} indexes created successfully");
}
// Map MongoDB document to domain entity with Zod validation
private mapDocumentToEntity(doc: WithId<Mongo{Entity}Document>): {Entity}Type {
const { _id, ...restOfDoc } = doc;
return {entity}Schema.parse({
...restOfDoc,
id: _id.toHexString(),
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
});
}
// Map domain data to MongoDB document
private mapEntityToDocument(
data: Create{Entity}Type,
createdByUserId: UserIdType,
): Omit<Mongo{Entity}Document, "_id"> {
const now = new Date();
return {
// Map each field from Create{Entity}Type
...data,
createdBy: createdByUserId,
createdAt: now,
updatedAt: now,
};
}
// ... implement interface methods (see below)
}
async findAll(
params: {Entity}QueryParamsType,
): Promise<PaginatedResultType<{Entity}Type>> {
const collection = await this.getCollection();
// Build MongoDB query filter
const filter: Filter<Mongo{Entity}Document> = {};
if (params.createdBy) {
filter.createdBy = params.createdBy;
}
if (params.search?.trim()) {
filter.$text = { $search: params.search.trim() };
}
// Pagination
const page = params.page ?? DEFAULT_PAGE;
const limit = params.limit ?? DEFAULT_LIMIT;
const skip = (page - 1) * limit;
// Sort criteria
const sortBy = params.sortBy ?? "createdAt";
const sortOrder = params.sortOrder === "asc" ? 1 : -1;
const sort: Sort = { [sortBy]: sortOrder };
// Execute queries in parallel
const [documents, total] = await Promise.all([
collection.find(filter).sort(sort).skip(skip).limit(limit).toArray(),
collection.countDocuments(filter),
]);
// Map to domain entities
const entities = documents.map((doc) => this.mapDocumentToEntity(doc));
const totalPages = Math.ceil(total / limit);
return {
data: entities,
total,
page,
limit,
totalPages,
};
}
async findById(id: {Entity}IdType): Promise<{Entity}Type | null> {
// Validate ObjectId format
if (!ObjectId.isValid(id)) {
return null;
}
const collection = await this.getCollection();
const document = await collection.findOne({ _id: new ObjectId(id) });
if (!document) {
return null;
}
return this.mapDocumentToEntity(document);
}
async findAllByIds(
ids: {Entity}IdType[],
params: {Entity}QueryParamsType,
): Promise<PaginatedResultType<{Entity}Type>> {
const collection = await this.getCollection();
// Convert to ObjectIds, filter invalid
const objectIds = ids
.filter((id) => ObjectId.isValid(id))
.map((id) => new ObjectId(id));
const filter: Filter<Mongo{Entity}Document> = { _id: { $in: objectIds } };
if (params.createdBy) {
filter.createdBy = params.createdBy;
}
if (params.search?.trim()) {
filter.$text = { $search: params.search.trim() };
}
const page = params.page ?? DEFAULT_PAGE;
const limit = params.limit ?? DEFAULT_LIMIT;
const skip = (page - 1) * limit;
const sortBy = params.sortBy === "id" ? "_id" : (params.sortBy ?? "createdAt");
const sortOrder = params.sortOrder === "asc" ? 1 : -1;
const sort: Sort = { [sortBy]: sortOrder };
const [documents, total] = await Promise.all([
collection.find(filter).sort(sort).skip(skip).limit(limit).toArray(),
collection.countDocuments(filter),
]);
const entities = documents.map((doc) => this.mapDocumentToEntity(doc));
const totalPages = Math.ceil(total / limit);
return {
data: entities,
total,
page,
limit,
totalPages,
};
}
async create(
data: Create{Entity}Type,
createdByUserId: UserIdType,
): Promise<{Entity}Type> {
const collection = await this.getCollection();
const documentToInsert = this.mapEntityToDocument(data, createdByUserId);
const result = await collection.insertOne(documentToInsert);
if (!result.insertedId) {
throw new Error("{Entity} creation failed, no ObjectId generated by database.");
}
return {entity}Schema.parse({
...documentToInsert,
id: result.insertedId.toHexString(),
});
}
async update(id: {Entity}IdType, data: Update{Entity}Type): Promise<{Entity}Type | null> {
if (!ObjectId.isValid(id)) {
return null;
}
const collection = await this.getCollection();
const updateDoc = {
$set: {
...data,
updatedAt: new Date(),
},
};
const result = await collection.findOneAndUpdate(
{ _id: new ObjectId(id) },
updateDoc,
{ returnDocument: "after" },
);
if (!result) {
return null;
}
return this.mapDocumentToEntity(result);
}
async remove(id: {Entity}IdType): Promise<boolean> {
if (!ObjectId.isValid(id)) {
return false;
}
const collection = await this.getCollection();
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
// Helper method for testing: clear all documents
async clear(): Promise<void> {
const collection = await this.getCollection();
await collection.deleteMany({});
}
// Helper method for testing: get collection stats
async getStats(): Promise<{ count: number; indexes: string[] }> {
const collection = await this.getCollection();
const count = await collection.countDocuments();
const indexes = await collection.listIndexes().toArray();
return {
count,
indexes: indexes.map((idx) => idx.name),
};
}
Always create a MongoDB-specific document interface:
interface Mongo{Entity}Document
extends Omit<{Entity}Type, "id" | "createdAt" | "updatedAt"> {
_id?: ObjectId; // MongoDB's primary key
createdAt: Date; // Required in documents
updatedAt: Date; // Required in documents
}
ObjectId.isValid(id) before usingnew ObjectId(id)_id.toHexString()null for invalid ObjectIds (don't throw)Always validate documents from MongoDB using Zod:
return {entity}Schema.parse({
...restOfDoc,
id: _id.toHexString(),
});
Initialize collection on first use, not in constructor:
private async getCollection(): Promise<Collection<...>> {
if (!this.collection) {
const db = await getDatabase();
this.collection = db.collection("{entities}");
await this.createIndexes(this.collection);
}
return this.collection;
}
createIndex is idempotent (safe to call multiple times){ name: "{entities}_fieldName" }Use Promise.all for independent queries:
const [documents, total] = await Promise.all([
collection.find(filter).sort(sort).skip(skip).limit(limit).toArray(),
collection.countDocuments(filter),
]);
See REFERENCE.md for a full implementation example including:
MongoDbNoteRepository classmongodb.setup.ts)sortBy: "id" → "_id" conversion