mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-04-19 21:09:46 +01:00
feat: Redesign container-copy flow to select destination account and enable cross-account container creation
This commit is contained in:
committed by
BChoudhury-ms
parent
eac5842176
commit
8698c6a3e2
@@ -34,6 +34,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
databaseId: params.databaseId,
|
||||
databaseLevelThroughput: params.databaseLevelThroughput,
|
||||
offerThroughput: params.offerThroughput,
|
||||
targetAccountOverride: params.targetAccountOverride,
|
||||
};
|
||||
await createDatabase(createDatabaseParams);
|
||||
}
|
||||
@@ -63,7 +64,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
};
|
||||
|
||||
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
if (!params.createNewDatabase) {
|
||||
if (!params.createNewDatabase && !params.targetAccountOverride) {
|
||||
const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId);
|
||||
if (!isValid) {
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
@@ -122,9 +123,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
||||
};
|
||||
|
||||
const createResponse = await createUpdateSqlContainer(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId,
|
||||
params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup,
|
||||
params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
rpPayload,
|
||||
|
||||
134
src/Common/dataAccess/createDatabase.test.ts
Normal file
134
src/Common/dataAccess/createDatabase.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
jest.mock("../../Utils/arm/request");
|
||||
jest.mock("../CosmosClient");
|
||||
jest.mock("../../Utils/arm/generatedClients/cosmos/sqlResources");
|
||||
|
||||
import ko from "knockout";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { CreateDatabaseParams, DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { SqlDatabaseGetResults } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { createDatabase } from "./createDatabase";
|
||||
|
||||
const mockCreateUpdateSqlDatabase = createUpdateSqlDatabase as jest.MockedFunction<typeof createUpdateSqlDatabase>;
|
||||
|
||||
describe("createDatabase", () => {
|
||||
beforeAll(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: { name: "default-account" } as DatabaseAccount,
|
||||
subscriptionId: "default-subscription",
|
||||
resourceGroup: "default-rg",
|
||||
apiType: "SQL",
|
||||
authType: AuthType.AAD,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockCreateUpdateSqlDatabase.mockResolvedValue({
|
||||
properties: { resource: { id: "db", _rid: "", _self: "", _ts: 0, _etag: "" } },
|
||||
} as SqlDatabaseGetResults);
|
||||
useDatabases.setState({
|
||||
databases: [],
|
||||
validateDatabaseId: () => true,
|
||||
} as unknown as ReturnType<typeof useDatabases.getState>);
|
||||
});
|
||||
|
||||
it("should call ARM createUpdateSqlDatabase when logged in with AAD", async () => {
|
||||
await createDatabase({ databaseId: "testDb" });
|
||||
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("targetAccountOverride behavior", () => {
|
||||
it("should use targetAccountOverride subscriptionId, resourceGroup, and accountName for SQL DB creation", async () => {
|
||||
const params: CreateDatabaseParams = {
|
||||
databaseId: "testDb",
|
||||
targetAccountOverride: {
|
||||
subscriptionId: "override-sub",
|
||||
resourceGroup: "override-rg",
|
||||
accountName: "override-account",
|
||||
},
|
||||
};
|
||||
|
||||
await createDatabase(params);
|
||||
|
||||
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
|
||||
"override-sub",
|
||||
"override-rg",
|
||||
"override-account",
|
||||
"testDb",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should use userContext values when targetAccountOverride is not provided", async () => {
|
||||
await createDatabase({ databaseId: "testDb" });
|
||||
|
||||
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
|
||||
"default-subscription",
|
||||
"default-rg",
|
||||
"default-account",
|
||||
"testDb",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should skip validateDatabaseId check when targetAccountOverride is provided", async () => {
|
||||
// Simulate database already existing — validateDatabaseId returns false
|
||||
useDatabases.setState({
|
||||
databases: [{ id: ko.observable("testDb") } as unknown as ViewModels.Database],
|
||||
validateDatabaseId: () => false,
|
||||
} as unknown as ReturnType<typeof useDatabases.getState>);
|
||||
|
||||
const params: CreateDatabaseParams = {
|
||||
databaseId: "testDb",
|
||||
targetAccountOverride: {
|
||||
subscriptionId: "override-sub",
|
||||
resourceGroup: "override-rg",
|
||||
accountName: "override-account",
|
||||
},
|
||||
};
|
||||
|
||||
// Should NOT throw even though the normal duplicate check would fail
|
||||
await expect(createDatabase(params)).resolves.not.toThrow();
|
||||
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw if validateDatabaseId returns false and no targetAccountOverride is set", async () => {
|
||||
useDatabases.setState({
|
||||
databases: [{ id: ko.observable("existingDb") } as unknown as ViewModels.Database],
|
||||
validateDatabaseId: () => false,
|
||||
} as unknown as ReturnType<typeof useDatabases.getState>);
|
||||
|
||||
await expect(createDatabase({ databaseId: "existingDb" })).rejects.toThrow();
|
||||
expect(mockCreateUpdateSqlDatabase).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should pass databaseId in request payload regardless of targetAccountOverride", async () => {
|
||||
const params: CreateDatabaseParams = {
|
||||
databaseId: "my-database",
|
||||
targetAccountOverride: {
|
||||
subscriptionId: "any-sub",
|
||||
resourceGroup: "any-rg",
|
||||
accountName: "any-account",
|
||||
},
|
||||
};
|
||||
|
||||
await createDatabase(params);
|
||||
|
||||
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
"my-database",
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
resource: expect.objectContaining({ id: "my-database" }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
|
||||
}
|
||||
|
||||
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||
if (!useDatabases.getState().validateDatabaseId(params.databaseId)) {
|
||||
if (!params.targetAccountOverride && !useDatabases.getState().validateDatabaseId(params.databaseId)) {
|
||||
const databaseName = getDatabaseName().toLocaleLowerCase();
|
||||
throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`);
|
||||
}
|
||||
@@ -72,13 +72,10 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
|
||||
options,
|
||||
},
|
||||
};
|
||||
const createResponse = await createUpdateSqlDatabase(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
rpPayload,
|
||||
);
|
||||
const sub = params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
|
||||
const rg = params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
|
||||
const acct = params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name;
|
||||
const createResponse = await createUpdateSqlDatabase(sub, rg, acct, params.databaseId, rpPayload);
|
||||
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
jest.mock("../../Utils/arm/request");
|
||||
jest.mock("../CosmosClient");
|
||||
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { client } from "../CosmosClient";
|
||||
import { readDatabases } from "./readDatabases";
|
||||
import { readDatabases, readDatabasesForAccount } from "./readDatabases";
|
||||
|
||||
describe("readDatabases", () => {
|
||||
beforeAll(() => {
|
||||
@@ -42,3 +43,64 @@ describe("readDatabases", () => {
|
||||
expect(client).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("readDatabasesForAccount", () => {
|
||||
const mockDatabase = { id: "testDb", _rid: "", _self: "", _etag: "", _ts: 0 };
|
||||
const mockArmResponse = { value: [{ properties: { resource: mockDatabase } }] };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should call ARM with a path that includes the provided subscriptionId, resourceGroup, and accountName", async () => {
|
||||
(armRequest as jest.Mock).mockResolvedValue(mockArmResponse);
|
||||
|
||||
await readDatabasesForAccount("test-sub", "test-rg", "test-account");
|
||||
|
||||
expect(armRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: expect.stringContaining("/subscriptions/test-sub/resourceGroups/test-rg/"),
|
||||
}),
|
||||
);
|
||||
expect(armRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: expect.stringContaining("/databaseAccounts/test-account/sqlDatabases"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return mapped database resources from the response", async () => {
|
||||
const db1 = { id: "db1", _rid: "r1", _self: "/dbs/db1", _etag: "", _ts: 1 };
|
||||
const db2 = { id: "db2", _rid: "r2", _self: "/dbs/db2", _etag: "", _ts: 2 };
|
||||
|
||||
(armRequest as jest.Mock).mockResolvedValue({
|
||||
value: [{ properties: { resource: db1 } }, { properties: { resource: db2 } }],
|
||||
});
|
||||
|
||||
const result = await readDatabasesForAccount("sub", "rg", "account");
|
||||
|
||||
expect(result).toEqual([db1, db2]);
|
||||
});
|
||||
|
||||
it("should return an empty array when the response is null", async () => {
|
||||
(armRequest as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const result = await readDatabasesForAccount("sub", "rg", "account");
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return an empty array when value is an empty list", async () => {
|
||||
(armRequest as jest.Mock).mockResolvedValue({ value: [] });
|
||||
|
||||
const result = await readDatabasesForAccount("sub", "rg", "account");
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should throw and propagate errors from the ARM call", async () => {
|
||||
(armRequest as jest.Mock).mockRejectedValue(new Error("ARM request failed"));
|
||||
|
||||
await expect(readDatabasesForAccount("sub", "rg", "account")).rejects.toThrow("ARM request failed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,3 +112,20 @@ async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
|
||||
|
||||
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database);
|
||||
}
|
||||
|
||||
export async function readDatabasesForAccount(
|
||||
subscriptionId: string,
|
||||
resourceGroup: string,
|
||||
accountName: string,
|
||||
): Promise<DataModels.Database[]> {
|
||||
const clearMessage = logConsoleProgress(`Querying databases for account ${accountName}`);
|
||||
try {
|
||||
const rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
|
||||
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? [];
|
||||
} catch (error) {
|
||||
handleError(error, "ReadDatabasesForAccount", `Error while querying databases for account ${accountName}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user