diff --git a/extensions/ql-vscode/src/databases/config/db-config-store.ts b/extensions/ql-vscode/src/databases/config/db-config-store.ts index bacc2f8cf..1565213ed 100644 --- a/extensions/ql-vscode/src/databases/config/db-config-store.ts +++ b/extensions/ql-vscode/src/databases/config/db-config-store.ts @@ -1,6 +1,13 @@ import { pathExists, outputJSON, readJSON, readJSONSync } from "fs-extra"; import { join } from "path"; -import { cloneDbConfig, DbConfig, SelectedDbItem } from "./db-config"; +import { + cloneDbConfig, + DbConfig, + renameLocalDb, + renameLocalList, + renameRemoteList, + SelectedDbItem, +} from "./db-config"; import * as chokidar from "chokidar"; import { DisposableObject, DisposeHandler } from "../../pure/disposable-object"; import { DbConfigValidator } from "./db-config-validator"; @@ -11,6 +18,11 @@ import { DbConfigValidationErrorKind, } from "../db-validation-errors"; import { ValueResult } from "../../common/value-result"; +import { + LocalDatabaseDbItem, + LocalListDbItem, + RemoteUserDefinedListDbItem, +} from "../db-item"; export class DbConfigStore extends DisposableObject { public readonly onDidChangeConfig: AppEvent; @@ -161,6 +173,65 @@ export class DbConfigStore extends DisposableObject { await this.writeConfig(config); } + public async renameLocalList( + currentDbItem: LocalListDbItem, + newName: string, + ) { + if (!this.config) { + throw Error("Cannot rename local list if config is not loaded"); + } + + this.validateLocalListName(newName); + + const updatedConfig = renameLocalList( + this.config, + currentDbItem.listName, + newName, + ); + + await this.writeConfig(updatedConfig); + } + + public async renameRemoteList( + currentDbItem: RemoteUserDefinedListDbItem, + newName: string, + ) { + if (!this.config) { + throw Error("Cannot rename remote list if config is not loaded"); + } + + this.validateRemoteListName(newName); + + const updatedConfig = renameRemoteList( + this.config, + currentDbItem.listName, + newName, + ); + + await this.writeConfig(updatedConfig); + } + + public async renameLocalDb( + currentDbItem: LocalDatabaseDbItem, + newName: string, + parentListName?: string, + ): Promise { + if (!this.config) { + throw Error("Cannot rename local db if config is not loaded"); + } + + this.validateLocalDbName(newName); + + const updatedConfig = renameLocalDb( + this.config, + currentDbItem.databaseName, + newName, + parentListName, + ); + + await this.writeConfig(updatedConfig); + } + public doesRemoteListExist(listName: string): boolean { if (!this.config) { throw Error("Cannot check remote list existence if config is not loaded"); @@ -179,6 +250,23 @@ export class DbConfigStore extends DisposableObject { return this.config.databases.local.lists.some((l) => l.name === listName); } + public doesLocalDbExist(dbName: string, listName?: string): boolean { + if (!this.config) { + throw Error( + "Cannot check remote database existence if config is not loaded", + ); + } + + if (listName) { + return this.config.databases.local.lists.some( + (l) => + l.name === listName && l.databases.some((d) => d.name === dbName), + ); + } + + return this.config.databases.local.databases.some((d) => d.name === dbName); + } + public doesRemoteDbExist(dbName: string, listName?: string): boolean { if (!this.config) { throw Error( @@ -344,4 +432,14 @@ export class DbConfigStore extends DisposableObject { throw Error(`A remote list with the name '${listName}' already exists`); } } + + private validateLocalDbName(dbName: string): void { + if (dbName === "") { + throw Error("Database name cannot be empty"); + } + + if (this.doesLocalDbExist(dbName)) { + throw Error(`A local database with the name '${dbName}' already exists`); + } + } } diff --git a/extensions/ql-vscode/src/databases/config/db-config.ts b/extensions/ql-vscode/src/databases/config/db-config.ts index 688551cc2..7d16de9fb 100644 --- a/extensions/ql-vscode/src/databases/config/db-config.ts +++ b/extensions/ql-vscode/src/databases/config/db-config.ts @@ -114,6 +114,102 @@ export function cloneDbConfig(config: DbConfig): DbConfig { }; } +export function renameLocalList( + originalConfig: DbConfig, + currentListName: string, + newListName: string, +): DbConfig { + const config = cloneDbConfig(originalConfig); + + const list = config.databases.local.lists.find( + (l) => l.name === currentListName, + ); + if (!list) { + throw Error(`Cannot find list '${currentListName}' to rename`); + } + list.name = newListName; + + if ( + config.selected?.kind === SelectedDbItemKind.LocalUserDefinedList || + config.selected?.kind === SelectedDbItemKind.LocalDatabase + ) { + if (config.selected.listName === currentListName) { + config.selected.listName = newListName; + } + } + + return config; +} + +export function renameRemoteList( + originalConfig: DbConfig, + currentListName: string, + newListName: string, +): DbConfig { + const config = cloneDbConfig(originalConfig); + + const list = config.databases.remote.repositoryLists.find( + (l) => l.name === currentListName, + ); + if (!list) { + throw Error(`Cannot find list '${currentListName}' to rename`); + } + list.name = newListName; + + if ( + config.selected?.kind === SelectedDbItemKind.RemoteUserDefinedList || + config.selected?.kind === SelectedDbItemKind.RemoteRepository + ) { + if (config.selected.listName === currentListName) { + config.selected.listName = newListName; + } + } + + return config; +} + +export function renameLocalDb( + originalConfig: DbConfig, + currentDbName: string, + newDbName: string, + parentListName?: string, +): DbConfig { + const config = cloneDbConfig(originalConfig); + + if (parentListName) { + const list = config.databases.local.lists.find( + (l) => l.name === parentListName, + ); + if (!list) { + throw Error(`Cannot find parent list '${parentListName}'`); + } + const dbIndex = list.databases.findIndex((db) => db.name === currentDbName); + if (dbIndex === -1) { + throw Error( + `Cannot find database '${currentDbName}' in list '${parentListName}'`, + ); + } + list.databases[dbIndex].name = newDbName; + } else { + const dbIndex = config.databases.local.databases.findIndex( + (db) => db.name === currentDbName, + ); + if (dbIndex === -1) { + throw Error(`Cannot find database '${currentDbName}' in local databases`); + } + config.databases.local.databases[dbIndex].name = newDbName; + } + + if ( + config.selected?.kind === SelectedDbItemKind.LocalDatabase && + config.selected.databaseName === currentDbName + ) { + config.selected.databaseName = newDbName; + } + + return config; +} + function cloneDbConfigSelectedItem(selected: SelectedDbItem): SelectedDbItem { switch (selected.kind) { case SelectedDbItemKind.LocalUserDefinedList: diff --git a/extensions/ql-vscode/test/unit-tests/databases/config/db-config-store.test.ts b/extensions/ql-vscode/test/unit-tests/databases/config/db-config-store.test.ts index f2a098503..3af6958f1 100644 --- a/extensions/ql-vscode/test/unit-tests/databases/config/db-config-store.test.ts +++ b/extensions/ql-vscode/test/unit-tests/databases/config/db-config-store.test.ts @@ -1,6 +1,20 @@ -import { ensureDir, remove, pathExists } from "fs-extra"; +import { ensureDir, remove, pathExists, writeJSON, readJSON } from "fs-extra"; import { join } from "path"; +import { App } from "../../../../src/common/app"; +import { + DbConfig, + SelectedDbItemKind, +} from "../../../../src/databases/config/db-config"; import { DbConfigStore } from "../../../../src/databases/config/db-config-store"; +import { + createDbConfig, + createLocalDbConfigItem, +} from "../../../factories/db-config-factories"; +import { + createLocalDatabaseDbItem, + createLocalListDbItem, + createRemoteUserDefinedListDbItem, +} from "../../../factories/db-item-factories"; import { createMockApp } from "../../../__mocks__/appMock"; describe("db config store", () => { @@ -167,4 +181,188 @@ describe("db config store", () => { configStore.dispose(); }); }); + + describe("db and list renaming", () => { + let app: App; + let configPath: string; + + beforeEach(async () => { + app = createMockApp({ + extensionPath, + workspaceStoragePath: tempWorkspaceStoragePath, + }); + + configPath = join(tempWorkspaceStoragePath, "workspace-databases.json"); + }); + + it("should allow renaming a remote list", async () => { + // Initial set up + const dbConfig = createDbConfig({ + remoteLists: [ + { + name: "list1", + repositories: ["owner/repo1", "owner/repo2"], + }, + ], + selected: { + kind: SelectedDbItemKind.RemoteRepository, + repositoryName: "owner/repo2", + listName: "list1", + }, + }); + + await writeJSON(configPath, dbConfig); + + const configStore = new DbConfigStore(app); + await configStore.initialize(); + + // Rename + const currentDbItem = createRemoteUserDefinedListDbItem({ + listName: "list1", + }); + await configStore.renameRemoteList(currentDbItem, "listRenamed"); + + // Read the config file + const updatedDbConfig = (await readJSON(configPath)) as DbConfig; + + // Check that the config file has been updated + const updatedRemoteDbs = updatedDbConfig.databases.remote; + expect(updatedRemoteDbs.repositoryLists).toHaveLength(1); + expect(updatedRemoteDbs.repositoryLists[0].name).toEqual("listRenamed"); + + expect(updatedDbConfig.selected).toEqual({ + kind: SelectedDbItemKind.RemoteRepository, + repositoryName: "owner/repo2", + listName: "listRenamed", + }); + + configStore.dispose(); + }); + + it("should allow renaming a local list", async () => { + // Initial set up + const dbConfig = createDbConfig({ + localLists: [ + { + name: "list1", + databases: [ + createLocalDbConfigItem(), + createLocalDbConfigItem(), + createLocalDbConfigItem(), + ], + }, + ], + selected: { + kind: SelectedDbItemKind.LocalUserDefinedList, + listName: "list1", + }, + }); + + await writeJSON(configPath, dbConfig); + + const configStore = new DbConfigStore(app); + await configStore.initialize(); + + // Rename + const currentDbItem = createLocalListDbItem({ + listName: "list1", + }); + await configStore.renameLocalList(currentDbItem, "listRenamed"); + + // Read the config file + const updatedDbConfig = (await readJSON(configPath)) as DbConfig; + + // Check that the config file has been updated + const updatedLocalDbs = updatedDbConfig.databases.local; + expect(updatedLocalDbs.lists).toHaveLength(1); + expect(updatedLocalDbs.lists[0].name).toEqual("listRenamed"); + + expect(updatedDbConfig.selected).toEqual({ + kind: SelectedDbItemKind.LocalUserDefinedList, + listName: "listRenamed", + }); + + configStore.dispose(); + }); + + it("should allow renaming of a local db", async () => { + // Initial set up + const dbConfig = createDbConfig({ + localLists: [ + { + name: "list1", + databases: [ + createLocalDbConfigItem({ name: "db1" }), + createLocalDbConfigItem({ name: "db2" }), + createLocalDbConfigItem({ name: "db3" }), + ], + }, + ], + selected: { + kind: SelectedDbItemKind.LocalDatabase, + databaseName: "db1", + listName: "list1", + }, + }); + + await writeJSON(configPath, dbConfig); + + const configStore = new DbConfigStore(app); + await configStore.initialize(); + + // Rename + const currentDbItem = createLocalDatabaseDbItem({ + databaseName: "db1", + }); + await configStore.renameLocalDb(currentDbItem, "dbRenamed", "list1"); + + // Read the config file + const updatedDbConfig = (await readJSON(configPath)) as DbConfig; + + // Check that the config file has been updated + const updatedLocalDbs = updatedDbConfig.databases.local; + expect(updatedLocalDbs.lists).toHaveLength(1); + expect(updatedLocalDbs.lists[0].name).toEqual("list1"); + expect(updatedLocalDbs.lists[0].databases.length).toEqual(3); + expect(updatedLocalDbs.lists[0].databases[0].name).toEqual("dbRenamed"); + expect(updatedDbConfig.selected).toEqual({ + kind: SelectedDbItemKind.LocalDatabase, + databaseName: "dbRenamed", + listName: "list1", + }); + + configStore.dispose(); + }); + + it("should throw if the name of a list is taken", async () => { + // Initial set up + const dbConfig = createDbConfig({ + remoteLists: [ + { + name: "list1", + repositories: ["owner/repo1", "owner/repo2"], + }, + { + name: "list2", + repositories: ["owner/repo1", "owner/repo2"], + }, + ], + }); + + await writeJSON(configPath, dbConfig); + + const configStore = new DbConfigStore(app); + await configStore.initialize(); + + // Rename + const currentDbItem = createRemoteUserDefinedListDbItem({ + listName: "list1", + }); + await expect( + configStore.renameRemoteList(currentDbItem, "list2"), + ).rejects.toThrow(`A remote list with the name 'list2' already exists`); + + configStore.dispose(); + }); + }); }); diff --git a/extensions/ql-vscode/test/unit-tests/databases/config/db-config.test.ts b/extensions/ql-vscode/test/unit-tests/databases/config/db-config.test.ts new file mode 100644 index 000000000..3cde3a67b --- /dev/null +++ b/extensions/ql-vscode/test/unit-tests/databases/config/db-config.test.ts @@ -0,0 +1,363 @@ +import { + LocalList, + renameLocalDb, + renameLocalList, + renameRemoteList, + SelectedDbItemKind, +} from "../../../../src/databases/config/db-config"; +import { + createDbConfig, + createLocalDbConfigItem, +} from "../../../factories/db-config-factories"; + +describe("db config", () => { + describe("renameLocalList", () => { + it("should rename a local list", () => { + const originalConfig = createDbConfig({ + localLists: [ + { + name: "list1", + databases: [], + }, + { + name: "list2", + databases: [], + }, + ], + }); + + const updatedConfig = renameLocalList( + originalConfig, + "list1", + "listRenamed", + ); + + expect(updatedConfig.databases.local.lists).toEqual([ + { + name: "listRenamed", + databases: [], + }, + { + name: "list2", + databases: [], + }, + ]); + }); + + it("should rename a selected local list", () => { + const originalConfig = createDbConfig({ + localLists: [ + { + name: "list1", + databases: [], + }, + { + name: "list2", + databases: [], + }, + ], + selected: { + kind: SelectedDbItemKind.LocalUserDefinedList, + listName: "list1", + }, + }); + + const updatedConfig = renameLocalList( + originalConfig, + "list1", + "listRenamed", + ); + + expect(updatedConfig.databases.local.lists).toEqual([ + { + name: "listRenamed", + databases: [], + }, + { + name: "list2", + databases: [], + }, + ]); + + expect(updatedConfig.selected).toEqual({ + kind: SelectedDbItemKind.LocalUserDefinedList, + listName: "listRenamed", + }); + }); + + it("should rename a local list with a db that is selected", () => { + const selectedLocalDb = createLocalDbConfigItem(); + const list1: LocalList = { + name: "list1", + databases: [ + createLocalDbConfigItem(), + selectedLocalDb, + createLocalDbConfigItem(), + ], + }; + const list2: LocalList = { + name: "list2", + databases: [], + }; + + const originalConfig = createDbConfig({ + localLists: [list1, list2], + selected: { + kind: SelectedDbItemKind.LocalDatabase, + databaseName: selectedLocalDb.name, + listName: list1.name, + }, + }); + + const updatedConfig = renameLocalList( + originalConfig, + list1.name, + "listRenamed", + ); + + expect(updatedConfig.databases.local.lists.length).toEqual(2); + expect(updatedConfig.databases.local.lists[0]).toEqual({ + ...list1, + name: "listRenamed", + }); + expect(updatedConfig.databases.local.lists[1]).toEqual(list2); + + expect(updatedConfig.selected).toEqual({ + kind: SelectedDbItemKind.LocalDatabase, + databaseName: selectedLocalDb.name, + listName: "listRenamed", + }); + }); + }); + + describe("renameRemoteList", () => { + it("should rename a remote list", () => { + const originalConfig = createDbConfig({ + remoteLists: [ + { + name: "list1", + repositories: [], + }, + { + name: "list2", + repositories: [], + }, + ], + }); + + const updatedConfig = renameRemoteList( + originalConfig, + "list1", + "listRenamed", + ); + + expect(updatedConfig.databases.remote.repositoryLists).toEqual([ + { + name: "listRenamed", + repositories: [], + }, + { + name: "list2", + repositories: [], + }, + ]); + }); + + it("should rename a selected remote list", () => { + const originalConfig = createDbConfig({ + remoteLists: [ + { + name: "list1", + repositories: [], + }, + { + name: "list2", + repositories: [], + }, + ], + selected: { + kind: SelectedDbItemKind.RemoteUserDefinedList, + listName: "list1", + }, + }); + + const updatedConfig = renameRemoteList( + originalConfig, + "list1", + "listRenamed", + ); + + expect(updatedConfig.databases.remote.repositoryLists).toEqual([ + { + name: "listRenamed", + repositories: [], + }, + { + name: "list2", + repositories: [], + }, + ]); + + expect(updatedConfig.selected).toEqual({ + kind: SelectedDbItemKind.RemoteUserDefinedList, + listName: "listRenamed", + }); + }); + + it("should rename a remote list with a db that is selected", () => { + const selectedRemoteRepo = "owner/repo2"; + const originalConfig = createDbConfig({ + remoteLists: [ + { + name: "list1", + repositories: ["owner1/repo1", selectedRemoteRepo, "owner1/repo3"], + }, + { + name: "list2", + repositories: [], + }, + ], + selected: { + kind: SelectedDbItemKind.RemoteRepository, + repositoryName: selectedRemoteRepo, + listName: "list1", + }, + }); + + const updatedConfig = renameRemoteList( + originalConfig, + "list1", + "listRenamed", + ); + const updatedRepositoryLists = + updatedConfig.databases.remote.repositoryLists; + + expect(updatedRepositoryLists.length).toEqual(2); + expect(updatedRepositoryLists[0]).toEqual({ + ...originalConfig.databases.remote.repositoryLists[0], + name: "listRenamed", + }); + expect(updatedRepositoryLists[1]).toEqual( + originalConfig.databases.remote.repositoryLists[1], + ); + + expect(updatedConfig.selected).toEqual({ + kind: SelectedDbItemKind.RemoteRepository, + repositoryName: selectedRemoteRepo, + listName: "listRenamed", + }); + }); + }); + + describe("renameLocalDb", () => { + it("should rename a local db", () => { + const db1 = createLocalDbConfigItem({ name: "db1" }); + const db2 = createLocalDbConfigItem({ name: "db2" }); + + const originalConfig = createDbConfig({ + localLists: [ + { + name: "list1", + databases: [ + createLocalDbConfigItem({ name: "db1" }), + createLocalDbConfigItem({ name: "db2" }), + ], + }, + ], + localDbs: [db1, db2], + }); + + const updatedConfig = renameLocalDb(originalConfig, "db1", "dbRenamed"); + + const updatedLocalDbs = updatedConfig.databases.local; + const originalLocalDbs = originalConfig.databases.local; + + expect(updatedLocalDbs.lists).toEqual(originalLocalDbs.lists); + expect(updatedLocalDbs.databases.length).toEqual(2); + expect(updatedLocalDbs.databases[0]).toEqual({ + ...db1, + name: "dbRenamed", + }); + expect(updatedLocalDbs.databases[1]).toEqual(db2); + }); + + it("should rename a local db inside a list", () => { + const db1List1 = createLocalDbConfigItem({ name: "db1" }); + const db2List1 = createLocalDbConfigItem({ name: "db2" }); + + const originalConfig = createDbConfig({ + localLists: [ + { + name: "list1", + databases: [db1List1, db2List1], + }, + { + name: "list2", + databases: [ + createLocalDbConfigItem({ name: "db1" }), + createLocalDbConfigItem({ name: "db2" }), + ], + }, + ], + localDbs: [ + createLocalDbConfigItem({ name: "db1" }), + createLocalDbConfigItem({ name: "db2" }), + ], + }); + + const updatedConfig = renameLocalDb( + originalConfig, + db1List1.name, + "dbRenamed", + "list1", + ); + + const updatedLocalDbs = updatedConfig.databases.local; + const originalLocalDbs = originalConfig.databases.local; + expect(updatedLocalDbs.databases).toEqual(originalLocalDbs.databases); + expect(updatedLocalDbs.lists.length).toEqual(2); + expect(updatedLocalDbs.lists[0].databases.length).toEqual(2); + expect(updatedLocalDbs.lists[0].databases[0]).toEqual({ + ...db1List1, + name: "dbRenamed", + }); + expect(updatedLocalDbs.lists[0].databases[1]).toEqual(db2List1); + expect(updatedLocalDbs.lists[1]).toEqual(originalLocalDbs.lists[1]); + }); + + it("should rename a local db that is selected", () => { + const db1 = createLocalDbConfigItem({ name: "db1" }); + const db2 = createLocalDbConfigItem({ name: "db2" }); + + const originalConfig = createDbConfig({ + localLists: [ + { + name: "list1", + databases: [ + createLocalDbConfigItem({ name: "db1" }), + createLocalDbConfigItem({ name: "db2" }), + ], + }, + ], + localDbs: [db1, db2], + selected: { + kind: SelectedDbItemKind.LocalDatabase, + databaseName: "db1", + }, + }); + + const updatedConfig = renameLocalDb(originalConfig, "db1", "dbRenamed"); + + const updatedLocalDbs = updatedConfig.databases.local; + const originalLocalDbs = originalConfig.databases.local; + + expect(updatedLocalDbs.lists).toEqual(originalLocalDbs.lists); + expect(updatedLocalDbs.databases.length).toEqual(2); + expect(updatedLocalDbs.databases[0]).toEqual({ + ...db1, + name: "dbRenamed", + }); + expect(updatedLocalDbs.databases[1]).toEqual(db2); + }); + }); +});