UNPKG

@imigueldiaz/mongodb-labeler

Version:

A MongoDB-based labeling system for content moderation with cryptographic signing

203 lines (202 loc) 8.34 kB
import { MongoClient, ObjectId } from "mongodb"; /** * Client for interacting with the MongoDB database where labels are stored. * * This class handles connecting to the database, creating necessary indexes, * and performing basic CRUD operations on labels. * * @param uri The URI to connect to the MongoDB instance. * @param databaseName The name of the database to use. Defaults to 'labeler'. * @param collectionName The name of the collection to use. Defaults to 'labels'. */ export class MongoDBClient { /** * Create a new MongoDBClient instance. * @param uri The URI to connect to the MongoDB instance. */ constructor(uri) { if (!uri) { throw new Error("Missing required parameter: mongoUri"); } if (!uri.startsWith("mongodb://") && !uri.startsWith("mongodb+srv://")) { throw new Error("Invalid scheme, expected connection string to start with \"mongodb://\" or \"mongodb+srv://\""); } this._url = uri; } /** * Connect to the MongoDB instance and initialize the collection. * If the collection doesn't exist, it will be created with the necessary indexes. * * This method should be called before any other method in this class. */ async connect() { try { this._client = await MongoClient.connect(this._url); this._db = this._client.db(); this._labels = this._db.collection("labels"); // Verificar si la colección existe const collections = await this._db.listCollections({ name: "labels" }).toArray(); const collectionExists = collections.length > 0; if (!collectionExists) { console.log(`Creating collection labels`); // Crear la colección explícitamente await this._db.createCollection("labels"); // Obtener referencia a la colección recién creada this._labels = this._db.collection("labels"); console.log(`Creating indexes for new collection labels`); // Crear los índices necesarios await this._labels.createIndex({ uri: 1 }); await this._labels.createIndex({ src: 1 }); } else { // Si la colección ya existe, solo obtener la referencia this._labels = this._db.collection("labels"); } } catch (error) { throw new Error(`Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`); } } /** * Close the connection to MongoDB. */ async close() { if (!this._client) { return; } try { await this._client.close(); } catch (error) { const wrappedError = new Error(`Failed to close MongoDB client: ${error instanceof Error ? error.message : String(error)}`); throw wrappedError; } } /** * Save a label to the MongoDB collection. * * @param label - The label to save, including a signature as an ArrayBuffer. * @returns A promise that resolves to the saved label. */ async saveLabel(label) { if (!this._labels) { throw new Error("Failed to save label: Collection is not initialized"); } try { const objectId = new ObjectId(); // Convert ISO string dates to Date objects for MongoDB const labelWithDates = { ...label, // cts siempre tendrá un valor porque se genera en createLabel si no está presente cts: new Date(String(label.cts)).toISOString(), exp: label.exp ? new Date(label.exp).toISOString() : undefined }; // eslint-disable-next-line @typescript-eslint/naming-convention const savedLabel = { ...labelWithDates, _id: objectId }; console.log('Saving label:', { val: savedLabel.val, exp: savedLabel.exp }); const result = await this._labels.insertOne(savedLabel); if (!result.acknowledged) { throw new Error("Operation not acknowledged"); } return savedLabel; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Failed to save label: ${message}`); } } /** * Find labels in the MongoDB collection matching the given query. * * @param query - The query object to filter labels. * @param options - Optional settings for the query, such as sort and limit. * @returns A promise that resolves to an array of labels matching the query. */ async findLabels(query = {}, options = {}) { if (!this._labels) { return []; } try { const { allowExpired, ...restQuery } = query; let finalQuery = restQuery; // Only filter expired labels if allowExpired is false if (!allowExpired) { const now = new Date(); console.log('Current time:', now.toISOString()); console.log('Query before expiration filter:', JSON.stringify(restQuery)); finalQuery = { $and: [ restQuery, { $or: [ { exp: { $exists: false } }, { exp: { $gt: new Date(now.toISOString()).toISOString() } } ] } ] }; console.log('Final query:', JSON.stringify(finalQuery)); } const results = await this._labels.find(finalQuery, options).toArray(); console.log('Found labels:', results.map(l => ({ val: l.val, exp: l.exp }))); return results; } catch (error) { throw new Error(`Failed to find labels: ${error instanceof Error ? error.message : String(error)}`); } } /** * Find a single label in the MongoDB collection matching the given query. * * @param query - The query object to filter labels. * @returns A promise that resolves to the matching label or null if not found. */ async findOne(query) { if (!this._labels) { return null; } try { return await this._labels.findOne(query); } catch (error) { throw new Error(`Failed to find label: ${error instanceof Error ? error.message : String(error)}`); } } /** * Retrieve a limited number of labels from the database that have an ID greater than the specified cursor. * * @param cursor - The ID after which labels should be retrieved. * @param limit - The maximum number of labels to return. * @returns A promise that resolves to an array of labels with IDs greater than the cursor. */ async getLabelsAfterCursor(cursor, limit) { try { // eslint-disable-next-line @typescript-eslint/naming-convention return this._labels?.find({ _id: { $gt: cursor } }).sort({ _id: 1 }).limit(limit).toArray() ?? []; } catch (error) { throw new Error(`Failed to get labels after cursor: ${error instanceof Error ? error.message : String(error)}`); } } /** * Update an existing label in the MongoDB collection. * * @param id - The ID of the label to update. * @param label - The new label data. * @returns A promise that resolves to true if the update was successful, false if the label wasn't found. */ async updateLabel(id, label) { if (!this._labels) { throw new Error("Collection is not initialized"); } try { const result = await this._labels.updateOne( // eslint-disable-next-line @typescript-eslint/naming-convention { _id: id }, { $set: label }); return result.modifiedCount > 0; } catch (error) { throw new Error(`Failed to update label: ${error instanceof Error ? error.message : String(error)}`); } } }