Skip to content

Commit

Permalink
Scope repositories by organisation in the database
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
danlivings-dxw committed Jan 2, 2025
1 parent 016db38 commit 156b9bd
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 13 deletions.
21 changes: 21 additions & 0 deletions db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class TowtruckDatabase {
#saveStatement;
#getStatement;
#getAllForNameStatement;
#getAllForNameLikeStatement;
#getAllForScopeStatement;
#deleteAllForScopeStatement;

Expand All @@ -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 = ?;");
}
Expand All @@ -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);

Expand Down Expand Up @@ -97,6 +114,10 @@ export class TowtruckDatabase {
return this.#getAllForScope("repository");
}

getAllRepositoriesForOrg(org) {
return this.#getAllForNameLike("repository", `${org}/%`);
}

deleteAllRepositories() {
return this.#deleteAllForScope("repository");
}
Expand Down
55 changes: 55 additions & 0 deletions db/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion utils/githubApi/fetchAllRepos.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
24 changes: 12 additions & 12 deletions utils/githubApi/fetchAllRepos.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]);
Expand Down

0 comments on commit 156b9bd

Please sign in to comment.