From 3223ff76854407c77308fb5a8fcee86881c893f3 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Tue, 25 Aug 2020 15:45:37 -0700 Subject: [PATCH] Move createDatabase to RP (#166) --- src/Common/DataAccessUtilityBase.ts | 51 ----- src/Common/DocumentClientUtilityBase.ts | 33 ---- src/Common/dataAccess/createDatabase.ts | 250 ++++++++++++++++++++++++ src/Contracts/DataModels.ts | 5 +- src/Explorer/Panes/AddDatabasePane.ts | 156 ++------------- src/Utils/arm/request.ts | 16 +- 6 files changed, 269 insertions(+), 242 deletions(-) create mode 100644 src/Common/dataAccess/createDatabase.ts diff --git a/src/Common/DataAccessUtilityBase.ts b/src/Common/DataAccessUtilityBase.ts index 30620b7ff..b10b75629 100644 --- a/src/Common/DataAccessUtilityBase.ts +++ b/src/Common/DataAccessUtilityBase.ts @@ -6,7 +6,6 @@ import * as ViewModels from "../Contracts/ViewModels"; import Q from "q"; import { ConflictDefinition, - DatabaseResponse, FeedOptions, ItemDefinition, PartitionKeyDefinition, @@ -544,26 +543,6 @@ export function getOrCreateDatabaseAndCollection( ); } -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 (configContext.platform === Platform.Portal) { return sendCachedDataMessage(MessageTypes.RefreshOffers, []); @@ -592,33 +571,3 @@ export function queryConflicts( .conflicts.query(query, options); return Q(documentsIterator); } - -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( - 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 dbc8adec5..67e330ae2 100644 --- a/src/Common/DocumentClientUtilityBase.ts +++ b/src/Common/DocumentClientUtilityBase.ts @@ -890,36 +890,3 @@ export function getOrCreateDatabaseAndCollection( return deferred.promise; } - -export function createDatabase( - request: DataModels.CreateDatabaseRequest, - options: any = {} -): Q.Promise { - const deferred: Q.Deferred = Q.defer(); - const id = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Creating a new database ${request.databaseId}` - ); - - DataAccessUtilityBase.createDatabase(request, options) - .then( - (database: DataModels.Database) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - `Successfully created database ${request.databaseId}` - ); - deferred.resolve(database); - }, - (error: any) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while creating database ${request.databaseId}:\n ${JSON.stringify(error)}` - ); - sendNotificationForError(error); - deferred.reject(error); - } - ) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); - - return deferred.promise; -} diff --git a/src/Common/dataAccess/createDatabase.ts b/src/Common/dataAccess/createDatabase.ts new file mode 100644 index 000000000..57e25ca14 --- /dev/null +++ b/src/Common/dataAccess/createDatabase.ts @@ -0,0 +1,250 @@ +import * as DataModels from "../../Contracts/DataModels"; +import { AuthType } from "../../AuthType"; +import { DatabaseResponse } from "@azure/cosmos"; +import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; +import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; +import { RequestOptions } from "@azure/cosmos/dist-esm"; +import { + SqlDatabaseCreateUpdateParameters, + CreateUpdateOptions +} from "../../Utils/arm/generatedClients/2020-04-01/types"; +import { client } from "../CosmosClient"; +import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; +import { + createUpdateCassandraKeyspace, + getCassandraKeyspace +} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; +import { + createUpdateMongoDBDatabase, + getMongoDBDatabase +} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; +import { + createUpdateGremlinDatabase, + getGremlinDatabase +} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; +import { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; +import { logError } from "../Logger"; +import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase"; +import { sendNotificationForError } from "./sendNotificationForError"; +import { userContext } from "../../UserContext"; + +export async function createDatabase(params: DataModels.CreateDatabaseParams): Promise { + let database: DataModels.Database; + const clearMessage = logConsoleProgress(`Creating a new database ${params.databaseId}`); + try { + if ( + window.authType === AuthType.AAD && + !userContext.useSDKOperations && + userContext.defaultExperience !== DefaultAccountExperienceType.Table + ) { + database = await createDatabaseWithARM(params); + } else { + database = await createDatabaseWithSDK(params); + } + } catch (error) { + logConsoleError(`Error while creating database ${params.databaseId}:\n ${error.message}`); + logError(JSON.stringify(error), "CreateDatabase", error.code); + sendNotificationForError(error); + clearMessage(); + throw error; + } + logConsoleInfo(`Successfully created database ${params.databaseId}`); + await refreshCachedResources(); + await refreshCachedOffers(); + clearMessage(); + return database; +} + +async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise { + const defaultExperience = userContext.defaultExperience; + switch (defaultExperience) { + case DefaultAccountExperienceType.DocumentDB: + return createSqlDatabase(params); + case DefaultAccountExperienceType.MongoDB: + return createMongoDatabase(params); + case DefaultAccountExperienceType.Cassandra: + return createCassandraKeyspace(params); + case DefaultAccountExperienceType.Graph: + return createGremlineDatabase(params); + default: + throw new Error(`Unsupported default experience type: ${defaultExperience}`); + } +} + +async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise { + try { + const getResponse = await getSqlDatabase( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId + ); + if (getResponse && getResponse.properties && getResponse.properties.resource) { + throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); + } + } catch (error) { + if (error.code !== "NotFound") { + throw error; + } + } + + const options: CreateUpdateOptions = constructRpOptions(params); + const rpPayload: SqlDatabaseCreateUpdateParameters = { + properties: { + resource: { + id: params.databaseId + }, + options + } + }; + const createResponse = await createUpdateSqlDatabase( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId, + rpPayload + ); + return createResponse && (createResponse.properties.resource as DataModels.Database); +} + +async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise { + try { + const getResponse = await getMongoDBDatabase( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId + ); + if (getResponse && getResponse.properties && getResponse.properties.resource) { + throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); + } + } catch (error) { + if (error.code !== "NotFound") { + throw error; + } + } + + const options: CreateUpdateOptions = constructRpOptions(params); + const rpPayload: SqlDatabaseCreateUpdateParameters = { + properties: { + resource: { + id: params.databaseId + }, + options + } + }; + const createResponse = await createUpdateMongoDBDatabase( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId, + rpPayload + ); + return createResponse && (createResponse.properties.resource as DataModels.Database); +} + +async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise { + try { + const getResponse = await getCassandraKeyspace( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId + ); + if (getResponse?.properties?.resource) { + throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); + } + } catch (error) { + if (error.code !== "NotFound") { + throw error; + } + } + + const options: CreateUpdateOptions = constructRpOptions(params); + const rpPayload: SqlDatabaseCreateUpdateParameters = { + properties: { + resource: { + id: params.databaseId + }, + options + } + }; + const createResponse = await createUpdateCassandraKeyspace( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId, + rpPayload + ); + return createResponse && (createResponse.properties.resource as DataModels.Database); +} + +async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise { + try { + const getResponse = await getGremlinDatabase( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId + ); + if (getResponse && getResponse.properties && getResponse.properties.resource) { + throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); + } + } catch (error) { + if (error.code !== "NotFound") { + throw error; + } + } + + const options: CreateUpdateOptions = constructRpOptions(params); + const rpPayload: SqlDatabaseCreateUpdateParameters = { + properties: { + resource: { + id: params.databaseId + }, + options + } + }; + const createResponse = await createUpdateGremlinDatabase( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId, + rpPayload + ); + return createResponse && (createResponse.properties.resource as DataModels.Database); +} + +async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): Promise { + const createBody: DatabaseRequest = { id: params.databaseId }; + const databaseOptions: RequestOptions = {}; + // TODO: replace when SDK support autopilot + if (params.databaseLevelThroughput) { + if (params.autoPilotMaxThroughput) { + createBody.maxThroughput = params.autoPilotMaxThroughput; + } else { + createBody.throughput = params.offerThroughput; + } + } + + const response: DatabaseResponse = await client().databases.create(createBody, databaseOptions); + return response.resource; +} + +function constructRpOptions(params: DataModels.CreateDatabaseParams): CreateUpdateOptions { + if (!params.databaseLevelThroughput) { + return {}; + } + + if (params.autoPilotMaxThroughput) { + return { + autoscaleSettings: { + maxThroughput: params.autoPilotMaxThroughput + } + }; + } + + return { + throughput: params.offerThroughput + }; +} diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 4abe8a0f4..77ac52cb7 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -327,12 +327,11 @@ export interface AutoPilotOfferSettings { targetMaxThroughput?: number; } -export interface CreateDatabaseRequest { +export interface CreateDatabaseParams { + autoPilotMaxThroughput?: number; databaseId: string; databaseLevelThroughput?: boolean; offerThroughput?: number; - autoPilot?: AutoPilotCreationSettings; - hasAutoPilotV2FeatureFlag?: boolean; } export interface SharedThroughputRange { diff --git a/src/Explorer/Panes/AddDatabasePane.ts b/src/Explorer/Panes/AddDatabasePane.ts index 8355d7dcf..e5e831180 100644 --- a/src/Explorer/Panes/AddDatabasePane.ts +++ b/src/Explorer/Panes/AddDatabasePane.ts @@ -14,8 +14,8 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan import { AddDbUtilities } from "../../Shared/AddDatabaseUtility"; import { CassandraAPIDataClient } from "../Tables/TableDataClient"; import { ContextualPaneBase } from "./ContextualPaneBase"; +import { createDatabase } from "../../Common/dataAccess/createDatabase"; import { PlatformType } from "../../PlatformType"; -import { refreshCachedOffers, refreshCachedResources, createDatabase } from "../../Common/DocumentClientUtilityBase"; import { userContext } from "../../UserContext"; export default class AddDatabasePane extends ContextualPaneBase { @@ -304,76 +304,23 @@ export default class AddDatabasePane extends ContextualPaneBase { this.formErrors(""); this.isExecuting(true); - const createDatabaseParameters: DataModels.RpParameters = { - db: addDatabasePaneStartMessage.database.id, - st: addDatabasePaneStartMessage.database.shared, - offerThroughput: addDatabasePaneStartMessage.offerThroughput, - sid: userContext.subscriptionId, - rg: userContext.resourceGroup, - dba: addDatabasePaneStartMessage.databaseAccountName + const createDatabaseParams: DataModels.CreateDatabaseParams = { + autoPilotMaxThroughput: this.maxAutoPilotThroughputSet(), + databaseId: addDatabasePaneStartMessage.database.id, + databaseLevelThroughput: addDatabasePaneStartMessage.database.shared, + offerThroughput: addDatabasePaneStartMessage.offerThroughput }; - const autopilotSettings = this._getAutopilotSettings(); - - if (this.container.isPreferredApiCassandra()) { - this._createKeyspace(createDatabaseParameters, autopilotSettings, startKey); - } else if (this.container.isPreferredApiMongoDB() && EnvironmentUtility.isAadUser()) { - this._createMongoDatabase(createDatabaseParameters, autopilotSettings, startKey); - } else if (this.container.isPreferredApiGraph() && EnvironmentUtility.isAadUser()) { - this._createGremlinDatabase(createDatabaseParameters, autopilotSettings, startKey); - } else if (this.container.isPreferredApiDocumentDB() && EnvironmentUtility.isAadUser()) { - this._createSqlDatabase(createDatabaseParameters, autopilotSettings, startKey); - } else { - this._createDatabase(offerThroughput, startKey); - } - } - - private _createSqlDatabase( - createDatabaseParameters: DataModels.RpParameters, - autoPilotSettings: DataModels.RpOptions, - startKey: number - ) { - AddDbUtilities.createSqlDatabase(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings).then( - () => { - Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => { - this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey); - }); + createDatabase(createDatabaseParams).then( + (database: DataModels.Database) => { + this._onCreateDatabaseSuccess(offerThroughput, startKey); + }, + (reason: any) => { + this._onCreateDatabaseFailure(reason, offerThroughput, reason); } ); } - private _createMongoDatabase( - createDatabaseParameters: DataModels.RpParameters, - autoPilotSettings: DataModels.RpOptions, - startKey: number - ) { - AddDbUtilities.createMongoDatabaseWithARM( - this.container.armEndpoint(), - createDatabaseParameters, - autoPilotSettings - ).then(() => { - Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => { - this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey); - }); - }); - } - - private _createGremlinDatabase( - createDatabaseParameters: DataModels.RpParameters, - autoPilotSettings: DataModels.RpOptions, - startKey: number - ) { - AddDbUtilities.createGremlinDatabase( - this.container.armEndpoint(), - createDatabaseParameters, - autoPilotSettings - ).then(() => { - Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => { - this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey); - }); - }); - } - public resetData() { this.databaseId(""); this.databaseCreateNewShared(this.getSharedThroughputDefault()); @@ -396,71 +343,6 @@ export default class AddDatabasePane extends ContextualPaneBase { return true; } - private _createDatabase(offerThroughput: number, telemetryStartKey: number): void { - const autoPilot: DataModels.AutoPilotCreationSettings = this._isAutoPilotSelectedAndWhatTier(); - const createRequest: DataModels.CreateDatabaseRequest = { - databaseId: this.databaseId().trim(), - offerThroughput, - databaseLevelThroughput: this.databaseCreateNewShared(), - autoPilot, - hasAutoPilotV2FeatureFlag: this.hasAutoPilotV2FeatureFlag() - }; - createDatabase(createRequest).then( - (database: DataModels.Database) => { - this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey); - }, - (reason: any) => { - this._onCreateDatabaseFailure(reason, offerThroughput, reason); - } - ); - } - - private _createKeyspace( - createDatabaseParameters: DataModels.RpParameters, - autoPilotSettings: DataModels.RpOptions, - startKey: number - ): void { - if (EnvironmentUtility.isAadUser()) { - this._createKeyspaceUsingRP(createDatabaseParameters, autoPilotSettings, startKey); - } else { - this._createKeyspaceUsingProxy(createDatabaseParameters.offerThroughput, startKey); - } - } - - private _createKeyspaceUsingProxy(offerThroughput: number, telemetryStartKey: number): void { - const provisionThroughputQueryPart: string = this.databaseCreateNewShared() - ? `AND cosmosdb_provisioned_throughput=${offerThroughput}` - : ""; - const createKeyspaceQuery: string = `CREATE KEYSPACE ${this.databaseId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 } ${provisionThroughputQueryPart};`; - (this.container.tableDataClient as CassandraAPIDataClient) - .createKeyspace( - this.container.databaseAccount().properties.cassandraEndpoint, - this.container.databaseAccount().id, - this.container, - createKeyspaceQuery - ) - .then( - () => { - this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey); - }, - (reason: any) => { - this._onCreateDatabaseFailure(reason, offerThroughput, telemetryStartKey); - } - ); - } - - private _createKeyspaceUsingRP( - createKeyspaceParameters: DataModels.RpParameters, - autoPilotSettings: DataModels.RpOptions, - startKey: number - ): void { - AddDbUtilities.createCassandraKeyspace(createKeyspaceParameters, autoPilotSettings).then(() => { - Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => { - this._onCreateDatabaseSuccess(createKeyspaceParameters.offerThroughput, startKey); - }); - }); - } - private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void { this.isExecuting(false); this.close(); @@ -581,20 +463,6 @@ export default class AddDatabasePane extends ContextualPaneBase { return undefined; } - private _getAutopilotSettings(): DataModels.RpOptions { - if ( - (!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) || - (this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier()) - ) { - return !this.hasAutoPilotV2FeatureFlag() - ? { - [Constants.HttpHeaders.autoPilotThroughput]: { maxThroughput: this.maxAutoPilotThroughputSet() * 1 } - } - : { [Constants.HttpHeaders.autoPilotTier]: this.selectedAutoPilotTier().toString() }; - } - return undefined; - } - private _updateThroughputLimitByDatabase() { const throughputDefaults = this.container.collectionCreationDefaults.throughput; this.throughput(throughputDefaults.shared); diff --git a/src/Utils/arm/request.ts b/src/Utils/arm/request.ts index 95c10b20d..857841463 100644 --- a/src/Utils/arm/request.ts +++ b/src/Utils/arm/request.ts @@ -6,15 +6,9 @@ Instead, generate ARM clients that consume this function with stricter typing. */ import promiseRetry, { AbortError } from "p-retry"; +import { ErrorResponse } from "./generatedClients/2020-04-01/types"; import { userContext } from "../../UserContext"; -interface ErrorResponse { - error: { - code: string; - message: string; - }; -} - interface ARMError extends Error { code: string; } @@ -40,8 +34,8 @@ export async function armRequest({ host, path, apiVersion, method, body: requ }); if (!response.ok) { const errorResponse = (await response.json()) as ErrorResponse; - const error = new Error(errorResponse.error?.message) as ARMError; - error.code = errorResponse.error.code; + const error = new Error(errorResponse.message) as ARMError; + error.code = errorResponse.code; throw error; } @@ -84,8 +78,8 @@ async function getOperationStatus(operationStatusUrl: string) { }); if (!response.ok) { const errorResponse = (await response.json()) as ErrorResponse; - const error = new Error(errorResponse.error?.message) as ARMError; - error.code = errorResponse.error.code; + const error = new Error(errorResponse.message) as ARMError; + error.code = errorResponse.code; throw new AbortError(error); } const body = (await response.json()) as OperationResponse;