module botbuildercommunity /**
 * Initial code copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */ import { Storage , StoreItems , StoreItem } from "botbuilder" ; import * as azure from "azure-storage" ; import { flatten , unflatten } from "flat" ; import { Host } from "botbuilder-azure" ; const EntityGenerator = azure . TableUtilities . entityGenerator ; /**
 * Additional settings for configuring an instance of `TableStorage`.
 */ export interface TableStorageSettings { remarks tableName : string ; /** (Optional) storage access key. */ storageAccessKey ? : string ; /** (Optional) storage account to use or connection string. */ storageAccountOrConnectionString ? : string ; /** (Optional) azure storage host. */ host ? : Host ; } private const checkedTables = { } ; remarks export class TableStorage implements Storage { private settings : TableStorageSettings ; private tableService : TableServiceAsync ; param settings public constructor ( settings ) { if ( ! settings ) { throw new Error ( 'The settings parameter is required.' ) ; } if ( ! /^[A-Za-z][A-Za-z0-9]{2,62}$/ . test ( settings . tableName ) ) { throw new Error ( 'The table name contains invalid characters.' ) ; } this . settings = Object . assign ( { } , settings ) ; this . tableService = this . createTableService ( this . settings . storageAccountOrConnectionString , this . settings . storageAccessKey , this . settings . host ) ; } /** Ensure the table is created. */ public ensureTable ( ) { if ( ! checkedTables [ this . settings . tableName ] ) { try { checkedTables [ this . settings . tableName ] = this . tableService . createTableIfNotExistsAsync ( this . settings . tableName ) ; } catch ( e ) { console . log ( e ) ; } } return checkedTables [ this . settings . tableName ] ; } /** Delete backing table (mostly used for unit testing.) */ public deleteTable ( ) { if ( checkedTables [ this . settings . tableName ] ) { delete checkedTables [ this . settings . tableName ] ; } return this . tableService . deleteTableIfExistsAsync ( this . settings . tableName ) ; } public read ( keys ) { if ( ! keys ) { throw new Error ( 'Please provide at least one key to read from storage.' ) ; } const sanitizedKeys = keys . filter ( ( k ) => k ) . map ( ( key ) => sanitizeKey ( key ) ) ; return this . ensureTable ( ) . then ( ( container ) => { return new Promise < StoreItems > ( ( resolve , reject ) : void => { Promise . all < StoreItem > ( sanitizedKeys . map ( ( key ) => { return this . tableService . doesTableExistAsync ( this . settings . tableName ) . then ( ( tableResult ) => { if ( tableResult . exists ) { return this . tableService . retrieveEntityAsync < any > ( this . settings . tableName , key , '' , { entityResolver : entityResolver } ) . then ( result => { let value = unflatten ( result , flattenOptions ) ; value . eTag = value [ '.metadata' ] . etag ; [ 'PartitionKey' , 'RowKey' , '.metadata' ] . forEach ( k => delete value [ k ] ) ; return { key , value } ; } ) . catch ( handleNotFoundWith ( { key , value : null } ) ) ; } else { return { } as StoreItem ; } } ) ; } ) ) . then ( ( items ) => { if ( items !== null && items . length > 0 ) { const storeItems = items . filter ( prop => ( < any > prop ) . value !== null ) . map ( ( propValue ) => ( { ... propValue , key : unSanitizeKey ( propValue . key ) } ) ) . reduce ( propsReducer , { } ) ; resolve ( storeItems ) ; } } ) . catch ( ( error ) => { reject ( error ) ; } ) ; } ) ; } ) . catch ( ( error ) => { throw error ; } ) ; } public write ( changes ) { if ( ! changes ) { throw new Error ( 'Please provide a StoreItems with changes to persist.' ) ; } Object . keys ( changes ) . map ( key => { let eTag = changes [ key ] . eTag ; if ( eTag != null && eTag . trim ( ) === "" ) { throw new Error ( 'Etag empty for key ' + key ) ; } } ) ; return this . ensureTable ( ) . then ( ( ) => { var writes = Object . keys ( changes ) . map ( key => { let storeItem = changes [ key ] ; let flat = flatten ( storeItem , flattenOptions ) ; let entity = asEntityDescriptor ( flat ) ; delete entity . eTag ; let pk = sanitizeKey ( key ) ; entity . PartitionKey = EntityGenerator . String ( pk ) ; entity . RowKey = EntityGenerator . String ( '' ) ; entity [ '.metadata' ] = { etag : storeItem . eTag } ; if ( storeItem . eTag == null || storeItem . eTag === "*" ) { return this . tableService . insertOrReplaceEntityAsync ( this . settings . tableName , entity ) ; } else if ( storeItem . eTag . length > 0 ) { return this . tableService . replaceEntityAsync ( this . settings . tableName , entity ) ; } } ) ; return Promise . all ( writes ) . then ( ( ) => { } ) . catch ( err => console . log ( err ) ) ; } ) . catch ( e => console . log ( e ) ) ; } ; public delete ( keys ) { if ( ! keys || ! keys . length ) return Promise . resolve ( ) ; return this . ensureTable ( ) . then ( ( ) => { let deletes = keys . map ( key => { let pk = sanitizeKey ( key ) ; let entity = { PartitionKey : EntityGenerator . String ( pk ) , RowKey : EntityGenerator . String ( '' ) } ; entity [ '.metadata' ] = { etag : '*' } ; return this . tableService . deleteEntityAsync ( this . settings . tableName , entity ) . catch ( handleNotFoundWith ( null ) ) ; } ) ; return Promise . all ( deletes ) . then ( ( ) => { } ) . catch ( err => console . log ( err ) ) ; } ) . catch ( e => console . log ( e ) ) ; } private createTableService ( storageAccountOrConnectionString , storageAccessKey , host ) { const tableService = storageAccountOrConnectionString ? azure . createTableService ( storageAccountOrConnectionString , storageAccessKey , host ) : azure . createTableService ( ) ; return { createTableIfNotExistsAsync : this . denodeify ( tableService , tableService . createTableIfNotExists ) , deleteTableIfExistsAsync : this . denodeify ( tableService , tableService . deleteTableIfExists ) , retrieveEntityAsync : this . denodeify ( tableService , tableService . retrieveEntity ) , doesTableExistAsync : this . denodeify ( tableService , tableService . doesTableExist ) , insertOrReplaceEntityAsync : this . denodeify ( tableService , tableService . insertOrReplaceEntity ) , replaceEntityAsync : this . denodeify ( tableService , tableService . replaceEntity ) , deleteEntityAsync : this . denodeify ( tableService , tableService . deleteEntity ) } as any ; } private denodeify < T > ( thisArg , fn ) { return ( ... args ) => { return new Promise < T > ( ( resolve , reject ) => { args . push ( ( error , result ) => ( error ) ? reject ( error ) : resolve ( result ) ) ; fn . apply ( thisArg , args ) ; } ) ; } ; } } private interface TableServiceAsync extends azure . TableService { createTableIfNotExistsAsync ( table ) : Promise < azure . TableService . TableResult > ; deleteTableIfExistsAsync ( table ) : Promise < boolean > ; retrieveEntityAsync < T > ( table , partitionKey , rowKey , options ) : Promise < T > ; doesTableExistAsync ( table ) : Promise < azure . TableService . TableResult > ; replaceEntityAsync < T > ( table , entityDescriptor ) : Promise < azure . TableService . EntityMetadata > ; insertOrReplaceEntityAsync < T > ( table , entityDescriptor ) : Promise < azure . TableService . EntityMetadata > ; deleteEntityAsync < T > ( table , entityDescriptor ) : Promise < void > ; } private const handleNotFoundWith = ( defaultValue ) => ( error ) => { if ( error . statusCode === 404 || error . statusCode === 204 ) return defaultValue ; else throw error ; } ; private const asEntityDescriptor = ( obj ) : any => { return Object . keys ( obj ) . map ( key => ( { key , value : asEntityProperty ( obj [ key ] ) } ) ) . reduce ( propsReducer , { } ) ; } ; private const asEntityProperty = ( value ) => { switch ( getTypeOf ( value ) ) { case 'date' : return EntityGenerator . DateTime ( value ) ; case 'boolean' : return EntityGenerator . Boolean ( value ) ; case 'number' : let maxSafeInt32 = Math . pow ( 2 , 32 ) - 1 ; if ( isFloat ( value ) ) return EntityGenerator . Double ( value ) ; if ( Math . abs ( value ) > maxSafeInt32 ) return EntityGenerator . Int64 ( value ) ; return EntityGenerator . Int32 ( value ) ; case 'string' : default : return EntityGenerator . String ( value ) ; } } ; private const getTypeOf = ( obj ) => ( { } ) . toString . call ( obj ) . match ( /\s([a-zA-Z]+)/ ) [ 1 ] . toLowerCase ( ) ; private const isFloat = ( n ) => Number ( n ) === n && n % 1 !== 0 ; private const entityResolver = ( entity ) => { return Object . keys ( entity ) . map ( key => ( { key , value : getEdmValue ( entity [ key ] ) } ) ) . reduce ( propsReducer , { } ) ; } ; private const getEdmValue = ( entityValue ) => { return entityValue . $ === azure . TableUtilities . EdmType . INT64 ? Number ( entityValue . _ ) : entityValue . _ ; } ; private const propsReducer = ( resolved , propValue ) : any => { resolved [ propValue . key ] = propValue . value ; return resolved ; } ; /**
 * Generate list of chars to replace
 */ const charToSanitizeList = [ { char : '?' , value : '%' + '?' . charCodeAt ( 0 ) . toString ( 16 ) , regexp : /\?/g , } , { char : '/' , value : '%' + '/' . charCodeAt ( 0 ) . toString ( 16 ) , regexp : /\//g , } , { char : '#' , value : '%' + '#' . charCodeAt ( 0 ) . toString ( 16 ) , regexp : /#/g , } , { char : '\t' , value : '%' + '\t' . charCodeAt ( 0 ) . toString ( 16 ) , regexp : /\t/g , } , { char : '\n' , value : '%' + '\n' . charCodeAt ( 0 ) . toString ( 16 ) , regexp : /\n/g , } , { char : '\r' , value : '%' + '\r' . charCodeAt ( 0 ) . toString ( 16 ) , regexp : /\r/g , } , { char : '\\' , value : '%' + '\\' . charCodeAt ( 0 ) . toString ( 16 ) , regexp : /\\/g , } , ] ; private const sanitizeKey = ( key ) : string => { let sb = key ; charToSanitizeList . forEach ( ( charInfo ) => { sb = sb . replace ( charInfo . regexp , charInfo . value ) ; } ) ; return sb ; } ; private const unSanitizeKey = ( key ) : string => { let result = key ; charToSanitizeList . forEach ( ( charInfo ) => { result = result . replace ( new RegExp ( charInfo . value , 'g' ) , charInfo . char ) ; } ) ; return result ; } ; private const flattenOptions = { delimiter : '_' } ;