diff --git a/CODEOWNERS b/CODEOWNERS index eded95f8b..f201e92c8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,3 +1,4 @@ **/* @github/codeql-vscode-reviewers **/remote-queries/ @github/code-scanning-secexp-reviewers **/variant-analysis/ @github/code-scanning-secexp-reviewers +**/databases/ @github/code-scanning-secexp-reviewers diff --git a/extensions/ql-vscode/src/databases/README.md b/extensions/ql-vscode/src/databases/README.md new file mode 100644 index 000000000..b7ffdc5e9 --- /dev/null +++ b/extensions/ql-vscode/src/databases/README.md @@ -0,0 +1,3 @@ +### Databases + +This folder contains code for the new experimental databases panel and new query run experience. diff --git a/extensions/ql-vscode/src/databases/db-config-store.ts b/extensions/ql-vscode/src/databases/db-config-store.ts new file mode 100644 index 000000000..7d5250b25 --- /dev/null +++ b/extensions/ql-vscode/src/databases/db-config-store.ts @@ -0,0 +1,46 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { cloneDbConfig, DbConfig } from './db-config'; + +export class DbConfigStore { + private readonly configPath: string; + + private config: DbConfig; + + public constructor(workspaceStoragePath: string) { + this.configPath = path.join(workspaceStoragePath, 'dbconfig.json'); + + this.config = this.createEmptyConfig(); + } + + public async initialize(): Promise { + await this.loadConfig(); + } + + public getConfig(): DbConfig { + // Clone the config so that it's not modified outside of this class. + return cloneDbConfig(this.config); + } + + private async loadConfig(): Promise { + if (!await fs.pathExists(this.configPath)) { + await fs.writeJSON(this.configPath, this.createEmptyConfig(), { spaces: 2 }); + } + + await this.readConfig(); + } + + private async readConfig(): Promise { + this.config = await fs.readJSON(this.configPath); + } + + private createEmptyConfig(): DbConfig { + return { + remote: { + repositoryLists: [], + owners: [], + repositories: [], + } + }; + } +} diff --git a/extensions/ql-vscode/src/databases/db-config.ts b/extensions/ql-vscode/src/databases/db-config.ts new file mode 100644 index 000000000..cd5e04868 --- /dev/null +++ b/extensions/ql-vscode/src/databases/db-config.ts @@ -0,0 +1,29 @@ +// Contains models for the data we want to store in the database config + +export interface DbConfig { + remote: RemoteDbConfig; +} + +export interface RemoteDbConfig { + repositoryLists: RemoteRepositoryList[]; + owners: string[]; + repositories: string[]; +} + +export interface RemoteRepositoryList { + name: string; + repositories: string[]; +} + +export function cloneDbConfig(config: DbConfig): DbConfig { + return { + remote: { + repositoryLists: config.remote.repositoryLists.map((list) => ({ + name: list.name, + repositories: [...list.repositories], + })), + owners: [...config.remote.owners], + repositories: [...config.remote.repositories], + } + }; +} diff --git a/extensions/ql-vscode/test/pure-tests/databases/data/dbconfig.json b/extensions/ql-vscode/test/pure-tests/databases/data/dbconfig.json new file mode 100644 index 000000000..64807c4d2 --- /dev/null +++ b/extensions/ql-vscode/test/pure-tests/databases/data/dbconfig.json @@ -0,0 +1,12 @@ +{ + "remote": { + "repositoryLists": [ + { + "name": "repoList1", + "repositories": ["foo/bar", "foo/baz"] + } + ], + "owners": [], + "repositories": ["owner/repo1", "owner/repo2", "owner/repo3"] + } +} diff --git a/extensions/ql-vscode/test/pure-tests/databases/db-config-store.test.ts b/extensions/ql-vscode/test/pure-tests/databases/db-config-store.test.ts new file mode 100644 index 000000000..a20364708 --- /dev/null +++ b/extensions/ql-vscode/test/pure-tests/databases/db-config-store.test.ts @@ -0,0 +1,57 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { DbConfigStore } from '../../../src/databases/db-config-store'; +import { expect } from 'chai'; + + +describe('db config store', async () => { + const workspaceStoragePath = path.join(__dirname, 'test-workspace'); + const testStoragePath = path.join(__dirname, 'data'); + + beforeEach(async () => { + await fs.ensureDir(workspaceStoragePath); + }); + + afterEach(async () => { + await fs.remove(workspaceStoragePath); + }); + + it('should create a new config if one does not exist', async () => { + const configPath = path.join(workspaceStoragePath, 'dbconfig.json'); + + const configStore = new DbConfigStore(workspaceStoragePath); + await configStore.initialize(); + + expect(await fs.pathExists(configPath)).to.be.true; + const config = configStore.getConfig(); + expect(config.remote.repositoryLists).to.be.empty; + expect(config.remote.owners).to.be.empty; + expect(config.remote.repositories).to.be.empty; + }); + + it('should load an existing config', async () => { + const configStore = new DbConfigStore(testStoragePath); + await configStore.initialize(); + + const config = configStore.getConfig(); + expect(config.remote.repositoryLists).to.have.length(1); + expect(config.remote.repositoryLists[0]).to.deep.equal({ + 'name': 'repoList1', + 'repositories': ['foo/bar', 'foo/baz'] + }); + expect(config.remote.owners).to.be.empty; + expect(config.remote.repositories).to.have.length(3); + expect(config.remote.repositories).to.deep.equal(['owner/repo1', 'owner/repo2', 'owner/repo3']); + }); + + it('should not allow modification of the config', async () => { + const configStore = new DbConfigStore(testStoragePath); + await configStore.initialize(); + + const config = configStore.getConfig(); + config.remote.repositoryLists = []; + + const reRetrievedConfig = configStore.getConfig(); + expect(reRetrievedConfig.remote.repositoryLists).to.have.length(1); + }); +});