258 lines
11 KiB
TypeScript
258 lines
11 KiB
TypeScript
import { Injectable, Inject } from '@nestjs/common';
|
|
import { Db, Document, Collection, Filter, ObjectId, UpdateResult } from 'mongodb';
|
|
|
|
@Injectable()
|
|
export class MongoService {
|
|
|
|
private collection: Collection<Document>;
|
|
|
|
constructor(@Inject('MONGO_DB') private readonly db: Db) { this.collection = this.db.collection('mongoCache') }
|
|
|
|
async set(collectionName: string) { this.collection = this.db.collection(collectionName) }
|
|
|
|
async getDb() { return this.collection }
|
|
|
|
/**
|
|
* Find a document by UUID or create it if it doesn't exist
|
|
* @param data Document data with UUID field
|
|
* @returns The found or created document
|
|
*
|
|
* @example
|
|
* Create a new user or retrieve existing one
|
|
* const userData = { uuid: 'TOKEN:12345:user', name: 'John Doe', email: 'john@example.com' };
|
|
* const user = await mongoService.findOrCreate(userData);
|
|
*/
|
|
async findOrCreate(data: Record<string, any>): Promise<{ data: Document, isCreated: boolean }> {
|
|
if (!data.uuid) { throw new Error('UUID is required for findOrCreate operation') }
|
|
// Use direct UUID lookup instead of regex for exact match
|
|
const existingDoc = await this.collection.findOne({ uuid: data.uuid } as Filter<Document>);
|
|
if (existingDoc) { return { data: existingDoc, isCreated: false } }
|
|
const insertResult = await this.collection.insertOne(data);
|
|
if (!insertResult.acknowledged) { throw new Error('Failed to insert document') }
|
|
return { data: await this.getOne(insertResult.insertedId), isCreated: true };
|
|
}
|
|
|
|
/**
|
|
* Get all documents from the collection
|
|
* @returns Array of all documents
|
|
*
|
|
* @example
|
|
* Get all users in the collection
|
|
* const allUsers = await mongoService.getAll();
|
|
*/
|
|
async getAll(): Promise<Document[]> { return await this.collection.find().toArray() }
|
|
|
|
/**
|
|
* Find a document by ID key using regex pattern
|
|
* @param idKey ID key to search for
|
|
* @returns The found document
|
|
* @throws Error if document is not found
|
|
*
|
|
* @example
|
|
* Find a user by ID key
|
|
* const user = await mongoService.findOne('12345');
|
|
* This will search for documents with uuid matching pattern ^TOKEN:12345:
|
|
*/
|
|
async findOne(filter: Filter<Document>): Promise<Document> {
|
|
const result = await this.collection.findOne(filter);
|
|
if (!result) { throw new Error(`Document with ID key ${filter} not found`) }
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Find multiple documents using a filter
|
|
* @param filter MongoDB filter
|
|
* @param limit Optional limit of results (default: no limit)
|
|
* @param skip Optional number of documents to skip (default: 0)
|
|
* @returns Array of matching documents
|
|
*
|
|
* @example
|
|
* Find active users with pagination
|
|
* const filter = { active: true } as Filter<Document>;
|
|
* const activeUsers = await mongoService.findMany(filter, 10, 20); // limit 10, skip 20
|
|
*
|
|
* @example
|
|
* Find users by role
|
|
* const admins = await mongoService.findMany({ role: 'admin' } as Filter<Document>);
|
|
*/
|
|
async findMany(filter: Filter<Document>, limit?: number, skip?: number): Promise<Document[]> {
|
|
let query = this.collection.find(filter);
|
|
if (typeof skip === 'number') { query = query.skip(skip) }
|
|
if (typeof limit === 'number') { query = query.limit(limit) }
|
|
return await query.toArray();
|
|
}
|
|
|
|
/**
|
|
* Get a document by its MongoDB ObjectId
|
|
* @param id MongoDB ObjectId
|
|
* @returns The found document
|
|
* @throws Error if document is not found
|
|
*
|
|
* @example
|
|
* Get a user by ObjectId
|
|
* const userId = new ObjectId('507f1f77bcf86cd799439011');
|
|
* const user = await mongoService.getOne(userId);
|
|
*/
|
|
async getOne(id: ObjectId): Promise<Document> {
|
|
const result = await this.collection.findOne({ _id: id });
|
|
if (!result) { throw new Error(`Document with ID ${id.toString()} not found`) }
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Find documents by regex pattern on UUID field
|
|
* @param idKey ID key to search for
|
|
* @returns Array of matching documents
|
|
*
|
|
* @example
|
|
* Find all users with a specific ID key pattern
|
|
* const users = await mongoService.findByRegex('12345');
|
|
* This will return all documents with uuid matching pattern ^TOKEN:12345:
|
|
*/
|
|
async findByRegex(idKey: string): Promise<Document[]> {
|
|
if (!idKey) { throw new Error('ID key is required for regex search') }
|
|
const pattern = `^${idKey}`;
|
|
return await this.collection.find({ uuid: { $regex: pattern } } as Filter<Document>).toArray();
|
|
}
|
|
|
|
/**
|
|
* Update a single document by its MongoDB ObjectId
|
|
* @param id MongoDB ObjectId
|
|
* @param data Data to update
|
|
* @returns The updated document
|
|
* @throws Error if document is not found or update fails
|
|
*
|
|
* @example
|
|
* Update a user's profile
|
|
* const userId = new ObjectId('507f1f77bcf86cd799439011');
|
|
* const updates = { name: 'Jane Doe', lastLogin: new Date() };
|
|
* const updatedUser = await mongoService.updateOne(userId, updates);
|
|
*/
|
|
async updateOne(id: ObjectId, data: Record<string, any>): Promise<Document> {
|
|
const updateResult = await this.collection.updateOne(
|
|
{ _id: id },
|
|
{ $set: data }
|
|
);
|
|
if (!updateResult.acknowledged) { throw new Error('Update operation failed') }
|
|
if (updateResult.matchedCount === 0) { throw new Error(`Document with ID ${id.toString()} not found`) }
|
|
return await this.getOne(id);
|
|
}
|
|
|
|
/**
|
|
* Update multiple documents matching a filter
|
|
* @param filter MongoDB filter
|
|
* @param data Data to update
|
|
* @returns Update result with count of modified documents
|
|
* @throws Error if update fails
|
|
*
|
|
* @example
|
|
* Mark all inactive users as archived
|
|
* const filter = { active: false } as Filter<Document>;
|
|
* const updates = { status: 'archived', archivedAt: new Date() };
|
|
* const result = await mongoService.updateMany(filter, updates);
|
|
* console.log(`${result.modifiedCount} users archived`);
|
|
*/
|
|
async updateMany(filter: Filter<Document>, data: Record<string, any>): Promise<UpdateResult> {
|
|
const updateResult = await this.collection.updateMany(filter, { $set: data });
|
|
if (!updateResult.acknowledged) { throw new Error('Update operation failed') }
|
|
return updateResult;
|
|
}
|
|
|
|
/**
|
|
* Delete a document by its MongoDB ObjectId
|
|
* @param id MongoDB ObjectId
|
|
* @returns True if document was deleted, false otherwise
|
|
*
|
|
* @example
|
|
* Delete a user account
|
|
* const userId = new ObjectId('507f1f77bcf86cd799439011');
|
|
* const deleted = await mongoService.deleteOne(userId);
|
|
* if (deleted) console.log('User successfully deleted');
|
|
*/
|
|
async deleteOne(id: ObjectId): Promise<boolean> {
|
|
const deleteResult = await this.collection.deleteOne({ _id: id });
|
|
return deleteResult.acknowledged && deleteResult.deletedCount > 0;
|
|
}
|
|
|
|
/**
|
|
* Delete multiple documents matching a filter
|
|
* @param filter MongoDB filter
|
|
* @returns Number of deleted documents
|
|
*
|
|
* @example
|
|
* Delete all expired sessions
|
|
* const filter = { expiresAt: { $lt: new Date() } } as Filter<Document>;
|
|
* const count = await mongoService.deleteMany(filter);
|
|
* console.log(`${count} expired sessions deleted`);
|
|
*/
|
|
async deleteMany(filter: Filter<Document>): Promise<number> {
|
|
const deleteResult = await this.collection.deleteMany(filter);
|
|
return deleteResult.acknowledged ? deleteResult.deletedCount : 0;
|
|
}
|
|
|
|
/**
|
|
* Find documents by regex pattern on any specified field
|
|
* @param field The field name to apply the regex filter on
|
|
* @param value The value to search for in the field
|
|
* @param options Optional regex options (e.g., 'i' for case-insensitive)
|
|
* @param prefix Optional prefix to add before the value (default: '')
|
|
* @param suffix Optional suffix to add after the value (default: '')
|
|
* @returns Array of matching documents
|
|
*
|
|
* @example
|
|
* Find users with email from a specific domain (case-insensitive)
|
|
* const gmailUsers = await mongoService.findByFieldRegex('email', 'gmail.com', 'i');
|
|
*
|
|
* @example
|
|
* Find users with names starting with 'J'
|
|
* const usersStartingWithJ = await mongoService.findByFieldRegex('name', 'J', 'i', '^');
|
|
*
|
|
* @example
|
|
* Find users with phone numbers ending in specific digits
|
|
* const specificPhoneUsers = await mongoService.findByFieldRegex('phone', '5555', '', '', '$');
|
|
*/
|
|
async findByFieldRegex(field: string, value: string, options?: string, prefix: string = '', suffix: string = ''): Promise<Document[]> {
|
|
if (!field || !value) { throw new Error('Field name and value are required for regex search') }
|
|
const pattern = `${prefix}${value}${suffix}`;
|
|
const query: Record<string, any> = {};
|
|
query[field] = { $regex: pattern };
|
|
if (options) { query[field].$options = options; }
|
|
return await this.collection.find(query as unknown as Filter<Document>).toArray();
|
|
}
|
|
|
|
/**
|
|
* Find documents by regex pattern across all fields (including nested)
|
|
* @param value The value to search for
|
|
* @param options Optional regex options (e.g., 'i' for case-insensitive)
|
|
* @returns Array of matching documents
|
|
*
|
|
* @example
|
|
* Find any document containing a specific value anywhere
|
|
* const docs = await mongoService.findByRegexAcrossFields('someValue', 'i');
|
|
*/
|
|
async findByRegexAcrossFields(value: string, options?: string, searchType: 'value' | 'key' | 'both' = 'value'): Promise<Document[]> {
|
|
if (!value) { throw new Error('Search value is required') }
|
|
const query: any = { $or: [] };
|
|
if (searchType === 'value' || searchType === 'both') {
|
|
query.$or.push(
|
|
{ '$expr': { $regexMatch: { input: { $toString: '$$ROOT' }, regex: value, options } } },
|
|
{ 'data': { $type: 'object', $regex: value, $options: options } }
|
|
);
|
|
}
|
|
|
|
if (searchType === 'key' || searchType === 'both') {
|
|
query.$where = function () {
|
|
const searchRegex = new RegExp(value, options);
|
|
function checkKeys(obj: Record<string, any>) {
|
|
for (const key in obj) {
|
|
if (searchRegex.test(key)) return true; if (obj[key] && typeof obj[key] === 'object') { if (checkKeys(obj[key])) return true }
|
|
}
|
|
return false;
|
|
} return checkKeys(this)
|
|
}.toString();
|
|
}
|
|
|
|
return await this.collection.find(query as unknown as Filter<Document>).toArray();
|
|
}
|
|
}
|