import * as crypto from "crypto" ; import Debug from "debug" ; import cloneDeep from "lodash.clonedeep" ; import slug from "slug" ; import { FullItem , FullItemAllOfFields , FullItemAllOfSections , GeneratorRecipe , ItemUrls , ItemVault , } from "../model/models" ; const debug = Debug ( "opconnect:builder" ) ; export interface ItemFieldOptions { value ? : string ; type ? : FullItemAllOfFields . TypeEnum ; sectionName ? : string ; purpose ? : FullItemAllOfFields . PurposeEnum ; label ? : string ; generate ? : boolean ; recipe ? : GeneratorRecipe ; } export class ItemBuilder { private private item : FullItem ; private private sections : Map < string , FullItemAllOfSections > ; private urls : { [ key ] : any ; } ; public constructor ( ) { this . reset ( ) ; } /**
     * Performs final assembly of the new Item.
     */ public build ( ) { if ( ! this . item . category ) { throw Error ( "Item Category is required." ) ; } this . item . sections = Array . from ( this . sections . values ( ) ) ; this . item . urls = this . urls . hrefs . map ( ( href ) => this . urls . primaryUrl === href ? ( { primary : true , href } as ItemUrls ) : ( { href } as ItemUrls ) ) ; const builtItem = cloneDeep ( this . item ) ; debug ( "Successfully built Item (id: %s, vault: %s)" , builtItem . id ) ; this . reset ( ) ; return builtItem ; } /**
     * Clears accumulated properties and puts
     * ItemBuilder back to a "pristine" state
     */ public reset ( ) { this . item = new FullItem ( ) ; this . item . fields = [ ] ; this . item . tags = [ ] ; this . sections = new Map ( ) ; this . urls = { primaryUrl : "" , hrefs : [ ] } ; } deprecated param { string } vaultId returns { ItemBuilder } public setVault ( vaultId ) { this . item . vault = { id : vaultId } as ItemVault ; return this ; } param { string } title returns { ItemBuilder } public setTitle ( title ) { this . item . title = title ; return this ; } param { string } tag returns { ItemBuilder } public addTag ( tag ) { this . item . tags . push ( tag ) ; return this ; } param { ItemFieldOptions } opts returns { ItemBuilder } public addField ( opts = { } ) { const field = { type : opts . type || FullItemAllOfFields . TypeEnum . String , purpose : opts . purpose || FullItemAllOfFields . PurposeEnum . Empty , label : opts . label , value : opts . value , generate : opts . generate || false , recipe : opts . generate ? { length : opts . recipe . length , characterSets : [ ... new Set ( opts . recipe . characterSets ) ] } as GeneratorRecipe : undefined , } ; if ( opts . sectionName ) { const { id : sectionId } = this . getOrCreateSection ( opts . sectionName ) ; field . section = { id : sectionId } ; } if ( opts . generate && ! validRecipe ( opts . recipe ) ) { throw TypeError ( `Field '${ opts . label }' contains an invalid Recipe.` ) ; } this . item . fields . push ( field ) ; return this ; } param sectionName returns { ItemBuilder } public addSection ( sectionName ) { this . getOrCreateSection ( sectionName ) ; return this ; } returns { ItemBuilder } public toggleFavorite ( ) { this . item . favorite = ! this . item . favorite ; return this ; } param url returns { ItemBuilder } public addUrl ( url ) { if ( url . primary ) this . urls . primaryUrl = url . href ; this . urls . hrefs . push ( url . href ) ; return this ; } param category returns { ItemBuilder } public setCategory ( category ) { if ( Object . values ( FullItem . CategoryEnum ) . indexOf ( category ) === - 1 ) { throw TypeError ( "Item Category is invalid" ) ; } this . item . category = category as FullItem . CategoryEnum ; return this ; } param sectionName private return { FullItemAllOfSections } private getOrCreateSection ( sectionName ) { const normalizedName = slug ( sectionName , { lower : true } ) ; if ( this . sections . has ( normalizedName ) ) { return this . sections . get ( normalizedName ) ; } const section = { id : generateSectionId ( ) , label : sectionName , } ; this . sections . set ( normalizedName , section ) ; return section ; } } param { GeneratorRecipe } recipe returns { boolean } const validRecipe = ( recipe ) : boolean => { if ( ! recipe . characterSets || ! recipe . characterSets . length ) return true ; const allowedCharactersSets = Object . values ( GeneratorRecipe . CharacterSetsEnum ) ; if ( recipe . characterSets . length > allowedCharactersSets . length ) return false ; for ( const cs of recipe . characterSets ) { if ( allowedCharactersSets . indexOf ( cs ) === - 1 ) { return false ; } } return true ; } ; param { number } length returns { string } const generateSectionId = ( length ? ) : string => crypto . randomBytes ( length || 13 ) . toString ( "hex" ) ;