import * as ko from "knockout"; import Q from "q"; import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import { AuthType } from "../../AuthType"; import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import * as Constants from "../../Common/Constants"; import * as Entities from "./Entities"; import EnvironmentUtility from "../../Common/EnvironmentUtility"; import * as HeadersUtility from "../../Common/HeadersUtility"; import * as Logger from "../../Common/Logger"; import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils"; 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 DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase"; export interface CassandraTableKeys { partitionKeys: CassandraTableKey[]; clusteringKeys: CassandraTableKey[]; } export interface CassandraTableKey { property: string; type: string; } export abstract class TableDataClient { public documentClientUtility: DocumentClientUtilityBase; constructor(documentClientUtility: DocumentClientUtilityBase) { this.documentClientUtility = documentClientUtility; } public abstract createDocument( collection: ViewModels.Collection, entity: Entities.ITableEntity ): Q.Promise; public abstract updateDocument( collection: ViewModels.Collection, originalDocument: any, newEntity: Entities.ITableEntity ): Q.Promise; public abstract queryDocuments( collection: ViewModels.Collection, query: string, shouldNotify?: boolean, paginationToken?: string ): Q.Promise; public abstract deleteDocuments( collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[] ): Q.Promise; } export class TablesAPIDataClient extends TableDataClient { public createDocument( collection: ViewModels.Collection, entity: Entities.ITableEntity ): Q.Promise { const deferred = Q.defer(); this.documentClientUtility .createDocument( collection, TableEntityProcessor.convertEntityToNewDocument(entity) ) .then( (newDocument: any) => { const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; deferred.resolve(newEntity); }, reason => { deferred.reject(reason); } ); return deferred.promise; } public updateDocument( collection: ViewModels.Collection, originalDocument: any, entity: Entities.ITableEntity ): Q.Promise { const deferred = Q.defer(); this.documentClientUtility .updateDocument( collection, originalDocument, TableEntityProcessor.convertEntityToNewDocument(entity) ) .then( (newDocument: any) => { const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; deferred.resolve(newEntity); }, reason => { deferred.reject(reason); } ); return deferred.promise; } public queryDocuments( collection: ViewModels.Collection, query: string ): Q.Promise { const deferred = Q.defer(); let options: any = {}; options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); this.documentClientUtility.queryDocuments(collection.databaseId, collection.id(), query, options).then( iterator => { iterator .fetchNext() .then(response => response.resources) .then( (documents: any[] = []) => { let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents); let finalEntities: Entities.IListTableEntitiesResult = { Results: entities, ContinuationToken: iterator.hasMoreResults(), iterator: iterator }; deferred.resolve(finalEntities); }, reason => { deferred.reject(reason); } ); }, reason => { deferred.reject(reason); } ); return deferred.promise; } public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise { let documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments( entitiesToDelete, collection ); let promiseArray: Q.Promise[] = []; documentsToDelete && documentsToDelete.forEach(document => { document.id = ko.observable(document.id); let promise: Q.Promise = this.documentClientUtility.deleteDocument(collection, document); promiseArray.push(promise); }); return Q.all(promiseArray); } } export class CassandraAPIDataClient extends TableDataClient { public createDocument( collection: ViewModels.Collection, entity: Entities.ITableEntity ): Q.Promise { const notificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, `Adding new row to table ${collection.id()}` ); let properties = "("; let values = "("; for (let property in entity) { if (entity[property]._ === null) { continue; } properties = properties.concat(`${property}, `); const propertyType = entity[property].$; if (this.isStringType(propertyType)) { values = values.concat(`'${entity[property]._}', `); } else { values = values.concat(`${entity[property]._}, `); } } properties = properties.slice(0, properties.length - 2) + ")"; values = values.slice(0, values.length - 2) + ")"; const query = `INSERT INTO ${collection.databaseId}.${collection.id()} ${properties} VALUES ${values}`; const deferred = Q.defer(); this.queryDocuments(collection, query) .then( (data: any) => { entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)]; entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString(); NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Info, `Successfully added new row to table ${collection.id()}` ); deferred.resolve(entity); }, reason => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, `Error while adding new row to table ${collection.id()}:\n ${JSON.stringify(reason)}` ); Logger.logError(JSON.stringify(reason), "AddRowCassandra", reason.code); this._checkForbiddenError(reason); deferred.reject(reason); } ) .finally(() => { NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); }); return deferred.promise; } public updateDocument( collection: ViewModels.Collection, originalDocument: any, newEntity: Entities.ITableEntity ): Q.Promise { const notificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, `Updating row ${originalDocument.RowKey._}` ); const deferred = Q.defer(); let promiseArray: Q.Promise[] = []; let query = `UPDATE ${collection.databaseId}.${collection.id()}`; let isChange: boolean = false; for (let property in newEntity) { if (!originalDocument[property] || newEntity[property]._.toString() !== originalDocument[property]._.toString()) { if (this.isStringType(newEntity[property].$)) { query = `${query} SET ${property} = '${newEntity[property]._}',`; } else { query = `${query} SET ${property} = ${newEntity[property]._},`; } isChange = true; } } query = query.slice(0, query.length - 1); let whereSegment = " WHERE"; let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat( collection.cassandraKeys.clusteringKeys ); for (let keyIndex in keys) { const key = keys[keyIndex].property; const keyType = keys[keyIndex].type; if (this.isStringType(keyType)) { whereSegment = `${whereSegment} ${key} = '${newEntity[key]._}' AND`; } else { whereSegment = `${whereSegment} ${key} = ${newEntity[key]._} AND`; } } whereSegment = whereSegment.slice(0, whereSegment.length - 4); query = query + whereSegment; if (isChange) { promiseArray.push(this.queryDocuments(collection, query)); } query = `DELETE `; for (let property in originalDocument) { if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) { query = `${query} ${property},`; } } if (query.length > 7) { query = query.slice(0, query.length - 1); query = `${query} FROM ${collection.databaseId}.${collection.id()}${whereSegment}`; promiseArray.push(this.queryDocuments(collection, query)); } Q.all(promiseArray) .then( (data: any) => { newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey]; NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Info, `Successfully updated row ${newEntity.RowKey._}` ); deferred.resolve(newEntity); }, reason => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, `Failed to update row ${newEntity.RowKey._}: ${JSON.stringify(reason)}` ); Logger.logError(JSON.stringify(reason), "UpdateRowCassandra", reason.code); this._checkForbiddenError(reason); deferred.reject(reason); } ) .finally(() => { NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); }); return deferred.promise; } public queryDocuments( collection: ViewModels.Collection, query: string, shouldNotify?: boolean, paginationToken?: string ): Q.Promise { let notificationId: string; if (shouldNotify) { notificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, `Querying rows for table ${collection.id()}` ); } const deferred = Q.defer(); const authType = window.authType; const apiEndpoint: string = authType === AuthType.EncryptedToken ? Constants.CassandraBackend.guestQueryApi : Constants.CassandraBackend.queryApi; $.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`, { type: "POST", data: { accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, cassandraEndpoint: this.trimCassandraEndpoint( collection.container.databaseAccount().properties.cassandraEndpoint ), resourceId: collection.container.databaseAccount().id, keyspaceId: collection.databaseId, tableId: collection.id(), query: query, paginationToken: paginationToken }, beforeSend: this.setAuthorizationHeader, error: this.handleAjaxError, cache: false }) .then( (data: any) => { if (shouldNotify) { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Info, `Successfully fetched ${data.result.length} rows for table ${collection.id()}` ); } deferred.resolve({ Results: data.result, ContinuationToken: data.paginationToken }); }, reason => { if (shouldNotify) { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, `Failed to query rows for table ${collection.id()}: ${JSON.stringify(reason)}` ); Logger.logError(JSON.stringify(reason), "QueryDocumentsCassandra", reason.status); this._checkForbiddenError(reason); } deferred.reject(reason); } ) .done(() => { if (shouldNotify) { NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); } }); return deferred.promise; } public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise { const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `; let promiseArray: Q.Promise[] = []; let partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection); for (let i = 0, len = entitiesToDelete.length; i < len; i++) { let currEntityToDelete: Entities.ITableEntity = entitiesToDelete[i]; let currQuery = query; let partitionKeyValue = currEntityToDelete[partitionKeyProperty]; if (partitionKeyValue._ != null && this.isStringType(partitionKeyValue.$)) { currQuery = `${currQuery}${partitionKeyProperty} = '${partitionKeyValue._}' AND `; } else { currQuery = `${currQuery}${partitionKeyProperty} = ${partitionKeyValue._} AND `; } currQuery = currQuery.slice(0, currQuery.length - 5); const notificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, `Deleting row ${currEntityToDelete.RowKey._}` ); promiseArray.push( this.queryDocuments(collection, currQuery) .then( () => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Info, `Successfully deleted row ${currEntityToDelete.RowKey._}` ); }, reason => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, `Error while deleting row ${currEntityToDelete.RowKey._}:\n ${JSON.stringify(reason)}` ); Logger.logError(JSON.stringify(reason), "DeleteRowCassandra", reason.code); this._checkForbiddenError(reason); } ) .finally(() => { NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); }) ); } return Q.all(promiseArray); } public createKeyspace( cassandraEndpoint: string, resourceId: string, explorer: ViewModels.Explorer, createKeyspaceQuery: string ): Q.Promise { if (!createKeyspaceQuery) { return Q.reject("No query specified"); } const deferred: Q.Deferred = Q.defer(); const notificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, `Creating a new keyspace with query ${createKeyspaceQuery}` ); this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery, explorer) .then( (data: any) => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Info, `Successfully created a keyspace with query ${createKeyspaceQuery}` ); explorer.documentClientUtility.refreshCachedResources().finally(() => deferred.resolve()); }, reason => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, `Error while creating a keyspace with query ${createKeyspaceQuery}:\n ${JSON.stringify(reason)}` ); Logger.logError(JSON.stringify(reason), "CreateKeyspaceCassandra", reason.code); this._checkForbiddenError(reason); deferred.reject(reason); } ) .finally(() => { NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); }); return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs); } public createTableAndKeyspace( cassandraEndpoint: string, resourceId: string, explorer: ViewModels.Explorer, createTableQuery: string, createKeyspaceQuery?: string ): Q.Promise { let createKeyspacePromise: Q.Promise; if (createKeyspaceQuery) { createKeyspacePromise = this.createKeyspace(cassandraEndpoint, resourceId, explorer, createKeyspaceQuery); } else { createKeyspacePromise = Q.resolve(null); } const deferred = Q.defer(); createKeyspacePromise.then( () => { const notificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, `Creating a new table with query ${createTableQuery}` ); this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery, explorer) .then( (data: any) => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Info, `Successfully created a table with query ${createTableQuery}` ); this.documentClientUtility.refreshCachedResources(null).then( () => { deferred.resolve(); }, reason => { // Still resolve since the keyspace/table was successfully created at this point. deferred.resolve(); } ); }, reason => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, `Error while creating a table with query ${createTableQuery}:\n ${JSON.stringify(reason)}` ); Logger.logError(JSON.stringify(reason), "CreateTableCassandra", reason.code); this._checkForbiddenError(reason); deferred.reject(reason); } ) .finally(() => { NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); }); }, reason => { deferred.reject(reason); } ); return deferred.promise; } public deleteTableOrKeyspace( cassandraEndpoint: string, resourceId: string, deleteQuery: string, explorer: ViewModels.Explorer ): Q.Promise { const deferred = Q.defer(); const notificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, `Deleting resource with query ${deleteQuery}` ); this.createOrDeleteQuery(cassandraEndpoint, resourceId, deleteQuery, explorer) .then( () => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Info, `Successfully deleted resource with query ${deleteQuery}` ); this.documentClientUtility.refreshCachedResources(null).then( () => { deferred.resolve(); }, reason => { // Still resolve since the keyspace/table was successfully deleted at this point. deferred.resolve(); } ); }, reason => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, `Error while deleting resource with query ${deleteQuery}:\n ${JSON.stringify(reason)}` ); Logger.logError(JSON.stringify(reason), "DeleteKeyspaceOrTableCassandra", reason.code); this._checkForbiddenError(reason); deferred.reject(reason); } ) .finally(() => { NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); }); return deferred.promise; } public getTableKeys(collection: ViewModels.Collection): Q.Promise { if (!!collection.cassandraKeys) { return Q.resolve(collection.cassandraKeys); } const notificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, `Fetching keys for table ${collection.id()}` ); const authType = window.authType; const apiEndpoint: string = authType === AuthType.EncryptedToken ? Constants.CassandraBackend.guestKeysApi : Constants.CassandraBackend.keysApi; let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`; const deferred = Q.defer(); $.ajax(endpoint, { type: "POST", data: { accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, cassandraEndpoint: this.trimCassandraEndpoint( collection.container.databaseAccount().properties.cassandraEndpoint ), resourceId: collection.container.databaseAccount().id, keyspaceId: collection.databaseId, tableId: collection.id() }, beforeSend: this.setAuthorizationHeader, error: this.handleAjaxError, cache: false }) .then( (data: CassandraTableKeys) => { collection.cassandraKeys = data; NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Info, `Successfully fetched keys for table ${collection.id()}` ); deferred.resolve(data); }, reason => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, `Error fetching keys for table ${collection.id()}:\n ${JSON.stringify(reason)}` ); Logger.logError(JSON.stringify(reason), "FetchKeysCassandra", reason.status); this._checkForbiddenError(reason); deferred.reject(reason); } ) .done(() => { NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); }); return deferred.promise; } public getTableSchema(collection: ViewModels.Collection): Q.Promise { if (!!collection.cassandraSchema) { return Q.resolve(collection.cassandraSchema); } const notificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, `Fetching schema for table ${collection.id()}` ); const authType = window.authType; const apiEndpoint: string = authType === AuthType.EncryptedToken ? Constants.CassandraBackend.guestSchemaApi : Constants.CassandraBackend.schemaApi; let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`; const deferred = Q.defer(); $.ajax(endpoint, { type: "POST", data: { accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, cassandraEndpoint: this.trimCassandraEndpoint( collection.container.databaseAccount().properties.cassandraEndpoint ), resourceId: collection.container.databaseAccount().id, keyspaceId: collection.databaseId, tableId: collection.id() }, beforeSend: this.setAuthorizationHeader, error: this.handleAjaxError, cache: false }) .then( (data: any) => { collection.cassandraSchema = data.columns; NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Info, `Successfully fetched schema for table ${collection.id()}` ); deferred.resolve(data.columns); }, reason => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, `Error fetching schema for table ${collection.id()}:\n ${JSON.stringify(reason)}` ); Logger.logError(JSON.stringify(reason), "FetchSchemaCassandra", reason.status); this._checkForbiddenError(reason); deferred.reject(reason); } ) .done(() => { NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); }); return deferred.promise; } private createOrDeleteQuery( cassandraEndpoint: string, resourceId: string, query: string, explorer: ViewModels.Explorer ): Q.Promise { const deferred = Q.defer(); const authType = window.authType; const apiEndpoint: string = authType === AuthType.EncryptedToken ? Constants.CassandraBackend.guestCreateOrDeleteApi : Constants.CassandraBackend.createOrDeleteApi; $.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(explorer)}${apiEndpoint}`, { type: "POST", data: { accountName: explorer.databaseAccount() && explorer.databaseAccount().name, cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint), resourceId: resourceId, query: query }, beforeSend: this.setAuthorizationHeader, error: this.handleAjaxError, cache: false }).then( (data: any) => { deferred.resolve(); }, reason => { deferred.reject(reason); } ); return deferred.promise; } private trimCassandraEndpoint(cassandraEndpoint: string): string { if (!cassandraEndpoint) { return cassandraEndpoint; } if (cassandraEndpoint.indexOf("https://") === 0) { cassandraEndpoint = cassandraEndpoint.slice(8, cassandraEndpoint.length); } if (cassandraEndpoint.indexOf(":443/", cassandraEndpoint.length - 5) !== -1) { cassandraEndpoint = cassandraEndpoint.slice(0, cassandraEndpoint.length - 5); } return cassandraEndpoint; } private setAuthorizationHeader: (xhr: XMLHttpRequest) => boolean = (xhr: XMLHttpRequest): boolean => { const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token); return true; }; private isStringType(dataType: string): boolean { // TODO figure out rest of types that are considered strings by Cassandra (if any have been missed) return ( dataType === TableConstants.CassandraType.Text || dataType === TableConstants.CassandraType.Inet || dataType === TableConstants.CassandraType.Ascii || dataType === TableConstants.CassandraType.Varchar ); } private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string { return collection.cassandraKeys.partitionKeys[0].property; } private handleAjaxError = (xhrObj: XMLHttpRequest, textStatus: string, errorThrown: string): void => { if (!xhrObj) { return; } displayTokenRenewalPromptForStatus(xhrObj.status); }; private _checkForbiddenError(reason: any) { if (reason && reason.code === Constants.HttpStatusCodes.Forbidden) { MessageHandler.sendMessage({ type: MessageTypes.ForbiddenError, reason: typeof reason === "string" ? "reason" : JSON.stringify(reason) }); } } }