import { MongoClient , Db , Collection , MongoError } from 'mongodb' ; import { CausalRepoStore , CausalRepoObject , CausalRepoBranch , getObjectHash , CausalRepoIndex , getAtomHashes , CausalRepoReflog , reflog , CausalRepoSitelog , sitelog , CausalRepoSitelogType , CausalRepoSitelogConnectionReason , CausalRepoBranchSettings , } from '@casual-simulation/causal-trees/core2' ; const MONGO_CODE_DUPLICATE_OBJECT = 11000 ; /**
 * Defines a class that is able to store a causal tree in MongoDB.
 */ export class MongoDBRepoStore implements CausalRepoStore { private _objects : Collection < MongoDBObject > ; private _heads : Collection < MongoDBHead > ; private _indexes : Collection < MongoDBIndex > ; private _reflog : Collection < MongoDBReflog > ; private _sitelog : Collection < MongoDBSitelog > ; private _settings : Collection < MongoDBBranchSettings > ; constructor ( objectsCollection , headsCollection , indexesCollection , reflogCollection , sitelogCollection , branchSettingsCollection ) { this . _objects = objectsCollection ; this . _heads = headsCollection ; this . _indexes = indexesCollection ; this . _reflog = reflogCollection ; this . _sitelog = sitelogCollection ; this . _settings = branchSettingsCollection ; } async init ( ) { await this . _heads . createIndex ( { name : 1 } , { unique : true } ) ; await this . _reflog . createIndex ( { branch : 1 , time : - 1 } ) ; await this . _sitelog . createIndex ( { branch : 1 , time : - 1 } ) ; await this . _sitelog . createIndex ( { site : 1 , branch : 1 , time : - 1 } ) ; } async getBranchSettings ( branch ) { const settings = await this . _settings . findOne ( { branch } , { sort : { time : - 1 } } ) ; if ( ! settings ) { return null ; } return { type : 'branch_settings' , branch : settings . branch , passwordHash : settings . passwordHash , time : settings . time , } ; } async saveSettings ( settings ) { await this . _settings . insertOne ( settings ) ; } async getSitelog ( branch ) { const sitelog = await this . _sitelog . find ( { branch : branch } ) . sort ( { time : - 1 , } ) . toArray ( ) ; return sitelog . map ( ( ref ) => ( { type : 'sitelog' , branch : ref . branch , site : ref . site , time : ref . time , sitelogType : ref . sitelogType , } as CausalRepoSitelog ) ) ; } async logSite ( branch , site , type , connectionReason ) { const log = sitelog ( branch , site , type , connectionReason ) ; await this . _sitelog . insertOne ( log ) ; return log ; } async getReflog ( branch ) { const reflog = await this . _reflog . find ( { branch : branch } ) . sort ( { time : - 1 , } ) . toArray ( ) ; return reflog . map ( ( ref ) => ( { type : 'reflog' , branch : ref . branch , hash : ref . hash , time : ref . time , } as CausalRepoReflog ) ) ; } async loadIndex ( head , index ) { let idx = await this . _indexes . findOne ( { _id : index . data . hash , } ) ; if ( ! idx ) { return await this . getObjects ( head , getAtomHashes ( index . data . atoms ) ) ; } return idx . objects ; } async storeIndex ( head , index , objects ) { const idx = { _id : index , objects , } ; await this . _indexes . replaceOne ( { _id : index } , idx , { upsert : true } ) ; await this . storeObjects ( head , objects ) ; } async getObjects ( head , keys ) { const objs = await this . _objects . find ( { _id : { $in : keys } , } ) . sort ( { 'atom.id.timestamp' : 1 } ) . map ( ( a ) => a . object ) . toArray ( ) ; return objs ; } async getObject ( key ) { const obj = await this . _objects . findOne ( { _id : key , } ) ; if ( obj ) { return obj . object ; } else { return null ; } } param objects async storeObjects ( head , objects ) { const mongoObjects = objects . map ( ( o ) => ( { _id : getObjectHash ( o ) , object : o , } ) ) ; if ( mongoObjects . length <= 0 ) { return ; } let op = this . _objects . initializeUnorderedBulkOp ( ) ; mongoObjects . forEach ( ( o ) => { op . find ( { _id : o . _id } ) . upsert ( ) . replaceOne ( o ) ; } ) ; try { await op . execute ( ) ; } catch ( err ) { handleMongoDBDuplicateErrors ( err ) ; } } param prefix async getBranches ( prefix ) { if ( prefix ) { prefix = escapeRegex ( prefix ) ; const branches = await this . _heads . find ( { name : { $regex : new RegExp ( `^${ prefix }` ) } , } ) . sort ( { name : 1 , } ) . toArray ( ) ; return branches ; } else { const branches = await this . _heads . find ( { } ) . sort ( { name : 1 , } ) . toArray ( ) ; return branches ; } } param head async saveBranch ( head ) { const result = await this . _heads . replaceOne ( { name : head . name } , head , { upsert : true , } ) ; if ( result . modifiedCount > 0 || result . upsertedCount > 0 ) { const ref = reflog ( head ) ; await this . _reflog . insertOne ( ref ) ; } } param head async deleteBranch ( head ) { await this . _heads . deleteOne ( { name : head . name , } ) ; } } export function handleMongoDBDuplicateErrors ( err ) { if ( err instanceof MongoError ) { const errors = ( < any > err ) . writeErrors as MongoError [ ] ; let valid = false ; if ( errors && Array . isArray ( errors ) ) { if ( errors . every ( ( e ) => e . code === MONGO_CODE_DUPLICATE_OBJECT ) ) { valid = true ; } } if ( ! valid ) { throw err ; } } else { throw err ; } } export interface MongoDBHead extends CausalRepoBranch { } export interface MongoDBObject { _id : string ; object : CausalRepoObject ; } export interface MongoDBIndex { _id : string ; objects : CausalRepoObject [ ] ; } export interface MongoDBReflog { _id ? : any ; branch : string ; hash : string ; time : Date ; } export interface MongoDBSitelog { _id ? : any ; branch : string ; site : string ; time : Date ; sitelogType ? : CausalRepoSitelogType ; } export interface MongoDBBranchSettings { _id ? : any ; branch : string ; time : Date ; passwordHash ? : string ; } export function escapeRegex ( value ) { let final = value ; for ( let char of REGEX_SPECIAL_CHARACTERS ) { final = final . replace ( char , `\\${ char }` ) ; } return final ; } export const REGEX_SPECIAL_CHARACTERS = [ '\\' , '^' , '$' , '.' , '*' , '+' , '?' , '(' , ')' , '[' , ']' , '{' , '}' , '|' , ] ;