import { createServiceLogger } from '../../utils/logger'; import { openDB } from '../../orbit/orbitDBService'; import { validateDocument } from '../schema/validator'; import { ErrorCode, StoreType, StoreOptions, CreateResult, UpdateResult, PaginatedResult, QueryOptions, ListOptions, } from '../types'; import { DBError } from '../core/error'; const logger = createServiceLogger('DB_STORE'); /** * Base Store interface that all store implementations should extend */ export interface BaseStore { create>( collection: string, id: string, data: Omit, options?: StoreOptions, ): Promise; get>( collection: string, id: string, options?: StoreOptions & { skipCache?: boolean }, ): Promise; update>( collection: string, id: string, data: Partial>, options?: StoreOptions & { upsert?: boolean }, ): Promise; remove(collection: string, id: string, options?: StoreOptions): Promise; list>( collection: string, options?: ListOptions, ): Promise>; query>( collection: string, filter: (doc: T) => boolean, options?: QueryOptions, ): Promise>; createIndex(collection: string, field: string, options?: StoreOptions): Promise; } /** * Open a store of the specified type */ export async function openStore( collection: string, storeType: StoreType, options?: StoreOptions, ): Promise { try { // Log minimal connection info to avoid leaking sensitive data logger.info( `Opening ${storeType} store for collection: ${collection} (connection ID: ${options?.connectionId || 'default'})`, ); return await openDB(collection, storeType).catch((err) => { throw new Error(`OrbitDB openDB failed: ${err.message}`); }); } catch (error) { logger.error(`Error opening ${storeType} store for collection ${collection}:`, error); // Add more context to the error for improved debugging const errorMessage = error instanceof Error ? error.message : String(error); throw new DBError( ErrorCode.OPERATION_FAILED, `Failed to open ${storeType} store for collection ${collection}: ${errorMessage}`, error, ); } } /** * Recursively sanitize an object by removing undefined values * This is necessary because IPLD doesn't support undefined values */ function deepSanitizeUndefined(obj: any): any { if (obj === null || obj === undefined) { return null; } if (Array.isArray(obj)) { return obj.map(deepSanitizeUndefined).filter((item) => item !== undefined); } if (typeof obj === 'object' && obj.constructor === Object) { const sanitized: any = {}; for (const [key, value] of Object.entries(obj)) { const sanitizedValue = deepSanitizeUndefined(value); // Only include the property if it's not undefined if (sanitizedValue !== undefined) { sanitized[key] = sanitizedValue; } } return sanitized; } return obj; } /** * Helper function to prepare a document for storage */ export function prepareDocument>( collection: string, data: Omit, existingDoc?: T | null, ): T { const timestamp = Date.now(); // Deep sanitize the input data by removing undefined values const sanitizedData = deepSanitizeUndefined(data) as Omit; // Prepare document for validation let docToValidate: T; // If it's an update to an existing document if (existingDoc) { docToValidate = { ...existingDoc, ...sanitizedData, updatedAt: timestamp, } as T; } else { // Otherwise it's a new document docToValidate = { ...sanitizedData, createdAt: timestamp, updatedAt: timestamp, } as unknown as T; } // Deep sanitize the final document to ensure no undefined values remain const finalDocument = deepSanitizeUndefined(docToValidate) as T; // Validate the document BEFORE processing validateDocument(collection, finalDocument); // Return the validated document return finalDocument; }