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
40 changed files with 1163 additions and 1442 deletions

View File

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

View File

@@ -4,8 +4,8 @@ import GraphTab from ".././Tabs/GraphTab";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { userContext } from "../../UserContext";
interface SampleDataFile extends DataModels.CreateCollectionParams {
@@ -95,12 +95,15 @@ export class ContainerSampleGenerator {
.reduce((previous, current) => previous.then(current), Promise.resolve());
} else {
// For SQL all queries are executed at the same time
this.sampleDataFile.data.map(doc => {
const subPromise = createDocument(collection, doc);
subPromise.catch(reason => NotificationConsoleUtils.logConsoleError(reason));
promises.push(subPromise);
});
await Promise.all(promises);
await Promise.all(
this.sampleDataFile.data.map(async doc => {
try {
await createDocument(collection, doc);
} catch (error) {
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 * as sinon from "sinon";
import { mount, ReactWrapper } from "enzyme";
@@ -12,7 +13,8 @@ import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import GraphTab from "../../Tabs/GraphTab";
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", () => {
it("should reject null as vertex array", () => {
@@ -299,12 +301,12 @@ describe("GraphExplorer", () => {
ignoreD3Update: boolean
): GraphExplorer => {
(queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => {
return Q.resolve({
return {
_query: query,
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
hasMoreResults: () => false,
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
});
};
});
(queryDocumentsPage as jest.Mock).mockImplementation(
(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 { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
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 { FeedOptions } from "@azure/cosmos";
export interface GraphAccessor {
applyFilter: () => void;
@@ -725,26 +727,32 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/**
* Execute DocDB query and get all results
*/
public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> {
// TODO maxItemCount: this reduces throttling, but won't cap the # of results
return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.PAGE_ALL,
enableCrossPartitionQuery:
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
"true"
}).then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
return iterator.fetchNext().then(response => response.resources);
},
(reason: any) => {
GraphExplorer.reportToConsole(
ConsoleDataType.Error,
`Failed to execute non-paged query ${query}. Reason:${reason}`,
reason
);
return null;
}
);
public async executeNonPagedDocDbQuery(query: string): Promise<DataModels.DocumentId[]> {
try {
// TODO maxItemCount: this reduces throttling, but won't cap the # of results
const iterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
this.props.databaseId,
this.props.collectionId,
query,
{
maxItemCount: GraphExplorer.PAGE_ALL,
enableCrossPartitionQuery:
StorageUtility.LocalStorageUtility.getEntryString(
StorageUtility.StorageKey.IsCrossPartitionQueryEnabled
) === "true"
} as FeedOptions
);
const response = await iterator.fetchNext();
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
*/
public submitQuery(query: string): void {
public async submitQuery(query: string): Promise<void> {
// Clear any progress indicator
this.executeCounter = 0;
this.setState({
@@ -882,24 +890,22 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Remember query
this.pushToLatestQueryFragments(query);
let backendPromise;
if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
backendPromise = this.executeDocDbGVQuery();
} else {
backendPromise = 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
});
try {
let result: UserQueryResult;
if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
result = await this.executeDocDbGVQuery();
} else {
result = await this.executeGremlinQuery(query);
}
);
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
*/
private updatePossibleVertices(): Q.Promise<PossibleVertex[]> {
private updatePossibleVertices(): Promise<PossibleVertex[]> {
const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null;
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";
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`;
}
return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
})
.then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
this.currentDocDBQueryInfo = {
iterator: iterator,
index: 0,
query: query
};
},
(reason: any) => {
GraphExplorer.reportToConsole(
ConsoleDataType.Error,
`Failed to execute CosmosDB query: ${query} reason:${reason}`
);
}
)
.then(() => this.loadMoreRootNodes());
try {
const iterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
this.props.databaseId,
this.props.collectionId,
query,
{
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
} as FeedOptions
);
this.currentDocDBQueryInfo = {
iterator: iterator,
index: 0,
query: query
};
return await this.loadMoreRootNodes();
} catch (error) {
GraphExplorer.reportToConsole(
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) {
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
.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`;
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
return queryDocumentsPage(
this.props.collectionId,
this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.index,
{
enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
}
)
.then((results: ViewModels.QueryResults) => {
GraphExplorer.clearConsoleProgress(id);
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
this.setState({ hasMoreRoots: results.hasMoreResults });
RU = results.requestCharge.toString();
GraphExplorer.reportToConsole(
ConsoleDataType.Info,
`Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}`
);
const documents = results.documents || [];
return documents.map(
(item: DataModels.DocumentId) => {
return GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty);
},
(reason: any) => {
// Failure
GraphExplorer.clearConsoleProgress(id);
const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${reason}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({
filterQueryError: errorMsg
});
this.setFilterQueryStatus(FilterQueryStatus.ErrorResult);
throw reason;
}
);
})
.then((pkIds: string[]) => {
const arg = pkIds.join(",");
return this.executeGremlinQuery(`g.V(${arg})`);
})
.then(() => ({ requestCharge: RU }));
try {
const results: ViewModels.QueryResults = await queryDocumentsPage(
this.props.collectionId,
this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.index
);
GraphExplorer.clearConsoleProgress(id);
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
this.setState({ hasMoreRoots: results.hasMoreResults });
RU = results.requestCharge.toString();
GraphExplorer.reportToConsole(
ConsoleDataType.Info,
`Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}`
);
const pkIds: string[] = (results.documents || []).map((item: DataModels.DocumentId) =>
GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty)
);
const arg = pkIds.join(",");
await this.executeGremlinQuery(`g.V(${arg})`);
return { requestCharge: RU };
} catch (error) {
GraphExplorer.clearConsoleProgress(id);
const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({
filterQueryError: errorMsg
});
this.setFilterQueryStatus(FilterQueryStatus.ErrorResult);
throw error;
}
}
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.
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
*/
private prefetchData(
private async prefetchData(
tableQuery: Entities.ITableQuery,
downloadSize: number,
currentRetry: number = 0
): Q.Promise<any> {
): Promise<IListTableEntitiesSegmentedResult> {
if (!this.cache.serverCallInProgress) {
this.cache.serverCallInProgress = true;
this.allDownloaded = false;
this.lastPrefetchTime = new Date().getTime();
var time = this.lastPrefetchTime;
const time = this.lastPrefetchTime;
var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
if (this._documentIterator && this.continuationToken) {
// 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(
(documents: any[]) => {
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
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 {
Results: entities,
ContinuationToken: this._documentIterator.hasMoreResults()
};
}
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) {
this._documentIterator = result.iterator;
this._documentIterator = documents.iterator;
}
var actualDownloadSize: number = 0;
@@ -478,11 +472,11 @@ export default class TableEntityListViewModel extends DataTableViewModel {
return Q.resolve(null);
}
var entities = result.Results;
var entities = documents.Results;
actualDownloadSize = entities.length;
// 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) {
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.2, go to next round prefetch.
if (this.allDownloaded || nextDownloadSize === 0) {
return Q.resolve(result);
return documents;
}
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
result.ExceedMaximumRetries = true;
return Q.resolve(result);
documents.ExceedMaximumRetries = true;
return documents;
}
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
})
.catch((error: Error) => {
this.cache.serverCallInProgress = false;
return Q.reject(error);
});
return await this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
}
} catch (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 { AuthType } from "../../AuthType";
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { FeedOptions } from "@azure/cosmos";
import * as Constants from "../../Common/Constants";
import * as Entities from "./Entities";
import * as HeadersUtility from "../../Common/HeadersUtility";
@@ -12,9 +13,12 @@ import * as TableConstants from "./Constants";
import * as TableEntityProcessor from "./TableEntityProcessor";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import { queryDocuments, deleteDocument, updateDocument, createDocument } from "../../Common/DocumentClientUtilityBase";
import { configContext } from "../../ConfigContext";
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 {
partitionKeys: CassandraTableKey[];
@@ -38,19 +42,19 @@ export abstract class TableDataClient {
collection: ViewModels.Collection,
originalDocument: any,
newEntity: Entities.ITableEntity
): Q.Promise<Entities.ITableEntity>;
): Promise<Entities.ITableEntity>;
public abstract queryDocuments(
collection: ViewModels.Collection,
query: string,
shouldNotify?: boolean,
paginationToken?: string
): Q.Promise<Entities.IListTableEntitiesResult>;
): Promise<Entities.IListTableEntitiesResult>;
public abstract deleteDocuments(
collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[]
): Q.Promise<any>;
): Promise<any>;
}
export class TablesAPIDataClient extends TableDataClient {
@@ -74,77 +78,63 @@ export class TablesAPIDataClient extends TableDataClient {
return deferred.promise;
}
public updateDocument(
public async updateDocument(
collection: ViewModels.Collection,
originalDocument: any,
entity: Entities.ITableEntity
): Q.Promise<Entities.ITableEntity> {
const deferred = Q.defer<Entities.ITableEntity>();
updateDocument(
collection,
originalDocument,
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
).then(
(newDocument: any) => {
const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
deferred.resolve(newEntity);
},
reason => {
deferred.reject(reason);
}
);
return deferred.promise;
): Promise<Entities.ITableEntity> {
try {
const newDocument = await updateDocument(
collection,
originalDocument,
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
);
return TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
} catch (error) {
handleError(error, "TablesAPIDataClient/updateDocument");
throw error;
}
}
public queryDocuments(
public async queryDocuments(
collection: ViewModels.Collection,
query: string
): Q.Promise<Entities.IListTableEntitiesResult> {
const deferred = Q.defer<Entities.IListTableEntitiesResult>();
): Promise<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 = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
queryDocuments(collection.databaseId, collection.id(), query, options).then(
iterator => {
iterator
.fetchNext()
.then(response => response.resources)
.then(
(documents: any[] = []) => {
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
let finalEntities: Entities.IListTableEntitiesResult = <Entities.IListTableEntitiesResult>{
Results: entities,
ContinuationToken: iterator.hasMoreResults(),
iterator: iterator
};
deferred.resolve(finalEntities);
},
reason => {
deferred.reject(reason);
}
);
},
reason => {
deferred.reject(reason);
}
);
return deferred.promise;
return {
Results: entities,
ContinuationToken: iterator.hasMoreResults(),
iterator: iterator
};
} catch (error) {
handleError(error, "TablesAPIDataClient/queryDocuments", "Query documents failed");
throw error;
}
}
public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> {
let documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
public async deleteDocuments(
collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[]
): Promise<any> {
const documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
<Entities.ITableEntityForTablesAPI[]>entitiesToDelete,
collection
);
let promiseArray: Q.Promise<any>[] = [];
documentsToDelete &&
documentsToDelete.forEach(document => {
await Promise.all(
documentsToDelete?.map(async document => {
document.id = ko.observable<string>(document.id);
let promise: Q.Promise<any> = deleteDocument(collection, document);
promiseArray.push(promise);
});
return Q.all(promiseArray);
await deleteDocument(collection, document);
})
);
}
}
@@ -180,10 +170,7 @@ export class CassandraAPIDataClient extends TableDataClient {
(data: any) => {
entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)];
entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString();
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully added new row to table ${collection.id()}`
);
NotificationConsoleUtils.logConsoleInfo(`Successfully added new row to table ${collection.id()}`);
deferred.resolve(entity);
},
error => {
@@ -197,181 +184,149 @@ export class CassandraAPIDataClient extends TableDataClient {
return deferred.promise;
}
public updateDocument(
public async updateDocument(
collection: ViewModels.Collection,
originalDocument: any,
newEntity: Entities.ITableEntity
): Q.Promise<Entities.ITableEntity> {
const notificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating row ${originalDocument.RowKey._}`
);
const deferred = Q.defer<Entities.ITableEntity>();
let promiseArray: Q.Promise<any>[] = [];
let query = `UPDATE ${collection.databaseId}.${collection.id()}`;
let isChange: boolean = false;
for (let property in newEntity) {
if (!originalDocument[property] || newEntity[property]._.toString() !== originalDocument[property]._.toString()) {
if (this.isStringType(newEntity[property].$)) {
query = `${query} SET ${property} = '${newEntity[property]._}',`;
} else {
query = `${query} SET ${property} = ${newEntity[property]._},`;
): Promise<Entities.ITableEntity> {
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Updating row ${originalDocument.RowKey._}`);
try {
let whereSegment = " WHERE";
let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat(
collection.cassandraKeys.clusteringKeys
);
for (let keyIndex in keys) {
const key = keys[keyIndex].property;
const keyType = keys[keyIndex].type;
whereSegment += this.isStringType(keyType)
? ` ${key} = '${newEntity[key]._}' AND`
: ` ${key} = ${newEntity[key]._} AND`;
}
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);
let whereSegment = " WHERE";
let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat(
collection.cassandraKeys.clusteringKeys
);
for (let keyIndex in keys) {
const key = keys[keyIndex].property;
const keyType = keys[keyIndex].type;
if (this.isStringType(keyType)) {
whereSegment = `${whereSegment} ${key} = '${newEntity[key]._}' AND`;
} else {
whereSegment = `${whereSegment} ${key} = ${newEntity[key]._} AND`;
if (isPropertyUpdated) {
updateQuery = updateQuery.slice(0, updateQuery.length - 1);
updateQuery += whereSegment;
await this.queryDocuments(collection, updateQuery);
}
}
whereSegment = whereSegment.slice(0, whereSegment.length - 4);
query = query + whereSegment;
if (isChange) {
promiseArray.push(this.queryDocuments(collection, query));
}
query = `DELETE `;
for (let property in originalDocument) {
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
query = `${query} ${property},`;
}
}
if (query.length > 7) {
query = query.slice(0, query.length - 1);
query = `${query} FROM ${collection.databaseId}.${collection.id()}${whereSegment}`;
promiseArray.push(this.queryDocuments(collection, query));
}
Q.all(promiseArray)
.then(
(data: any) => {
newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey];
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated row ${newEntity.RowKey._}`
);
deferred.resolve(newEntity);
},
error => {
handleError(error, "UpdateRowCassandra", `Failed to update row ${newEntity.RowKey._}`);
deferred.reject(error);
let deleteQuery = `DELETE `;
let isPropertyDeleted = false;
for (let property in originalDocument) {
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
deleteQuery += ` ${property},`;
isPropertyDeleted = true;
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
});
return deferred.promise;
}
if (isPropertyDeleted) {
deleteQuery = deleteQuery.slice(0, deleteQuery.length - 1);
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,
query: string,
shouldNotify?: boolean,
paginationToken?: string
): Q.Promise<Entities.IListTableEntitiesResult> {
let notificationId: string;
if (shouldNotify) {
notificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying rows for table ${collection.id()}`
);
}
const deferred = Q.defer<Entities.IListTableEntitiesResult>();
const authType = window.authType;
const apiEndpoint: string =
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestQueryApi
: Constants.CassandraBackend.queryApi;
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
type: "POST",
data: {
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
cassandraEndpoint: this.trimCassandraEndpoint(
collection.container.databaseAccount().properties.cassandraEndpoint
),
resourceId: collection.container.databaseAccount().id,
keyspaceId: collection.databaseId,
tableId: collection.id(),
query: query,
paginationToken: paginationToken
},
beforeSend: this.setAuthorizationHeader,
error: this.handleAjaxError,
cache: false
})
.then(
(data: any) => {
if (shouldNotify) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`
);
}
deferred.resolve({
Results: data.result,
ContinuationToken: data.paginationToken
});
): Promise<Entities.IListTableEntitiesResult> {
const clearMessage =
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
try {
const authType = window.authType;
const apiEndpoint: string =
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestQueryApi
: Constants.CassandraBackend.queryApi;
const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
type: "POST",
data: {
accountName:
collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
cassandraEndpoint: this.trimCassandraEndpoint(
collection.container.databaseAccount().properties.cassandraEndpoint
),
resourceId: collection.container.databaseAccount().id,
keyspaceId: collection.databaseId,
tableId: collection.id(),
query,
paginationToken
},
(error: any) => {
if (shouldNotify) {
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`);
}
deferred.reject(error);
}
)
.done(() => {
if (shouldNotify) {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
}
beforeSend: this.setAuthorizationHeader,
error: this.handleAjaxError,
cache: false
});
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 `;
let promiseArray: Q.Promise<any>[] = [];
let partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection);
for (let i = 0, len = entitiesToDelete.length; i < len; i++) {
let currEntityToDelete: Entities.ITableEntity = entitiesToDelete[i];
let currQuery = query;
let partitionKeyValue = currEntityToDelete[partitionKeyProperty];
if (partitionKeyValue._ != null && this.isStringType(partitionKeyValue.$)) {
currQuery = `${currQuery}${partitionKeyProperty} = '${partitionKeyValue._}' AND `;
} else {
currQuery = `${currQuery}${partitionKeyProperty} = ${partitionKeyValue._} AND `;
}
currQuery = currQuery.slice(0, currQuery.length - 5);
const notificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting row ${currEntityToDelete.RowKey._}`
);
promiseArray.push(
this.queryDocuments(collection, currQuery)
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted row ${currEntityToDelete.RowKey._}`
);
},
error => {
handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
})
);
}
return Q.all(promiseArray);
const partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection);
await Promise.all(
entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => {
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`);
const partitionKeyValue = currEntityToDelete[partitionKeyProperty];
const currQuery =
query + this.isStringType(partitionKeyValue.$)
? `${partitionKeyProperty} = '${partitionKeyValue._}'`
: `${partitionKeyProperty} = ${partitionKeyValue._}`;
try {
await this.queryDocuments(collection, currQuery);
NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`);
} catch (error) {
handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`);
throw error;
} finally {
clearMessage();
}
})
);
}
public createKeyspace(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
super.buildCommandBarOptions();
}
public onSaveNewDocumentClick = (): Q.Promise<any> => {
public onSaveNewDocumentClick = (): Promise<any> => {
const documentContent = JSON.parse(this.selectedDocumentContent());
this.displayedError("");
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
@@ -78,12 +78,12 @@ export default class MongoDocumentsTab extends DocumentsTab {
startKey
);
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.isExecuting(true);
return Q(createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent))
return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)
.then(
(savedDocument: any) => {
let partitionKeyArray = extractPartitionKey(
@@ -136,7 +136,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
.finally(() => this.isExecuting(false));
};
public onSaveExisitingDocumentClick = (): Q.Promise<any> => {
public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId();
const documentContent = this.selectedDocumentContent();
this.isExecutionError(false);
@@ -148,7 +148,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
tabTitle: this.tabTitle()
});
return Q(updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent))
return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent)
.then(
(updatedDocument: any) => {
let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
@@ -204,13 +204,10 @@ export default class MongoDocumentsTab extends DocumentsTab {
return filter || "{}";
}
public selectDocument(documentId: DocumentId): Q.Promise<any> {
public async selectDocument(documentId: DocumentId): Promise<void> {
this.selectedDocumentId(documentId);
return Q(
readDocument(this.collection.databaseId, this.collection, documentId).then((content: any) => {
this.initDocumentEditor(documentId, content);
})
);
const content = await readDocument(this.collection.databaseId, this.collection, documentId);
this.initDocumentEditor(documentId, content);
}
public loadNextPage(): Q.Promise<any> {
@@ -330,7 +327,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
return partitionKey;
}
protected __deleteDocument(documentId: DocumentId): Q.Promise<any> {
return Q(deleteDocument(this.collection.databaseId, this.collection, documentId));
protected __deleteDocument(documentId: DocumentId): Promise<void> {
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> {
return super.onTabClick().then(() => {
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
});
public onTabClick(): void {
super.onTabClick();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
}
public handleMessage(event: MessageEvent) {

View File

@@ -15,9 +15,10 @@ import { QueryUtils } from "../../Utils/QueryUtils";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { queryDocuments, queryDocumentsPage } from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
enum ToggleState {
Result,
@@ -163,20 +164,19 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this._buildCommandBarOptions();
}
public onTabClick(): Q.Promise<any> {
return super.onTabClick().then(() => {
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
});
public onTabClick(): void {
super.onTabClick();
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();
this.sqlStatementToExecute(sqlStatement);
this.allResultsMetadata([]);
this.queryResults("");
this._iterator = null;
this._iterator = undefined;
return this._executeQueryDocumentsPage(0);
await this._executeQueryDocumentsPage(0);
};
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();
};
public onFetchNextPageClick(): Q.Promise<any> {
public async onFetchNextPageClick(): Promise<void> {
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
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 => {
@@ -265,19 +265,18 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
return true;
};
private _executeQueryDocumentsPage(firstItemIndex: number): Q.Promise<any> {
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
this.error("");
this.roundTrips(undefined);
if (this._iterator == null) {
const queryIteratorPromise = this._initIterator();
return queryIteratorPromise.finally(() => this._queryDocumentsPage(firstItemIndex));
if (this._iterator === undefined) {
this._initIterator();
}
return this._queryDocumentsPage(firstItemIndex);
await this._queryDocumentsPage(firstItemIndex);
}
// 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._resetAggregateQueryMetrics();
const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, {
@@ -289,90 +288,90 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
const queryDocuments = (firstItemIndex: number) =>
queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex, options);
const queryDocuments = async (firstItemIndex: number) =>
await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex);
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) {
// 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;
}
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
const documents: any[] = queryResults.documents;
const results = this.renderObjectForEditor(documents, null, 4);
if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) {
// 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 =
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
this.showingDocumentsDisplayText(resultsDisplay);
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
const documents: any[] = queryResults.documents;
const results = this.renderObjectForEditor(documents, null, 4);
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");
}
const resultsDisplay: string =
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
this.showingDocumentsDisplayText(resultsDisplay);
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
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(
Action.ExecuteQuery,
{
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
defaultExperience: this.collection && this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle()
},
startKey
);
this.queryResults(results);
TelemetryProcessor.traceSuccess(
Action.ExecuteQuery,
{
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
defaultExperience: this.collection && this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle()
},
(error: any) => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
this.error(errorMessage);
TelemetryProcessor.traceFailure(
Action.ExecuteQuery,
{
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
defaultExperience: this.collection && this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error)
},
startKey
);
document.getElementById("error-display").focus();
}
)
.finally(() => {
this.isExecuting(false);
this.togglesOnFocus();
});
startKey
);
} catch (error) {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
this.error(errorMessage);
TelemetryProcessor.traceFailure(
Action.ExecuteQuery,
{
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
defaultExperience: this.collection && this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: errorMessage,
errorStack: getErrorStack(error)
},
startKey
);
document.getElementById("error-display").focus();
} finally {
this.isExecuting(false);
this.togglesOnFocus();
}
}
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);
if (this._resourceTokenPartitionKey) {
options.partitionKey = this._resourceTokenPartitionKey;
}
return Q(
queryDocuments(this.collection.databaseId, this.collection.id(), this.sqlStatementToExecute(), options).then(
iterator => (this._iterator = iterator)
)
this._iterator = queryDocuments(
this.collection.databaseId,
this.collection.id(),
this.sqlStatementToExecute(),
options
);
}

View File

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

View File

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

View File

@@ -42,54 +42,49 @@ export default class SettingsTabV2 extends TabsBase {
});
}
public onActivate(): Q.Promise<unknown> {
this.isExecuting(true);
this.currentCollection.loadOffer().then(
() => {
// passed in options and set by parent as "Settings" by default
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);
}
);
public async onActivate(): Promise<void> {
try {
this.isExecuting(true);
await this.currentCollection.loadOffer();
// passed in options and set by parent as "Settings" by default
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
return super.onActivate().then(() => {
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2);
});
this.options.getPendingNotification.then(
(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 {

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);
return Q();
}
protected updateSelectedNode(): void {
@@ -128,7 +127,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick());
};
public onActivate(): Q.Promise<any> {
public onActivate(): void {
this.updateSelectedNode();
if (!!this.collection) {
this.collection.selectedSubnodeKind(this.tabKind);
@@ -151,7 +150,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
tabTitle: this.tabTitle(),
tabId: this.tabId
});
return Q();
}
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 { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
import * as Logger from "../../Common/Logger";
@@ -39,6 +38,7 @@ import Explorer from "../Explorer";
import { userContext } from "../../UserContext";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
export default class Collection implements ViewModels.Collection {
public nodeKind: string;
@@ -1091,8 +1091,7 @@ export default class Collection implements ViewModels.Collection {
return deferred.promise;
}
private _createDocumentsFromFile(fileName: string, documentContent: string): Q.Promise<UploadDetailsRecord> {
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
const record: UploadDetailsRecord = {
fileName: fileName,
numSucceeded: 0,
@@ -1102,39 +1101,25 @@ export default class Collection implements ViewModels.Collection {
try {
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)) {
for (let i = 0; i < content.length; i++) {
promises.push(triggerCreateDocument(content[i]));
}
await Promise.all(
content.map(async documentContent => {
await createDocument(this, documentContent);
record.numSucceeded++;
})
);
} else {
promises.push(triggerCreateDocument(content));
await createDocument(this, documentContent);
record.numSucceeded++;
}
Q.all(promises).then(() => {
deferred.resolve(record);
});
} catch (e) {
return record;
} catch (error) {
record.numFailed++;
record.errors = [...record.errors, e.message];
deferred.resolve(record);
record.errors = [...record.errors, error.message];
return record;
}
return deferred.promise;
}
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 { extractPartitionKey } from "@azure/cosmos";
import ConflictsTab from "../Tabs/ConflictsTab";
import { readDocument } from "../../Common/DocumentClientUtilityBase";
import { readDocument } from "../../Common/dataAccess/readDocument";
export default class ConflictId {
public container: ConflictsTab;
@@ -59,41 +59,42 @@ export default class ConflictId {
return;
}
public loadConflict(): Q.Promise<any> {
const conflictsTab = this.container;
public async loadConflict(): Promise<void> {
this.container.selectedConflictId(this);
if (this.operationType === Constants.ConflictOperationType.Create) {
this.container.initDocumentEditorForCreate(this, this.content);
return Q();
return;
}
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
if (
reason &&
reason.code === Constants.HttpStatusCodes.NotFound &&
this.operationType === Constants.ConflictOperationType.Delete
) {
this.container.initDocumentEditorForNoOp(this);
return Q();
}
try {
const currentDocumentContent = await readDocument(
this.container.collection,
this.buildDocumentIdFromConflict(this.partitionKeyValue)
);
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 {

View File

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

View File

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