cosmos-explorer/src/Explorer/Tabs/ConflictsTab.ts

721 lines
27 KiB
TypeScript

import * as ko from "knockout";
import Q from "q";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import { KeyCodes } from "../../Common/Constants";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import ConflictId from "../Tree/ConflictId";
import editable from "../../Common/EditableUtility";
import * as HeadersUtility from "../../Common/HeadersUtility";
import TabsBase from "./TabsBase";
import { DocumentsGridMetrics } from "../../Common/Constants";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
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 { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import Explorer from "../Explorer";
import {
queryConflicts,
deleteConflict,
deleteDocument,
createDocument,
updateDocument
} from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
export default class ConflictsTab extends TabsBase {
public selectedConflictId: ko.Observable<ConflictId>;
public selectedConflictContent: ViewModels.Editable<string>;
public selectedConflictCurrent: ViewModels.Editable<string>;
public documentContentsGridId: string;
public documentContentsContainerId: string;
public isEditorDirty: ko.Computed<boolean>;
public editorState: ko.Observable<ViewModels.DocumentExplorerState>;
public acceptChangesButton: ViewModels.Button;
public discardButton: ViewModels.Button;
public deleteButton: ViewModels.Button;
public accessibleDocumentList: AccessibleVerticalList;
public dataContentsGridScrollHeight: ko.Observable<string>;
public shouldShowDiffEditor: ko.Computed<boolean>;
public shouldShowEditor: ko.Computed<boolean>;
public shouldShowWatermark: ko.Computed<boolean>;
public loadingConflictData: ko.Observable<boolean> = ko.observable<boolean>(false);
public isEditorReadOnly: ko.Computed<boolean>;
public splitter: Splitter;
public partitionKey: DataModels.PartitionKey;
public partitionKeyPropertyHeader: string;
public partitionKeyProperty: string;
public conflictOperation: ko.Observable<string> = ko.observable<string>();
public conflictIds: ko.ObservableArray<ConflictId>;
private _documentsIterator: MinimalQueryIterator;
private _container: Explorer;
private _acceptButtonLabel: ko.Observable<string> = ko.observable("Save");
constructor(options: ViewModels.ConflictsTabOptions) {
super(options);
this._container = options.collection && options.collection.container;
this.documentContentsGridId = `conflictsContentsGrid${this.tabId}`;
this.documentContentsContainerId = `conflictsContentsContainer${this.tabId}`;
this.editorState = ko.observable<ViewModels.DocumentExplorerState>(
ViewModels.DocumentExplorerState.noDocumentSelected
);
this.selectedConflictId = ko.observable<ConflictId>();
this.selectedConflictContent = editable.observable<any>("");
this.selectedConflictCurrent = editable.observable<any>("");
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
this.conflictIds = options.conflictIds;
this.partitionKeyPropertyHeader =
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
? this.partitionKeyPropertyHeader
.replace(/[/]+/g, ".")
.substr(1)
.replace(/[']+/g, "")
: null;
this.dataContentsGridScrollHeight = ko.observable<string>(null);
// initialize splitter only after template has been loaded so dom elements are accessible
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady) {
const tabContainer: HTMLElement = document.getElementById("content");
const splitterBounds: SplitterBounds = {
min: Constants.DocumentsGridMetrics.DocumentEditorMinWidthRatio * tabContainer.clientWidth,
max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth
};
this.splitter = new Splitter({
splitterId: "h_splitter2",
leftId: this.documentContentsContainerId,
bounds: splitterBounds,
direction: SplitterDirection.Vertical
});
}
});
this.accessibleDocumentList = new AccessibleVerticalList(this.conflictIds());
this.accessibleDocumentList.setOnSelect(
(selectedDocument: ConflictId) => selectedDocument && selectedDocument.click()
);
this.selectedConflictId.subscribe((newSelectedDocumentId: ConflictId) => {
this.accessibleDocumentList.updateCurrentItem(newSelectedDocumentId);
this.conflictOperation(newSelectedDocumentId && newSelectedDocumentId.operationType);
});
this.conflictIds.subscribe((newDocuments: ConflictId[]) => {
this.accessibleDocumentList.updateItemList(newDocuments);
this.dataContentsGridScrollHeight(
newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px"
);
});
this.isEditorDirty = ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.noDocumentSelected:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return false;
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
return true;
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
return (
this.selectedConflictCurrent.getEditableOriginalValue() !==
this.selectedConflictCurrent.getEditableCurrentValue()
);
default:
return false;
}
});
this.acceptChangesButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
return this.conflictOperation() !== Constants.ConflictOperationType.Delete || !!this.selectedConflictContent();
})
};
this.discardButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
return this.conflictOperation() !== Constants.ConflictOperationType.Delete || !!this.selectedConflictContent();
})
};
this.deleteButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits:
return true;
}
return false;
}),
visible: ko.computed<boolean>(() => {
return true;
})
};
this.buildCommandBarOptions();
this.shouldShowDiffEditor = ko.pureComputed<boolean>(() => {
const documentHasContent: boolean =
this.selectedConflictContent() != null && this.selectedConflictContent().length > 0;
const operationIsReplace: boolean = this.conflictOperation() === Constants.ConflictOperationType.Replace;
const isLoadingData: boolean = this.loadingConflictData();
return documentHasContent && operationIsReplace && !isLoadingData;
});
this.shouldShowEditor = ko.pureComputed<boolean>(() => {
const documentHasContent: boolean =
this.selectedConflictContent() != null && this.selectedConflictContent().length > 0;
const operationIsInsert: boolean = this.conflictOperation() === Constants.ConflictOperationType.Create;
const operationIsDelete: boolean = this.conflictOperation() === Constants.ConflictOperationType.Delete;
const isLoadingData: boolean = this.loadingConflictData();
return documentHasContent && (operationIsInsert || operationIsDelete) && !isLoadingData;
});
this.shouldShowWatermark = ko.pureComputed<boolean>(() => !this.shouldShowDiffEditor() && !this.shouldShowEditor());
this.isEditorReadOnly = ko.pureComputed<boolean>(() => {
const operationIsDelete: boolean = this.conflictOperation() === Constants.ConflictOperationType.Delete;
const operationIsReplace: boolean = this.conflictOperation() === Constants.ConflictOperationType.Replace;
return operationIsDelete || operationIsReplace;
});
this.selectedConflictContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
this.conflictOperation.subscribe((newOperationType: string) => {
let operationLabel = "Save";
if (newOperationType === Constants.ConflictOperationType.Replace) {
operationLabel = "Update";
}
this._acceptButtonLabel(operationLabel);
});
}
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(reason => {
const message = ErrorParserUtility.parse(reason)[0].message;
window.alert(message);
});
}
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.refreshDocumentsGrid();
event.stopPropagation();
return false;
}
return true;
};
public onConflictIdClick(clickedDocumentId: ConflictId): Q.Promise<any> {
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
return Q();
}
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
return Q();
}
public onAcceptChangesClick = (): Q.Promise<any> => {
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
return Q();
}
this.isExecutionError(false);
this.isExecuting(true);
const selectedConflict = this.selectedConflictId();
const startKey: number = TelemetryProcessor.traceStart(Action.ResolveConflict, {
databaseAccountName: this._container.databaseAccount().name,
defaultExperience: this._container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
conflictResourceType: selectedConflict.resourceType,
conflictOperationType: selectedConflict.operationType,
conflictResourceId: selectedConflict.resourceId
});
let operationPromise: Q.Promise<any> = Q();
if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) {
const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = updateDocument(
this.collection,
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]),
documentContent
);
}
if (selectedConflict.operationType === Constants.ConflictOperationType.Create) {
const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = createDocument(this.collection, documentContent);
}
if (selectedConflict.operationType === Constants.ConflictOperationType.Delete && !!this.selectedConflictContent()) {
const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = 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
);
});
},
reason => {
this.isExecutionError(true);
const message = ErrorParserUtility.parse(reason)[0].message;
window.alert(message);
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
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
};
public onDeleteClick = (): Q.Promise<any> => {
this.isExecutionError(false);
this.isExecuting(true);
const selectedConflict = this.selectedConflictId();
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteConflict, {
databaseAccountName: this._container.databaseAccount().name,
defaultExperience: this._container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
conflictResourceType: selectedConflict.resourceType,
conflictOperationType: selectedConflict.operationType,
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
);
},
reason => {
this.isExecutionError(true);
const message = ErrorParserUtility.parse(reason)[0].message;
window.alert(message);
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
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
};
public onDiscardClick = (): Q.Promise<any> => {
this.selectedConflictContent(this.selectedConflictContent.getEditableOriginalValue());
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
return Q();
};
public onValidDocumentEdit(): Q.Promise<any> {
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid);
return Q();
}
public onInvalidDocumentEdit(): Q.Promise<any> {
if (
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits ||
this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid
) {
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid);
return Q();
}
return Q();
}
public onTabClick(): Q.Promise<any> {
return super.onTabClick().then(() => {
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);
}
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: 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>> {
// 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);
}
public loadNextPage(): Q.Promise<any> {
this.isExecuting(true);
this.isExecutionError(false);
return this._loadNextPageInternal()
.then(
(conflictIdsResponse: DataModels.ConflictId[]) => {
const currentConflicts = this.conflictIds();
const currentDocumentsRids = currentConflicts.map(currentConflict => currentConflict.rid);
const nextConflictIds = conflictIdsResponse
// filter documents already loaded in observable
.filter((d: any) => {
return currentDocumentsRids.indexOf(d._rid) < 0;
})
// map raw response to view model
.map((rawDocument: any) => {
return <ConflictId>new ConflictId(this, rawDocument);
});
const merged = currentConflicts.concat(nextConflictIds);
this.conflictIds(merged);
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceSuccess(
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()
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
},
error => {
this.isExecutionError(true);
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: error
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
}
)
.finally(() => this.isExecuting(false));
}
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.loadNextPage();
document.getElementById(this.documentContentsGridId).focus();
event.stopPropagation();
}
};
protected _loadNextPageInternal(): Q.Promise<DataModels.ConflictId[]> {
return Q(this._documentsIterator.fetchNext().then(response => response.resources));
}
protected _onEditorContentChange(newContent: string) {
try {
const parsed: any = JSON.parse(newContent);
this.onValidDocumentEdit();
} catch (e) {
this.onInvalidDocumentEdit();
}
}
public initDocumentEditorForCreate(documentId: ConflictId, documentToInsert: any): Q.Promise<any> {
if (documentId) {
let parsedConflictContent: any = JSON.parse(documentToInsert);
const renderedConflictContent: string = this.renderObjectForEditor(parsedConflictContent, null, 4);
this.selectedConflictContent.setBaseline(renderedConflictContent);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
}
return Q();
}
public initDocumentEditorForReplace(
documentId: ConflictId,
conflictContent: any,
currentContent: any
): Q.Promise<any> {
if (documentId) {
currentContent = ConflictsTab.removeSystemProperties(currentContent);
const renderedCurrentContent: string = this.renderObjectForEditor(currentContent, null, 4);
this.selectedConflictCurrent.setBaseline(renderedCurrentContent);
let parsedConflictContent: any = JSON.parse(conflictContent);
parsedConflictContent = ConflictsTab.removeSystemProperties(parsedConflictContent);
const renderedConflictContent: string = this.renderObjectForEditor(parsedConflictContent, null, 4);
this.selectedConflictContent.setBaseline(renderedConflictContent);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
}
return Q();
}
public initDocumentEditorForDelete(documentId: ConflictId, documentToDelete: any): Q.Promise<any> {
if (documentId) {
let parsedDocumentToDelete: any = JSON.parse(documentToDelete);
parsedDocumentToDelete = ConflictsTab.removeSystemProperties(parsedDocumentToDelete);
const renderedDocumentToDelete: string = this.renderObjectForEditor(parsedDocumentToDelete, null, 4);
this.selectedConflictContent.setBaseline(renderedDocumentToDelete);
this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits);
}
return Q();
}
public initDocumentEditorForNoOp(documentId: ConflictId): Q.Promise<any> {
this.selectedConflictContent(null);
this.selectedConflictCurrent(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
return Q();
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = this._acceptButtonLabel();
if (this.acceptChangesButton.visible()) {
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onAcceptChangesClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.acceptChangesButton.enabled()
});
}
if (this.discardButton.visible()) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onDiscardClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardButton.enabled()
});
}
if (this.deleteButton.visible()) {
const label = "Delete";
buttons.push({
iconSrc: DeleteIcon,
iconAlt: label,
onCommandClick: this.onDeleteClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.deleteButton.enabled()
});
}
return buttons;
}
protected buildCommandBarOptions(): void {
ko.computed(() =>
ko.toJSON([
this._acceptButtonLabel,
this.acceptChangesButton.visible,
this.acceptChangesButton.enabled,
this.discardButton.visible,
this.discardButton.enabled,
this.deleteButton.visible,
this.deleteButton.enabled
])
).subscribe(() => this.updateNavbarWithTabsButtons());
this.updateNavbarWithTabsButtons();
}
/** Remove system properties from the JSON object.
* This includes: _etag, _rid, _self, _attachments, _ts
*/
public static removeSystemProperties(jsonObject: any): any {
if (!jsonObject) {
return null;
}
delete jsonObject["_etag"];
delete jsonObject["_ts"];
delete jsonObject["_rid"];
delete jsonObject["_self"];
delete jsonObject["_attachments"];
return jsonObject;
}
private _isIgnoreDirtyEditor = (): boolean => {
var msg: string = "Changes will be lost. Do you want to continue?";
return window.confirm(msg);
};
private _getPartitionKeyPropertyHeader(): string {
return (
(this.partitionKey &&
this.partitionKey.paths &&
this.partitionKey.paths.length > 0 &&
this.partitionKey.paths[0]) ||
null
);
}
}