diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 7edae316b..a3896dc96 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -25,12 +25,12 @@ const fetchMock = () => { }); }; -const partitionKeyProperty = "pk"; +const partitionKeyProperties = ["pk"]; const collection = { id: () => "testCollection", rid: "testCollectionrid", - partitionKeyProperty, + partitionKeyProperties, partitionKey: { paths: ["/pk"], kind: "Hash", @@ -41,7 +41,7 @@ const collection = { const documentId = ({ partitionKeyHeader: () => "[]", self: "db/testDB/db/testCollection/docs/testId", - partitionKeyProperty, + partitionKeyProperties, partitionKey: { paths: ["/pk"], kind: "Hash", diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 1a0c75a4a..6d6212dd8 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -76,7 +76,7 @@ export function queryDocuments( dba: databaseAccount.name, pk: collection && collection.partitionKey && !collection.partitionKey.systemKey - ? collection.partitionKeyProperty + ? collection.partitionKeyProperties?.[0] : "", }; @@ -139,7 +139,7 @@ export function readDocument( dba: databaseAccount.name, pk: documentId && documentId.partitionKey && !documentId.partitionKey.systemKey - ? documentId.partitionKeyProperty + ? documentId.partitionKeyProperties?.[0] : "", }; @@ -225,7 +225,7 @@ export function updateDocument( dba: databaseAccount.name, pk: documentId && documentId.partitionKey && !documentId.partitionKey.systemKey - ? documentId.partitionKeyProperty + ? documentId.partitionKeyProperties?.[0] : "", }; const endpoint = getFeatureEndpointOrDefault("updateDocument"); @@ -266,7 +266,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum dba: databaseAccount.name, pk: documentId && documentId.partitionKey && !documentId.partitionKey.systemKey - ? documentId.partitionKeyProperty + ? documentId.partitionKeyProperties?.[0] : "", }; const endpoint = getFeatureEndpointOrDefault("deleteDocument"); diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index 534d12879..688e2c2db 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -149,10 +149,10 @@ export class QueriesClient { const documentId = new DocumentId( { partitionKey: QueriesClient.PartitionKey, - partitionKeyProperty: "id", + partitionKeyProperties: ["id"], } as DocumentsTab, query, - query.queryName + [query.queryName] ); // TODO: Remove DocumentId's dependency on DocumentsTab const options: any = { partitionKey: query.resourceId }; return deleteDocument(queriesCollection, documentId) diff --git a/src/Common/dataAccess/readDocument.ts b/src/Common/dataAccess/readDocument.ts index d399f25f0..fce09cceb 100644 --- a/src/Common/dataAccess/readDocument.ts +++ b/src/Common/dataAccess/readDocument.ts @@ -1,21 +1,28 @@ -import { Item } from "@azure/cosmos"; +import { Item, RequestOptions } from "@azure/cosmos"; import { CollectionBase } from "../../Contracts/ViewModels"; +import DocumentId from "../../Explorer/Tree/DocumentId"; +import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import { HttpHeaders } from "../Constants"; import { client } from "../CosmosClient"; import { getEntityName } from "../DocumentUtility"; import { handleError } from "../ErrorHandlingUtils"; -import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import DocumentId from "../../Explorer/Tree/DocumentId"; export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise => { const entityName = getEntityName(); const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`); try { + const options: RequestOptions = + documentId.partitionKey.kind === "MultiHash" + ? { + [HttpHeaders.partitionKey]: documentId.partitionKeyValue, + } + : {}; const response = await client() .database(collection.databaseId) .container(collection.id()) .item(documentId.id(), documentId.partitionKeyValue) - .read(); + .read(options); return response?.resource; } catch (error) { diff --git a/src/Common/dataAccess/updateDocument.ts b/src/Common/dataAccess/updateDocument.ts index 9e1b50fd9..7cda6566d 100644 --- a/src/Common/dataAccess/updateDocument.ts +++ b/src/Common/dataAccess/updateDocument.ts @@ -1,10 +1,11 @@ +import { Item, RequestOptions } from "@azure/cosmos"; +import { HttpHeaders } from "Common/Constants"; import { CollectionBase } from "../../Contracts/ViewModels"; -import { Item } from "@azure/cosmos"; +import DocumentId from "../../Explorer/Tree/DocumentId"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; import { getEntityName } from "../DocumentUtility"; import { handleError } from "../ErrorHandlingUtils"; -import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import DocumentId from "../../Explorer/Tree/DocumentId"; export const updateDocument = async ( collection: CollectionBase, @@ -15,11 +16,17 @@ export const updateDocument = async ( const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`); try { + const options: RequestOptions = + documentId.partitionKey.kind === "MultiHash" + ? { + [HttpHeaders.partitionKey]: documentId.partitionKeyValue, + } + : {}; const response = await client() .database(collection.databaseId) .container(collection.id()) .item(documentId.id(), documentId.partitionKeyValue) - .replace(newDocument); + .replace(newDocument, options); logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`); return response?.resource; diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 210d34075..ef775e2ce 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -106,8 +106,8 @@ export interface CollectionBase extends TreeNode { self: string; rawDataModel: DataModels.Collection; partitionKey: DataModels.PartitionKey; - partitionKeyProperty: string; - partitionKeyPropertyHeader: string; + partitionKeyProperties: string[]; + partitionKeyPropertyHeaders: string[]; id: ko.Observable; selectedSubnodeKind: ko.Observable; children: ko.ObservableArray; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx index 84913b022..42c72de6a 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx @@ -65,8 +65,8 @@ export class SubSettingsComponent extends React.Component { + if (userContext.apiType === "Mongo") { + return this.props.collection.partitionKeyProperties?.[0] || ""; + } + + return (this.props.collection.partitionKeyProperties || []).map((property) => "/" + property).join(", "); + }; + private getPartitionKeyComponent = (): JSX.Element => ( {this.getPartitionKeyVisible() && ( @@ -310,7 +318,8 @@ export class SubSettingsComponent extends React.Component { return; }, diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index c1552323f..ea8341d10 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -1669,7 +1669,9 @@ exports[`SettingsComponent renders 1`] = ` "paths": Array [], "version": 2, }, - "partitionKeyProperty": "partitionKey", + "partitionKeyProperties": Array [ + "partitionKey", + ], "readSettings": [Function], "uniqueKeyPolicy": Object {}, "usageSizeInKB": [Function], @@ -3348,7 +3350,9 @@ exports[`SettingsComponent renders 1`] = ` "paths": Array [], "version": 2, }, - "partitionKeyProperty": "partitionKey", + "partitionKeyProperties": Array [ + "partitionKey", + ], "readSettings": [Function], "uniqueKeyPolicy": Object {}, "usageSizeInKB": [Function], diff --git a/src/Explorer/OpenActions/OpenActions.tsx b/src/Explorer/OpenActions/OpenActions.tsx index ff17adf21..3317a31d9 100644 --- a/src/Explorer/OpenActions/OpenActions.tsx +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -8,23 +8,23 @@ import { CassandraAddCollectionPane } from "../Panes/CassandraAddCollectionPane/ import { SettingsPane } from "../Panes/SettingsPane/SettingsPane"; import { CassandraAPIDataClient } from "../Tables/TableDataClient"; -function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string { +function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperties: string[]): string { if (!action.query) { return "SELECT * FROM c"; } else if (action.query.text) { return action.query.text; - } else if (!!action.query.partitionKeys && action.query.partitionKeys.length > 0) { + } else if (action.query.partitionKeys?.length > 0 && partitionKeyProperties?.length > 0) { let query = "SELECT * FROM c WHERE"; for (let i = 0; i < action.query.partitionKeys.length; i++) { const partitionKey = action.query.partitionKeys[i]; if (!partitionKey) { // null partition key case - query = query.concat(` c.${partitionKeyProperty} = ${action.query.partitionKeys[i]}`); + query = query.concat(` c.${partitionKeyProperties[i]} = ${action.query.partitionKeys[i]}`); } else if (typeof partitionKey !== "string") { // Undefined partition key case - query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperty})`); + query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperties[i]})`); } else { - query = query.concat(` c.${partitionKeyProperty} = "${action.query.partitionKeys[i]}"`); + query = query.concat(` c.${partitionKeyProperties[i]} = "${action.query.partitionKeys[i]}"`); } if (i !== action.query.partitionKeys.length - 1) { query = query.concat(" OR"); @@ -109,7 +109,7 @@ function openCollectionTab( collection.onNewQueryClick( collection, undefined, - generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperty) + generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties) ); break; } diff --git a/src/Explorer/Tables/TableEntityProcessor.ts b/src/Explorer/Tables/TableEntityProcessor.ts index c18f7bab9..dfe4c6c98 100644 --- a/src/Explorer/Tables/TableEntityProcessor.ts +++ b/src/Explorer/Tables/TableEntityProcessor.ts @@ -126,7 +126,7 @@ export function convertEntitiesToDocuments( }; if (collection.partitionKey) { document["partitionKey"] = collection.partitionKey; - document[collection.partitionKeyProperty] = entity.PartitionKey._; + document[collection.partitionKeyProperties[0]] = entity.PartitionKey._; document["partitionKeyValue"] = entity.PartitionKey._; } for (var property in entity) { diff --git a/src/Explorer/Tabs/ConflictsTab.ts b/src/Explorer/Tabs/ConflictsTab.ts index fb51394e2..89d29d682 100644 --- a/src/Explorer/Tabs/ConflictsTab.ts +++ b/src/Explorer/Tabs/ConflictsTab.ts @@ -74,7 +74,7 @@ export default class ConflictsTab extends TabsBase { this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey); this.conflictIds = options.conflictIds; this.partitionKeyPropertyHeader = - (this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader(); + this.collection?.partitionKeyPropertyHeaders?.[0] || this._getPartitionKeyPropertyHeader(); this.partitionKeyProperty = !!this.partitionKeyPropertyHeader ? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "") : null; diff --git a/src/Explorer/Tabs/DocumentsTab.html b/src/Explorer/Tabs/DocumentsTab.html index 725c9fe2b..096db482e 100644 --- a/src/Explorer/Tabs/DocumentsTab.html +++ b/src/Explorer/Tabs/DocumentsTab.html @@ -143,16 +143,18 @@ + + - - - + + + + + diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 504797a68..17538b624 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -50,7 +50,7 @@ export default class DocumentsTab extends TabsBase { public editorState: ko.Observable; public newDocumentButton: ViewModels.Button; public saveNewDocumentButton: ViewModels.Button; - public saveExisitingDocumentButton: ViewModels.Button; + public saveExistingDocumentButton: ViewModels.Button; public discardNewDocumentChangesButton: ViewModels.Button; public discardExisitingDocumentChangesButton: ViewModels.Button; public deleteExisitingDocumentButton: ViewModels.Button; @@ -65,8 +65,8 @@ export default class DocumentsTab extends TabsBase { // TODO need to refactor public partitionKey: DataModels.PartitionKey; - public partitionKeyPropertyHeader: string; - public partitionKeyProperty: string; + public partitionKeyPropertyHeaders: string[]; + public partitionKeyProperties: string[]; public documentIds: ko.ObservableArray; private _documentsIterator: QueryIterator; @@ -90,11 +90,10 @@ export default class DocumentsTab extends TabsBase { this._resourceTokenPartitionKey = options.resourceTokenPartitionKey; this.documentIds = options.documentIds; - this.partitionKeyPropertyHeader = - (this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader(); - this.partitionKeyProperty = !!this.partitionKeyPropertyHeader - ? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "") - : null; + this.partitionKeyPropertyHeaders = this.collection?.partitionKeyPropertyHeaders || this.partitionKey?.paths; + this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => + partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, "") + ); this.isFilterExpanded = ko.observable(false); this.isFilterCreated = ko.observable(true); @@ -227,7 +226,7 @@ export default class DocumentsTab extends TabsBase { }), }; - this.saveExisitingDocumentButton = { + this.saveExistingDocumentButton = { enabled: ko.computed(() => { switch (this.editorState()) { case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: @@ -445,8 +444,7 @@ export default class DocumentsTab extends TabsBase { savedDocument, this.partitionKey as PartitionKeyDefinition ); - const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0]; - let id = new DocumentId(this, savedDocument, partitionKeyValue); + let id = new DocumentId(this, savedDocument, partitionKeyValueArray); let ids = this.documentIds(); ids.push(id); @@ -489,14 +487,12 @@ export default class DocumentsTab extends TabsBase { return Q(); }; - public onSaveExisitingDocumentClick = (): Promise => { + public onSaveExistingDocumentClick = (): Promise => { const selectedDocumentId = this.selectedDocumentId(); const documentContent = JSON.parse(this.selectedDocumentContent()); const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition); - const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0]; - - selectedDocumentId.partitionKeyValue = partitionKeyValue; + selectedDocumentId.partitionKeyValue = partitionKeyValueArray; this.isExecutionError(false); const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { @@ -800,7 +796,7 @@ export default class DocumentsTab extends TabsBase { } public buildQuery(filter: string): string { - return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperty, this.partitionKey); + return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperties, this.partitionKey); } protected getTabsButtons(): CommandButtonComponentProps[] { @@ -844,16 +840,16 @@ export default class DocumentsTab extends TabsBase { }); } - if (this.saveExisitingDocumentButton.visible()) { + if (this.saveExistingDocumentButton.visible()) { const label = "Update"; buttons.push({ iconSrc: SaveIcon, iconAlt: label, - onCommandClick: this.onSaveExisitingDocumentClick, + onCommandClick: this.onSaveExistingDocumentClick, commandButtonLabel: label, ariaLabel: label, hasPopup: false, - disabled: !this.saveExisitingDocumentButton.enabled(), + disabled: !this.saveExistingDocumentButton.enabled(), }); } @@ -899,8 +895,8 @@ export default class DocumentsTab extends TabsBase { this.saveNewDocumentButton.enabled, this.discardNewDocumentChangesButton.visible, this.discardNewDocumentChangesButton.enabled, - this.saveExisitingDocumentButton.visible, - this.saveExisitingDocumentButton.enabled, + this.saveExistingDocumentButton.visible, + this.saveExistingDocumentButton.enabled, this.discardExisitingDocumentChangesButton.visible, this.discardExisitingDocumentChangesButton.enabled, this.deleteExisitingDocumentButton.visible, @@ -910,16 +906,6 @@ export default class DocumentsTab extends TabsBase { this.updateNavbarWithTabsButtons(); } - private _getPartitionKeyPropertyHeader(): string { - return ( - (this.partitionKey && - this.partitionKey.paths && - this.partitionKey.paths.length > 0 && - this.partitionKey.paths[0]) || - null - ); - } - public static _createUploadButton(container: Explorer): CommandButtonComponentProps { const label = "Upload Item"; return { diff --git a/src/Explorer/Tabs/MongoDocumentsTab.ts b/src/Explorer/Tabs/MongoDocumentsTab.ts index fff7cb042..861dafc3a 100644 --- a/src/Explorer/Tabs/MongoDocumentsTab.ts +++ b/src/Explorer/Tabs/MongoDocumentsTab.ts @@ -29,15 +29,19 @@ export default class MongoDocumentsTab extends DocumentsTab { super(options); this.lastFilterContents = ko.observableArray(['{"id":"foo"}', "{ qty: { $gte: 20 } }"]); - if (this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) { - this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, ""); - } + this.partitionKeyProperties = this.partitionKeyProperties?.map((partitionKeyProperty, i) => { + if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) { + partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, ""); + } - if (this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1) { - // From $v.shard.$v.key.$v > shard.key - this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); - this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty; - } + if (partitionKeyProperty && partitionKeyProperty.indexOf("$v") > -1) { + // From $v.shard.$v.key.$v > shard.key + partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); + this.partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty; + } + + return partitionKeyProperty; + }); this.isFilterExpanded = ko.observable(true); super.buildCommandBarOptions.bind(this); @@ -52,12 +56,9 @@ export default class MongoDocumentsTab extends DocumentsTab { tabTitle: this.tabTitle(), }); - if ( - this.partitionKeyProperty && - this.partitionKeyProperty !== "_id" && - !this._hasShardKeySpecified(documentContent) - ) { - const message = `The document is lacking the shard property: ${this.partitionKeyProperty}`; + const partitionKeyProperty = this.partitionKeyProperties?.[0]; + if (partitionKeyProperty !== "_id" && !this._hasShardKeySpecified(documentContent)) { + const message = `The document is lacking the shard property: ${partitionKeyProperty}`; this.displayedError(message); let that = this; setTimeout(() => { @@ -79,7 +80,12 @@ export default class MongoDocumentsTab extends DocumentsTab { this.isExecutionError(false); this.isExecuting(true); - return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent) + return createDocument( + this.collection.databaseId, + this.collection, + this.partitionKeyProperties?.[0], + documentContent + ) .then( (savedDocument: any) => { let partitionKeyArray = extractPartitionKey( @@ -87,9 +93,7 @@ export default class MongoDocumentsTab extends DocumentsTab { this._getPartitionKeyDefinition() as PartitionKeyDefinition ); - let partitionKeyValue = partitionKeyArray && partitionKeyArray[0]; - - let id = new ObjectId(this, savedDocument, partitionKeyValue); + let id = new ObjectId(this, savedDocument, partitionKeyArray); let ids = this.documentIds(); ids.push(id); delete savedDocument._self; @@ -128,7 +132,7 @@ export default class MongoDocumentsTab extends DocumentsTab { .finally(() => this.isExecuting(false)); }; - public onSaveExisitingDocumentClick = (): Promise => { + public onSaveExistingDocumentClick = (): Promise => { const selectedDocumentId = this.selectedDocumentId(); const documentContent = this.selectedDocumentContent(); this.isExecutionError(false); @@ -151,9 +155,7 @@ export default class MongoDocumentsTab extends DocumentsTab { this._getPartitionKeyDefinition() as PartitionKeyDefinition ); - let partitionKeyValue = partitionKeyArray && partitionKeyArray[0]; - - const id = new ObjectId(this, updatedDocument, partitionKeyValue); + const id = new ObjectId(this, updatedDocument, partitionKeyArray); documentId.id(id.id()); } }); @@ -214,7 +216,7 @@ export default class MongoDocumentsTab extends DocumentsTab { }) .map((rawDocument: any) => { const partitionKeyValue = rawDocument._partitionKeyValue; - return new DocumentId(this, rawDocument, partitionKeyValue); + return new DocumentId(this, rawDocument, [partitionKeyValue]); }); const merged = currentDocuments.concat(nextDocumentIds); @@ -303,7 +305,7 @@ export default class MongoDocumentsTab extends DocumentsTab { // Convert BsonSchema2 to /path format partitionKey = { kind: partitionKey.kind, - paths: ["/" + this.partitionKeyProperty.replace(/\./g, "/")], + paths: ["/" + this.partitionKeyProperties?.[0].replace(/\./g, "/")], version: partitionKey.version, }; } diff --git a/src/Explorer/Tree/Collection.test.ts b/src/Explorer/Tree/Collection.test.ts index 1c701798e..aa3f227da 100644 --- a/src/Explorer/Tree/Collection.test.ts +++ b/src/Explorer/Tree/Collection.test.ts @@ -37,7 +37,8 @@ describe("Collection", () => { version: 2, }); collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyProperty).toBe("somePartitionKey.anotherPartitionKey"); + expect(collection.partitionKeyProperties.length).toBe(1); + expect(collection.partitionKeyProperties[0]).toBe("somePartitionKey.anotherPartitionKey"); }); it("should strip out forward slashes from single partition key paths", () => { @@ -47,7 +48,8 @@ describe("Collection", () => { version: 2, }); collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyProperty).toBe("somePartitionKey"); + expect(collection.partitionKeyProperties.length).toBe(1); + expect(collection.partitionKeyProperties[0]).toBe("somePartitionKey"); }); }); @@ -61,7 +63,8 @@ describe("Collection", () => { version: 2, }); collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyPropertyHeader).toBe("/somePartitionKey/anotherPartitionKey"); + expect(collection.partitionKeyPropertyHeaders.length).toBe(1); + expect(collection.partitionKeyPropertyHeaders[0]).toBe("/somePartitionKey/anotherPartitionKey"); }); it("should preserve forward slash on a single partition key", () => { @@ -71,7 +74,8 @@ describe("Collection", () => { version: 2, }); collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyPropertyHeader).toBe("/somePartitionKey"); + expect(collection.partitionKeyPropertyHeaders.length).toBe(1); + expect(collection.partitionKeyPropertyHeaders[0]).toBe("/somePartitionKey"); }); it("should be null if there is no partition key", () => { @@ -81,7 +85,7 @@ describe("Collection", () => { kind: "Hash", }); collection = generateMockCollectionWithDataModel(collectionsDataModel); - expect(collection.partitionKeyPropertyHeader).toBeNull(); + expect(collection.partitionKeyPropertyHeaders.length).toBe(0); }); }); }); diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 7deed69e5..9ef429df4 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -50,8 +50,8 @@ export default class Collection implements ViewModels.Collection { public rid: string; public databaseId: string; public partitionKey: DataModels.PartitionKey; - public partitionKeyPropertyHeader: string; - public partitionKeyProperty: string; + public partitionKeyPropertyHeaders: string[]; + public partitionKeyProperties: string[]; public id: ko.Observable; public defaultTtl: ko.Observable; public indexingPolicy: ko.Observable; @@ -120,31 +120,25 @@ export default class Collection implements ViewModels.Collection { this.requestSchema = data.requestSchema; this.geospatialConfig = ko.observable(data.geospatialConfig); - // TODO fix this to only replace non-excaped single quotes - this.partitionKeyProperty = - (this.partitionKey && - this.partitionKey.paths && - this.partitionKey.paths.length && - this.partitionKey.paths.length > 0 && - this.partitionKey.paths[0].replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")) || - null; - this.partitionKeyPropertyHeader = - (this.partitionKey && - this.partitionKey.paths && - this.partitionKey.paths.length > 0 && - this.partitionKey.paths[0]) || - null; + this.partitionKeyPropertyHeaders = this.partitionKey?.paths; + this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => { + // TODO fix this to only replace non-excaped single quotes + let partitionKeyProperty = partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""); - if (userContext.apiType === "Mongo" && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) { - this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, ""); - } + if (userContext.apiType === "Mongo" && partitionKeyProperty) { + if (~partitionKeyProperty.indexOf(`"`)) { + partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, ""); + } + // TODO #10738269 : Add this logic in a derived class for Mongo + if (partitionKeyProperty.indexOf("$v") > -1) { + // From $v.shard.$v.key.$v > shard.key + partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); + this.partitionKeyPropertyHeaders[i] = partitionKeyProperty; + } + } - // TODO #10738269 : Add this logic in a derived class for Mongo - if (userContext.apiType === "Mongo" && this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1) { - // From $v.shard.$v.key.$v > shard.key - this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); - this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty; - } + return partitionKeyProperty; + }); this.documentIds = ko.observableArray([]); this.isCollectionExpanded = ko.observable(false); @@ -471,7 +465,7 @@ export default class Collection implements ViewModels.Collection { collection: this, masterKey: userContext.masterKey || "", - collectionPartitionKeyProperty: this.partitionKeyProperty, + collectionPartitionKeyProperty: this.partitionKeyProperties?.[0], collectionId: this.id(), databaseId: this.databaseId, isTabsContentExpanded: this.container.isTabsContentExpanded, @@ -710,7 +704,7 @@ export default class Collection implements ViewModels.Collection { tabPath: "", collection: this, masterKey: userContext.masterKey || "", - collectionPartitionKeyProperty: this.partitionKeyProperty, + collectionPartitionKeyProperty: this.partitionKeyProperties?.[0], collectionId: this.id(), databaseId: this.databaseId, isTabsContentExpanded: this.container.isTabsContentExpanded, diff --git a/src/Explorer/Tree/ConflictId.ts b/src/Explorer/Tree/ConflictId.ts index 89185b80a..2c35f528f 100644 --- a/src/Explorer/Tree/ConflictId.ts +++ b/src/Explorer/Tree/ConflictId.ts @@ -150,7 +150,7 @@ export default class ConflictId { partitionKeyValueResolved ); - documentId.partitionKeyProperty = this.partitionKeyProperty; + documentId.partitionKeyProperties = [this.partitionKeyProperty]; documentId.partitionKey = this.partitionKey; return documentId; diff --git a/src/Explorer/Tree/DocumentId.ts b/src/Explorer/Tree/DocumentId.ts index af2d0250c..41e102527 100644 --- a/src/Explorer/Tree/DocumentId.ts +++ b/src/Explorer/Tree/DocumentId.ts @@ -9,21 +9,21 @@ export default class DocumentId { public self: string; public ts: string; public id: ko.Observable; - public partitionKeyProperty: string; + public partitionKeyProperties: string[]; public partitionKey: DataModels.PartitionKey; - public partitionKeyValue: any; - public stringPartitionKeyValue: string; + public partitionKeyValue: any[]; + public stringPartitionKeyValues: string[]; public isDirty: ko.Observable; - constructor(container: DocumentsTab, data: any, partitionKeyValue: any) { + constructor(container: DocumentsTab, data: any, partitionKeyValue: any[]) { this.container = container; this.self = data._self; this.rid = data._rid; this.ts = data._ts; this.partitionKeyValue = partitionKeyValue; - this.partitionKeyProperty = container && container.partitionKeyProperty; + this.partitionKeyProperties = container?.partitionKeyProperties; this.partitionKey = container && container.partitionKey; - this.stringPartitionKeyValue = this.getPartitionKeyValueAsString(); + this.stringPartitionKeyValues = this.getPartitionKeyValueAsString(); this.id = ko.observable(data.id); this.isDirty = ko.observable(false); } @@ -46,34 +46,35 @@ export default class DocumentId { } public partitionKeyHeader(): Object { - if (!this.partitionKeyProperty) { + if (!this.partitionKeyProperties || this.partitionKeyProperties.length === 0) { return undefined; } - if (this.partitionKeyValue === undefined) { + if (!this.partitionKeyValue || this.partitionKeyValue.length === 0) { return [{}]; } return [this.partitionKeyValue]; } - public getPartitionKeyValueAsString(): string { - const partitionKeyValue: any = this.partitionKeyValue; - const typeOfPartitionKeyValue: string = typeof partitionKeyValue; + public getPartitionKeyValueAsString(): string[] { + return this.partitionKeyValue?.map((partitionKeyValue) => { + const typeOfPartitionKeyValue: string = typeof partitionKeyValue; - if ( - typeOfPartitionKeyValue === "undefined" || - typeOfPartitionKeyValue === "null" || - typeOfPartitionKeyValue === "object" - ) { - return ""; - } + if ( + typeOfPartitionKeyValue === "undefined" || + typeOfPartitionKeyValue === "null" || + typeOfPartitionKeyValue === "object" + ) { + return ""; + } - if (typeOfPartitionKeyValue === "string") { - return partitionKeyValue; - } + if (typeOfPartitionKeyValue === "string") { + return partitionKeyValue; + } - return JSON.stringify(partitionKeyValue); + return JSON.stringify(partitionKeyValue); + }); } public async loadDocument(): Promise { diff --git a/src/Explorer/Tree/ResourceTokenCollection.ts b/src/Explorer/Tree/ResourceTokenCollection.ts index 04daa74a3..a09a7c214 100644 --- a/src/Explorer/Tree/ResourceTokenCollection.ts +++ b/src/Explorer/Tree/ResourceTokenCollection.ts @@ -22,8 +22,8 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas public rid: string; public rawDataModel: DataModels.Collection; public partitionKey: DataModels.PartitionKey; - public partitionKeyProperty: string; - public partitionKeyPropertyHeader: string; + public partitionKeyProperties: string[]; + public partitionKeyPropertyHeaders: string[]; public id: ko.Observable; public children: ko.ObservableArray; public selectedSubnodeKind: ko.Observable; diff --git a/src/Utils/QueryUtils.ts b/src/Utils/QueryUtils.ts index 71d8a57a0..a4ca1aa76 100644 --- a/src/Utils/QueryUtils.ts +++ b/src/Utils/QueryUtils.ts @@ -3,15 +3,16 @@ import * as ViewModels from "../Contracts/ViewModels"; export function buildDocumentsQuery( filter: string, - partitionKeyProperty: string, + partitionKeyProperties: string[], partitionKey: DataModels.PartitionKey ): string { - let query = partitionKeyProperty - ? `select c.id, c._self, c._rid, c._ts, ${buildDocumentsQueryPartitionProjections( - "c", - partitionKey - )} as _partitionKeyValue from c` - : `select c.id, c._self, c._rid, c._ts from c`; + let query = + partitionKeyProperties && partitionKeyProperties.length > 0 + ? `select c.id, c._self, c._rid, c._ts, [${buildDocumentsQueryPartitionProjections( + "c", + partitionKey + )}] as _partitionKeyValue from c` + : `select c.id, c._self, c._rid, c._ts from c`; if (filter) { query += " " + filter;