Move queryDocuments out of DataAccessUtility (#334)

This commit is contained in:
victor-meng 2020-12-16 15:27:17 -08:00 committed by GitHub
parent ea39c1d092
commit f54e8eb692
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1163 additions and 1442 deletions

View File

@ -1,169 +0,0 @@
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import Q from "q";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import * as Constants from "./Constants";
import { client } from "./CosmosClient";
export function getCommonQueryOptions(options: FeedOptions): any {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
options = options || {};
options.populateQueryMetrics = true;
options.enableScanInQuery = options.enableScanInQuery || true;
if (!options.partitionKey) {
options.forceQueryPlan = true;
}
options.maxItemCount =
options.maxItemCount ||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Constants.Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
return options;
}
export function queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
options = getCommonQueryOptions(options);
const documentsIterator = client()
.database(databaseId)
.container(containerId)
.items.query(query, options);
return Q(documentsIterator);
}
export function getPartitionKeyHeaderForConflict(conflictId: 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;
}
if (partitionKeyValue === undefined) {
return [{}];
}
return [partitionKeyValue];
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
newDocument: any
): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.replace(newDocument)
.then(response => response.resource)
);
}
export function executeStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: any,
params: any[]
): Q.Promise<any> {
// TODO remove this deferred. Kept it because of timeout code at bottom of function
const deferred = Q.defer<any>();
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 createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument)
.then(response => response.resource)
);
}
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.read()
.then(response => response.resource)
);
}
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.delete()
);
}
export function deleteConflict(
collection: ViewModels.CollectionBase,
conflictId: ConflictId,
options: any = {}
): Q.Promise<any> {
options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId);
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.conflict(conflictId.id())
.delete(options)
);
}
export function queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
const documentsIterator = client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
return Q(documentsIterator);
}

View File

@ -1,217 +0,0 @@
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import Q from "q";
import * as ViewModels from "../Contracts/ViewModels";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import * as Constants from "./Constants";
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
import { handleError } from "./ErrorHandlingUtils";
// TODO: Log all promise resolutions and errors with verbosity levels
export function queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options);
}
export function queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options);
}
export function getEntityName() {
const defaultExperience =
window.dataExplorer && window.dataExplorer.defaultExperience && window.dataExplorer.defaultExperience();
if (defaultExperience === Constants.DefaultAccountExperience.MongoDB) {
return "document";
}
return "item";
}
export function executeStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: any,
params: any[]
): Q.Promise<any> {
var deferred = Q.defer<any>();
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
.then(
(response: any) => {
deferred.resolve(response);
logConsoleInfo(
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
},
(error: any) => {
handleError(
error,
"ExecuteStoredProcedure",
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function queryDocumentsPage(
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
options: any
): Q.Promise<ViewModels.QueryResults> {
var deferred = Q.defer<ViewModels.QueryResults>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
Q(nextPage(documentsIterator, firstItemIndex))
.then(
(result: ViewModels.QueryResults) => {
const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
deferred.resolve(result);
},
(error: any) => {
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.readDocument(collection, documentId)
.then(
(document: any) => {
deferred.resolve(document);
},
(error: any) => {
handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
newDocument: any
): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
.then(
(updatedDocument: any) => {
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
deferred.resolve(updatedDocument);
},
(error: any) => {
handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
DataAccessUtilityBase.createDocument(collection, newDocument)
.then(
(savedDocument: any) => {
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
deferred.resolve(savedDocument);
},
(error: any) => {
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.deleteDocument(collection, documentId)
.then(
(response: any) => {
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
deferred.resolve(response);
},
(error: any) => {
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function deleteConflict(
collection: ViewModels.CollectionBase,
conflictId: ConflictId,
options?: any
): Q.Promise<any> {
var deferred = Q.defer<any>();
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
.then(
(response: any) => {
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
deferred.resolve(response);
},
(error: any) => {
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}

View File

@ -0,0 +1,10 @@
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
import { userContext } from "../UserContext";
export const getEntityName = (): string => {
if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
return "document";
}
return "item";
};

View File

@ -3,16 +3,18 @@ import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils"; import { QueryUtils } from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase"; import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
import { createCollection } from "./dataAccess/createCollection"; import { createCollection } from "./dataAccess/createCollection";
import { handleError } from "./ErrorHandlingUtils"; import { handleError } from "./ErrorHandlingUtils";
import { createDocument } from "./dataAccess/createDocument";
import { deleteDocument } from "./dataAccess/deleteDocument";
import { queryDocuments } from "./dataAccess/queryDocuments";
export class QueriesClient { export class QueriesClient {
private static readonly PartitionKey: DataModels.PartitionKey = { private static readonly PartitionKey: DataModels.PartitionKey = {
@ -31,10 +33,7 @@ export class QueriesClient {
return Promise.resolve(queriesCollection.rawDataModel); return Promise.resolve(queriesCollection.rawDataModel);
} }
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries");
ConsoleDataType.InProgress,
"Setting up account for saving queries"
);
return createCollection({ return createCollection({
collectionId: SavedQueries.CollectionName, collectionId: SavedQueries.CollectionName,
createNewDatabase: true, createNewDatabase: true,
@ -45,10 +44,7 @@ export class QueriesClient {
}) })
.then( .then(
(collection: DataModels.Collection) => { (collection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries");
ConsoleDataType.Info,
"Successfully set up account for saving queries"
);
return Promise.resolve(collection); return Promise.resolve(collection);
}, },
(error: any) => { (error: any) => {
@ -56,17 +52,14 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => clearMessage());
} }
public async saveQuery(query: DataModels.Query): Promise<void> { public async saveQuery(query: DataModels.Query): Promise<void> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`);
ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
@ -74,25 +67,16 @@ export class QueriesClient {
this.validateQuery(query); this.validateQuery(query);
} catch (error) { } catch (error) {
const errorMessage: string = "Invalid query specified"; const errorMessage: string = "Invalid query specified";
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`);
ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`);
ConsoleDataType.InProgress,
`Saving query ${query.queryName}`
);
query.id = query.queryName; query.id = query.queryName;
return createDocument(queriesCollection, query) return createDocument(queriesCollection, query)
.then( .then(
(savedQuery: DataModels.Query) => { (savedQuery: DataModels.Query) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`);
ConsoleDataType.Info,
`Successfully saved query ${query.queryName}`
);
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
@ -103,74 +87,65 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => clearMessage());
} }
public async getQueries(): Promise<DataModels.Query[]> { public async getQueries(): Promise<DataModels.Query[]> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`);
ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const options: any = { enableCrossPartitionQuery: true }; const options: any = { enableCrossPartitionQuery: true };
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries"); const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries");
return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options) const queryIterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
SavedQueries.DatabaseName,
SavedQueries.CollectionName,
this.fetchQueriesQuery(),
options
);
const fetchQueries = async (firstItemIndex: number): Promise<ViewModels.QueryResults> =>
await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex);
return QueryUtils.queryAllPages(fetchQueries)
.then( .then(
(queryIterator: QueryIterator<ItemDefinition & Resource>) => { (results: ViewModels.QueryResults) => {
const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> => let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options); if (!document) {
return QueryUtils.queryAllPages(fetchQueries).then( return undefined;
(results: ViewModels.QueryResults) => {
let queries: DataModels.Query[] = _.map(results.documents, (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.logConsoleMessage(ConsoleDataType.Info, "Successfully fetched saved queries");
return Promise.resolve(queries);
},
(error: any) => {
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
return Promise.reject(error);
} }
); 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");
return Promise.resolve(queries);
}, },
(error: any) => { (error: any) => {
// should never get into this state but we handle this regardless
handleError(error, "getSavedQueries", "Failed to fetch saved queries"); handleError(error, "getSavedQueries", "Failed to fetch saved queries");
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => clearMessage());
} }
public async deleteQuery(query: DataModels.Query): Promise<void> { public async deleteQuery(query: DataModels.Query): Promise<void> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`);
ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
@ -178,16 +153,10 @@ export class QueriesClient {
this.validateQuery(query); this.validateQuery(query);
} catch (error) { } catch (error) {
const errorMessage: string = "Invalid query specified"; const errorMessage: string = "Invalid query specified";
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`);
ConsoleDataType.Error,
`Failed to delete query ${query.queryName}: ${errorMessage}`
);
} }
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`);
ConsoleDataType.InProgress,
`Deleting query ${query.queryName}`
);
query.id = query.queryName; query.id = query.queryName;
const documentId = new DocumentId( const documentId = new DocumentId(
{ {
@ -201,10 +170,7 @@ export class QueriesClient {
return deleteDocument(queriesCollection, documentId) return deleteDocument(queriesCollection, documentId)
.then( .then(
() => { () => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`);
ConsoleDataType.Info,
`Successfully deleted query ${query.queryName}`
);
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
@ -212,7 +178,7 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => clearMessage());
} }
public getResourceId(): string { public getResourceId(): string {

View File

@ -1,6 +1,5 @@
jest.mock("../../Utils/arm/request"); jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient"); jest.mock("../CosmosClient");
jest.mock("../DataAccessUtilityBase");
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";

View File

@ -0,0 +1,25 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise<unknown> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument);
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
return response?.resource;
} catch (error) {
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@ -0,0 +1,36 @@
import ConflictId from "../../Explorer/Tree/ConflictId";
import { CollectionBase } from "../../Contracts/ViewModels";
import { RequestOptions } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
export const deleteConflict = async (collection: CollectionBase, conflictId: ConflictId): Promise<void> => {
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
try {
const options = {
partitionKey: getPartitionKeyHeaderForConflict(conflictId)
};
await client()
.database(collection.databaseId)
.container(collection.id())
.conflict(conflictId.id())
.delete(options as RequestOptions);
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
} catch (error) {
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
throw error;
} finally {
clearMessage();
}
};
const getPartitionKeyHeaderForConflict = (conflictId: ConflictId): unknown => {
if (!conflictId.partitionKey) {
return undefined;
}
return conflictId.partitionKeyValue === undefined ? [{}] : [conflictId.partitionKeyValue];
};

View File

@ -0,0 +1,25 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
const entityName: string = getEntityName();
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
try {
await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue)
.delete();
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
} catch (error) {
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@ -0,0 +1,48 @@
import { Collection } from "../../Contracts/ViewModels";
import { ClientDefaults, HttpHeaders } from "../Constants";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import StoredProcedure from "../../Explorer/Tree/StoredProcedure";
export interface ExecuteSprocResult {
result: StoredProcedure;
scriptLogs: string;
}
export const executeStoredProcedure = async (
collection: Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: string,
params: string[]
): Promise<ExecuteSprocResult> => {
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
const timeout = setTimeout(() => {
throw Error(`Request timed out while executing stored procedure ${storedProcedure.id()}`);
}, ClientDefaults.requestTimeoutMs);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id())
.execute(partitionKeyValue, params, { enableScriptLogging: true });
clearTimeout(timeout);
logConsoleInfo(
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
return {
result: response.resource,
scriptLogs: response.headers[HttpHeaders.scriptLogResults] as string
};
} catch (error) {
handleError(
error,
"ExecuteStoredProcedure",
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
throw error;
} finally {
clearMessage();
}
};

View File

@ -0,0 +1,14 @@
import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos";
import { client } from "../CosmosClient";
export const queryConflicts = (
databaseId: string,
containerId: string,
query: string,
options: FeedOptions
): QueryIterator<ConflictDefinition & Resource> => {
return client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
};

View File

@ -1,13 +1,13 @@
import { getCommonQueryOptions } from "./DataAccessUtilityBase"; import { getCommonQueryOptions } from "./queryDocuments";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
describe("getCommonQueryOptions", () => { describe("getCommonQueryOptions", () => {
it("builds the correct default options objects", () => { it("builds the correct default options objects", () => {
expect(getCommonQueryOptions({})).toMatchSnapshot(); expect(getCommonQueryOptions({})).toMatchSnapshot();
}); });
it("reads from localStorage", () => { it("reads from localStorage", () => {
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37); LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37);
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17);
expect(getCommonQueryOptions({})).toMatchSnapshot(); expect(getCommonQueryOptions({})).toMatchSnapshot();
}); });
}); });

View File

@ -0,0 +1,34 @@
import { Queries } from "../Constants";
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { client } from "../CosmosClient";
export const queryDocuments = (
databaseId: string,
containerId: string,
query: string,
options: FeedOptions
): QueryIterator<ItemDefinition & Resource> => {
options = getCommonQueryOptions(options);
return client()
.database(databaseId)
.container(containerId)
.items.query(query, options);
};
export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
options = options || {};
options.populateQueryMetrics = true;
options.enableScanInQuery = options.enableScanInQuery || true;
if (!options.partitionKey) {
options.forceQueryPlan = true;
}
options.maxItemCount =
options.maxItemCount ||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
return options;
};

View File

@ -0,0 +1,26 @@
import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
import { handleError } from "../ErrorHandlingUtils";
import { getEntityName } from "../DocumentUtility";
export const queryDocumentsPage = async (
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number
): Promise<QueryResults> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
try {
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result;
} catch (error) {
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
throw error;
} finally {
clearMessage();
}
};

View File

@ -0,0 +1,27 @@
import { Item } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue)
.read();
return response?.resource;
} catch (error) {
handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@ -0,0 +1,32 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { Item } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const updateDocument = async (
collection: CollectionBase,
documentId: DocumentId,
newDocument: Item
): Promise<Item> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue)
.replace(newDocument);
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
return response?.resource;
} catch (error) {
handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@ -1,11 +1,11 @@
jest.mock("../../Common/DocumentClientUtilityBase");
jest.mock("../Graph/GraphExplorerComponent/GremlinClient"); jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection"); jest.mock("../../Common/dataAccess/createCollection");
jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import Q from "q"; import Q from "q";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { createDocument } from "../../Common/DocumentClientUtilityBase"; import { createDocument } from "../../Common/dataAccess/createDocument";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";

View File

@ -4,8 +4,8 @@ import GraphTab from ".././Tabs/GraphTab";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { createCollection } from "../../Common/dataAccess/createCollection"; import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
interface SampleDataFile extends DataModels.CreateCollectionParams { interface SampleDataFile extends DataModels.CreateCollectionParams {
@ -95,12 +95,15 @@ export class ContainerSampleGenerator {
.reduce((previous, current) => previous.then(current), Promise.resolve()); .reduce((previous, current) => previous.then(current), Promise.resolve());
} else { } else {
// For SQL all queries are executed at the same time // For SQL all queries are executed at the same time
this.sampleDataFile.data.map(doc => { await Promise.all(
const subPromise = createDocument(collection, doc); this.sampleDataFile.data.map(async doc => {
subPromise.catch(reason => NotificationConsoleUtils.logConsoleError(reason)); try {
promises.push(subPromise); await createDocument(collection, doc);
}); } catch (error) {
await Promise.all(promises); NotificationConsoleUtils.logConsoleError(error);
}
})
);
} }
} }

View File

@ -1,4 +1,5 @@
jest.mock("../../../Common/DocumentClientUtilityBase"); jest.mock("../../../Common/dataAccess/queryDocuments");
jest.mock("../../../Common/dataAccess/queryDocumentsPage");
import React from "react"; import React from "react";
import * as sinon from "sinon"; import * as sinon from "sinon";
import { mount, ReactWrapper } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
@ -12,7 +13,8 @@ import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility"; import * as StorageUtility from "../../../Shared/StorageUtility";
import GraphTab from "../../Tabs/GraphTab"; import GraphTab from "../../Tabs/GraphTab";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase"; import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
describe("Check whether query result is vertex array", () => { describe("Check whether query result is vertex array", () => {
it("should reject null as vertex array", () => { it("should reject null as vertex array", () => {
@ -299,12 +301,12 @@ describe("GraphExplorer", () => {
ignoreD3Update: boolean ignoreD3Update: boolean
): GraphExplorer => { ): GraphExplorer => {
(queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => { (queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => {
return Q.resolve({ return {
_query: query, _query: query,
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {}, nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
hasMoreResults: () => false, hasMoreResults: () => false,
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {} executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
}); };
}); });
(queryDocumentsPage as jest.Mock).mockImplementation( (queryDocumentsPage as jest.Mock).mockImplementation(
(rid: string, iterator: any, firstItemIndex: number, options: any) => { (rid: string, iterator: any, firstItemIndex: number, options: any) => {

View File

@ -28,8 +28,10 @@ import * as Constants from "../../../Common/Constants";
import { InputProperty } from "../../../Contracts/ViewModels"; import { InputProperty } from "../../../Contracts/ViewModels";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase"; import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
import { FeedOptions } from "@azure/cosmos";
export interface GraphAccessor { export interface GraphAccessor {
applyFilter: () => void; applyFilter: () => void;
@ -725,26 +727,32 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* Execute DocDB query and get all results * Execute DocDB query and get all results
*/ */
public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> { public async executeNonPagedDocDbQuery(query: string): Promise<DataModels.DocumentId[]> {
// TODO maxItemCount: this reduces throttling, but won't cap the # of results try {
return queryDocuments(this.props.databaseId, this.props.collectionId, query, { // TODO maxItemCount: this reduces throttling, but won't cap the # of results
maxItemCount: GraphExplorer.PAGE_ALL, const iterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
enableCrossPartitionQuery: this.props.databaseId,
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) === this.props.collectionId,
"true" query,
}).then( {
(iterator: QueryIterator<ItemDefinition & Resource>) => { maxItemCount: GraphExplorer.PAGE_ALL,
return iterator.fetchNext().then(response => response.resources); enableCrossPartitionQuery:
}, StorageUtility.LocalStorageUtility.getEntryString(
(reason: any) => { StorageUtility.StorageKey.IsCrossPartitionQueryEnabled
GraphExplorer.reportToConsole( ) === "true"
ConsoleDataType.Error, } as FeedOptions
`Failed to execute non-paged query ${query}. Reason:${reason}`, );
reason const response = await iterator.fetchNext();
);
return null; return response?.resources;
} } catch (error) {
); GraphExplorer.reportToConsole(
ConsoleDataType.Error,
`Failed to execute non-paged query ${query}. Reason:${error}`,
error
);
return null;
}
} }
/** /**
@ -864,7 +872,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* User executes query * User executes query
*/ */
public submitQuery(query: string): void { public async submitQuery(query: string): Promise<void> {
// Clear any progress indicator // Clear any progress indicator
this.executeCounter = 0; this.executeCounter = 0;
this.setState({ this.setState({
@ -882,24 +890,22 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Remember query // Remember query
this.pushToLatestQueryFragments(query); this.pushToLatestQueryFragments(query);
let backendPromise; try {
let result: UserQueryResult;
if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) { if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
backendPromise = this.executeDocDbGVQuery(); result = await this.executeDocDbGVQuery();
} else { } else {
backendPromise = this.executeGremlinQuery(query); result = await this.executeGremlinQuery(query);
}
backendPromise.then(
(result: UserQueryResult) => (this.queryTotalRequestCharge = result.requestCharge),
(error: any) => {
const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({
filterQueryError: errorMsg
});
} }
);
this.queryTotalRequestCharge = result.requestCharge;
} catch (error) {
const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({
filterQueryError: errorMsg
});
}
} }
/** /**
@ -1390,7 +1396,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* Update possible vertices to display in UI * Update possible vertices to display in UI
*/ */
private updatePossibleVertices(): Q.Promise<PossibleVertex[]> { private updatePossibleVertices(): Promise<PossibleVertex[]> {
const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null; const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null;
const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() || const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() ||
@ -1721,85 +1727,81 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
); );
} }
private executeDocDbGVQuery(): Q.Promise<UserQueryResult> { private async executeDocDbGVQuery(): Promise<UserQueryResult> {
let query = "select root.id from root where IS_DEFINED(root._isEdge) = false order by root._ts desc"; let query = "select root.id from root where IS_DEFINED(root._isEdge) = false order by root._ts desc";
if (this.props.collectionPartitionKeyProperty) { if (this.props.collectionPartitionKeyProperty) {
query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`; query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`;
} }
return queryDocuments(this.props.databaseId, this.props.collectionId, query, { try {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, const iterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true" this.props.databaseId,
}) this.props.collectionId,
.then( query,
(iterator: QueryIterator<ItemDefinition & Resource>) => { {
this.currentDocDBQueryInfo = { maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
iterator: iterator, enableCrossPartitionQuery:
index: 0, LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
query: query } as FeedOptions
}; );
}, this.currentDocDBQueryInfo = {
(reason: any) => { iterator: iterator,
GraphExplorer.reportToConsole( index: 0,
ConsoleDataType.Error, query: query
`Failed to execute CosmosDB query: ${query} reason:${reason}` };
); return await this.loadMoreRootNodes();
} } catch (error) {
) GraphExplorer.reportToConsole(
.then(() => this.loadMoreRootNodes()); ConsoleDataType.Error,
`Failed to execute CosmosDB query: ${query} reason:${error}`
);
throw error;
}
} }
private loadMoreRootNodes(): Q.Promise<UserQueryResult> { private async loadMoreRootNodes(): Promise<UserQueryResult> {
if (!this.currentDocDBQueryInfo) { if (!this.currentDocDBQueryInfo) {
return Q.resolve(null); return undefined;
} }
let RU: string = GraphExplorer.REQUEST_CHARGE_UNKNOWN_MSG;
let RU: string = GraphExplorer.REQUEST_CHARGE_UNKNOWN_MSG;
const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${this const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${this
.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`; .currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`;
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`); const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
return queryDocumentsPage( try {
this.props.collectionId, const results: ViewModels.QueryResults = await queryDocumentsPage(
this.currentDocDBQueryInfo.iterator, this.props.collectionId,
this.currentDocDBQueryInfo.index, this.currentDocDBQueryInfo.iterator,
{ this.currentDocDBQueryInfo.index
enableCrossPartitionQuery: );
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
} GraphExplorer.clearConsoleProgress(id);
) this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
.then((results: ViewModels.QueryResults) => { this.setState({ hasMoreRoots: results.hasMoreResults });
GraphExplorer.clearConsoleProgress(id); RU = results.requestCharge.toString();
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1; GraphExplorer.reportToConsole(
this.setState({ hasMoreRoots: results.hasMoreResults }); ConsoleDataType.Info,
RU = results.requestCharge.toString(); `Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}`
GraphExplorer.reportToConsole( );
ConsoleDataType.Info, const pkIds: string[] = (results.documents || []).map((item: DataModels.DocumentId) =>
`Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}` GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty)
); );
const documents = results.documents || [];
return documents.map( const arg = pkIds.join(",");
(item: DataModels.DocumentId) => { await this.executeGremlinQuery(`g.V(${arg})`);
return GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty);
}, return { requestCharge: RU };
(reason: any) => { } catch (error) {
// Failure GraphExplorer.clearConsoleProgress(id);
GraphExplorer.clearConsoleProgress(id); const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`;
const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${reason}`; GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); this.setState({
this.setState({ filterQueryError: errorMsg
filterQueryError: errorMsg });
}); this.setFilterQueryStatus(FilterQueryStatus.ErrorResult);
this.setFilterQueryStatus(FilterQueryStatus.ErrorResult); throw error;
throw reason; }
}
);
})
.then((pkIds: string[]) => {
const arg = pkIds.join(",");
return this.executeGremlinQuery(`g.V(${arg})`);
})
.then(() => ({ requestCharge: RU }));
} }
private executeGremlinQuery(query: string): Q.Promise<UserQueryResult> { private executeGremlinQuery(query: string): Q.Promise<UserQueryResult> {

View File

@ -421,53 +421,47 @@ export default class TableEntityListViewModel extends DataTableViewModel {
* Note that this also means that we can get less entities than the requested download size in a successful call. * Note that this also means that we can get less entities than the requested download size in a successful call.
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx * See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
*/ */
private prefetchData( private async prefetchData(
tableQuery: Entities.ITableQuery, tableQuery: Entities.ITableQuery,
downloadSize: number, downloadSize: number,
currentRetry: number = 0 currentRetry: number = 0
): Q.Promise<any> { ): Promise<IListTableEntitiesSegmentedResult> {
if (!this.cache.serverCallInProgress) { if (!this.cache.serverCallInProgress) {
this.cache.serverCallInProgress = true; this.cache.serverCallInProgress = true;
this.allDownloaded = false; this.allDownloaded = false;
this.lastPrefetchTime = new Date().getTime(); this.lastPrefetchTime = new Date().getTime();
var time = this.lastPrefetchTime; const time = this.lastPrefetchTime;
var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
if (this._documentIterator && this.continuationToken) { if (this._documentIterator && this.continuationToken) {
// TODO handle Cassandra case // TODO handle Cassandra case
const response = await this._documentIterator.fetchNext();
const entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(response?.resources);
promise = Q(this._documentIterator.fetchNext().then(response => response.resources)).then( return {
(documents: any[]) => { Results: entities,
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents); ContinuationToken: this._documentIterator.hasMoreResults()
let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{ };
Results: entities,
ContinuationToken: this._documentIterator.hasMoreResults()
};
return Q.resolve(finalEntities);
}
);
} else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
promise = this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
this.cqlQuery(),
true,
this.continuationToken
);
} else {
let query = this.sqlQuery();
if (this.queryTablesTab.container.isPreferredApiCassandra()) {
query = this.cqlQuery();
}
promise = this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
query,
true
);
} }
return promise
.then((result: IListTableEntitiesSegmentedResult) => { try {
let documents: IListTableEntitiesSegmentedResult;
if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
documents = await this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
this.cqlQuery(),
true,
this.continuationToken
);
} else {
const query = this.queryTablesTab.container.isPreferredApiCassandra() ? this.cqlQuery() : this.sqlQuery();
documents = await this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
query,
true
);
if (!this._documentIterator) { if (!this._documentIterator) {
this._documentIterator = result.iterator; this._documentIterator = documents.iterator;
} }
var actualDownloadSize: number = 0; var actualDownloadSize: number = 0;
@ -478,11 +472,11 @@ export default class TableEntityListViewModel extends DataTableViewModel {
return Q.resolve(null); return Q.resolve(null);
} }
var entities = result.Results; var entities = documents.Results;
actualDownloadSize = entities.length; actualDownloadSize = entities.length;
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method. // Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
this.continuationToken = this.isCancelled ? null : result.ContinuationToken; this.continuationToken = this.isCancelled ? null : documents.ContinuationToken;
if (!this.continuationToken) { if (!this.continuationToken) {
this.allDownloaded = true; this.allDownloaded = true;
@ -514,20 +508,22 @@ export default class TableEntityListViewModel extends DataTableViewModel {
// For #2.1, set prefetch exceeds maximum retry number and end prefetch. // For #2.1, set prefetch exceeds maximum retry number and end prefetch.
// For #2.2, go to next round prefetch. // For #2.2, go to next round prefetch.
if (this.allDownloaded || nextDownloadSize === 0) { if (this.allDownloaded || nextDownloadSize === 0) {
return Q.resolve(result); return documents;
} }
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) { if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
result.ExceedMaximumRetries = true; documents.ExceedMaximumRetries = true;
return Q.resolve(result); return documents;
} }
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
}) return await this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
.catch((error: Error) => { }
this.cache.serverCallInProgress = false; } catch (error) {
return Q.reject(error); this.cache.serverCallInProgress = false;
}); throw error;
}
} }
return null;
return undefined;
} }
} }

View File

@ -4,6 +4,7 @@ import Q from "q";
import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { FeedOptions } from "@azure/cosmos";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as Entities from "./Entities"; import * as Entities from "./Entities";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
@ -12,9 +13,12 @@ import * as TableConstants from "./Constants";
import * as TableEntityProcessor from "./TableEntityProcessor"; import * as TableEntityProcessor from "./TableEntityProcessor";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { queryDocuments, deleteDocument, updateDocument, createDocument } from "../../Common/DocumentClientUtilityBase";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { handleError } from "../../Common/ErrorHandlingUtils"; import { handleError } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
export interface CassandraTableKeys { export interface CassandraTableKeys {
partitionKeys: CassandraTableKey[]; partitionKeys: CassandraTableKey[];
@ -38,19 +42,19 @@ export abstract class TableDataClient {
collection: ViewModels.Collection, collection: ViewModels.Collection,
originalDocument: any, originalDocument: any,
newEntity: Entities.ITableEntity newEntity: Entities.ITableEntity
): Q.Promise<Entities.ITableEntity>; ): Promise<Entities.ITableEntity>;
public abstract queryDocuments( public abstract queryDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
query: string, query: string,
shouldNotify?: boolean, shouldNotify?: boolean,
paginationToken?: string paginationToken?: string
): Q.Promise<Entities.IListTableEntitiesResult>; ): Promise<Entities.IListTableEntitiesResult>;
public abstract deleteDocuments( public abstract deleteDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[] entitiesToDelete: Entities.ITableEntity[]
): Q.Promise<any>; ): Promise<any>;
} }
export class TablesAPIDataClient extends TableDataClient { export class TablesAPIDataClient extends TableDataClient {
@ -74,77 +78,63 @@ export class TablesAPIDataClient extends TableDataClient {
return deferred.promise; return deferred.promise;
} }
public updateDocument( public async updateDocument(
collection: ViewModels.Collection, collection: ViewModels.Collection,
originalDocument: any, originalDocument: any,
entity: Entities.ITableEntity entity: Entities.ITableEntity
): Q.Promise<Entities.ITableEntity> { ): Promise<Entities.ITableEntity> {
const deferred = Q.defer<Entities.ITableEntity>(); try {
const newDocument = await updateDocument(
updateDocument( collection,
collection, originalDocument,
originalDocument, TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity) );
).then( return TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
(newDocument: any) => { } catch (error) {
const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; handleError(error, "TablesAPIDataClient/updateDocument");
deferred.resolve(newEntity); throw error;
}, }
reason => {
deferred.reject(reason);
}
);
return deferred.promise;
} }
public queryDocuments( public async queryDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
query: string query: string
): Q.Promise<Entities.IListTableEntitiesResult> { ): Promise<Entities.IListTableEntitiesResult> {
const deferred = Q.defer<Entities.IListTableEntitiesResult>(); try {
const options = {
enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey()
} as FeedOptions;
const iterator = queryDocuments(collection.databaseId, collection.id(), query, options);
const response = await iterator.fetchNext();
const documents = response?.resources;
const entities = TableEntityProcessor.convertDocumentsToEntities(documents);
let options: any = {}; return {
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); Results: entities,
queryDocuments(collection.databaseId, collection.id(), query, options).then( ContinuationToken: iterator.hasMoreResults(),
iterator => { iterator: iterator
iterator };
.fetchNext() } catch (error) {
.then(response => response.resources) handleError(error, "TablesAPIDataClient/queryDocuments", "Query documents failed");
.then( throw error;
(documents: any[] = []) => { }
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
let finalEntities: Entities.IListTableEntitiesResult = <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<any> { public async deleteDocuments(
let documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments( collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[]
): Promise<any> {
const documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
<Entities.ITableEntityForTablesAPI[]>entitiesToDelete, <Entities.ITableEntityForTablesAPI[]>entitiesToDelete,
collection collection
); );
let promiseArray: Q.Promise<any>[] = [];
documentsToDelete && await Promise.all(
documentsToDelete.forEach(document => { documentsToDelete?.map(async document => {
document.id = ko.observable<string>(document.id); document.id = ko.observable<string>(document.id);
let promise: Q.Promise<any> = deleteDocument(collection, document); await deleteDocument(collection, document);
promiseArray.push(promise); })
}); );
return Q.all(promiseArray);
} }
} }
@ -180,10 +170,7 @@ export class CassandraAPIDataClient extends TableDataClient {
(data: any) => { (data: any) => {
entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)]; entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)];
entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString(); entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString();
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleInfo(`Successfully added new row to table ${collection.id()}`);
ConsoleDataType.Info,
`Successfully added new row to table ${collection.id()}`
);
deferred.resolve(entity); deferred.resolve(entity);
}, },
error => { error => {
@ -197,181 +184,149 @@ export class CassandraAPIDataClient extends TableDataClient {
return deferred.promise; return deferred.promise;
} }
public updateDocument( public async updateDocument(
collection: ViewModels.Collection, collection: ViewModels.Collection,
originalDocument: any, originalDocument: any,
newEntity: Entities.ITableEntity newEntity: Entities.ITableEntity
): Q.Promise<Entities.ITableEntity> { ): Promise<Entities.ITableEntity> {
const notificationId = NotificationConsoleUtils.logConsoleMessage( const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Updating row ${originalDocument.RowKey._}`);
ConsoleDataType.InProgress,
`Updating row ${originalDocument.RowKey._}` try {
); let whereSegment = " WHERE";
const deferred = Q.defer<Entities.ITableEntity>(); let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat(
let promiseArray: Q.Promise<any>[] = []; collection.cassandraKeys.clusteringKeys
let query = `UPDATE ${collection.databaseId}.${collection.id()}`; );
let isChange: boolean = false; for (let keyIndex in keys) {
for (let property in newEntity) { const key = keys[keyIndex].property;
if (!originalDocument[property] || newEntity[property]._.toString() !== originalDocument[property]._.toString()) { const keyType = keys[keyIndex].type;
if (this.isStringType(newEntity[property].$)) { whereSegment += this.isStringType(keyType)
query = `${query} SET ${property} = '${newEntity[property]._}',`; ? ` ${key} = '${newEntity[key]._}' AND`
} else { : ` ${key} = ${newEntity[key]._} AND`;
query = `${query} SET ${property} = ${newEntity[property]._},`; }
whereSegment = whereSegment.slice(0, whereSegment.length - 4);
let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`;
let isPropertyUpdated = false;
for (let property in newEntity) {
if (
!originalDocument[property] ||
newEntity[property]._.toString() !== originalDocument[property]._.toString()
) {
updateQuery += this.isStringType(newEntity[property].$)
? ` SET ${property} = '${newEntity[property]._}',`
: ` SET ${property} = ${newEntity[property]._},`;
isPropertyUpdated = true;
} }
isChange = true;
} }
}
query = query.slice(0, query.length - 1); if (isPropertyUpdated) {
let whereSegment = " WHERE"; updateQuery = updateQuery.slice(0, updateQuery.length - 1);
let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat( updateQuery += whereSegment;
collection.cassandraKeys.clusteringKeys await this.queryDocuments(collection, updateQuery);
);
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); let deleteQuery = `DELETE `;
query = query + whereSegment; let isPropertyDeleted = false;
if (isChange) { for (let property in originalDocument) {
promiseArray.push(this.queryDocuments(collection, query)); if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
} deleteQuery += ` ${property},`;
query = `DELETE `; isPropertyDeleted = true;
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);
},
error => {
handleError(error, "UpdateRowCassandra", `Failed to update row ${newEntity.RowKey._}`);
deferred.reject(error);
} }
) }
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); if (isPropertyDeleted) {
}); deleteQuery = deleteQuery.slice(0, deleteQuery.length - 1);
return deferred.promise; deleteQuery += ` FROM ${collection.databaseId}.${collection.id()}${whereSegment}`;
await this.queryDocuments(collection, deleteQuery);
}
newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey];
NotificationConsoleUtils.logConsoleInfo(`Successfully updated row ${newEntity.RowKey._}`);
return newEntity;
} catch (error) {
handleError(error, "UpdateRowCassandra", "Failed to update row ${newEntity.RowKey._}");
throw error;
} finally {
clearMessage();
}
} }
public queryDocuments( public async queryDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
query: string, query: string,
shouldNotify?: boolean, shouldNotify?: boolean,
paginationToken?: string paginationToken?: string
): Q.Promise<Entities.IListTableEntitiesResult> { ): Promise<Entities.IListTableEntitiesResult> {
let notificationId: string; const clearMessage =
if (shouldNotify) { shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
notificationId = NotificationConsoleUtils.logConsoleMessage( try {
ConsoleDataType.InProgress, const authType = window.authType;
`Querying rows for table ${collection.id()}` const apiEndpoint: string =
); authType === AuthType.EncryptedToken
} ? Constants.CassandraBackend.guestQueryApi
const deferred = Q.defer<Entities.IListTableEntitiesResult>(); : Constants.CassandraBackend.queryApi;
const authType = window.authType; const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
const apiEndpoint: string = type: "POST",
authType === AuthType.EncryptedToken data: {
? Constants.CassandraBackend.guestQueryApi accountName:
: Constants.CassandraBackend.queryApi; collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, { cassandraEndpoint: this.trimCassandraEndpoint(
type: "POST", collection.container.databaseAccount().properties.cassandraEndpoint
data: { ),
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, resourceId: collection.container.databaseAccount().id,
cassandraEndpoint: this.trimCassandraEndpoint( keyspaceId: collection.databaseId,
collection.container.databaseAccount().properties.cassandraEndpoint tableId: collection.id(),
), query,
resourceId: collection.container.databaseAccount().id, paginationToken
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
});
}, },
(error: any) => { beforeSend: this.setAuthorizationHeader,
if (shouldNotify) { error: this.handleAjaxError,
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); cache: false
}
deferred.reject(error);
}
)
.done(() => {
if (shouldNotify) {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
}
}); });
return deferred.promise; shouldNotify &&
NotificationConsoleUtils.logConsoleInfo(
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`
);
return {
Results: data.result,
ContinuationToken: data.paginationToken
};
} catch (error) {
shouldNotify &&
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`);
throw error;
} finally {
clearMessage?.();
}
} }
public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> { public async deleteDocuments(
collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[]
): Promise<any> {
const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `; const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `;
let promiseArray: Q.Promise<any>[] = []; const partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection);
let partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection);
for (let i = 0, len = entitiesToDelete.length; i < len; i++) { await Promise.all(
let currEntityToDelete: Entities.ITableEntity = entitiesToDelete[i]; entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => {
let currQuery = query; const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`);
let partitionKeyValue = currEntityToDelete[partitionKeyProperty]; const partitionKeyValue = currEntityToDelete[partitionKeyProperty];
if (partitionKeyValue._ != null && this.isStringType(partitionKeyValue.$)) { const currQuery =
currQuery = `${currQuery}${partitionKeyProperty} = '${partitionKeyValue._}' AND `; query + this.isStringType(partitionKeyValue.$)
} else { ? `${partitionKeyProperty} = '${partitionKeyValue._}'`
currQuery = `${currQuery}${partitionKeyProperty} = ${partitionKeyValue._} AND `; : `${partitionKeyProperty} = ${partitionKeyValue._}`;
}
currQuery = currQuery.slice(0, currQuery.length - 5); try {
const notificationId = NotificationConsoleUtils.logConsoleMessage( await this.queryDocuments(collection, currQuery);
ConsoleDataType.InProgress, NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`);
`Deleting row ${currEntityToDelete.RowKey._}` } catch (error) {
); handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`);
promiseArray.push( throw error;
this.queryDocuments(collection, currQuery) } finally {
.then( clearMessage();
() => { }
NotificationConsoleUtils.logConsoleMessage( })
ConsoleDataType.Info, );
`Successfully deleted row ${currEntityToDelete.RowKey._}`
);
},
error => {
handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
})
);
}
return Q.all(promiseArray);
} }
public createKeyspace( public createKeyspace(

View File

@ -16,18 +16,16 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
import { QueryIterator, ItemDefinition, Resource, ConflictDefinition } from "@azure/cosmos"; import { QueryIterator, Resource, ConflictDefinition, FeedOptions } from "@azure/cosmos";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import {
queryConflicts,
deleteConflict,
deleteDocument,
createDocument,
updateDocument
} from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
export default class ConflictsTab extends TabsBase { export default class ConflictsTab extends TabsBase {
public selectedConflictId: ko.Observable<ConflictId>; public selectedConflictId: ko.Observable<ConflictId>;
@ -225,25 +223,15 @@ export default class ConflictsTab extends TabsBase {
}); });
} }
public refreshDocumentsGrid(): Q.Promise<any> { public async refreshDocumentsGrid(): Promise<void> {
// clear documents grid try {
this.conflictIds([]); // clear documents grid
return this.createIterator() this.conflictIds([]);
.then( this._documentsIterator = this.createIterator();
// reset iterator await this.loadNextPage();
iterator => { } catch (error) {
this._documentsIterator = iterator; window.alert(getErrorMessage(error));
} }
)
.then(
// load documents
() => {
return this.loadNextPage();
}
)
.catch(error => {
window.alert(getErrorMessage(error));
});
} }
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
@ -265,9 +253,9 @@ export default class ConflictsTab extends TabsBase {
return Q(); return Q();
} }
public onAcceptChangesClick = (): Q.Promise<any> => { public onAcceptChangesClick = async (): Promise<void> => {
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
return Q(); return;
} }
this.isExecutionError(false); this.isExecutionError(false);
@ -285,81 +273,79 @@ export default class ConflictsTab extends TabsBase {
conflictResourceId: selectedConflict.resourceId conflictResourceId: selectedConflict.resourceId
}); });
let operationPromise: Q.Promise<any> = Q(); try {
if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) { if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) {
const documentContent = JSON.parse(this.selectedConflictContent()); const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = updateDocument( await updateDocument(
this.collection, this.collection,
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]), selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]),
documentContent documentContent
); );
} }
if (selectedConflict.operationType === Constants.ConflictOperationType.Create) { if (selectedConflict.operationType === Constants.ConflictOperationType.Create) {
const documentContent = JSON.parse(this.selectedConflictContent()); const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = createDocument(this.collection, documentContent); await createDocument(this.collection, documentContent);
} }
if (selectedConflict.operationType === Constants.ConflictOperationType.Delete && !!this.selectedConflictContent()) { if (
const documentContent = JSON.parse(this.selectedConflictContent()); selectedConflict.operationType === Constants.ConflictOperationType.Delete &&
!!this.selectedConflictContent()
) {
const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = deleteDocument( await deleteDocument(
this.collection, this.collection,
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]) selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty])
); );
} }
return operationPromise await deleteConflict(this.collection, selectedConflict);
.then( this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid);
() => { this.selectedConflictContent("");
return deleteConflict(this.collection, selectedConflict).then(() => { this.selectedConflictCurrent("");
this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); this.selectedConflictId(null);
this.selectedConflictContent(""); this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
this.selectedConflictCurrent(""); TelemetryProcessor.traceSuccess(
this.selectedConflictId(null); Action.ResolveConflict,
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); {
TelemetryProcessor.traceSuccess( databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
Action.ResolveConflict, defaultExperience: this.collection && this.collection.container.defaultExperience(),
{ dataExplorerArea: Constants.Areas.Tab,
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, tabTitle: this.tabTitle(),
defaultExperience: this.collection && this.collection.container.defaultExperience(), conflictResourceType: selectedConflict.resourceType,
dataExplorerArea: Constants.Areas.Tab, conflictOperationType: selectedConflict.operationType,
tabTitle: this.tabTitle(), conflictResourceId: selectedConflict.resourceId
conflictResourceType: selectedConflict.resourceType,
conflictOperationType: selectedConflict.operationType,
conflictResourceId: selectedConflict.resourceId
},
startKey
);
});
}, },
error => { startKey
this.isExecutionError(true); );
const errorMessage = getErrorMessage(error); } catch (error) {
window.alert(errorMessage); this.isExecutionError(true);
TelemetryProcessor.traceFailure( const errorMessage = getErrorMessage(error);
Action.ResolveConflict, window.alert(errorMessage);
{ TelemetryProcessor.traceFailure(
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, Action.ResolveConflict,
defaultExperience: this.collection && this.collection.container.defaultExperience(), {
dataExplorerArea: Constants.Areas.Tab, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
tabTitle: this.tabTitle(), defaultExperience: this.collection && this.collection.container.defaultExperience(),
conflictResourceType: selectedConflict.resourceType, dataExplorerArea: Constants.Areas.Tab,
conflictOperationType: selectedConflict.operationType, tabTitle: this.tabTitle(),
conflictResourceId: selectedConflict.resourceId, conflictResourceType: selectedConflict.resourceType,
error: errorMessage, conflictOperationType: selectedConflict.operationType,
errorStack: getErrorStack(error) conflictResourceId: selectedConflict.resourceId,
}, error: errorMessage,
startKey errorStack: getErrorStack(error)
); },
} startKey
) );
.finally(() => this.isExecuting(false)); } finally {
this.isExecuting(false);
}
}; };
public onDeleteClick = (): Q.Promise<any> => { public onDeleteClick = async (): Promise<void> => {
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
@ -375,50 +361,48 @@ export default class ConflictsTab extends TabsBase {
conflictResourceId: selectedConflict.resourceId conflictResourceId: selectedConflict.resourceId
}); });
return deleteConflict(this.collection, selectedConflict) try {
.then( await deleteConflict(this.collection, selectedConflict);
() => { this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid);
this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); this.selectedConflictContent("");
this.selectedConflictContent(""); this.selectedConflictCurrent("");
this.selectedConflictCurrent(""); this.selectedConflictId(null);
this.selectedConflictId(null); this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); TelemetryProcessor.traceSuccess(
TelemetryProcessor.traceSuccess( Action.DeleteConflict,
Action.DeleteConflict, {
{ databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(),
defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab,
dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(),
tabTitle: this.tabTitle(), conflictResourceType: selectedConflict.resourceType,
conflictResourceType: selectedConflict.resourceType, conflictOperationType: selectedConflict.operationType,
conflictOperationType: selectedConflict.operationType, conflictResourceId: selectedConflict.resourceId
conflictResourceId: selectedConflict.resourceId
},
startKey
);
}, },
error => { startKey
this.isExecutionError(true); );
const errorMessage = getErrorMessage(error); } catch (error) {
window.alert(errorMessage); this.isExecutionError(true);
TelemetryProcessor.traceFailure( const errorMessage = getErrorMessage(error);
Action.DeleteConflict, window.alert(errorMessage);
{ TelemetryProcessor.traceFailure(
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, Action.DeleteConflict,
defaultExperience: this.collection && this.collection.container.defaultExperience(), {
dataExplorerArea: Constants.Areas.Tab, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
tabTitle: this.tabTitle(), defaultExperience: this.collection && this.collection.container.defaultExperience(),
conflictResourceType: selectedConflict.resourceType, dataExplorerArea: Constants.Areas.Tab,
conflictOperationType: selectedConflict.operationType, tabTitle: this.tabTitle(),
conflictResourceId: selectedConflict.resourceId, conflictResourceType: selectedConflict.resourceType,
error: errorMessage, conflictOperationType: selectedConflict.operationType,
errorStack: getErrorStack(error) conflictResourceId: selectedConflict.resourceId,
}, error: errorMessage,
startKey errorStack: getErrorStack(error)
); },
} startKey
) );
.finally(() => this.isExecuting(false)); } finally {
this.isExecuting(false);
}
}; };
public onDiscardClick = (): Q.Promise<any> => { public onDiscardClick = (): Q.Promise<any> => {
@ -445,60 +429,47 @@ export default class ConflictsTab extends TabsBase {
return Q(); return Q();
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts); this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts);
});
} }
public onActivate(): Q.Promise<any> { public async onActivate(): Promise<void> {
return super.onActivate().then(() => { super.onActivate();
if (this._documentsIterator) {
return Q.resolve(this._documentsIterator);
}
return this.createIterator().then( if (!this._documentsIterator) {
(iterator: QueryIterator<ItemDefinition & Resource>) => { try {
this._documentsIterator = iterator; this._documentsIterator = await this.createIterator();
return this.loadNextPage(); await this.loadNextPage();
}, } catch (error) {
error => { if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { TelemetryProcessor.traceFailure(
TelemetryProcessor.traceFailure( Action.Tab,
Action.Tab, {
{ databaseAccountName: this.collection.container.databaseAccount().name,
databaseAccountName: this.collection.container.databaseAccount().name, databaseName: this.collection.databaseId,
databaseName: this.collection.databaseId, collectionName: this.collection.id(),
collectionName: this.collection.id(), defaultExperience: this.collection.container.defaultExperience(),
defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab,
dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(),
tabTitle: this.tabTitle(), error: getErrorMessage(error),
error: getErrorMessage(error), errorStack: getErrorStack(error)
errorStack: getErrorStack(error) },
}, this.onLoadStartKey
this.onLoadStartKey );
); this.onLoadStartKey = null;
this.onLoadStartKey = null;
}
} }
); }
}); }
} }
public onRefreshClick(): Q.Promise<any> { public createIterator(): QueryIterator<ConflictDefinition & Resource> {
return this.refreshDocumentsGrid().then(() => {
this.selectedConflictContent("");
this.selectedConflictId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
});
}
public createIterator(): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
// TODO: Conflict Feed does not allow filtering atm // TODO: Conflict Feed does not allow filtering atm
const query: string = undefined; const query: string = undefined;
let options: any = {}; const options = {
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey()
return queryConflicts(this.collection.databaseId, this.collection.id(), query, options); };
return queryConflicts(this.collection.databaseId, this.collection.id(), query, options as FeedOptions);
} }
public loadNextPage(): Q.Promise<any> { public loadNextPage(): Q.Promise<any> {

View File

@ -429,11 +429,10 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return Q(); return Q();
}; };
public onActivate(): Q.Promise<any> { public async onActivate(): Promise<void> {
return super.onActivate().then(async () => { super.onActivate();
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
await this.database.loadOffer(); await this.database.loadOffer();
});
} }
private _setBaseline() { private _setBaseline() {

View File

@ -103,7 +103,7 @@
<button <button
class="filterbtnstyle queryButton" class="filterbtnstyle queryButton"
data-bind=" data-bind="
click: onApplyFilterClick, click: refreshDocumentsGrid,
enable: applyFilterButton.enabled" enable: applyFilterButton.enabled"
aria-label="Apply filter" aria-label="Apply filter"
tabindex="0" tabindex="0"

View File

@ -19,19 +19,24 @@ import SaveIcon from "../../../images/save-cosmos.svg";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import UploadIcon from "../../../images/Upload_16x16.svg"; import UploadIcon from "../../../images/Upload_16x16.svg";
import { extractPartitionKey, PartitionKeyDefinition, QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import {
extractPartitionKey,
PartitionKeyDefinition,
QueryIterator,
ItemDefinition,
Resource,
Item
} from "@azure/cosmos";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import {
readDocument,
queryDocuments,
deleteDocument,
updateDocument,
createDocument
} from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { readDocument } from "../../Common/dataAccess/readDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import { createDocument } from "../../Common/dataAccess/createDocument";
export default class DocumentsTab extends TabsBase { export default class DocumentsTab extends TabsBase {
public selectedDocumentId: ko.Observable<DocumentId>; public selectedDocumentId: ko.Observable<DocumentId>;
@ -369,36 +374,22 @@ export default class DocumentsTab extends TabsBase {
return true; return true;
}; };
public onApplyFilterClick(): Q.Promise<any> { public async refreshDocumentsGrid(): Promise<void> {
// clear documents grid // clear documents grid
this.documentIds([]); this.documentIds([]);
return this.createIterator()
.then(
// reset iterator
iterator => {
this._documentsIterator = iterator;
}
)
.then(
// load documents
() => {
return this.loadNextPage();
}
)
.then(() => {
// collapse filter
this.appliedFilter(this.filterContent());
this.isFilterExpanded(false);
const focusElement = document.getElementById("errorStatusIcon");
focusElement && focusElement.focus();
})
.catch(error => {
window.alert(getErrorMessage(error));
});
}
public refreshDocumentsGrid(): Q.Promise<any> { try {
return this.onApplyFilterClick(); // reset iterator
this._documentsIterator = this.createIterator();
// load documents
await this.loadNextPage();
// collapse filter
this.appliedFilter(this.filterContent());
this.isFilterExpanded(false);
document.getElementById("errorStatusIcon")?.focus();
} catch (error) {
window.alert(getErrorMessage(error));
}
} }
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
@ -434,7 +425,7 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
}; };
public onSaveNewDocumentClick = (): Q.Promise<any> => { public onSaveNewDocumentClick = (): Promise<any> => {
this.isExecutionError(false); this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
@ -502,7 +493,7 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
}; };
public onSaveExisitingDocumentClick = (): Q.Promise<any> => { public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId(); const selectedDocumentId = this.selectedDocumentId();
const documentContent = JSON.parse(this.selectedDocumentContent()); const documentContent = JSON.parse(this.selectedDocumentContent());
@ -571,17 +562,15 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
}; };
public onDeleteExisitingDocumentClick = (): Q.Promise<any> => { public onDeleteExisitingDocumentClick = async (): Promise<void> => {
const selectedDocumentId = this.selectedDocumentId(); const selectedDocumentId = this.selectedDocumentId();
const msg = !this.isPreferredApiMongoDB const msg = !this.isPreferredApiMongoDB
? "Are you sure you want to delete the selected item ?" ? "Are you sure you want to delete the selected item ?"
: "Are you sure you want to delete the selected document ?"; : "Are you sure you want to delete the selected document ?";
if (window.confirm(msg)) { if (window.confirm(msg)) {
return this._deleteDocument(selectedDocumentId); await this._deleteDocument(selectedDocumentId);
} }
return Q();
}; };
public onValidDocumentEdit(): Q.Promise<any> { public onValidDocumentEdit(): Q.Promise<any> {
@ -617,63 +606,50 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
});
} }
public onActivate(): Q.Promise<any> { public async onActivate(): Promise<void> {
return super.onActivate().then(() => { super.onActivate();
if (this._documentsIterator) {
return Q.resolve(this._documentsIterator);
}
return this.createIterator().then( if (!this._documentsIterator) {
(iterator: QueryIterator<ItemDefinition & Resource>) => { try {
this._documentsIterator = iterator; this._documentsIterator = this.createIterator();
return this.loadNextPage(); await this.loadNextPage();
}, } catch (error) {
error => { if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { TelemetryProcessor.traceFailure(
TelemetryProcessor.traceFailure( Action.Tab,
Action.Tab, {
{ databaseAccountName: this.collection.container.databaseAccount().name,
databaseAccountName: this.collection.container.databaseAccount().name, databaseName: this.collection.databaseId,
databaseName: this.collection.databaseId, collectionName: this.collection.id(),
collectionName: this.collection.id(), defaultExperience: this.collection.container.defaultExperience(),
defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab,
dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(),
tabTitle: this.tabTitle(), error: getErrorMessage(error),
error: getErrorMessage(error), errorStack: getErrorStack(error)
errorStack: getErrorStack(error) },
}, this.onLoadStartKey
this.onLoadStartKey );
); this.onLoadStartKey = null;
this.onLoadStartKey = null;
}
} }
); }
}); }
} }
public onRefreshClick(): Q.Promise<any> {
return this.refreshDocumentsGrid().then(() => {
this.selectedDocumentContent("");
this.selectedDocumentId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
});
}
private _isIgnoreDirtyEditor = (): boolean => { private _isIgnoreDirtyEditor = (): boolean => {
var msg: string = "Changes will be lost. Do you want to continue?"; var msg: string = "Changes will be lost. Do you want to continue?";
return window.confirm(msg); return window.confirm(msg);
}; };
protected __deleteDocument(documentId: DocumentId): Q.Promise<any> { protected __deleteDocument(documentId: DocumentId): Promise<void> {
return deleteDocument(this.collection, documentId); return deleteDocument(this.collection, documentId);
} }
private _deleteDocument(selectedDocumentId: DocumentId): Q.Promise<any> { private _deleteDocument(selectedDocumentId: DocumentId): Promise<void> {
this.isExecutionError(false); this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
@ -684,7 +660,7 @@ export default class DocumentsTab extends TabsBase {
this.isExecuting(true); this.isExecuting(true);
return this.__deleteDocument(selectedDocumentId) return this.__deleteDocument(selectedDocumentId)
.then( .then(
(result: any) => { () => {
this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid);
this.selectedDocumentContent(""); this.selectedDocumentContent("");
this.selectedDocumentId(null); this.selectedDocumentId(null);
@ -720,7 +696,7 @@ export default class DocumentsTab extends TabsBase {
.finally(() => this.isExecuting(false)); .finally(() => this.isExecuting(false));
} }
public createIterator(): Q.Promise<QueryIterator<ItemDefinition & Resource>> { public createIterator(): QueryIterator<ItemDefinition & Resource> {
let filters = this.lastFilterContents(); let filters = this.lastFilterContents();
const filter: string = this.filterContent().trim(); const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter); const query: string = this.buildQuery(filter);
@ -734,11 +710,10 @@ export default class DocumentsTab extends TabsBase {
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options); return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
} }
public selectDocument(documentId: DocumentId): Q.Promise<any> { public async selectDocument(documentId: DocumentId): Promise<void> {
this.selectedDocumentId(documentId); this.selectedDocumentId(documentId);
return readDocument(this.collection, documentId).then((content: any) => { const content = await readDocument(this.collection, documentId);
this.initDocumentEditor(documentId, content); this.initDocumentEditor(documentId, content);
});
} }
public loadNextPage(): Q.Promise<any> { public loadNextPage(): Q.Promise<any> {

View File

@ -114,10 +114,9 @@ export default class GraphTab extends TabsBase {
: `${account.name}.graphs.azure.com:443/`; : `${account.name}.graphs.azure.com:443/`;
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph);
});
} }
/** /**

View File

@ -289,7 +289,7 @@
<button <button
class="filterbtnstyle queryButton" class="filterbtnstyle queryButton"
data-bind=" data-bind="
click: onApplyFilterClick, click: refreshDocumentsGrid,
enable: applyFilterButton.enabled" enable: applyFilterButton.enabled"
> >
Apply Filter Apply Filter

View File

@ -44,7 +44,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
super.buildCommandBarOptions(); super.buildCommandBarOptions();
} }
public onSaveNewDocumentClick = (): Q.Promise<any> => { public onSaveNewDocumentClick = (): Promise<any> => {
const documentContent = JSON.parse(this.selectedDocumentContent()); const documentContent = JSON.parse(this.selectedDocumentContent());
this.displayedError(""); this.displayedError("");
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
@ -78,12 +78,12 @@ export default class MongoDocumentsTab extends DocumentsTab {
startKey startKey
); );
Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab"); Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
return Q.reject("Document without shard key"); throw new Error("Document without shard key");
} }
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
return Q(createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)) return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)
.then( .then(
(savedDocument: any) => { (savedDocument: any) => {
let partitionKeyArray = extractPartitionKey( let partitionKeyArray = extractPartitionKey(
@ -136,7 +136,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
.finally(() => this.isExecuting(false)); .finally(() => this.isExecuting(false));
}; };
public onSaveExisitingDocumentClick = (): Q.Promise<any> => { public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId(); const selectedDocumentId = this.selectedDocumentId();
const documentContent = this.selectedDocumentContent(); const documentContent = this.selectedDocumentContent();
this.isExecutionError(false); this.isExecutionError(false);
@ -148,7 +148,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
tabTitle: this.tabTitle() tabTitle: this.tabTitle()
}); });
return Q(updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent)) return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent)
.then( .then(
(updatedDocument: any) => { (updatedDocument: any) => {
let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4); let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
@ -204,13 +204,10 @@ export default class MongoDocumentsTab extends DocumentsTab {
return filter || "{}"; return filter || "{}";
} }
public selectDocument(documentId: DocumentId): Q.Promise<any> { public async selectDocument(documentId: DocumentId): Promise<void> {
this.selectedDocumentId(documentId); this.selectedDocumentId(documentId);
return Q( const content = await readDocument(this.collection.databaseId, this.collection, documentId);
readDocument(this.collection.databaseId, this.collection, documentId).then((content: any) => { this.initDocumentEditor(documentId, content);
this.initDocumentEditor(documentId, content);
})
);
} }
public loadNextPage(): Q.Promise<any> { public loadNextPage(): Q.Promise<any> {
@ -330,7 +327,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
return partitionKey; return partitionKey;
} }
protected __deleteDocument(documentId: DocumentId): Q.Promise<any> { protected __deleteDocument(documentId: DocumentId): Promise<void> {
return Q(deleteDocument(this.collection.databaseId, this.collection, documentId)); return deleteDocument(this.collection.databaseId, this.collection, documentId);
} }
} }

View File

@ -53,10 +53,9 @@ export default class MongoShellTab extends TabsBase {
// } // }
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
});
} }
public handleMessage(event: MessageEvent) { public handleMessage(event: MessageEvent) {

View File

@ -15,9 +15,10 @@ import { QueryUtils } from "../../Utils/QueryUtils";
import SaveQueryIcon from "../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { queryDocuments, queryDocumentsPage } from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
enum ToggleState { enum ToggleState {
Result, Result,
@ -163,20 +164,19 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this._buildCommandBarOptions(); this._buildCommandBarOptions();
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query); this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
});
} }
public onExecuteQueryClick = (): Q.Promise<any> => { public onExecuteQueryClick = async (): Promise<void> => {
const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent(); const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent();
this.sqlStatementToExecute(sqlStatement); this.sqlStatementToExecute(sqlStatement);
this.allResultsMetadata([]); this.allResultsMetadata([]);
this.queryResults(""); this.queryResults("");
this._iterator = null; this._iterator = undefined;
return this._executeQueryDocumentsPage(0); await this._executeQueryDocumentsPage(0);
}; };
public onLoadQueryClick = (): void => { public onLoadQueryClick = (): void => {
@ -191,13 +191,13 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this.collection && this.collection.container && this.collection.container.browseQueriesPane.open(); this.collection && this.collection.container && this.collection.container.browseQueriesPane.open();
}; };
public onFetchNextPageClick(): Q.Promise<any> { public async onFetchNextPageClick(): Promise<void> {
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || []; const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1; const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
const itemCount: number = (metadata && Number(metadata.itemCount)) || 0; const itemCount: number = (metadata && Number(metadata.itemCount)) || 0;
return this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1); await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
} }
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
@ -265,19 +265,18 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
return true; return true;
}; };
private _executeQueryDocumentsPage(firstItemIndex: number): Q.Promise<any> { private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
this.error(""); this.error("");
this.roundTrips(undefined); this.roundTrips(undefined);
if (this._iterator == null) { if (this._iterator === undefined) {
const queryIteratorPromise = this._initIterator(); this._initIterator();
return queryIteratorPromise.finally(() => this._queryDocumentsPage(firstItemIndex));
} }
return this._queryDocumentsPage(firstItemIndex); await this._queryDocumentsPage(firstItemIndex);
} }
// TODO: Position and enable spinner when request is in progress // TODO: Position and enable spinner when request is in progress
private _queryDocumentsPage(firstItemIndex: number): Q.Promise<any> { private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
this.isExecutionError(false); this.isExecutionError(false);
this._resetAggregateQueryMetrics(); this._resetAggregateQueryMetrics();
const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, { const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, {
@ -289,90 +288,90 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
let options: any = {}; let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
const queryDocuments = (firstItemIndex: number) => const queryDocuments = async (firstItemIndex: number) =>
queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex, options); await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex);
this.isExecuting(true); this.isExecuting(true);
return QueryUtils.queryPagesUntilContentPresent(firstItemIndex, queryDocuments)
.then(
(queryResults: ViewModels.QueryResults) => {
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const resultsMetadata: ViewModels.QueryResultsMetadata = {
hasMoreResults: queryResults.hasMoreResults,
itemCount: queryResults.itemCount,
firstItemIndex: queryResults.firstItemIndex,
lastItemIndex: queryResults.lastItemIndex
};
this.allResultsMetadata.push(resultsMetadata);
this.activityId(queryResults.activityId);
this.roundTrips(queryResults.roundTrips);
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]); try {
const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent(
firstItemIndex,
queryDocuments
);
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const resultsMetadata: ViewModels.QueryResultsMetadata = {
hasMoreResults: queryResults.hasMoreResults,
itemCount: queryResults.itemCount,
firstItemIndex: queryResults.firstItemIndex,
lastItemIndex: queryResults.lastItemIndex
};
this.allResultsMetadata.push(resultsMetadata);
this.activityId(queryResults.activityId);
this.roundTrips(queryResults.roundTrips);
if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) { this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
// we let users query for the next page because the SDK sometimes specifies there are more elements
// even though there aren't any so we should not update the prior query results.
return;
}
const documents: any[] = queryResults.documents; if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) {
const results = this.renderObjectForEditor(documents, null, 4); // we let users query for the next page because the SDK sometimes specifies there are more elements
// even though there aren't any so we should not update the prior query results.
return;
}
const resultsDisplay: string = const documents: any[] = queryResults.documents;
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; const results = this.renderObjectForEditor(documents, null, 4);
this.showingDocumentsDisplayText(resultsDisplay);
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
if (!this.queryResults() && !results) { const resultsDisplay: string =
const errorMessage: string = JSON.stringify({ queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
error: `Returned no results after query execution`, this.showingDocumentsDisplayText(resultsDisplay);
accountName: this.collection && this.collection.container.databaseAccount(), this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
sqlQuery: this.sqlStatementToExecute(),
hasMoreResults: resultsMetadata.hasMoreResults,
itemCount: resultsMetadata.itemCount,
responseHeaders: queryResults && queryResults.headers
});
Logger.logError(errorMessage, "QueryTab");
}
this.queryResults(results); if (!this.queryResults() && !results) {
const errorMessage: string = JSON.stringify({
error: `Returned no results after query execution`,
accountName: this.collection && this.collection.container.databaseAccount(),
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
sqlQuery: this.sqlStatementToExecute(),
hasMoreResults: resultsMetadata.hasMoreResults,
itemCount: resultsMetadata.itemCount,
responseHeaders: queryResults && queryResults.headers
});
Logger.logError(errorMessage, "QueryTab");
}
TelemetryProcessor.traceSuccess( this.queryResults(results);
Action.ExecuteQuery,
{ TelemetryProcessor.traceSuccess(
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, Action.ExecuteQuery,
defaultExperience: this.collection && this.collection.container.defaultExperience(), {
dataExplorerArea: Constants.Areas.Tab, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
tabTitle: this.tabTitle() defaultExperience: this.collection && this.collection.container.defaultExperience(),
}, dataExplorerArea: Constants.Areas.Tab,
startKey tabTitle: this.tabTitle()
);
}, },
(error: any) => { startKey
this.isExecutionError(true); );
const errorMessage = getErrorMessage(error); } catch (error) {
this.error(errorMessage); this.isExecutionError(true);
TelemetryProcessor.traceFailure( const errorMessage = getErrorMessage(error);
Action.ExecuteQuery, this.error(errorMessage);
{ TelemetryProcessor.traceFailure(
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, Action.ExecuteQuery,
defaultExperience: this.collection && this.collection.container.defaultExperience(), {
dataExplorerArea: Constants.Areas.Tab, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
tabTitle: this.tabTitle(), defaultExperience: this.collection && this.collection.container.defaultExperience(),
error: errorMessage, dataExplorerArea: Constants.Areas.Tab,
errorStack: getErrorStack(error) tabTitle: this.tabTitle(),
}, error: errorMessage,
startKey errorStack: getErrorStack(error)
); },
document.getElementById("error-display").focus(); startKey
} );
) document.getElementById("error-display").focus();
.finally(() => { } finally {
this.isExecuting(false); this.isExecuting(false);
this.togglesOnFocus(); this.togglesOnFocus();
}); }
} }
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void { private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
@ -477,16 +476,17 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
} }
} }
protected _initIterator(): Q.Promise<MinimalQueryIterator> { protected _initIterator(): void {
const options: any = QueryTab.getIteratorOptions(this.collection); const options: any = QueryTab.getIteratorOptions(this.collection);
if (this._resourceTokenPartitionKey) { if (this._resourceTokenPartitionKey) {
options.partitionKey = this._resourceTokenPartitionKey; options.partitionKey = this._resourceTokenPartitionKey;
} }
return Q( this._iterator = queryDocuments(
queryDocuments(this.collection.databaseId, this.collection.id(), this.sqlStatementToExecute(), options).then( this.collection.databaseId,
iterator => (this._iterator = iterator) this.collection.id(),
) this.sqlStatementToExecute(),
options
); );
} }

View File

@ -161,17 +161,16 @@ export default class QueryTablesTab extends TabsBase {
return null; return null;
}; };
public onActivate(): Q.Promise<any> { public onActivate(): void {
return super.onActivate().then(() => { super.onActivate();
const columns = const columns =
!!this.tableEntityListViewModel() && !!this.tableEntityListViewModel() &&
!!this.tableEntityListViewModel().table && !!this.tableEntityListViewModel().table &&
this.tableEntityListViewModel().table.columns; this.tableEntityListViewModel().table.columns;
if (!!columns) { if (!!columns) {
columns.adjust(); columns.adjust();
$(window).resize(); $(window).resize();
} }
});
} }
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {

View File

@ -186,12 +186,11 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
this._setBaselines(); this._setBaselines();
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
return super.onTabClick().then(() => { super.onTabClick();
if (this.isNew()) { if (this.isNew()) {
this.collection.selectedSubnodeKind(this.tabKind); this.collection.selectedSubnodeKind(this.tabKind);
} }
});
} }
public abstract onSaveClick: () => Promise<any>; public abstract onSaveClick: () => Promise<any>;

View File

@ -42,54 +42,49 @@ export default class SettingsTabV2 extends TabsBase {
}); });
} }
public onActivate(): Q.Promise<unknown> { public async onActivate(): Promise<void> {
this.isExecuting(true); try {
this.currentCollection.loadOffer().then( this.isExecuting(true);
() => { await this.currentCollection.loadOffer();
// passed in options and set by parent as "Settings" by default // passed in options and set by parent as "Settings" by default
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings"); this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
this.offerRead(true);
this.options.getPendingNotification.then(
(data: DataModels.Notification) => {
this.notification = data;
this.notificationRead(true);
this.isExecuting(false);
},
error => {
const errorMessage = getErrorMessage(error);
this.notification = undefined;
this.notificationRead(true);
this.isExecuting(false);
traceFailure(
Action.Tab,
{
databaseAccountName: this.options.collection.container.databaseAccount().name,
databaseName: this.options.collection.databaseId,
collectionName: this.options.collection.id(),
defaultExperience: this.options.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle,
error: errorMessage,
errorStack: getErrorStack(error)
},
this.options.onLoadStartKey
);
logConsoleError(
`Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}`
);
throw error;
}
);
},
() => {
this.offerRead(true);
this.isExecuting(false);
}
);
return super.onActivate().then(() => { this.options.getPendingNotification.then(
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2); (data: DataModels.Notification) => {
}); this.notification = data;
this.notificationRead(true);
},
error => {
const errorMessage = getErrorMessage(error);
this.notification = undefined;
this.notificationRead(true);
traceFailure(
Action.Tab,
{
databaseAccountName: this.options.collection.container.databaseAccount().name,
databaseName: this.options.collection.databaseId,
collectionName: this.options.collection.id(),
defaultExperience: this.options.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle,
error: errorMessage,
errorStack: getErrorStack(error)
},
this.options.onLoadStartKey
);
logConsoleError(
`Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}`
);
throw error;
}
);
} finally {
this.offerRead(true);
this.isExecuting(false);
}
super.onActivate();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2);
} }
public getSettingsTabContainer(): Explorer { public getSettingsTabContainer(): Explorer {

View File

@ -94,9 +94,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}); });
} }
public onTabClick(): Q.Promise<any> { public onTabClick(): void {
this.getContainer().tabsManager.activateTab(this); this.getContainer().tabsManager.activateTab(this);
return Q();
} }
protected updateSelectedNode(): void { protected updateSelectedNode(): void {
@ -128,7 +127,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick()); return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick());
}; };
public onActivate(): Q.Promise<any> { public onActivate(): void {
this.updateSelectedNode(); this.updateSelectedNode();
if (!!this.collection) { if (!!this.collection) {
this.collection.selectedSubnodeKind(this.tabKind); this.collection.selectedSubnodeKind(this.tabKind);
@ -151,7 +150,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
tabTitle: this.tabTitle(), tabTitle: this.tabTitle(),
tabId: this.tabId tabId: this.tabId
}); });
return Q();
} }
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {

View File

@ -8,7 +8,6 @@ import * as Constants from "../../Common/Constants";
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures"; import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
import { readTriggers } from "../../Common/dataAccess/readTriggers"; import { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions"; import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer"; import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize"; import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
@ -39,6 +38,7 @@ import Explorer from "../Explorer";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
export default class Collection implements ViewModels.Collection { export default class Collection implements ViewModels.Collection {
public nodeKind: string; public nodeKind: string;
@ -1091,8 +1091,7 @@ export default class Collection implements ViewModels.Collection {
return deferred.promise; return deferred.promise;
} }
private _createDocumentsFromFile(fileName: string, documentContent: string): Q.Promise<UploadDetailsRecord> { private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
const record: UploadDetailsRecord = { const record: UploadDetailsRecord = {
fileName: fileName, fileName: fileName,
numSucceeded: 0, numSucceeded: 0,
@ -1102,39 +1101,25 @@ export default class Collection implements ViewModels.Collection {
try { try {
const content = JSON.parse(documentContent); const content = JSON.parse(documentContent);
const promises: Array<Q.Promise<any>> = [];
const triggerCreateDocument: (documentContent: any) => Q.Promise<any> = (documentContent: any) => {
return createDocument(this, documentContent).then(
doc => {
record.numSucceeded++;
return Q.resolve();
},
error => {
record.numFailed++;
record.errors = [...record.errors, getErrorMessage(error)];
return Q.resolve();
}
);
};
if (Array.isArray(content)) { if (Array.isArray(content)) {
for (let i = 0; i < content.length; i++) { await Promise.all(
promises.push(triggerCreateDocument(content[i])); content.map(async documentContent => {
} await createDocument(this, documentContent);
record.numSucceeded++;
})
);
} else { } else {
promises.push(triggerCreateDocument(content)); await createDocument(this, documentContent);
record.numSucceeded++;
} }
Q.all(promises).then(() => { return record;
deferred.resolve(record); } catch (error) {
});
} catch (e) {
record.numFailed++; record.numFailed++;
record.errors = [...record.errors, e.message]; record.errors = [...record.errors, error.message];
deferred.resolve(record); return record;
} }
return deferred.promise;
} }
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> { private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {

View File

@ -6,7 +6,7 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { extractPartitionKey } from "@azure/cosmos"; import { extractPartitionKey } from "@azure/cosmos";
import ConflictsTab from "../Tabs/ConflictsTab"; import ConflictsTab from "../Tabs/ConflictsTab";
import { readDocument } from "../../Common/DocumentClientUtilityBase"; import { readDocument } from "../../Common/dataAccess/readDocument";
export default class ConflictId { export default class ConflictId {
public container: ConflictsTab; public container: ConflictsTab;
@ -59,41 +59,42 @@ export default class ConflictId {
return; return;
} }
public loadConflict(): Q.Promise<any> { public async loadConflict(): Promise<void> {
const conflictsTab = this.container;
this.container.selectedConflictId(this); this.container.selectedConflictId(this);
if (this.operationType === Constants.ConflictOperationType.Create) { if (this.operationType === Constants.ConflictOperationType.Create) {
this.container.initDocumentEditorForCreate(this, this.content); this.container.initDocumentEditorForCreate(this, this.content);
return Q(); return;
} }
this.container.loadingConflictData(true); this.container.loadingConflictData(true);
return readDocument(this.container.collection, this.buildDocumentIdFromConflict(this.partitionKeyValue)).then(
(currentDocumentContent: any) => {
this.container.loadingConflictData(false);
if (this.operationType === Constants.ConflictOperationType.Replace) {
this.container.initDocumentEditorForReplace(this, this.content, currentDocumentContent);
} else {
this.container.initDocumentEditorForDelete(this, currentDocumentContent);
}
},
(reason: any) => {
this.container.loadingConflictData(false);
// Document could be deleted try {
if ( const currentDocumentContent = await readDocument(
reason && this.container.collection,
reason.code === Constants.HttpStatusCodes.NotFound && this.buildDocumentIdFromConflict(this.partitionKeyValue)
this.operationType === Constants.ConflictOperationType.Delete );
) {
this.container.initDocumentEditorForNoOp(this);
return Q();
}
return Q.reject(reason); if (this.operationType === Constants.ConflictOperationType.Replace) {
this.container.initDocumentEditorForReplace(this, this.content, currentDocumentContent);
} else {
this.container.initDocumentEditorForDelete(this, currentDocumentContent);
} }
); } catch (error) {
// Document could be deleted
if (
error &&
error.code === Constants.HttpStatusCodes.NotFound &&
this.operationType === Constants.ConflictOperationType.Delete
) {
this.container.initDocumentEditorForNoOp(this);
return;
}
throw error;
} finally {
this.container.loadingConflictData(false);
}
} }
public getPartitionKeyValueAsString(): string { public getPartitionKeyValueAsString(): string {

View File

@ -65,7 +65,7 @@ export default class DocumentId {
return JSON.stringify(partitionKeyValue); return JSON.stringify(partitionKeyValue);
} }
public loadDocument(): Q.Promise<any> { public async loadDocument(): Promise<void> {
return this.container.selectDocument(this); await this.container.selectDocument(this);
} }
} }

View File

@ -2,7 +2,7 @@ import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure"; import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure";
import { executeStoredProcedure } from "../../Common/DocumentClientUtilityBase"; import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";

View File

@ -58,41 +58,36 @@ export class QueryUtils {
return projections.join(","); return projections.join(",");
} }
public static queryPagesUntilContentPresent( public static async queryPagesUntilContentPresent(
firstItemIndex: number, firstItemIndex: number,
queryItems: (itemIndex: number) => Q.Promise<ViewModels.QueryResults> queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
): Q.Promise<ViewModels.QueryResults> { ): Promise<ViewModels.QueryResults> {
let roundTrips: number = 0; let roundTrips: number = 0;
let netRequestCharge: number = 0; let netRequestCharge: number = 0;
const doRequest = (itemIndex: number): Q.Promise<ViewModels.QueryResults> => const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
queryItems(itemIndex).then( const results: ViewModels.QueryResults = await queryItems(itemIndex);
(results: ViewModels.QueryResults) => { roundTrips = roundTrips + 1;
roundTrips = roundTrips + 1; results.roundTrips = roundTrips;
results.roundTrips = roundTrips; results.requestCharge = Number(results.requestCharge) + netRequestCharge;
results.requestCharge = Number(results.requestCharge) + netRequestCharge; netRequestCharge = Number(results.requestCharge);
netRequestCharge = Number(results.requestCharge); const resultsMetadata: ViewModels.QueryResultsMetadata = {
const resultsMetadata: ViewModels.QueryResultsMetadata = { hasMoreResults: results.hasMoreResults,
hasMoreResults: results.hasMoreResults, itemCount: results.itemCount,
itemCount: results.itemCount, firstItemIndex: results.firstItemIndex,
firstItemIndex: results.firstItemIndex, lastItemIndex: results.lastItemIndex
lastItemIndex: results.lastItemIndex };
}; if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) {
if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) { return await doRequest(resultsMetadata.lastItemIndex);
return doRequest(resultsMetadata.lastItemIndex); }
} return results;
return Q.resolve(results); };
},
(error: any) => {
return Q.reject(error);
}
);
return doRequest(firstItemIndex); return await doRequest(firstItemIndex);
} }
public static queryAllPages( public static async queryAllPages(
queryItems: (itemIndex: number) => Q.Promise<ViewModels.QueryResults> queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
): Q.Promise<ViewModels.QueryResults> { ): Promise<ViewModels.QueryResults> {
const queryResults: ViewModels.QueryResults = { const queryResults: ViewModels.QueryResults = {
documents: [], documents: [],
activityId: undefined, activityId: undefined,
@ -103,25 +98,20 @@ export class QueryUtils {
requestCharge: 0, requestCharge: 0,
roundTrips: 0 roundTrips: 0
}; };
const doRequest = (itemIndex: number): Q.Promise<ViewModels.QueryResults> => const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
queryItems(itemIndex).then( const results: ViewModels.QueryResults = await queryItems(itemIndex);
(results: ViewModels.QueryResults) => { const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results;
const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results; queryResults.roundTrips = queryResults.roundTrips + 1;
queryResults.roundTrips = queryResults.roundTrips + 1; queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge);
queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge); queryResults.hasMoreResults = hasMoreResults;
queryResults.hasMoreResults = hasMoreResults; queryResults.itemCount = queryResults.itemCount + itemCount;
queryResults.itemCount = queryResults.itemCount + itemCount; queryResults.lastItemIndex = lastItemIndex;
queryResults.lastItemIndex = lastItemIndex; queryResults.documents = queryResults.documents.concat(documents);
queryResults.documents = queryResults.documents.concat(documents); if (queryResults.hasMoreResults) {
if (queryResults.hasMoreResults) { return doRequest(queryResults.lastItemIndex + 1);
return doRequest(queryResults.lastItemIndex + 1); }
} return queryResults;
return Q.resolve(queryResults); };
},
(error: any) => {
return Q.reject(error);
}
);
return doRequest(0); return doRequest(0);
} }