feat: local-seeds draft + commit searcher

This commit is contained in:
zkldi
2022-09-20 01:02:55 +01:00
parent d617060e6d
commit 55271117a2
3 changed files with 130 additions and 4 deletions
@@ -8,6 +8,7 @@ import importsRouter from "./imports/router";
import oauthRouter from "./oauth/router";
import scoresRouter from "./scores/router";
import searchRouter from "./search/router";
import seedsRouter from "./seeds/router";
import sessionsRouter from "./sessions/router";
import statusRouter from "./status/router";
import usersRouter from "./users/router";
@@ -35,6 +36,7 @@ router.use("/sessions", sessionsRouter);
router.use("/oauth", oauthRouter);
router.use("/clients", clientsRouter);
router.use("/config", configRouter);
router.use("/seeds", seedsRouter);
/**
* Return a JSON 404 response if an endpoint is hit that does not exist.
@@ -0,0 +1,111 @@
import { Router } from "express";
import { PullDatabaseSeeds } from "lib/database-seeds/repo";
import CreateLogCtx from "lib/logger/logger";
import { Environment } from "lib/setup/config";
import prValidate from "server/middleware/prudence-validate";
import { RequireLocalDevelopment } from "server/middleware/type-require";
import { ListGitCommitsInPath } from "utils/git";
import { IsString } from "utils/misc";
import fs from "fs";
import path from "path";
const logger = CreateLogCtx(__filename);
// Routes for interacting with the `database-seeds` folder in this instance of Tachi.
// Why do we have this, and why is it limited to only local development?
// The answer is that we have a "Seeds UI" that runs in the client. For local development
// it's useful to be able to see the current state of the seeds on-disk, and diff that
// against various local commits. As such, we need an api such that the client can
// interface with our local seeds.
// In production/staging, we use GitHub as a source of truth for our git repository.
// In local dev, we have this option available too, but we also enable this API.
const router: Router = Router({ mergeParams: true });
router.use(RequireLocalDevelopment);
// there's a lady who's sure
// all that glitters is gold
// and she's buying a...
const LOCAL_SEEDS_PATH = path.join(__dirname, "../../../../../../../database-seeds");
if (Environment.nodeEnv === "dev" || Environment.nodeEnv === "test") {
if (!fs.existsSync(LOCAL_SEEDS_PATH)) {
logger.error(
`Failed to load seeds routes, could not find any database-seeds checked out at ${LOCAL_SEEDS_PATH}.
These were expected to be present as this is local-development!
All seeds routes will return 500.`
);
}
}
/**
* No-Op route for checking whether this feature is supported by this instance of Tachi.
*
* @name GET /api/v1/seeds
*/
router.get("/", (req, res) => {
return res.status(200).json({
success: true,
description: `Local seeds are available on this instance of Tachi.`,
body: {},
});
});
/**
* List commits that have affected seeds.
*
* This format is a partial implementation of what GitHub's REST API returns. As such,
* an implementing client has far less work to do with respect to handling local + remote
* servers.
*
* @param file - If provided, only returns commits that have touched this specific file.
*
* @name GET /api/v1/seeds/commits
*/
router.get(
"/commits",
prValidate({
file: "*string",
}),
async (req, res) => {
// validated by prudence.
const file = req.query.file as string | undefined;
const seeds = await PullDatabaseSeeds(LOCAL_SEEDS_PATH);
const collections = await seeds.ListCollections();
if (IsString(file)) {
// @ts-expect-error it's complaining because collections
// and string might not have overlap; fair enough, but the point of this
// test is to check that!
if (!collections.includes(file)) {
return res.status(400).json({
success: false,
description: `Invalid file of '${file}' requested. Expected any of ${collections.join(
", "
)}`,
});
}
}
// if we have a file, suffix it with .json
// otherwise, use the do-nothing path.
const realFile = file ? `${file}.json` : ".";
// only check commits in database-seeds/collections
const commits = await ListGitCommitsInPath(
path.join("database-seeds", "collections", realFile)
);
return res.status(200).json({
success: true,
description: `Found ${commits.length} commits.`,
body: commits,
});
}
);
export default router;
+17 -4
View File
@@ -1,4 +1,5 @@
import { asyncExec } from "./misc";
import path from "path";
/**
* We parse git commits into this format, it's convenient to work with and compatible
@@ -26,9 +27,15 @@ export interface GitCommit {
* that can be easily manipulated, rather than the raw strings that git log normally
* returns.
*
* @param path - The path to list commits for. Defaults to showing all commits.
* **DO NOT PASS UNTRUSTED USER INPUT INTO THIS FUNCTION, AS IT IS POSSIBLE TO EXECUTE
* ARBITRARY SHELL COMMANDS AS A RESULT.**
*
* @param filepath - The path to list commits for. Defaults to showing all commits.
*/
export async function ListGitCommitsInPath(path = "."): Promise<Array<GitCommit>> {
export async function ListGitCommitsInPath(filepath = "."): Promise<Array<GitCommit>> {
// make sure our path goes from the root of the repository
const realPath = path.join(__dirname, "../../../", filepath);
const format = [
"h", // shorthash
"B", // subject + body raw.
@@ -57,7 +64,7 @@ export async function ListGitCommitsInPath(path = "."): Promise<Array<GitCommit>
// the -z flag replaces \n separators with NUL bytes, which aren't legal in commit
// messages as far as I'm aware.
// everything else should be obvious.
const COMMAND = `PAGER=cat git log -z --pretty="${gitLogPrettyFormat}" -- '${path}'`;
const COMMAND = `PAGER=cat git log -z --pretty="${gitLogPrettyFormat}" -- '${realPath}'`;
const { stdout, stderr } = await asyncExec(COMMAND);
@@ -67,6 +74,9 @@ export async function ListGitCommitsInPath(path = "."): Promise<Array<GitCommit>
const rows = stdout.split("\0");
// last row always has 0 elements because the output ends with a NUL.
rows.pop();
const commits: Array<GitCommit> = rows.map((line) => {
const split = line.split(ASCII_GROUP_SEPARATOR);
@@ -90,7 +100,10 @@ export async function ListGitCommitsInPath(path = "."): Promise<Array<GitCommit>
email: cmtEmail,
name: cmtName,
},
message,
// trim off the trailing \n from every message.
// all messages end with an extraneous newline, and we don't want it.
message: message.trim(),
},
};
});