botbuilder-azure
Version:
Azure extensions for Microsoft BotBuilder.
254 lines • 10.7 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BlobStorage = void 0;
/**
* @module botbuilder-azure
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const storage_blob_1 = require("@azure/storage-blob");
const querystring_1 = require("querystring");
const consumers_1 = __importDefault(require("stream/consumers"));
/**
* @private
*/
const ContainerNameCheck = new RegExp('^[a-z0-9](?!.*--)[a-z0-9-]{1,61}[a-z0-9]$');
/**
* @private
*/
// tslint:disable-next-line:max-line-length typedef align no-shadowed-variable
const ResolvePromisesSerial = (values, promise) => values
.map((value) => () => promise(value))
.reduce((promise, func) => promise.then((result) => func().then(Array.prototype.concat.bind(result))), Promise.resolve([]));
/**
* @private
*/
// tslint:disable-next-line: typedef align
const ResolvePromisesParallel = (values, promise) => Promise.all(values.map(promise));
/**
* @private
* Internal dictionary with the containers where entities will be stored.
*/
const checkedCollections = {};
/**
* Middleware that implements a BlobStorage based storage provider for a bot.
*
* @remarks
* The BlobStorage implements its storage using a single Azure Storage Blob Container. Each entity
* is serialized into a JSON string and stored in an individual text blob. Each blob
* is named after the key which is encoded and ensure it conforms a valid blob name.
*
* @deprecated This class is deprecated in favor of [BlobsStorage](xref:botbuilder-azure-blobs.BlobsStorage)
*/
class BlobStorage {
/**
* Creates a new BlobStorage instance.
*
* @param settings Settings for configuring an instance of BlobStorage.
*/
constructor(settings) {
if (!settings) {
throw new Error('The settings parameter is required.');
}
if (!settings.containerName) {
throw new Error('The containerName is required.');
}
if (!this.checkContainerName(settings.containerName)) {
throw new Error('Invalid container name.');
}
if (!settings.storageAccountOrConnectionString) {
throw new Error('The storageAccountOrConnectionString is required.');
}
this.settings = Object.assign({}, settings);
const pipeline = (0, storage_blob_1.newPipeline)(new storage_blob_1.AnonymousCredential(), {
retryOptions: {
retryPolicyType: storage_blob_1.StorageRetryPolicyType.FIXED,
maxTries: 5,
retryDelayInMs: 500,
}, // Retry options
});
this.containerClient = new storage_blob_1.ContainerClient(this.settings.storageAccountOrConnectionString, this.settings.containerName, pipeline.options);
this.useEmulator = settings.storageAccountOrConnectionString === 'UseDevelopmentStorage=true;';
}
/**
* Retrieve entities from the configured blob container.
*
* @param keys An array of entity keys.
* @returns The read items.
*/
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) => this.sanitizeKey(key));
return this.ensureContainerExists()
.then(() => {
return new Promise((resolve, reject) => {
Promise.all(sanitizedKeys.map((key) => __awaiter(this, void 0, void 0, function* () {
const blob = this.containerClient.getBlobClient(key);
if (yield blob.exists()) {
const result = yield blob.download();
const { etag: eTag, readableStreamBody } = result;
if (!readableStreamBody) {
return { document: {} };
}
const document = (yield consumers_1.default.json(readableStreamBody));
document.document.eTag = eTag;
return document;
}
else {
// If blob does not exist, return an empty DocumentStoreItem.
return { document: {} };
}
})))
.then((items) => {
if (items !== null && items.length > 0) {
const storeItems = {};
items
.filter((x) => x)
.forEach((item) => {
storeItems[item.realId] = item.document;
});
resolve(storeItems);
}
})
.catch((error) => {
reject(error);
});
});
})
.catch((error) => {
throw error;
});
}
/**
* Store a new entity in the configured blob container.
*
* @param changes The changes to write to storage.
* @returns A promise representing the asynchronous operation.
*/
write(changes) {
if (!changes) {
throw new Error('Please provide a StoreItems with changes to persist.');
}
return this.ensureContainerExists().then(() => {
const blobs = Object.keys(changes).map((key) => {
const documentChange = {
id: this.sanitizeKey(key),
realId: key,
document: changes[key],
};
const payload = JSON.stringify(documentChange);
const options = {
conditions: changes[key].eTag === '*' ? {} : { ifMatch: changes[key].eTag },
};
return {
id: documentChange.id,
data: payload,
options: options,
};
});
// A block blob can be uploaded using a single PUT operation or divided into multiple PUT block operations
// depending on the payload's size. The default maximum size for a single blob upload is 128MB.
// An 'InvalidBlockList' error is commonly caused due to concurrently uploading an object larger than 128MB in size.
const promise = (blob) => {
const blockBlobClient = this.containerClient.getBlockBlobClient(blob.id);
const uploadBlobResponse = blockBlobClient.upload(blob.data, blob.data.length, blob.options);
return uploadBlobResponse;
};
// if the blob service client is using the storage emulator, all write operations must be performed in a sequential mode
// because of the storage emulator internal implementation, that includes a SQL LocalDb
// that crash with a deadlock when performing parallel uploads.
// This behavior does not occur when using an Azure Blob Storage account.
const results = this.useEmulator
? ResolvePromisesSerial(blobs, promise)
: ResolvePromisesParallel(blobs, promise);
return results
.then(() => {
return;
})
.catch((error) => {
throw error;
});
});
}
/**
* Delete entity blobs from the configured container.
*
* @param keys An array of entity keys.
* @returns A promise representing the asynchronous operation.
*/
delete(keys) {
if (!keys) {
throw new Error('Please provide at least one key to delete from storage.');
}
const sanitizedKeys = keys.filter((k) => k).map((key) => this.sanitizeKey(key));
return this.ensureContainerExists()
.then(() => {
return Promise.all(sanitizedKeys.map((key) => __awaiter(this, void 0, void 0, function* () {
const blockBlobClient = this.containerClient.getBlockBlobClient(key);
return yield blockBlobClient.deleteIfExists();
})));
})
.then(() => {
return;
})
.catch((error) => {
throw error;
});
}
/**
* Get a blob name validated representation of an entity to be used as a key.
*
* @param key The key used to identify the entity.
* @returns An appropriately escaped version of the key.
*/
sanitizeKey(key) {
if (!key || key.length < 1) {
throw new Error('Please provide a not empty key.');
}
const segments = key.split('/').filter((x) => x);
const base = segments.splice(0, 1)[0];
// The number of path segments comprising the blob name cannot exceed 254
const validKey = segments.reduce((acc, curr, index) => [acc, curr].join(index < 255 ? '/' : ''), base);
// Reserved URL characters must be escaped.
return (0, querystring_1.escape)(validKey).substr(0, 1024);
}
/**
* Check if a container name is valid.
*
* @param container String representing the container name to validate.
* @returns A boolean value that indicates whether or not the name is valid.
*/
checkContainerName(container) {
return ContainerNameCheck.test(container);
}
/**
* Delay Container creation if it does not exist.
*
* @returns A promise representing the asynchronous operation.
*/
ensureContainerExists() {
const key = this.settings.containerName;
if (!checkedCollections[key]) {
checkedCollections[key] = this.containerClient.createIfNotExists();
}
return checkedCollections[key];
}
}
exports.BlobStorage = BlobStorage;
//# sourceMappingURL=blobStorage.js.map