From 6d142f16f99dbb16ec55dd13eeb22b606ee15ecc Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Fri, 24 Jul 2020 16:45:48 -0500 Subject: [PATCH] Refactor Data Access Utility (#112) --- src/Common/DataAccessUtilityBase.ts | 1259 ++++++++++---------- src/Common/DocumentClientUtilityBase.ts | 120 +- src/Common/Logger.test.ts | 32 +- src/Common/Logger.ts | 4 +- src/Common/MessageHandler.test.ts | 56 +- src/Common/MessageHandler.ts | 122 +- src/Common/MongoProxyClient.ts | 4 +- src/Controls/Heatmap/Heatmap.ts | 6 +- src/Explorer/Explorer.ts | 18 +- src/Explorer/Tables/TableDataClient.ts | 4 +- src/Explorer/Tabs/DocumentsTab.test.ts | 13 +- src/Explorer/Tabs/SettingsTab.test.ts | 17 +- src/Explorer/Tabs/TabsManager.test.ts | 3 +- src/Platform/Emulator/DataAccessUtility.ts | 12 - src/Platform/Emulator/ExplorerFactory.ts | 3 +- src/Platform/Hosted/DataAccessUtility.ts | 12 - src/Platform/Hosted/ExplorerFactory.ts | 6 +- src/Platform/Hosted/Main.ts | 12 +- src/Platform/Portal/DataAccessUtility.ts | 96 -- src/Platform/Portal/ExplorerFactory.ts | 4 +- src/RouteHandlers/RouteHandler.ts | 4 +- src/Shared/AddCollectionUtility.ts | 8 +- src/Shared/AddDatabaseUtility.ts | 4 +- src/Shared/Telemetry/TelemetryProcessor.ts | 16 +- src/applyExplorerBindings.ts | 4 +- 25 files changed, 825 insertions(+), 1014 deletions(-) delete mode 100644 src/Platform/Emulator/DataAccessUtility.ts delete mode 100644 src/Platform/Hosted/DataAccessUtility.ts delete mode 100644 src/Platform/Portal/DataAccessUtility.ts diff --git a/src/Common/DataAccessUtilityBase.ts b/src/Common/DataAccessUtilityBase.ts index 48bd85b5b..71e17c70f 100644 --- a/src/Common/DataAccessUtilityBase.ts +++ b/src/Common/DataAccessUtilityBase.ts @@ -20,11 +20,13 @@ import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/Contai import { CosmosClient } from "./CosmosClient"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; -import { MessageHandler } from "./MessageHandler"; +import { sendCachedDataMessage, canSendMessage } from "./MessageHandler"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { OfferUtils } from "../Utils/OfferUtils"; import { RequestOptions } from "@azure/cosmos/dist-esm"; import StoredProcedure from "../Explorer/Tree/StoredProcedure"; +import { Platform, config } from "../Config"; +import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; export function getCommonQueryOptions(options: FeedOptions): any { const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage); @@ -43,629 +45,670 @@ export function getCommonQueryOptions(options: FeedOptions): any { return options; } -// TODO: Add timeout for all promises -export abstract class DataAccessUtilityBase { - public queryDocuments( - databaseId: string, - containerId: string, - query: string, - options: any - ): Q.Promise> { - options = getCommonQueryOptions(options); - const documentsIterator = CosmosClient.client() - .database(databaseId) - .container(containerId) - .items.query(query, options); - return Q(documentsIterator); - } - - public readStoredProcedures( - collection: ViewModels.Collection, - options?: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.storedProcedures.readAll(options) - .fetchAll() - .then(response => response.resources as DataModels.StoredProcedure[]) - ); - } - - public readStoredProcedure( - collection: ViewModels.Collection, - requestedResource: DataModels.Resource, - options?: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.storedProcedure(requestedResource.id) - .read(options) - .then(response => response.resource as DataModels.StoredProcedure) - ); - } - public readUserDefinedFunctions( - collection: ViewModels.Collection, - options: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.userDefinedFunctions.readAll(options) - .fetchAll() - .then(response => response.resources as DataModels.UserDefinedFunction[]) - ); - } - public readUserDefinedFunction( - collection: ViewModels.Collection, - requestedResource: DataModels.Resource, - options?: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.userDefinedFunction(requestedResource.id) - .read(options) - .then(response => response.resource as DataModels.UserDefinedFunction) - ); - } - - public readTriggers(collection: ViewModels.Collection, options: any): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.triggers.readAll(options) - .fetchAll() - .then(response => response.resources as DataModels.Trigger[]) - ); - } - - public readTrigger( - collection: ViewModels.Collection, - requestedResource: DataModels.Resource, - options?: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.trigger(requestedResource.id) - .read(options) - .then(response => response.resource as DataModels.Trigger) - ); - } - - public executeStoredProcedure( - collection: ViewModels.Collection, - storedProcedure: StoredProcedure, - partitionKeyValue: any, - params: any[] - ): Q.Promise { - // TODO remove this deferred. Kept it because of timeout code at bottom of function - const deferred = Q.defer(); +export function queryDocuments( + databaseId: string, + containerId: string, + query: string, + options: any +): Q.Promise> { + options = getCommonQueryOptions(options); + const documentsIterator = CosmosClient.client() + .database(databaseId) + .container(containerId) + .items.query(query, options); + return Q(documentsIterator); +} +export function readStoredProcedures( + collection: ViewModels.Collection, + options?: any +): Q.Promise { + return Q( CosmosClient.client() .database(collection.databaseId) .container(collection.id()) - .scripts.storedProcedure(storedProcedure.id()) - .execute(partitionKeyValue, params, { enableScriptLogging: true }) - .then(response => - deferred.resolve({ - result: response.resource, - scriptLogs: response.headers[Constants.HttpHeaders.scriptLogResults] - }) - ) - .catch(error => deferred.reject(error)); + .scripts.storedProcedures.readAll(options) + .fetchAll() + .then(response => response.resources as DataModels.StoredProcedure[]) + ); +} - return deferred.promise.timeout( - Constants.ClientDefaults.requestTimeoutMs, - `Request timed out while executing stored procedure ${storedProcedure.id()}` - ); +export function readStoredProcedure( + collection: ViewModels.Collection, + requestedResource: DataModels.Resource, + options?: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.storedProcedure(requestedResource.id) + .read(options) + .then(response => response.resource as DataModels.StoredProcedure) + ); +} +export function readUserDefinedFunctions( + collection: ViewModels.Collection, + options: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.userDefinedFunctions.readAll(options) + .fetchAll() + .then(response => response.resources as DataModels.UserDefinedFunction[]) + ); +} +export function readUserDefinedFunction( + collection: ViewModels.Collection, + requestedResource: DataModels.Resource, + options?: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.userDefinedFunction(requestedResource.id) + .read(options) + .then(response => response.resource as DataModels.UserDefinedFunction) + ); +} + +export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.triggers.readAll(options) + .fetchAll() + .then(response => response.resources as DataModels.Trigger[]) + ); +} + +export function readTrigger( + collection: ViewModels.Collection, + requestedResource: DataModels.Resource, + options?: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.trigger(requestedResource.id) + .read(options) + .then(response => response.resource as DataModels.Trigger) + ); +} + +export function executeStoredProcedure( + collection: ViewModels.Collection, + storedProcedure: StoredProcedure, + partitionKeyValue: any, + params: any[] +): Q.Promise { + // TODO remove this deferred. Kept it because of timeout code at bottom of function + const deferred = Q.defer(); + + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.storedProcedure(storedProcedure.id()) + .execute(partitionKeyValue, params, { enableScriptLogging: true }) + .then(response => + deferred.resolve({ + result: response.resource, + scriptLogs: response.headers[Constants.HttpHeaders.scriptLogResults] + }) + ) + .catch(error => deferred.reject(error)); + + return deferred.promise.timeout( + Constants.ClientDefaults.requestTimeoutMs, + `Request timed out while executing stored procedure ${storedProcedure.id()}` + ); +} + +export function readDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise { + const partitionKey = documentId.partitionKeyValue; + + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .item(documentId.id(), partitionKey) + .read() + .then(response => response.resource) + ); +} + +export function getPartitionKeyHeaderForConflict(conflictId: ViewModels.ConflictId): Object { + const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey; + const partitionKeyValue: any = conflictId.partitionKeyValue; + + return getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue); +} + +export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object { + if (!partitionKeyDefinition) { + return undefined; } - public readDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise { - const partitionKey = documentId.partitionKeyValue; - - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .item(documentId.id(), partitionKey) - .read() - .then(response => response.resource) - ); + if (partitionKeyValue === undefined) { + return [{}]; } - public getPartitionKeyHeaderForConflict(conflictId: ViewModels.ConflictId): Object { - const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey; - const partitionKeyValue: any = conflictId.partitionKeyValue; + return [partitionKeyValue]; +} - return this.getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue); - } - - public getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object { - if (!partitionKeyDefinition) { - return undefined; - } - - if (partitionKeyValue === undefined) { - return [{}]; - } - - return [partitionKeyValue]; - } - - public updateCollection( - databaseId: string, - collectionId: string, - newCollection: DataModels.Collection, - options: any = {} - ): Q.Promise { - return Q( - CosmosClient.client() - .database(databaseId) - .container(collectionId) - .replace(newCollection as ContainerDefinition, options) - .then(async (response: ContainerResponse) => { - return this.refreshCachedResources().then(() => response.resource as DataModels.Collection); - }) - ); - } - - public updateDocument( - collection: ViewModels.CollectionBase, - documentId: ViewModels.DocumentId, - newDocument: any - ): Q.Promise { - const partitionKey = documentId.partitionKeyValue; - - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .item(documentId.id(), partitionKey) - .replace(newDocument) - .then(response => response.resource) - ); - } - - public updateOffer( - offer: DataModels.Offer, - newOffer: DataModels.Offer, - options?: RequestOptions - ): Q.Promise { - return Q( - CosmosClient.client() - .offer(offer.id) - .replace(newOffer, options) - .then(response => { - return Promise.all([this.refreshCachedOffers(), this.refreshCachedResources()]).then(() => response.resource); - }) - ); - } - - public updateStoredProcedure( - collection: ViewModels.Collection, - storedProcedure: DataModels.StoredProcedure, - options: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.storedProcedure(storedProcedure.id) - .replace(storedProcedure, options) - .then(response => response.resource as DataModels.StoredProcedure) - ); - } - - public updateUserDefinedFunction( - collection: ViewModels.Collection, - userDefinedFunction: DataModels.UserDefinedFunction, - options?: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.userDefinedFunction(userDefinedFunction.id) - .replace(userDefinedFunction, options) - .then(response => response.resource as DataModels.StoredProcedure) - ); - } - - public updateTrigger( - collection: ViewModels.Collection, - trigger: DataModels.Trigger, - options?: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.trigger(trigger.id) - .replace(trigger as TriggerDefinition, options) - .then(response => response.resource as DataModels.Trigger) - ); - } - - public createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .items.create(newDocument) - .then(response => response.resource as DataModels.StoredProcedure) - ); - } - - public createStoredProcedure( - collection: ViewModels.Collection, - newStoredProcedure: DataModels.StoredProcedure, - options?: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.storedProcedures.create(newStoredProcedure, options) - .then(response => response.resource as DataModels.StoredProcedure) - ); - } - - public createUserDefinedFunction( - collection: ViewModels.Collection, - newUserDefinedFunction: DataModels.UserDefinedFunction, - options: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.userDefinedFunctions.create(newUserDefinedFunction, options) - .then(response => response.resource as DataModels.UserDefinedFunction) - ); - } - - public createTrigger( - collection: ViewModels.Collection, - newTrigger: DataModels.Trigger, - options?: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.triggers.create(newTrigger as TriggerDefinition, options) - .then(response => response.resource as DataModels.Trigger) - ); - } - - public deleteDocument(collection: ViewModels.CollectionBase, documentId: ViewModels.DocumentId): Q.Promise { - const partitionKey = documentId.partitionKeyValue; - - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .item(documentId.id(), partitionKey) - .delete() - ); - } - - public deleteConflict( - collection: ViewModels.CollectionBase, - conflictId: ViewModels.ConflictId, - options: any = {} - ): Q.Promise { - options.partitionKey = options.partitionKey || this.getPartitionKeyHeaderForConflict(conflictId); - - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .conflict(conflictId.id()) - .delete(options) - ); - } - - public deleteCollection(collection: ViewModels.Collection, options: any): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .delete() - .then(() => this.refreshCachedResources()) - ); - } - - public deleteDatabase(database: ViewModels.Database, options: any): Q.Promise { - return Q( - CosmosClient.client() - .database(database.id()) - .delete() - .then(() => this.refreshCachedResources()) - ); - } - - public deleteStoredProcedure( - collection: ViewModels.Collection, - storedProcedure: DataModels.StoredProcedure, - options: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.storedProcedure(storedProcedure.id) - .delete() - ); - } - - public deleteUserDefinedFunction( - collection: ViewModels.Collection, - userDefinedFunction: DataModels.UserDefinedFunction, - options: any - ): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.userDefinedFunction(userDefinedFunction.id) - .delete() - ); - } - - public deleteTrigger(collection: ViewModels.Collection, trigger: DataModels.Trigger, options: any): Q.Promise { - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .scripts.trigger(trigger.id) - .delete() - ); - } - - public readCollections(database: ViewModels.Database, options: any): Q.Promise { - return Q( - CosmosClient.client() - .database(database.id()) - .containers.readAll() - .fetchAll() - .then(response => response.resources as DataModels.Collection[]) - ); - } - - public readCollection(databaseId: string, collectionId: string): Q.Promise { - return Q( - CosmosClient.client() - .database(databaseId) - .container(collectionId) - .read() - .then(response => response.resource as DataModels.Collection) - ); - } - - public readCollectionQuotaInfo( - collection: ViewModels.Collection, - options: any - ): Q.Promise { - options = options || {}; - options.populateQuotaInfo = true; - options.initialHeaders = options.initialHeaders || {}; - options.initialHeaders[Constants.HttpHeaders.populatePartitionStatistics] = true; - - return Q( - CosmosClient.client() - .database(collection.databaseId) - .container(collection.id()) - .read(options) - // TODO any needed because SDK does not properly type response.resource.statistics - .then((response: any) => { - let quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers); - quota["usageSizeInKB"] = response.resource.statistics.reduce( - ( - previousValue: number, - currentValue: DataModels.Statistic, - currentIndex: number, - array: DataModels.Statistic[] - ) => { - return previousValue + currentValue.sizeInKB; - }, - 0 - ); - quota["numPartitions"] = response.resource.statistics.length; - quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617) - return quota; - }) - ); - } - - public readOffers(options: any): Q.Promise { - return Q( - CosmosClient.client() - .offers.readAll() - .fetchAll() - .then(response => response.resources) - ); - } - - public readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise { - options = options || {}; - options.initialHeaders = options.initialHeaders || {}; - if (!OfferUtils.isOfferV1(requestedResource)) { - options.initialHeaders[Constants.HttpHeaders.populateCollectionThroughputInfo] = true; - } - - return Q( - CosmosClient.client() - .offer(requestedResource.id) - .read(options) - .then(response => ({ ...response.resource, headers: response.headers })) - ); - } - - public readDatabases(options: any): Q.Promise { - return Q( - CosmosClient.client() - .databases.readAll() - .fetchAll() - .then(response => response.resources as DataModels.Database[]) - ); - } - - public getOrCreateDatabaseAndCollection( - request: DataModels.CreateDatabaseAndCollectionRequest, - options: any - ): Q.Promise { - const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput"); - const { - databaseId, - databaseLevelThroughput, - collectionId, - partitionKey, - indexingPolicy, - uniqueKeyPolicy, - offerThroughput, - analyticalStorageTtl, - hasAutoPilotV2FeatureFlag - } = request; - - const createBody: DatabaseRequest = { - id: databaseId - }; - - // TODO: replace when SDK support autopilot - const initialHeaders = request.autoPilot - ? !hasAutoPilotV2FeatureFlag - ? { - [Constants.HttpHeaders.autoPilotThroughputSDK]: JSON.stringify({ - maxThroughput: request.autoPilot.maxThroughput - }) - } - : { - [Constants.HttpHeaders.autoPilotTier]: request.autoPilot.autopilotTier - } - : undefined; - if (databaseLevelThroughput) { - if (request.autoPilot) { - databaseOptions.initialHeaders = initialHeaders; - } - createBody.throughput = offerThroughput; - } - - return Q( - CosmosClient.client() - .databases.createIfNotExists(createBody, databaseOptions) - .then(response => { - return response.database.containers.create( - { - id: collectionId, - partitionKey: (partitionKey || undefined) as PartitionKeyDefinition, - indexingPolicy: indexingPolicy ? indexingPolicy : undefined, - uniqueKeyPolicy: uniqueKeyPolicy ? uniqueKeyPolicy : undefined, - analyticalStorageTtl: analyticalStorageTtl, - throughput: databaseLevelThroughput || request.autoPilot ? undefined : offerThroughput - } as ContainerRequest, // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed - { - initialHeaders: databaseLevelThroughput ? undefined : initialHeaders - } - ); - }) - .then(containerResponse => containerResponse.resource as DataModels.Collection) - .finally(() => this.refreshCachedResources(options)) - ); - } - - public createDatabase(request: DataModels.CreateDatabaseRequest, options: any): Q.Promise { - var deferred = Q.defer(); - - this._createDatabase(request, options).then( - (createdDatabase: DataModels.Database) => { - this.refreshCachedOffers().then(() => { - deferred.resolve(createdDatabase); - }); - }, - _createDatabaseError => { - deferred.reject(_createDatabaseError); - } - ); - - return deferred.promise; - } - - public refreshCachedOffers(): Q.Promise { - if (MessageHandler.canSendMessage()) { - return MessageHandler.sendCachedDataMessage(MessageTypes.RefreshOffers, []); - } else { - return Q(); - } - } - - public refreshCachedResources(options?: any): Q.Promise { - if (MessageHandler.canSendMessage()) { - return MessageHandler.sendCachedDataMessage(MessageTypes.RefreshResources, []); - } else { - return Q(); - } - } - - public queryConflicts( - databaseId: string, - containerId: string, - query: string, - options: any - ): Q.Promise> { - const documentsIterator = CosmosClient.client() +export function updateCollection( + databaseId: string, + collectionId: string, + newCollection: DataModels.Collection, + options: any = {} +): Q.Promise { + return Q( + CosmosClient.client() .database(databaseId) - .container(containerId) - .conflicts.query(query, options); - return Q(documentsIterator); + .container(collectionId) + .replace(newCollection as ContainerDefinition, options) + .then(async (response: ContainerResponse) => { + return refreshCachedResources().then(() => response.resource as DataModels.Collection); + }) + ); +} + +export function updateDocument( + collection: ViewModels.CollectionBase, + documentId: ViewModels.DocumentId, + newDocument: any +): Q.Promise { + const partitionKey = documentId.partitionKeyValue; + + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .item(documentId.id(), partitionKey) + .replace(newDocument) + .then(response => response.resource) + ); +} + +export function updateOffer( + offer: DataModels.Offer, + newOffer: DataModels.Offer, + options?: RequestOptions +): Q.Promise { + return Q( + CosmosClient.client() + .offer(offer.id) + .replace(newOffer, options) + .then(response => { + return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource); + }) + ); +} + +export function updateStoredProcedure( + collection: ViewModels.Collection, + storedProcedure: DataModels.StoredProcedure, + options: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.storedProcedure(storedProcedure.id) + .replace(storedProcedure, options) + .then(response => response.resource as DataModels.StoredProcedure) + ); +} + +export function updateUserDefinedFunction( + collection: ViewModels.Collection, + userDefinedFunction: DataModels.UserDefinedFunction, + options?: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.userDefinedFunction(userDefinedFunction.id) + .replace(userDefinedFunction, options) + .then(response => response.resource as DataModels.StoredProcedure) + ); +} + +export function updateTrigger( + collection: ViewModels.Collection, + trigger: DataModels.Trigger, + options?: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.trigger(trigger.id) + .replace(trigger as TriggerDefinition, options) + .then(response => response.resource as DataModels.Trigger) + ); +} + +export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .items.create(newDocument) + .then(response => response.resource as DataModels.StoredProcedure) + ); +} + +export function createStoredProcedure( + collection: ViewModels.Collection, + newStoredProcedure: DataModels.StoredProcedure, + options?: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.storedProcedures.create(newStoredProcedure, options) + .then(response => response.resource as DataModels.StoredProcedure) + ); +} + +export function createUserDefinedFunction( + collection: ViewModels.Collection, + newUserDefinedFunction: DataModels.UserDefinedFunction, + options: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.userDefinedFunctions.create(newUserDefinedFunction, options) + .then(response => response.resource as DataModels.UserDefinedFunction) + ); +} + +export function createTrigger( + collection: ViewModels.Collection, + newTrigger: DataModels.Trigger, + options?: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.triggers.create(newTrigger as TriggerDefinition, options) + .then(response => response.resource as DataModels.Trigger) + ); +} + +export function deleteDocument( + collection: ViewModels.CollectionBase, + documentId: ViewModels.DocumentId +): Q.Promise { + const partitionKey = documentId.partitionKeyValue; + + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .item(documentId.id(), partitionKey) + .delete() + ); +} + +export function deleteConflict( + collection: ViewModels.CollectionBase, + conflictId: ViewModels.ConflictId, + options: any = {} +): Q.Promise { + options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId); + + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .conflict(conflictId.id()) + .delete(options) + ); +} + +export function deleteCollection(collection: ViewModels.Collection, options: any): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .delete() + .then(() => refreshCachedResources()) + ); +} + +export function deleteDatabase(database: ViewModels.Database, options: any): Q.Promise { + return Q( + CosmosClient.client() + .database(database.id()) + .delete() + .then(() => refreshCachedResources()) + ); +} + +export function deleteStoredProcedure( + collection: ViewModels.Collection, + storedProcedure: DataModels.StoredProcedure, + options: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.storedProcedure(storedProcedure.id) + .delete() + ); +} + +export function deleteUserDefinedFunction( + collection: ViewModels.Collection, + userDefinedFunction: DataModels.UserDefinedFunction, + options: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.userDefinedFunction(userDefinedFunction.id) + .delete() + ); +} + +export function deleteTrigger( + collection: ViewModels.Collection, + trigger: DataModels.Trigger, + options: any +): Q.Promise { + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .scripts.trigger(trigger.id) + .delete() + ); +} + +export function readCollections(database: ViewModels.Database, options: any): Q.Promise { + return Q( + CosmosClient.client() + .database(database.id()) + .containers.readAll() + .fetchAll() + .then(response => response.resources as DataModels.Collection[]) + ); +} + +export function readCollection(databaseId: string, collectionId: string): Q.Promise { + return Q( + CosmosClient.client() + .database(databaseId) + .container(collectionId) + .read() + .then(response => response.resource as DataModels.Collection) + ); +} + +export function readCollectionQuotaInfo( + collection: ViewModels.Collection, + options: any +): Q.Promise { + options = options || {}; + options.populateQuotaInfo = true; + options.initialHeaders = options.initialHeaders || {}; + options.initialHeaders[Constants.HttpHeaders.populatePartitionStatistics] = true; + + return Q( + CosmosClient.client() + .database(collection.databaseId) + .container(collection.id()) + .read(options) + // TODO any needed because SDK does not properly type response.resource.statistics + .then((response: any) => { + let quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers); + quota["usageSizeInKB"] = response.resource.statistics.reduce( + ( + previousValue: number, + currentValue: DataModels.Statistic, + currentIndex: number, + array: DataModels.Statistic[] + ) => { + return previousValue + currentValue.sizeInKB; + }, + 0 + ); + quota["numPartitions"] = response.resource.statistics.length; + quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617) + return quota; + }) + ); +} + +export function readOffers(options: any): Q.Promise { + try { + if (config.platform === Platform.Portal) { + return sendCachedDataMessage(MessageTypes.AllOffers, [ + (window).dataExplorer.databaseAccount().id, + Constants.ClientDefaults.portalCacheTimeoutMs + ]); + } + } catch (error) { + // If error getting cached Offers, continue on and read via SDK + } + return Q( + CosmosClient.client() + .offers.readAll() + .fetchAll() + .then(response => response.resources) + ); +} + +export function readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise { + options = options || {}; + options.initialHeaders = options.initialHeaders || {}; + if (!OfferUtils.isOfferV1(requestedResource)) { + options.initialHeaders[Constants.HttpHeaders.populateCollectionThroughputInfo] = true; } - public updateOfferThroughputBeyondLimit( - request: DataModels.UpdateOfferThroughputRequest, - options: any - ): Q.Promise { + return Q( + CosmosClient.client() + .offer(requestedResource.id) + .read(options) + .then(response => ({ ...response.resource, headers: response.headers })) + ); +} + +export function readDatabases(options: any): Q.Promise { + try { + if (config.platform === Platform.Portal) { + return sendCachedDataMessage(MessageTypes.AllDatabases, [ + (window).dataExplorer.databaseAccount().id, + Constants.ClientDefaults.portalCacheTimeoutMs + ]); + } + } catch (error) { + // If error getting cached DBs, continue on and read via SDK + } + + return Q( + CosmosClient.client() + .databases.readAll() + .fetchAll() + .then(response => response.resources as DataModels.Database[]) + ); +} + +export function getOrCreateDatabaseAndCollection( + request: DataModels.CreateDatabaseAndCollectionRequest, + options: any +): Q.Promise { + const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput"); + const { + databaseId, + databaseLevelThroughput, + collectionId, + partitionKey, + indexingPolicy, + uniqueKeyPolicy, + offerThroughput, + analyticalStorageTtl, + hasAutoPilotV2FeatureFlag + } = request; + + const createBody: DatabaseRequest = { + id: databaseId + }; + + // TODO: replace when SDK support autopilot + const initialHeaders = request.autoPilot + ? !hasAutoPilotV2FeatureFlag + ? { + [Constants.HttpHeaders.autoPilotThroughputSDK]: JSON.stringify({ + maxThroughput: request.autoPilot.maxThroughput + }) + } + : { + [Constants.HttpHeaders.autoPilotTier]: request.autoPilot.autopilotTier + } + : undefined; + if (databaseLevelThroughput) { + if (request.autoPilot) { + databaseOptions.initialHeaders = initialHeaders; + } + createBody.throughput = offerThroughput; + } + + return Q( + CosmosClient.client() + .databases.createIfNotExists(createBody, databaseOptions) + .then(response => { + return response.database.containers.create( + { + id: collectionId, + partitionKey: (partitionKey || undefined) as PartitionKeyDefinition, + indexingPolicy: indexingPolicy ? indexingPolicy : undefined, + uniqueKeyPolicy: uniqueKeyPolicy ? uniqueKeyPolicy : undefined, + analyticalStorageTtl: analyticalStorageTtl, + throughput: databaseLevelThroughput || request.autoPilot ? undefined : offerThroughput + } as ContainerRequest, // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed + { + initialHeaders: databaseLevelThroughput ? undefined : initialHeaders + } + ); + }) + .then(containerResponse => containerResponse.resource as DataModels.Collection) + .finally(() => refreshCachedResources(options)) + ); +} + +export function createDatabase( + request: DataModels.CreateDatabaseRequest, + options: any +): Q.Promise { + var deferred = Q.defer(); + + _createDatabase(request, options).then( + (createdDatabase: DataModels.Database) => { + refreshCachedOffers().then(() => { + deferred.resolve(createdDatabase); + }); + }, + _createDatabaseError => { + deferred.reject(_createDatabaseError); + } + ); + + return deferred.promise; +} + +export function refreshCachedOffers(): Q.Promise { + if (canSendMessage()) { + return sendCachedDataMessage(MessageTypes.RefreshOffers, []); + } else { + return Q(); + } +} + +export function refreshCachedResources(options?: any): Q.Promise { + if (canSendMessage()) { + return sendCachedDataMessage(MessageTypes.RefreshResources, []); + } else { + return Q(); + } +} + +export function queryConflicts( + databaseId: string, + containerId: string, + query: string, + options: any +): Q.Promise> { + const documentsIterator = CosmosClient.client() + .database(databaseId) + .container(containerId) + .conflicts.query(query, options); + return Q(documentsIterator); +} + +export async function updateOfferThroughputBeyondLimit( + request: DataModels.UpdateOfferThroughputRequest +): Promise { + if (config.platform !== Platform.Portal) { throw new Error("Updating throughput beyond specified limit is not supported on this platform"); } - private _createDatabase( - request: DataModels.CreateDatabaseRequest, - options: any = {} - ): Q.Promise { - const { databaseId, databaseLevelThroughput, offerThroughput, autoPilot, hasAutoPilotV2FeatureFlag } = request; - const createBody: DatabaseRequest = { id: databaseId }; - const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput"); - // TODO: replace when SDK support autopilot - const initialHeaders = autoPilot - ? !hasAutoPilotV2FeatureFlag - ? { - [Constants.HttpHeaders.autoPilotThroughputSDK]: JSON.stringify({ maxThroughput: autoPilot.maxThroughput }) - } - : { - [Constants.HttpHeaders.autoPilotTier]: autoPilot.autopilotTier - } - : undefined; - if (!!databaseLevelThroughput) { - if (autoPilot) { - databaseOptions.initialHeaders = initialHeaders; - } - createBody.throughput = offerThroughput; - } + const explorer = window.dataExplorer; + const url = `${explorer.extensionEndpoint()}/api/offerthroughputrequest/updatebeyondspecifiedlimit`; + const authorizationHeader = getAuthorizationHeader(); - return Q( - CosmosClient.client() - .databases.create(createBody, databaseOptions) - .then((response: DatabaseResponse) => { - return this.refreshCachedResources(databaseOptions).then(() => response.resource); - }) - ); + const response = await fetch(url, { + method: "POST", + body: JSON.stringify(request), + headers: { [authorizationHeader.header]: authorizationHeader.token } + }); + + if (response.ok) { + return undefined; } + throw new Error(await response.text()); +} + +function _createDatabase(request: DataModels.CreateDatabaseRequest, options: any = {}): Q.Promise { + const { databaseId, databaseLevelThroughput, offerThroughput, autoPilot, hasAutoPilotV2FeatureFlag } = request; + const createBody: DatabaseRequest = { id: databaseId }; + const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput"); + // TODO: replace when SDK support autopilot + const initialHeaders = autoPilot + ? !hasAutoPilotV2FeatureFlag + ? { + [Constants.HttpHeaders.autoPilotThroughputSDK]: JSON.stringify({ maxThroughput: autoPilot.maxThroughput }) + } + : { + [Constants.HttpHeaders.autoPilotTier]: autoPilot.autopilotTier + } + : undefined; + if (!!databaseLevelThroughput) { + if (autoPilot) { + databaseOptions.initialHeaders = initialHeaders; + } + createBody.throughput = offerThroughput; + } + + return Q( + CosmosClient.client() + .databases.create(createBody, databaseOptions) + .then((response: DatabaseResponse) => { + return refreshCachedResources(databaseOptions).then(() => response.resource); + }) + ); } diff --git a/src/Common/DocumentClientUtilityBase.ts b/src/Common/DocumentClientUtilityBase.ts index 69fe26d5c..50c22206d 100644 --- a/src/Common/DocumentClientUtilityBase.ts +++ b/src/Common/DocumentClientUtilityBase.ts @@ -5,9 +5,9 @@ import * as ViewModels from "../Contracts/ViewModels"; import Q from "q"; import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; -import { DataAccessUtilityBase } from "./DataAccessUtilityBase"; +import * as DataAccessUtilityBase from "./DataAccessUtilityBase"; import * as Logger from "./Logger"; -import { MessageHandler } from "./MessageHandler"; +import { sendMessage } from "./MessageHandler"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MinimalQueryIterator, nextPage } from "./IteratorUtilities"; import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils"; @@ -16,15 +16,13 @@ import StoredProcedure from "../Explorer/Tree/StoredProcedure"; // TODO: Log all promise resolutions and errors with verbosity levels export default class DocumentClientUtilityBase { - constructor(private _dataAccessUtility: DataAccessUtilityBase) {} - public queryDocuments( databaseId: string, containerId: string, query: string, options: any ): Q.Promise> { - return this._dataAccessUtility.queryDocuments(databaseId, containerId, query, options); + return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options); } public queryConflicts( @@ -33,7 +31,7 @@ export default class DocumentClientUtilityBase { query: string, options: any ): Q.Promise> { - return this._dataAccessUtility.queryConflicts(databaseId, containerId, query, options); + return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options); } public getEntityName() { @@ -54,8 +52,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Querying stored procedures for container ${collection.id()}` ); - this._dataAccessUtility - .readStoredProcedures(collection, options) + DataAccessUtilityBase.readStoredProcedures(collection, options) .then( (storedProcedures: DataModels.StoredProcedure[]) => { deferred.resolve(storedProcedures); @@ -82,7 +79,7 @@ export default class DocumentClientUtilityBase { requestedResource: DataModels.Resource, options?: any ): Q.Promise { - return this._dataAccessUtility.readStoredProcedure(collection, requestedResource, options); + return DataAccessUtilityBase.readStoredProcedure(collection, requestedResource, options); } public readUserDefinedFunctions( @@ -94,8 +91,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Querying user defined functions for collection ${collection.id()}` ); - this._dataAccessUtility - .readUserDefinedFunctions(collection, options) + DataAccessUtilityBase.readUserDefinedFunctions(collection, options) .then( (userDefinedFunctions: DataModels.UserDefinedFunction[]) => { deferred.resolve(userDefinedFunctions); @@ -122,7 +118,7 @@ export default class DocumentClientUtilityBase { requestedResource: DataModels.Resource, options: any ): Q.Promise { - return this._dataAccessUtility.readUserDefinedFunction(collection, requestedResource, options); + return DataAccessUtilityBase.readUserDefinedFunction(collection, requestedResource, options); } public readTriggers(collection: ViewModels.Collection, options: any): Q.Promise { @@ -132,8 +128,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Querying triggers for container ${collection.id()}` ); - this._dataAccessUtility - .readTriggers(collection, options) + DataAccessUtilityBase.readTriggers(collection, options) .then( (triggers: DataModels.Trigger[]) => { deferred.resolve(triggers); @@ -160,7 +155,7 @@ export default class DocumentClientUtilityBase { requestedResource: DataModels.Resource, options?: any ): Q.Promise { - return this._dataAccessUtility.readTrigger(collection, requestedResource, options); + return DataAccessUtilityBase.readTrigger(collection, requestedResource, options); } public executeStoredProcedure( @@ -175,8 +170,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Executing stored procedure ${storedProcedure.id()}` ); - this._dataAccessUtility - .executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params) + DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params) .then( (response: any) => { deferred.resolve(response); @@ -250,8 +244,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Reading ${entityName} ${documentId.id()}` ); - this._dataAccessUtility - .readDocument(collection, documentId) + DataAccessUtilityBase.readDocument(collection, documentId) .then( (document: any) => { deferred.resolve(document); @@ -283,8 +276,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Updating container ${collection.id()}` ); - this._dataAccessUtility - .updateCollection(databaseId, collection.id(), newCollection) + DataAccessUtilityBase.updateCollection(databaseId, collection.id(), newCollection) .then( (replacedCollection: DataModels.Collection) => { NotificationConsoleUtils.logConsoleMessage( @@ -321,8 +313,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Updating ${entityName} ${documentId.id()}` ); - this._dataAccessUtility - .updateDocument(collection, documentId, newDocument) + DataAccessUtilityBase.updateDocument(collection, documentId, newDocument) .then( (updatedDocument: any) => { NotificationConsoleUtils.logConsoleMessage( @@ -358,8 +349,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Updating offer for resource ${offer.resource}` ); - this._dataAccessUtility - .updateOffer(offer, newOffer, options) + DataAccessUtilityBase.updateOffer(offer, newOffer, options) .then( (replacedOffer: DataModels.Offer) => { NotificationConsoleUtils.logConsoleMessage( @@ -393,10 +383,7 @@ export default class DocumentClientUtilityBase { return deferred.promise; } - public updateOfferThroughputBeyondLimit( - requestPayload: DataModels.UpdateOfferThroughputRequest, - options: any = {} - ): Q.Promise { + public updateOfferThroughputBeyondLimit(requestPayload: DataModels.UpdateOfferThroughputRequest): Q.Promise { const deferred: Q.Deferred = Q.defer(); const resourceDescriptionInfo: string = requestPayload.collectionName ? `database ${requestPayload.databaseName} and container ${requestPayload.collectionName}` @@ -405,8 +392,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Requesting increase in throughput to ${requestPayload.throughput} for ${resourceDescriptionInfo}` ); - this._dataAccessUtility - .updateOfferThroughputBeyondLimit(requestPayload, options) + DataAccessUtilityBase.updateOfferThroughputBeyondLimit(requestPayload) .then( () => { NotificationConsoleUtils.logConsoleMessage( @@ -439,8 +425,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Updating stored procedure ${storedProcedure.id}` ); - this._dataAccessUtility - .updateStoredProcedure(collection, storedProcedure, options) + DataAccessUtilityBase.updateStoredProcedure(collection, storedProcedure, options) .then( (updatedStoredProcedure: DataModels.StoredProcedure) => { NotificationConsoleUtils.logConsoleMessage( @@ -476,8 +461,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Updating user defined function ${userDefinedFunction.id}` ); - this._dataAccessUtility - .updateUserDefinedFunction(collection, userDefinedFunction, options) + DataAccessUtilityBase.updateUserDefinedFunction(collection, userDefinedFunction, options) .then( (updatedUserDefinedFunction: DataModels.UserDefinedFunction) => { NotificationConsoleUtils.logConsoleMessage( @@ -506,8 +490,7 @@ export default class DocumentClientUtilityBase { public updateTrigger(collection: ViewModels.Collection, trigger: DataModels.Trigger): Q.Promise { var deferred = Q.defer(); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Updating trigger ${trigger.id}`); - this._dataAccessUtility - .updateTrigger(collection, trigger) + DataAccessUtilityBase.updateTrigger(collection, trigger) .then( (updatedTrigger: DataModels.Trigger) => { NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Updated trigger ${trigger.id}`); @@ -537,8 +520,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Creating new ${entityName} for container ${collection.id()}` ); - this._dataAccessUtility - .createDocument(collection, newDocument) + DataAccessUtilityBase.createDocument(collection, newDocument) .then( (savedDocument: any) => { NotificationConsoleUtils.logConsoleMessage( @@ -574,8 +556,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Creating stored procedure for container ${collection.id()}` ); - this._dataAccessUtility - .createStoredProcedure(collection, newStoredProcedure, options) + DataAccessUtilityBase.createStoredProcedure(collection, newStoredProcedure, options) .then( (createdStoredProcedure: DataModels.StoredProcedure) => { NotificationConsoleUtils.logConsoleMessage( @@ -611,8 +592,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Creating user defined function for container ${collection.id()}` ); - this._dataAccessUtility - .createUserDefinedFunction(collection, newUserDefinedFunction, options) + DataAccessUtilityBase.createUserDefinedFunction(collection, newUserDefinedFunction, options) .then( (createdUserDefinedFunction: DataModels.UserDefinedFunction) => { NotificationConsoleUtils.logConsoleMessage( @@ -648,8 +628,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Creating trigger for container ${collection.id()}` ); - this._dataAccessUtility - .createTrigger(collection, newTrigger, options) + DataAccessUtilityBase.createTrigger(collection, newTrigger, options) .then( (createdTrigger: DataModels.Trigger) => { NotificationConsoleUtils.logConsoleMessage( @@ -682,8 +661,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Deleting ${entityName} ${documentId.id()}` ); - this._dataAccessUtility - .deleteDocument(collection, documentId) + DataAccessUtilityBase.deleteDocument(collection, documentId) .then( (response: any) => { NotificationConsoleUtils.logConsoleMessage( @@ -720,8 +698,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Deleting conflict ${conflictId.id()}` ); - this._dataAccessUtility - .deleteConflict(collection, conflictId, options) + DataAccessUtilityBase.deleteConflict(collection, conflictId, options) .then( (response: any) => { NotificationConsoleUtils.logConsoleMessage( @@ -754,8 +731,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Deleting container ${collection.id()}` ); - this._dataAccessUtility - .deleteCollection(collection, options) + DataAccessUtilityBase.deleteCollection(collection, options) .then( (response: any) => { NotificationConsoleUtils.logConsoleMessage( @@ -788,8 +764,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Deleting database ${database.id()}` ); - this._dataAccessUtility - .deleteDatabase(database, options) + DataAccessUtilityBase.deleteDatabase(database, options) .then( (response: any) => { NotificationConsoleUtils.logConsoleMessage( @@ -826,8 +801,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Deleting stored procedure ${storedProcedure.id}` ); - this._dataAccessUtility - .deleteStoredProcedure(collection, storedProcedure, options) + DataAccessUtilityBase.deleteStoredProcedure(collection, storedProcedure, options) .then( (response: any) => { NotificationConsoleUtils.logConsoleMessage( @@ -863,8 +837,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Deleting user defined function ${userDefinedFunction.id}` ); - this._dataAccessUtility - .deleteUserDefinedFunction(collection, userDefinedFunction, options) + DataAccessUtilityBase.deleteUserDefinedFunction(collection, userDefinedFunction, options) .then( (response: any) => { NotificationConsoleUtils.logConsoleMessage( @@ -897,8 +870,7 @@ export default class DocumentClientUtilityBase { ): Q.Promise { var deferred = Q.defer(); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Deleting trigger ${trigger.id}`); - this._dataAccessUtility - .deleteTrigger(collection, trigger, options) + DataAccessUtilityBase.deleteTrigger(collection, trigger, options) .then( (response: any) => { NotificationConsoleUtils.logConsoleMessage( @@ -925,11 +897,11 @@ export default class DocumentClientUtilityBase { } public refreshCachedResources(options: any = {}): Q.Promise { - return this._dataAccessUtility.refreshCachedResources(options); + return DataAccessUtilityBase.refreshCachedResources(options); } public refreshCachedOffers(): Q.Promise { - return this._dataAccessUtility.refreshCachedOffers(); + return DataAccessUtilityBase.refreshCachedOffers(); } public readCollections(database: ViewModels.Database, options: any = {}): Q.Promise { @@ -938,8 +910,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Querying containers for database ${database.id()}` ); - this._dataAccessUtility - .readCollections(database, options) + DataAccessUtilityBase.readCollections(database, options) .then( (collections: DataModels.Collection[]) => { deferred.resolve(collections); @@ -968,8 +939,7 @@ export default class DocumentClientUtilityBase { `Querying container ${collectionId}` ); - this._dataAccessUtility - .readCollection(databaseId, collectionId) + DataAccessUtilityBase.readCollection(databaseId, collectionId) .then( (collection: DataModels.Collection) => { deferred.resolve(collection); @@ -1001,8 +971,7 @@ export default class DocumentClientUtilityBase { ConsoleDataType.InProgress, `Querying quota info for container ${collection.id}` ); - this._dataAccessUtility - .readCollectionQuotaInfo(collection, options) + DataAccessUtilityBase.readCollectionQuotaInfo(collection, options) .then( (quota: DataModels.CollectionQuotaInfo) => { deferred.resolve(quota); @@ -1028,8 +997,7 @@ export default class DocumentClientUtilityBase { var deferred = Q.defer(); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offers"); - this._dataAccessUtility - .readOffers(options) + DataAccessUtilityBase.readOffers(options) .then( (offers: DataModels.Offer[]) => { deferred.resolve(offers); @@ -1055,8 +1023,7 @@ export default class DocumentClientUtilityBase { var deferred = Q.defer(); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offer"); - this._dataAccessUtility - .readOffer(requestedResource, options) + DataAccessUtilityBase.readOffer(requestedResource, options) .then( (offer: DataModels.OfferWithHeaders) => { deferred.resolve(offer); @@ -1081,8 +1048,7 @@ export default class DocumentClientUtilityBase { public readDatabases(options: any): Q.Promise { var deferred = Q.defer(); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying databases"); - this._dataAccessUtility - .readDatabases(options) + DataAccessUtilityBase.readDatabases(options) .then( (databases: DataModels.Database[]) => { deferred.resolve(databases); @@ -1114,8 +1080,7 @@ export default class DocumentClientUtilityBase { `Creating a new container ${request.collectionId} for database ${request.databaseId}` ); - this._dataAccessUtility - .getOrCreateDatabaseAndCollection(request, options) + DataAccessUtilityBase.getOrCreateDatabaseAndCollection(request, options) .then( (collection: DataModels.Collection) => { NotificationConsoleUtils.logConsoleMessage( @@ -1146,8 +1111,7 @@ export default class DocumentClientUtilityBase { `Creating a new database ${request.databaseId}` ); - this._dataAccessUtility - .createDatabase(request, options) + DataAccessUtilityBase.createDatabase(request, options) .then( (database: DataModels.Database) => { NotificationConsoleUtils.logConsoleMessage( @@ -1175,7 +1139,7 @@ export default class DocumentClientUtilityBase { if (error.message && error.message.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) { return; } - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.ForbiddenError, reason: error && error.message ? error.message : error }); diff --git a/src/Common/Logger.test.ts b/src/Common/Logger.test.ts index 6c33c3ca1..44a8c79fb 100644 --- a/src/Common/Logger.test.ts +++ b/src/Common/Logger.test.ts @@ -1,46 +1,26 @@ +jest.mock("./MessageHandler"); import { LogEntryLevel } from "../Contracts/Diagnostics"; import * as Logger from "./Logger"; -import { MessageHandler } from "./MessageHandler"; import { MessageTypes } from "../Contracts/ExplorerContracts"; +import { sendMessage } from "./MessageHandler"; describe("Logger", () => { - let sendMessageSpy: jasmine.Spy; - beforeEach(() => { - sendMessageSpy = spyOn(MessageHandler, "sendMessage"); - }); - - afterEach(() => { - sendMessageSpy = null; + jest.resetAllMocks(); }); it("should log info messages", () => { Logger.logInfo("Test info", "DocDB"); - const spyArgs = sendMessageSpy.calls.mostRecent().args[0]; - - expect(spyArgs.type).toBe(MessageTypes.LogInfo); - expect(spyArgs.data).toContain(LogEntryLevel.Verbose); - expect(spyArgs.data).toContain("DocDB"); - expect(spyArgs.data).toContain("Test info"); + expect(sendMessage).toBeCalled(); }); it("should log error messages", () => { Logger.logError("Test error", "DocDB"); - const spyArgs = sendMessageSpy.calls.mostRecent().args[0]; - - expect(spyArgs.type).toBe(MessageTypes.LogInfo); - expect(spyArgs.data).toContain(LogEntryLevel.Error); - expect(spyArgs.data).toContain("DocDB"); - expect(spyArgs.data).toContain("Test error"); + expect(sendMessage).toBeCalled(); }); it("should log warnings", () => { Logger.logWarning("Test warning", "DocDB"); - const spyArgs = sendMessageSpy.calls.mostRecent().args[0]; - - expect(spyArgs.type).toBe(MessageTypes.LogInfo); - expect(spyArgs.data).toContain(LogEntryLevel.Warning); - expect(spyArgs.data).toContain("DocDB"); - expect(spyArgs.data).toContain("Test warning"); + expect(sendMessage).toBeCalled(); }); }); diff --git a/src/Common/Logger.ts b/src/Common/Logger.ts index bb19035f0..73d50a15e 100644 --- a/src/Common/Logger.ts +++ b/src/Common/Logger.ts @@ -1,4 +1,4 @@ -import { MessageHandler } from "./MessageHandler"; +import { sendMessage } from "./MessageHandler"; import { Diagnostics, MessageTypes } from "../Contracts/ExplorerContracts"; import { appInsights } from "../Shared/appInsights"; import { SeverityLevel } from "@microsoft/applicationinsights-web"; @@ -33,7 +33,7 @@ export function logError(message: string | Error, area: string, code?: number): } function _logEntry(entry: Diagnostics.LogEntry): void { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.LogInfo, data: JSON.stringify(entry) }); diff --git a/src/Common/MessageHandler.test.ts b/src/Common/MessageHandler.test.ts index 63e502074..6f64b3fde 100644 --- a/src/Common/MessageHandler.test.ts +++ b/src/Common/MessageHandler.test.ts @@ -1,65 +1,29 @@ import Q from "q"; -import { CachedDataPromise, MessageHandler } from "./MessageHandler"; +import * as MessageHandler from "./MessageHandler"; import { MessageTypes } from "../Contracts/ExplorerContracts"; -class MockMessageHandler extends MessageHandler { - public static addToMap(key: string, value: CachedDataPromise): void { - MessageHandler.RequestMap[key] = value; - } - - public static mapContainsKey(key: string): boolean { - return MessageHandler.RequestMap[key] != null; - } - - public static clearAllEntries(): void { - MessageHandler.RequestMap = {}; - } - - public static runGarbageCollector(): void { - MessageHandler.runGarbageCollector(); - } -} - describe("Message Handler", () => { - beforeEach(() => { - MockMessageHandler.clearAllEntries(); - }); - - xit("should send cached data message", (done: any) => { - const testValidationCallback = (e: MessageEvent) => { - expect(e.data.data).toEqual( - jasmine.objectContaining({ type: MessageTypes.AllDatabases, params: ["some param"] }) - ); - e.currentTarget.removeEventListener(e.type, testValidationCallback); - done(); - }; - window.parent.addEventListener("message", testValidationCallback); - MockMessageHandler.sendCachedDataMessage(MessageTypes.AllDatabases, ["some param"]); - }); - - it("should handle cached message", () => { - let mockPromise: CachedDataPromise = { + it("should handle cached message", async () => { + let mockPromise = { id: "123", startTime: new Date(), deferred: Q.defer() }; let mockMessage = { message: { id: "123", data: "{}" } }; - - MockMessageHandler.addToMap(mockPromise.id, mockPromise); - MockMessageHandler.handleCachedDataMessage(mockMessage); + MessageHandler.RequestMap[mockPromise.id] = mockPromise; + MessageHandler.handleCachedDataMessage(mockMessage); expect(mockPromise.deferred.promise.isFulfilled()).toBe(true); }); - it("should delete fulfilled promises on running the garbage collector", () => { - let mockPromise: CachedDataPromise = { + it("should delete fulfilled promises on running the garbage collector", async () => { + let message = { id: "123", startTime: new Date(), deferred: Q.defer() }; - MockMessageHandler.addToMap(mockPromise.id, mockPromise); - mockPromise.deferred.reject("some error"); - MockMessageHandler.runGarbageCollector(); - expect(MockMessageHandler.mapContainsKey(mockPromise.id)).toBe(false); + MessageHandler.handleCachedDataMessage(message); + MessageHandler.runGarbageCollector(); + expect(MessageHandler.RequestMap["123"]).toBeUndefined(); }); }); diff --git a/src/Common/MessageHandler.ts b/src/Common/MessageHandler.ts index c022a5f9a..98b912d44 100644 --- a/src/Common/MessageHandler.ts +++ b/src/Common/MessageHandler.ts @@ -9,77 +9,65 @@ export interface CachedDataPromise { id: string; } -/** - * For some reason, typescript emits a Map() in the compiled js output(despite the target being set to ES5) forcing us to define our own polyfill, - * so we define our own custom implementation of the ES6 Map to work around it. - */ -type Map = { [key: string]: CachedDataPromise }; +export const RequestMap: Record> = {}; -export class MessageHandler { - protected static RequestMap: Map = {}; - - public static handleCachedDataMessage(message: any): void { - const messageContent = message && message.message; - if ( - message == null || - messageContent == null || - messageContent.id == null || - !MessageHandler.RequestMap[messageContent.id] - ) { - return; - } - - const cachedDataPromise = MessageHandler.RequestMap[messageContent.id]; - if (messageContent.error != null) { - cachedDataPromise.deferred.reject(messageContent.error); - } else { - cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data)); - } - MessageHandler.runGarbageCollector(); +export function handleCachedDataMessage(message: any): void { + const messageContent = message && message.message; + if (message == null || messageContent == null || messageContent.id == null || !RequestMap[messageContent.id]) { + return; } - public static sendCachedDataMessage( - messageType: MessageTypes, - params: Object[], - timeoutInMs?: number - ): Q.Promise { - let cachedDataPromise: CachedDataPromise = { - deferred: Q.defer(), - startTime: new Date(), - id: _.uniqueId() - }; - MessageHandler.RequestMap[cachedDataPromise.id] = cachedDataPromise; - MessageHandler.sendMessage({ type: messageType, params: params, id: cachedDataPromise.id }); + const cachedDataPromise = RequestMap[messageContent.id]; + if (messageContent.error != null) { + cachedDataPromise.deferred.reject(messageContent.error); + } else { + cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data)); + } + runGarbageCollector(); +} - //TODO: Use telemetry to measure optimal time to resolve/reject promises - return cachedDataPromise.deferred.promise.timeout( - timeoutInMs || Constants.ClientDefaults.requestTimeoutMs, - "Timed out while waiting for response from portal" +export function sendCachedDataMessage( + messageType: MessageTypes, + params: Object[], + timeoutInMs?: number +): Q.Promise { + let cachedDataPromise: CachedDataPromise = { + deferred: Q.defer(), + startTime: new Date(), + id: _.uniqueId() + }; + RequestMap[cachedDataPromise.id] = cachedDataPromise; + sendMessage({ type: messageType, params: params, id: cachedDataPromise.id }); + + //TODO: Use telemetry to measure optimal time to resolve/reject promises + return cachedDataPromise.deferred.promise.timeout( + timeoutInMs || Constants.ClientDefaults.requestTimeoutMs, + "Timed out while waiting for response from portal" + ); +} + +export function sendMessage(data: any): void { + if (canSendMessage()) { + window.parent.postMessage( + { + signature: "pcIframe", + data: data + }, + window.document.referrer ); } - - public static sendMessage(data: any): void { - if (MessageHandler.canSendMessage()) { - window.parent.postMessage( - { - signature: "pcIframe", - data: data - }, - window.document.referrer - ); - } - } - - public static canSendMessage(): boolean { - return window.parent !== window; - } - - protected static runGarbageCollector() { - Object.keys(MessageHandler.RequestMap).forEach((key: string) => { - const promise: Q.Promise = MessageHandler.RequestMap[key].deferred.promise; - if (promise.isFulfilled() || promise.isRejected()) { - delete MessageHandler.RequestMap[key]; - } - }); - } +} + +export function canSendMessage(): boolean { + return window.parent !== window; +} + +// TODO: This is exported just for testing. It should not be. +export function runGarbageCollector() { + Object.keys(RequestMap).forEach((key: string) => { + const promise: Q.Promise = RequestMap[key].deferred.promise; + if (promise.isFulfilled() || promise.isRejected()) { + delete RequestMap[key]; + } + }); } diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 4ddf6589b..ca7f7fb51 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -12,7 +12,7 @@ import { config } from "../Config"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { Constants as CosmosSDKConstants } from "@azure/cosmos"; import { CosmosClient } from "./CosmosClient"; -import { MessageHandler } from "./MessageHandler"; +import { sendMessage } from "./MessageHandler"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils"; import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient"; @@ -408,7 +408,7 @@ async function errorHandling(response: Response, action: string, params: unknown `Error ${action}: ${errorMessage}, Payload: ${JSON.stringify(params)}` ); if (response.status === HttpStatusCodes.Forbidden) { - MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage }); + sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage }); return; } throw new Error(errorMessage); diff --git a/src/Controls/Heatmap/Heatmap.ts b/src/Controls/Heatmap/Heatmap.ts index 05d96ae0b..92069ac7d 100644 --- a/src/Controls/Heatmap/Heatmap.ts +++ b/src/Controls/Heatmap/Heatmap.ts @@ -12,7 +12,7 @@ import { PortalTheme } from "./HeatmapDatatypes"; import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; -import { MessageHandler } from "../../Common/MessageHandler"; +import { sendCachedDataMessage, sendMessage } from "../../Common/MessageHandler"; import { MessageTypes } from "../../Contracts/ExplorerContracts"; import { StyleConstants } from "../../Common/Constants"; import "./Heatmap.less"; @@ -209,7 +209,7 @@ export class Heatmap { for (let i = 0; i < this._chartData.dataPoints.length; i++) { output.push(this._chartData.dataPoints[i][xAxisIndex]); } - MessageHandler.sendCachedDataMessage(MessageTypes.LogInfo, output); + sendCachedDataMessage(MessageTypes.LogInfo, output); }); } } @@ -266,4 +266,4 @@ export function handleMessage(event: MessageEvent) { } window.addEventListener("message", handleMessage, false); -MessageHandler.sendMessage("ready"); +sendMessage("ready"); diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index 7926f7c07..130e78b3e 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -52,7 +52,7 @@ import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; import { IGalleryItem } from "../Juno/JunoClient"; import { LoadQueryPane } from "./Panes/LoadQueryPane"; import * as Logger from "../Common/Logger"; -import { MessageHandler } from "../Common/MessageHandler"; +import { sendMessage, sendCachedDataMessage, handleCachedDataMessage } from "../Common/MessageHandler"; import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; @@ -1607,7 +1607,7 @@ export default class Explorer { public async getArcadiaToken(): Promise { return new Promise((resolve: (token: string) => void, reject: (error: any) => void) => { - MessageHandler.sendCachedDataMessage(MessageTypes.GetArcadiaToken, undefined /** params **/).then( + sendCachedDataMessage(MessageTypes.GetArcadiaToken, undefined /** params **/).then( (token: string) => { resolve(token); }, @@ -1645,11 +1645,11 @@ export default class Explorer { } public async createWorkspace(): Promise { - return MessageHandler.sendCachedDataMessage(MessageTypes.CreateWorkspace, undefined /** params **/); + return sendCachedDataMessage(MessageTypes.CreateWorkspace, undefined /** params **/); } public async createSparkPool(workspaceId: string): Promise { - return MessageHandler.sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]); + return sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]); } public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { @@ -1845,7 +1845,7 @@ export default class Explorer { } } if (message.actionType === ActionContracts.ActionType.TransmitCachedData) { - MessageHandler.handleCachedDataMessage(message); + handleCachedDataMessage(message); return; } if (message.type) { @@ -2041,7 +2041,7 @@ export default class Explorer { public signInAad = () => { TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" }); - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.AadSignIn }); }; @@ -2052,21 +2052,21 @@ export default class Explorer { }; public clickHostedAccountSwitch = () => { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.UpdateAccountSwitch, click: true }); }; public clickHostedDirectorySwitch = () => { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.UpdateDirectoryControl, click: true }); }; public refreshDatabaseAccount = () => { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.RefreshDatabaseAccount }); }; diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 224dc2289..1c1e437c4 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -14,7 +14,7 @@ import * as TableConstants from "./Constants"; import * as TableEntityProcessor from "./TableEntityProcessor"; import * as ViewModels from "../../Contracts/ViewModels"; import { MessageTypes } from "../../Contracts/ExplorerContracts"; -import { MessageHandler } from "../../Common/MessageHandler"; +import { sendMessage } from "../../Common/MessageHandler"; import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase"; import Explorer from "../Explorer"; @@ -738,7 +738,7 @@ export class CassandraAPIDataClient extends TableDataClient { private _checkForbiddenError(reason: any) { if (reason && reason.code === Constants.HttpStatusCodes.Forbidden) { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.ForbiddenError, reason: typeof reason === "string" ? "reason" : JSON.stringify(reason) }); diff --git a/src/Explorer/Tabs/DocumentsTab.test.ts b/src/Explorer/Tabs/DocumentsTab.test.ts index c39aa1d2d..0c715dc82 100644 --- a/src/Explorer/Tabs/DocumentsTab.test.ts +++ b/src/Explorer/Tabs/DocumentsTab.test.ts @@ -2,7 +2,6 @@ import * as ko from "knockout"; import * as ViewModels from "../../Contracts/ViewModels"; import * as Constants from "../../Common/Constants"; import DocumentsTab from "./DocumentsTab"; -import { DataAccessUtility } from "../../Platform/Portal/DataAccessUtility"; import Explorer from "../Explorer"; import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase"; @@ -15,7 +14,7 @@ describe("Documents tab", () => { tabKind: ViewModels.CollectionTabKind.Documents, title: "", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", isActive: ko.observable(false), @@ -98,7 +97,7 @@ describe("Documents tab", () => { tabKind: ViewModels.CollectionTabKind.Documents, title: "", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", isActive: ko.observable(false), @@ -117,7 +116,7 @@ describe("Documents tab", () => { tabKind: ViewModels.CollectionTabKind.Documents, title: "", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", isActive: ko.observable(false), @@ -136,7 +135,7 @@ describe("Documents tab", () => { tabKind: ViewModels.CollectionTabKind.Documents, title: "", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", isActive: ko.observable(false), @@ -155,7 +154,7 @@ describe("Documents tab", () => { tabKind: ViewModels.CollectionTabKind.Documents, title: "", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", isActive: ko.observable(false), @@ -174,7 +173,7 @@ describe("Documents tab", () => { tabKind: ViewModels.CollectionTabKind.Documents, title: "", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", isActive: ko.observable(false), diff --git a/src/Explorer/Tabs/SettingsTab.test.ts b/src/Explorer/Tabs/SettingsTab.test.ts index b2214fd7c..26fbf91ab 100644 --- a/src/Explorer/Tabs/SettingsTab.test.ts +++ b/src/Explorer/Tabs/SettingsTab.test.ts @@ -7,7 +7,6 @@ import Database from "../Tree/Database"; import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase"; import Explorer from "../Explorer"; import SettingsTab from "../Tabs/SettingsTab"; -import { DataAccessUtility } from "../../Platform/Portal/DataAccessUtility"; describe("Settings tab", () => { const baseCollection: DataModels.Collection = { @@ -189,7 +188,7 @@ describe("Settings tab", () => { tabKind: ViewModels.CollectionTabKind.Settings, title: "Scale & Settings", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", @@ -213,7 +212,7 @@ describe("Settings tab", () => { tabKind: ViewModels.CollectionTabKind.Settings, title: "Scale & Settings", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", @@ -232,7 +231,7 @@ describe("Settings tab", () => { tabKind: ViewModels.CollectionTabKind.Settings, title: "Scale & Settings", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", @@ -271,7 +270,7 @@ describe("Settings tab", () => { tabKind: ViewModels.CollectionTabKind.Settings, title: "Scale & Settings", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", @@ -288,7 +287,7 @@ describe("Settings tab", () => { tabKind: ViewModels.CollectionTabKind.Settings, title: "Scale & Settings", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", @@ -314,7 +313,7 @@ describe("Settings tab", () => { tabKind: ViewModels.CollectionTabKind.Settings, title: "Scale & Settings", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", @@ -395,7 +394,7 @@ describe("Settings tab", () => { tabKind: ViewModels.CollectionTabKind.Settings, title: "Scale & Settings", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", @@ -541,7 +540,7 @@ describe("Settings tab", () => { tabKind: ViewModels.CollectionTabKind.Settings, title: "Scale & Settings", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", diff --git a/src/Explorer/Tabs/TabsManager.test.ts b/src/Explorer/Tabs/TabsManager.test.ts index 3aed8e817..fdf1c8f67 100644 --- a/src/Explorer/Tabs/TabsManager.test.ts +++ b/src/Explorer/Tabs/TabsManager.test.ts @@ -1,6 +1,5 @@ import * as ko from "knockout"; import * as ViewModels from "../../Contracts/ViewModels"; -import { DataAccessUtility } from "../../Platform/Portal/DataAccessUtility"; import { TabsManager } from "./TabsManager"; import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase"; import DocumentsTab from "./DocumentsTab"; @@ -64,7 +63,7 @@ describe("Tabs manager tests", () => { collection, title: "", tabPath: "", - documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()), + documentClientUtility: new DocumentClientUtilityBase(), selfLink: "", hashLocation: "", isActive: ko.observable(false), diff --git a/src/Platform/Emulator/DataAccessUtility.ts b/src/Platform/Emulator/DataAccessUtility.ts deleted file mode 100644 index 3b36c1cd1..000000000 --- a/src/Platform/Emulator/DataAccessUtility.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Q from "q"; -import { DataAccessUtilityBase } from "../../Common/DataAccessUtilityBase"; - -export class DataAccessUtility extends DataAccessUtilityBase { - public refreshCachedOffers(): Q.Promise { - return Q(); - } - - public refreshCachedResources(options: any): Q.Promise { - return Q(); - } -} diff --git a/src/Platform/Emulator/ExplorerFactory.ts b/src/Platform/Emulator/ExplorerFactory.ts index 7a58e11e2..df85ace24 100644 --- a/src/Platform/Emulator/ExplorerFactory.ts +++ b/src/Platform/Emulator/ExplorerFactory.ts @@ -4,12 +4,11 @@ import Explorer from "../../Explorer/Explorer"; import { NotificationsClient } from "./NotificationsClient"; import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase"; -import { DataAccessUtility } from "./DataAccessUtility"; export default class EmulatorExplorerFactory { public static createExplorer(): Explorer { DocumentClientUtilityBase; - const documentClientUtility: DocumentClientUtilityBase = new DocumentClientUtilityBase(new DataAccessUtility()); + const documentClientUtility: DocumentClientUtilityBase = new DocumentClientUtilityBase(); const explorer: Explorer = new Explorer({ documentClientUtility: documentClientUtility, diff --git a/src/Platform/Hosted/DataAccessUtility.ts b/src/Platform/Hosted/DataAccessUtility.ts deleted file mode 100644 index 2447a6ae7..000000000 --- a/src/Platform/Hosted/DataAccessUtility.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Q from "q"; -import { DataAccessUtilityBase } from "../../Common/DataAccessUtilityBase"; - -export class DataAccessUtility extends DataAccessUtilityBase { - public refreshCachedOffers(): Q.Promise { - return Q(); - } - - public refreshCachedResources(): Q.Promise { - return Q(); - } -} diff --git a/src/Platform/Hosted/ExplorerFactory.ts b/src/Platform/Hosted/ExplorerFactory.ts index 69ea6caab..20b1628af 100644 --- a/src/Platform/Hosted/ExplorerFactory.ts +++ b/src/Platform/Hosted/ExplorerFactory.ts @@ -1,12 +1,10 @@ -import * as ViewModels from "../../Contracts/ViewModels"; import Explorer from "../../Explorer/Explorer"; import { NotificationsClient } from "./NotificationsClient"; import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase"; -import { DataAccessUtility } from "./DataAccessUtility"; export default class HostedExplorerFactory { public createExplorer(): Explorer { - var documentClientUtility = new DocumentClientUtilityBase(new DataAccessUtility()); + var documentClientUtility = new DocumentClientUtilityBase(); const explorer = new Explorer({ documentClientUtility: documentClientUtility, @@ -19,7 +17,7 @@ export default class HostedExplorerFactory { public static reInitializeDocumentClientUtilityForExplorer(explorer: Explorer): void { if (!!explorer) { - const documentClientUtility = new DocumentClientUtilityBase(new DataAccessUtility()); + const documentClientUtility = new DocumentClientUtilityBase(); explorer.rebindDocumentClientUtility(documentClientUtility); explorer.notificationConsoleData([]); } diff --git a/src/Platform/Hosted/Main.ts b/src/Platform/Hosted/Main.ts index 78fc7129a..92d251fa9 100644 --- a/src/Platform/Hosted/Main.ts +++ b/src/Platform/Hosted/Main.ts @@ -18,7 +18,7 @@ import { CosmosClient } from "../../Common/CosmosClient"; import { DataExplorerInputsFrame } from "../../Contracts/ViewModels"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { HostedUtils } from "./HostedUtils"; -import { MessageHandler } from "../../Common/MessageHandler"; +import { sendMessage } from "../../Common/MessageHandler"; import { MessageTypes } from "../../Contracts/ExplorerContracts"; import { SessionStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { SubscriptionUtilMappings } from "../../Shared/Constants"; @@ -77,7 +77,7 @@ export default class Main { const explorer: Explorer = this._instantiateExplorer(); if (authType === AuthType.EncryptedToken) { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.UpdateAccountSwitch, props: { authType: AuthType.EncryptedToken, @@ -102,7 +102,7 @@ export default class Main { } ); } else if (authType === AuthType.AAD) { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.GetAccessAadRequest }); if (this._getAadAccessDeferred != null) { @@ -287,7 +287,7 @@ export default class Main { const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind( Main._accessInputMetadata.apiKind ); - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.UpdateAccountSwitch, props: { authType: AuthType.EncryptedToken, @@ -385,7 +385,7 @@ export default class Main { window.addEventListener( "click", () => { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.ExplorerClickEvent }); }, @@ -514,7 +514,7 @@ export default class Main { ) { Main._initDataExplorerFrameInputs(explorer, masterKey, account, authorizationToken); explorer.isAccountReady.valueHasMutated(); - MessageHandler.sendMessage("ready"); + sendMessage("ready"); } private static _shouldProcessMessage(event: MessageEvent): boolean { diff --git a/src/Platform/Portal/DataAccessUtility.ts b/src/Platform/Portal/DataAccessUtility.ts deleted file mode 100644 index 724ccd39b..000000000 --- a/src/Platform/Portal/DataAccessUtility.ts +++ /dev/null @@ -1,96 +0,0 @@ -import "jquery"; -import * as _ from "underscore"; -import Q from "q"; - -import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import * as Constants from "../../Common/Constants"; -import { DataAccessUtilityBase } from "../../Common/DataAccessUtilityBase"; -import { MessageHandler } from "../../Common/MessageHandler"; -import { MessageTypes } from "../../Contracts/ExplorerContracts"; - -export class DataAccessUtility extends DataAccessUtilityBase { - public readDatabases(options: any): Q.Promise { - return MessageHandler.sendCachedDataMessage(MessageTypes.AllDatabases, [ - (window).dataExplorer.databaseAccount().id, - Constants.ClientDefaults.portalCacheTimeoutMs - ]).catch(error => { - return super.readDatabases(options); - }); - } - - // public readCollections(database: ViewModels.Database, options: any): Q.Promise { - // return MessageHandler.sendCachedDataMessage(MessageTypes.CollectionsForDatabase, [ - // (window).dataExplorer.databaseAccount().id, - // database.id() - // ]); - // } - - public readOffers(options: any): Q.Promise { - return MessageHandler.sendCachedDataMessage(MessageTypes.AllOffers, [ - (window).dataExplorer.databaseAccount().id, - Constants.ClientDefaults.portalCacheTimeoutMs - ]).catch(error => { - return super.readOffers(options); - }); - } - - public readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise { - const deferred: Q.Deferred = Q.defer(); - super.readOffer(requestedResource, options).then( - (offer: DataModels.OfferWithHeaders) => deferred.resolve(offer), - (reason: any) => { - const isThrottled: boolean = - !!reason && - !!reason.error && - !!reason.error.code && - reason.error.code == Constants.HttpStatusCodes.TooManyRequests; - if (isThrottled && MessageHandler.canSendMessage()) { - MessageHandler.sendCachedDataMessage(MessageTypes.SingleOffer, [ - (window).dataExplorer.databaseAccount().id, - requestedResource._self, - requestedResource.offerVersion - ]).then( - (offer: DataModels.OfferWithHeaders) => deferred.resolve(offer), - (reason: any) => deferred.reject(reason) - ); - return; - } - - deferred.reject(reason); - } - ); - - return deferred.promise; - } - - public updateOfferThroughputBeyondLimit( - updateThroughputRequestPayload: DataModels.UpdateOfferThroughputRequest, - options: any - ): Q.Promise { - const deferred: Q.Deferred = Q.defer(); - const explorer = window.dataExplorer; - const url: string = `${explorer.extensionEndpoint()}/api/offerthroughputrequest/updatebeyondspecifiedlimit`; - const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); - const requestOptions: any = _.extend({}, options, {}); - requestOptions[authorizationHeader.header] = authorizationHeader.token; - const requestSettings: JQueryAjaxSettings = { - type: "POST", - contentType: "application/json", - headers: requestOptions, - data: JSON.stringify(updateThroughputRequestPayload) - }; - - $.ajax(url, requestSettings).then( - (data: any, textStatus: string, xhr: JQueryXHR) => { - deferred.resolve(); - }, - (xhr: JQueryXHR, textStatus: string, error: any) => { - deferred.reject(xhr.responseText); - } - ); - - return deferred.promise; - } -} diff --git a/src/Platform/Portal/ExplorerFactory.ts b/src/Platform/Portal/ExplorerFactory.ts index bf38ffcf1..d23974fe6 100644 --- a/src/Platform/Portal/ExplorerFactory.ts +++ b/src/Platform/Portal/ExplorerFactory.ts @@ -1,13 +1,11 @@ -import * as ViewModels from "../../Contracts/ViewModels"; import Explorer from "../../Explorer/Explorer"; import { NotificationsClient } from "./NotificationsClient"; import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase"; -import { DataAccessUtility } from "./DataAccessUtility"; export default class PortalExplorerFactory { public createExplorer(): Explorer { - var documentClientUtility = new DocumentClientUtilityBase(new DataAccessUtility()); + var documentClientUtility = new DocumentClientUtilityBase(); var explorer = new Explorer({ documentClientUtility: documentClientUtility, diff --git a/src/RouteHandlers/RouteHandler.ts b/src/RouteHandlers/RouteHandler.ts index 276e5238f..ba512ae04 100644 --- a/src/RouteHandlers/RouteHandler.ts +++ b/src/RouteHandlers/RouteHandler.ts @@ -1,5 +1,5 @@ import { MessageTypes } from "../Contracts/ExplorerContracts"; -import { MessageHandler } from "../Common/MessageHandler"; +import { sendMessage } from "../Common/MessageHandler"; import { TabRouteHandler } from "./TabRouteHandler"; export class RouteHandler { @@ -27,7 +27,7 @@ export class RouteHandler { } public updateRouteHashLocation(hash: string): void { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.UpdateLocationHash, locationHash: hash }); diff --git a/src/Shared/AddCollectionUtility.ts b/src/Shared/AddCollectionUtility.ts index 3dbc4b7af..a926c448f 100644 --- a/src/Shared/AddCollectionUtility.ts +++ b/src/Shared/AddCollectionUtility.ts @@ -7,7 +7,7 @@ import { AddDbUtilities } from "../Shared/AddDatabaseUtility"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { CosmosClient } from "../Common/CosmosClient"; import { HttpStatusCodes } from "../Common/Constants"; -import { MessageHandler } from "../Common/MessageHandler"; +import { sendMessage } from "../Common/MessageHandler"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils"; import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient"; @@ -108,7 +108,7 @@ export class CreateSqlCollectionUtilities { `Error creating collection: ${JSON.stringify(response)}` ); if (response.status === HttpStatusCodes.Forbidden) { - MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError }); + sendMessage({ type: MessageTypes.ForbiddenError }); } throw new Error(`Error creating collection`); } @@ -202,7 +202,7 @@ export class CreateCollectionUtilities { `Error creating graph: ${JSON.stringify(response)}` ); if (response.status === HttpStatusCodes.Forbidden) { - MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError }); + sendMessage({ type: MessageTypes.ForbiddenError }); } throw new Error(`Error creating graph`); } @@ -248,7 +248,7 @@ export class Utilities { `Error creating table: ${JSON.stringify(reason)}, Payload: ${params}` ); if (reason.status === HttpStatusCodes.Forbidden) { - MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError }); + sendMessage({ type: MessageTypes.ForbiddenError }); return; } throw new Error(`Error creating table`); diff --git a/src/Shared/AddDatabaseUtility.ts b/src/Shared/AddDatabaseUtility.ts index 62e764842..4a78dba5b 100644 --- a/src/Shared/AddDatabaseUtility.ts +++ b/src/Shared/AddDatabaseUtility.ts @@ -4,7 +4,7 @@ import { config } from "../Config"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { CosmosClient } from "../Common/CosmosClient"; import { HttpStatusCodes } from "../Common/Constants"; -import { MessageHandler } from "../Common/MessageHandler"; +import { sendMessage } from "../Common/MessageHandler"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils"; import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient"; @@ -164,7 +164,7 @@ export class AddDbUtilities { `Error creating ${dbType}: ${JSON.stringify(reason)}, Payload: ${params}` ); if (reason.status === HttpStatusCodes.Forbidden) { - MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError }); + sendMessage({ type: MessageTypes.ForbiddenError }); return; } throw new Error(`Error creating ${dbType}`); diff --git a/src/Shared/Telemetry/TelemetryProcessor.ts b/src/Shared/Telemetry/TelemetryProcessor.ts index 24e763f8e..1a62926bd 100644 --- a/src/Shared/Telemetry/TelemetryProcessor.ts +++ b/src/Shared/Telemetry/TelemetryProcessor.ts @@ -1,5 +1,5 @@ import { Action, ActionModifiers } from "./TelemetryConstants"; -import { MessageHandler } from "../../Common/MessageHandler"; +import { sendMessage } from "../../Common/MessageHandler"; import { MessageTypes } from "../../Contracts/ExplorerContracts"; import { appInsights } from "../appInsights"; import { config } from "../../Config"; @@ -11,7 +11,7 @@ import { config } from "../../Config"; // TODO: Log telemetry for StorageExplorer case/other clients as well export default class TelemetryProcessor { public static trace(action: Action, actionModifier: string = ActionModifiers.Mark, data?: any): void { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.TelemetryInfo, data: { action: Action[action], @@ -25,7 +25,7 @@ export default class TelemetryProcessor { public static traceStart(action: Action, data?: any): number { const timestamp: number = Date.now(); - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.TelemetryInfo, data: { action: Action[action], @@ -40,7 +40,7 @@ export default class TelemetryProcessor { } public static traceSuccess(action: Action, data?: any, timestamp?: number): void { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.TelemetryInfo, data: { action: Action[action], @@ -54,7 +54,7 @@ export default class TelemetryProcessor { } public static traceFailure(action: Action, data?: any, timestamp?: number): void { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.TelemetryInfo, data: { action: Action[action], @@ -68,7 +68,7 @@ export default class TelemetryProcessor { } public static traceCancel(action: Action, data?: any, timestamp?: number): void { - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.TelemetryInfo, data: { action: Action[action], @@ -83,7 +83,7 @@ export default class TelemetryProcessor { public static traceOpen(action: Action, data?: any, timestamp?: number): number { const validTimestamp = timestamp || Date.now(); - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.TelemetryInfo, data: { action: Action[action], @@ -99,7 +99,7 @@ export default class TelemetryProcessor { public static traceMark(action: Action, data?: any, timestamp?: number): number { const validTimestamp = timestamp || Date.now(); - MessageHandler.sendMessage({ + sendMessage({ type: MessageTypes.TelemetryInfo, data: { action: Action[action], diff --git a/src/applyExplorerBindings.ts b/src/applyExplorerBindings.ts index a74dd0b40..1bed350f7 100644 --- a/src/applyExplorerBindings.ts +++ b/src/applyExplorerBindings.ts @@ -1,5 +1,5 @@ import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer"; -import { MessageHandler } from "./Common/MessageHandler"; +import { sendMessage } from "./Common/MessageHandler"; import * as ko from "knockout"; import Explorer from "./Explorer/Explorer"; @@ -8,7 +8,7 @@ export const applyExplorerBindings = (explorer: Explorer) => { ko.applyBindings(explorer); // This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times. // TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal - MessageHandler.sendMessage("ready"); + sendMessage("ready"); window.dataExplorer = explorer; BindingHandlersRegisterer.registerBindingHandlers(); $("#divExplorer").show();