From 156b9bd1d5cdc9390948cec2b4ee0bfee5238374 Mon Sep 17 00:00:00 2001 From: Dan Livings Date: Wed, 16 Oct 2024 16:50:05 +0100 Subject: [PATCH] Scope repositories by organisation in the database This now saves repository-scoped data with the `name` being of the format `org/reponame`. This allows for a method like `getAllRepositoriesForOrg`, which provides two benefits: 1. Towtruck can provide per-organisation dashboards. 2. Towtruck can restrict access to organisations for which the logged in user should not have access. --- db/index.js | 21 ++++++++++ db/index.test.js | 55 +++++++++++++++++++++++++++ utils/githubApi/fetchAllRepos.js | 5 ++- utils/githubApi/fetchAllRepos.test.js | 24 ++++++------ 4 files changed, 92 insertions(+), 13 deletions(-) diff --git a/db/index.js b/db/index.js index cd95619..a2b3b31 100644 --- a/db/index.js +++ b/db/index.js @@ -30,6 +30,7 @@ export class TowtruckDatabase { #saveStatement; #getStatement; #getAllForNameStatement; + #getAllForNameLikeStatement; #getAllForScopeStatement; #deleteAllForScopeStatement; @@ -39,6 +40,7 @@ export class TowtruckDatabase { this.#saveStatement = this.#db.prepare("INSERT INTO towtruck_data (scope, name, key, value) VALUES (?, ?, ?, ?) ON CONFLICT DO UPDATE SET value = excluded.value;"); this.#getStatement = this.#db.prepare("SELECT value FROM towtruck_data WHERE scope = ? AND name = ? AND key = ?;"); this.#getAllForNameStatement = this.#db.prepare("SELECT key, value FROM towtruck_data WHERE scope = ? AND name = ?;"); + this.#getAllForNameLikeStatement = this.#db.prepare("SELECT name, key, value FROM towtruck_data WHERE scope = ? AND name LIKE ?;") this.#getAllForScopeStatement = this.#db.prepare("SELECT name, key, value FROM towtruck_data WHERE scope = ?;"); this.#deleteAllForScopeStatement = this.#db.prepare("DELETE FROM towtruck_data WHERE scope = ?;"); } @@ -62,6 +64,21 @@ export class TowtruckDatabase { return result; } + #getAllForNameLike(scope, namePattern) { + const rows = this.#getAllForNameLikeStatement.all(scope, namePattern); + + const result = {}; + rows.forEach((row) => { + if (!result[row.name]) { + result[row.name] = {}; + } + + result[row.name][row.key] = JSON.parse(row.value); + }); + + return result; + } + #getAllForScope(scope) { const rows = this.#getAllForScopeStatement.all(scope); @@ -97,6 +114,10 @@ export class TowtruckDatabase { return this.#getAllForScope("repository"); } + getAllRepositoriesForOrg(org) { + return this.#getAllForNameLike("repository", `${org}/%`); + } + deleteAllRepositories() { return this.#deleteAllForScope("repository"); } diff --git a/db/index.test.js b/db/index.test.js index 16f351e..0fbecc0 100644 --- a/db/index.test.js +++ b/db/index.test.js @@ -228,6 +228,61 @@ describe("TowtruckDatabase", () => { }); }); + describe("getAllRepositoriesForOrg", () => { + it("retrieves the expected data from the table", () => { + const db = new TowtruckDatabase(testDbPath); + + const insertStatement = new Database(testDbPath).prepare("INSERT INTO towtruck_data (scope, name, key, value) VALUES (?, ?, ?, ?);"); + + const testRepoSomeData = { + array: [1, 2, 3], + text: "Text", + object: { + boolean: true, + missing: null, + }, + }; + + const testRepoSomeOtherData = { + foo: "bar", + baz: false, + quux: 0.123456789, + }; + + const anotherRepoSomeData = [1, "foo", true, null]; + + const expected = { + "org/test-repo": { + "some-data": testRepoSomeData, + "some-other-data": testRepoSomeOtherData, + }, + "org/another-repo": { + "some-data": anotherRepoSomeData, + }, + }; + + insertStatement.run("repository", "org/test-repo", "some-data", JSON.stringify(testRepoSomeData)); + insertStatement.run("repository", "org/test-repo", "some-other-data", JSON.stringify(testRepoSomeOtherData)); + insertStatement.run("repository", "org/another-repo", "some-data", JSON.stringify(anotherRepoSomeData)); + insertStatement.run("repository", "another-org/test-repo", "some-data", JSON.stringify({})); + + const actual = db.getAllRepositoriesForOrg("org"); + + expect.deepStrictEqual(actual, expected); + }); + + it("returns an empty object when no repositories exist for the given org", () => { + const db = new TowtruckDatabase(testDbPath); + + const insertStatement = new Database(testDbPath).prepare("INSERT INTO towtruck_data (scope, name, key, value) VALUES (?, ?, ?, ?);"); + insertStatement.run("repository", "another-org/test-repo", "some-data", JSON.stringify({})); + + const actual = db.getAllRepositoriesForOrg("org"); + + expect.deepStrictEqual(actual, {}); + }); + }); + describe("deleteAllRepositories", () => { it("removes the expected data from the table", () => { const db = new TowtruckDatabase(testDbPath); diff --git a/utils/githubApi/fetchAllRepos.js b/utils/githubApi/fetchAllRepos.js index 9763615..85f2430 100644 --- a/utils/githubApi/fetchAllRepos.js +++ b/utils/githubApi/fetchAllRepos.js @@ -66,8 +66,11 @@ export const saveAllRepos = async (allRepos, db) => { console.info("Saving all repos..."); const saveAllRepos = db.transaction((repos) => { repos.forEach((repo) => { - const name = repo.repo.name; + const repoName = repo.repo.name; const owner = repo.repo.owner; + + const name = `${owner}/${repoName}`; + db.saveToRepository(name, "main", repo.repo); db.saveToRepository(name, "owner", owner); db.saveToRepository(name, "dependencies", repo.dependencies); diff --git a/utils/githubApi/fetchAllRepos.test.js b/utils/githubApi/fetchAllRepos.test.js index 3692316..5f58522 100644 --- a/utils/githubApi/fetchAllRepos.test.js +++ b/utils/githubApi/fetchAllRepos.test.js @@ -324,63 +324,63 @@ describe("saveAllRepos", () => { expect.strictEqual(db.saveToRepository.mock.callCount(), 12); expect.deepStrictEqual(db.saveToRepository.mock.calls[0].arguments, [ - repo1.repo.name, + "dxw/repo1", "main", repo1.repo, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[1].arguments, [ - repo1.repo.name, + "dxw/repo1", "owner", repo1.repo.owner, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[2].arguments, [ - repo1.repo.name, + "dxw/repo1", "dependencies", repo1.dependencies, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[3].arguments, [ - repo1.repo.name, + "dxw/repo1", "pullRequests", repo1.prInfo, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[4].arguments, [ - repo1.repo.name, + "dxw/repo1", "issues", repo1.issueInfo, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[5].arguments, [ - repo1.repo.name, + "dxw/repo1", "dependabotAlerts", repo1.alerts, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[6].arguments, [ - repo2.repo.name, + "dxw/repo2", "main", repo2.repo, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[7].arguments, [ - repo2.repo.name, + "dxw/repo2", "owner", repo2.repo.owner, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[8].arguments, [ - repo2.repo.name, + "dxw/repo2", "dependencies", repo2.dependencies, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[9].arguments, [ - repo2.repo.name, + "dxw/repo2", "pullRequests", repo2.prInfo, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[10].arguments, [ - repo2.repo.name, + "dxw/repo2", "issues", repo2.issueInfo, ]); expect.deepStrictEqual(db.saveToRepository.mock.calls[11].arguments, [ - repo2.repo.name, + "dxw/repo2", "dependabotAlerts", repo2.alerts, ]);