import * as _ from "underscore"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "../Explorer/Explorer"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentId from "../Explorer/Tree/DocumentId"; import { useDatabases } from "../Explorer/useDatabases"; import { userContext } from "../UserContext"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { createCollection } from "./dataAccess/createCollection"; import { createDocument } from "./dataAccess/createDocument"; import { deleteDocument } from "./dataAccess/deleteDocument"; import { queryDocuments } from "./dataAccess/queryDocuments"; import { handleError } from "./ErrorHandlingUtils"; export class QueriesClient { private static readonly PartitionKey: DataModels.PartitionKey = { paths: [`/${SavedQueries.PartitionKeyProperty}`], kind: "Hash", version: BackendDefaults.partitionKeyVersion, }; private static readonly FetchQuery: string = "SELECT * FROM c"; private static readonly FetchMongoQuery: string = "{}"; public constructor(private container: Explorer) {} public async setupQueriesCollection(): Promise { const queriesCollection: ViewModels.Collection = this.findQueriesCollection(); if (queriesCollection) { return Promise.resolve(queriesCollection.rawDataModel); } const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries"); return createCollection({ collectionId: SavedQueries.CollectionName, createNewDatabase: true, databaseId: SavedQueries.DatabaseName, partitionKey: QueriesClient.PartitionKey, offerThroughput: SavedQueries.OfferThroughput, databaseLevelThroughput: false, }) .then( (collection: DataModels.Collection) => { NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries"); return Promise.resolve(collection); }, (error: any) => { handleError(error, "setupQueriesCollection", "Failed to set up account for saving queries"); return Promise.reject(error); } ) .finally(() => clearMessage()); } public async saveQuery(query: DataModels.Query): Promise { const queriesCollection = this.findQueriesCollection(); if (!queriesCollection) { const errorMessage: string = "Account not set up to perform saved query operations"; NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); return Promise.reject(errorMessage); } try { this.validateQuery(query); } catch (error) { const errorMessage: string = "Invalid query specified"; NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); return Promise.reject(errorMessage); } const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`); query.id = query.queryName; return createDocument(queriesCollection, query) .then( (savedQuery: DataModels.Query) => { NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`); return Promise.resolve(); }, (error: any) => { if (error.code === HttpStatusCodes.Conflict.toString()) { error = `Query ${query.queryName} already exists`; } handleError(error, "saveQuery", `Failed to save query ${query.queryName}`); return Promise.reject(error); } ) .finally(() => clearMessage()); } public async getQueries(): Promise { const queriesCollection = this.findQueriesCollection(); if (!queriesCollection) { const errorMessage: string = "Account not set up to perform saved query operations"; NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); return Promise.reject(errorMessage); } const options: any = { enableCrossPartitionQuery: true }; const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries"); const results = await queryDocuments( SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options ).fetchAll(); let queries: DataModels.Query[] = _.map(results.resources, (document: DataModels.Query) => { if (!document) { return undefined; } const { id, resourceId, query, queryName } = document; const parsedQuery: DataModels.Query = { resourceId: resourceId, queryName: queryName, query: query, id: id, }; try { this.validateQuery(parsedQuery); return parsedQuery; } catch (error) { return undefined; } }); queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries"); clearMessage(); return queries; } public async deleteQuery(query: DataModels.Query): Promise { const queriesCollection = this.findQueriesCollection(); if (!queriesCollection) { const errorMessage: string = "Account not set up to perform saved query operations"; NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); return Promise.reject(errorMessage); } try { this.validateQuery(query); } catch (error) { const errorMessage: string = "Invalid query specified"; NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`); } const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`); query.id = query.queryName; const documentId = new DocumentId( { partitionKey: QueriesClient.PartitionKey, partitionKeyProperty: "id", } as DocumentsTab, query, query.queryName ); // TODO: Remove DocumentId's dependency on DocumentsTab const options: any = { partitionKey: query.resourceId }; return deleteDocument(queriesCollection, documentId) .then( () => { NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`); return Promise.resolve(); }, (error: any) => { handleError(error, "deleteQuery", `Failed to delete query ${query.queryName}`); return Promise.reject(error); } ) .finally(() => clearMessage()); } public getResourceId(): string { const { databaseAccount, subscriptionId = "", resourceGroup = "" } = userContext; const databaseAccountName = databaseAccount?.name || ""; return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}`; } private findQueriesCollection(): ViewModels.Collection { const queriesDatabase: ViewModels.Database = _.find( useDatabases.getState().databases, (database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName ); if (!queriesDatabase) { return undefined; } return _.find( queriesDatabase.collections(), (collection: ViewModels.Collection) => collection.id() === SavedQueries.CollectionName ); } private validateQuery(query: DataModels.Query): void { if (!query || query.queryName == null || query.query == null || query.resourceId == null) { throw new Error("Invalid query specified"); } } private fetchQueriesQuery(): string { if (userContext.apiType === "Mongo") { return QueriesClient.FetchMongoQuery; } return QueriesClient.FetchQuery; } }