UNPKG

type2docfx

Version:

A tool to convert json format output from TypeDoc to universal reference model for DocFx to consume.

310 lines (272 loc) 12.5 kB
/** * @module botbuilder-azure */ /** * 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'; const EntityGenerator = azure.TableUtilities.entityGenerator; /** Additional settings for configuring an instance of [TableStorage](../classes/botbuilder_azure_v4.tablestorage.html). */ export interface TableStorageSettings { /** * Name of the table to use for storage. * Check table name rules: https://docs.microsoft.com/en-us/rest/api/storageservices/Understanding-the-Table-Service-Data-Model?redirectedfrom=MSDN#table-names */ tableName: string; /** Storage access key. */ storageAccessKey?: string; /** (Optional) storage account to use or connection string. */ storageAccountOrConnectionString?: string; /** (Optional) azure storage host. */ host?: azure.StorageHost; } /** * Map of already initialized tables. Key = tableName, Value = Promise with TableResult creation. */ let checkedTables: { [name: string]: Promise<azure.TableService.TableResult>; } = {}; /** * Middleware that implements an Azure Table based storage provider for a bot. * * **Usage Example** * * ```javascript * const BotBuilderAzure = require('botbuilder-azure'); * const storage = new BotBuilderAzure.TableStorage({ * storageAccountOrConnectionString: 'UseDevelopmentStorage=true', * tableName: 'mybotstate' * }); * * // Add state middleware * const state = new BotStateManager(storage); * adapter.use(state); * ``` */ export class TableStorage implements Storage { private settings: TableStorageSettings; private tableService: TableServiceAsync; /** * Creates a new instance of the storage provider. * * @param settings (Optional) Setting to configure the provider. */ public constructor(settings: TableStorageSettings) { if (!settings) { throw new Error('The settings parameter is required.'); } // https://docs.microsoft.com/en-us/rest/api/storageservices/Understanding-the-Table-Service-Data-Model?redirectedfrom=MSDN#table-names 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(): Promise<azure.TableService.TableResult> { if (!checkedTables[this.settings.tableName]) checkedTables[this.settings.tableName] = this.tableService.createTableIfNotExistsAsync(this.settings.tableName); return checkedTables[this.settings.tableName]; } /** Delete backing table (mostly used for unit testing.) */ public deleteTable(): Promise<boolean> { if (checkedTables[this.settings.tableName]) delete checkedTables[this.settings.tableName]; return this.tableService.deleteTableIfExistsAsync(this.settings.tableName); } /** * Loads store items from storage * * @param keys Array of item keys to read from the store. **/ public read(keys: string[]): Promise<StoreItems> { if (!keys || !keys.length) { throw new Error('Please provide at least one key to read from storage.'); } return this.ensureTable().then(() => { var reads = keys.map(key => { let pk = this.sanitizeKey(key); return this.tableService.retrieveEntityAsync<any>(this.settings.tableName, pk, '', { entityResolver: entityResolver }) .then(result => { let value = unflatten(result, flattenOptions); value.eTag = value['.metadata'].etag; // remove TableRow Properties from storeItem ['PartitionKey', 'RowKey', '.metadata'].forEach(k => delete value[k]); return { key, value }; }).catch(handleNotFoundWith({ key, value: null })); }); return Promise.all(reads) .then(items => items .filter(prop => prop.value !== null) .reduce(propsReducer, {})); // as StoreItems }); }; /** * Saves store items to storage. * * @param changes Map of items to write to storage. **/ public write(changes: StoreItems): Promise<void> { if (!changes) { throw new Error('Please provide a StoreItems with changes to persist.') } // Check for bogus etags 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: StoreItem = changes[key]; // flatten the object graph into single columns let flat = flatten(storeItem, flattenOptions); let entity = asEntityDescriptor(flat); delete entity.eTag; // add PK/RK and ETag let pk = this.sanitizeKey(key); entity.PartitionKey = EntityGenerator.String(pk); entity.RowKey = EntityGenerator.String(''); entity['.metadata'] = { etag: storeItem.eTag }; if (storeItem.eTag == null || storeItem.eTag === "*") { // if new item or * then insert or replace unconditionaly return this.tableService.insertOrReplaceEntityAsync(this.settings.tableName, entity); } else if (storeItem.eTag.length > 0) { // if we have an etag, do opt. concurrency replace return this.tableService.replaceEntityAsync(this.settings.tableName, entity); } }); return Promise.all(writes) .then(() => { }); // void }); }; /** * Removes store items from storage * * @param keys Array of item keys to remove from the store. **/ public delete(keys: string[]): Promise<void> { if (!keys || !keys.length) return Promise.resolve(); return this.ensureTable().then(() => { let deletes = keys.map(key => { let pk = this.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(() => { }); // void }); } private sanitizeKey(key: string): string { let badChars = ['\\', '?', '/', '#', '\t', '\n', '\r']; let sb = ''; for (let iCh = 0; iCh < key.length; iCh++) { let ch = key[iCh]; let isBad: boolean = false; for (let iBad in badChars) { let badChar = badChars[iBad]; if (ch === badChar) { sb += '%' + ch.charCodeAt(0).toString(16); isBad = true; break; } } if (!isBad) sb += ch; } return sb; } // create TableServiceAsync instance based on connection config private createTableService(storageAccountOrConnectionString: string, storageAccessKey: string, host: any): TableServiceAsync { const tableService = storageAccountOrConnectionString ? azure.createTableService(storageAccountOrConnectionString, storageAccessKey, host) : azure.createTableService(); // create TableServiceAsync by using denodeify to create promise wrappers around cb functions return { createTableIfNotExistsAsync: this.denodeify(tableService, tableService.createTableIfNotExists), deleteTableIfExistsAsync: this.denodeify(tableService, tableService.deleteTableIfExists), retrieveEntityAsync: this.denodeify(tableService, tableService.retrieveEntity), insertOrReplaceEntityAsync: this.denodeify(tableService, tableService.insertOrReplaceEntity), replaceEntityAsync: this.denodeify(tableService, tableService.replaceEntity), deleteEntityAsync: this.denodeify(tableService, tableService.deleteEntity) } as any; } // turn a cb based azure method into a Promisified one private denodeify<T>(thisArg: any, fn: Function): (...args: any[]) => Promise<T> { return (...args: any[]) => { return new Promise<T>((resolve, reject) => { args.push((error: Error, result: any) => (error) ? reject(error) : resolve(result)); fn.apply(thisArg, args); }); }; } } // Promise based methods created using denodeify function interface TableServiceAsync extends azure.TableService { createTableIfNotExistsAsync(table: string): Promise<azure.TableService.TableResult>; deleteTableIfExistsAsync(table: string): Promise<boolean>; retrieveEntityAsync<T>(table: string, partitionKey: string, rowKey: string, options: any): Promise<T>; replaceEntityAsync<T>(table: string, entityDescriptor: T): Promise<azure.TableService.EntityMetadata>; insertOrReplaceEntityAsync<T>(table: string, entityDescriptor: T): Promise<azure.TableService.EntityMetadata>; deleteEntityAsync<T>(table: string, entityDescriptor: T): Promise<void>; } // Handle service 404 and 204 responses as null returns, throw any other error const handleNotFoundWith = (defaultValue: any) => (error) => { // return defaultValue when not found or no content if (error.statusCode === 404 || error.statusCode === 204) return defaultValue; else throw error; }; // Convert an object into EDM types const asEntityDescriptor = (obj): any => { return Object.keys(obj) .map(key => ({ key, value: asEntityProperty(obj[key]) })).reduce(propsReducer, {}); }; 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); } }; const getTypeOf = (obj) => ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); const isFloat = (n) => Number(n) === n && n % 1 !== 0; // Convert EDM types back to an JS object const entityResolver = (entity) => { return Object.keys(entity) .map(key => ({ key, value: getEdmValue(entity[key]) })) .reduce(propsReducer, {}); }; const getEdmValue = (entityValue) => { return entityValue.$ === azure.TableUtilities.EdmType.INT64 ? Number(entityValue._) : entityValue._; } // Reduces pairs for key/value into an object (e.g.: StoreItems) const propsReducer = (resolved, propValue: { key, value }): any => { resolved[propValue.key] = propValue.value; return resolved; }; // flat/flatten options to use '_' as delimiter (same as C#'s TableEntity.Flatten default delimiter) const flattenOptions = { delimiter: '_' };