move over existing code into new repository

This commit is contained in:
Tyler Wilding
2021-11-12 14:20:23 -05:00
parent 95e9f47529
commit 84a8aa8693
16 changed files with 7449 additions and 0 deletions

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
dist/
node_modules/
tsconfig.json

14
.eslintrc.json Normal file
View File

@@ -0,0 +1,14 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {}
}

24
.github/workflows/build-backend.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Build
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
name: Build Backend
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Get Dependencies
run: npm ci
- name: Build App
run: npm run build

27
.github/workflows/lint-backend.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Linter
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
name: Linting & Formatting
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Get Dependencies
run: npm ci
- name: Check Formatting
run: npx prettier --check ./
- name: Check Linting
run: npx eslint ./

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
.env
dist/
certs/

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
dist/
node_modules/
tsconfig.json

0
.prettierrc.json Normal file
View File

View File

@@ -0,0 +1,77 @@
import { v4 as uuidv4 } from "uuid";
import { ReleaseCache } from "../models/ReleaseCache";
import { LogFactory } from "../utils/LogFactory";
import { Request, Response } from "express";
import crypto from "crypto";
export class GithubController {
private releaseCache: ReleaseCache;
private log = new LogFactory("gh-listener").getLogger();
private readonly webhookSecret;
constructor(releaseCache: ReleaseCache) {
this.releaseCache = releaseCache;
const secret = process.env.GH_WEBHOOK_SECRET;
if (secret == undefined) {
this.log.error("GH_WEBHOOK_SECRET isn't set. Aborting");
throw new Error("GH_WEBHOOK_SECRET isn't set. Aborting");
} else {
this.webhookSecret = secret;
}
}
// in the future, might change it from instead of listing all releases it just uses the content of the webhook to evict the cache
// for the foreseeable future though, this is fine
webhookHandler(req: Request, resp: Response) {
const cid = uuidv4();
this.log.info("Received request", req.headers);
const ghDigestRaw = req.header("x-hub-signature-256");
if (ghDigestRaw == undefined) {
resp.send(403);
return;
}
const ghDigest = Buffer.from(ghDigestRaw, "utf8");
const digest = Buffer.from(
`sha256=${crypto
.createHmac("sha256", this.webhookSecret)
.update(JSON.stringify(req.body))
.digest("hex")}`,
"utf8"
);
if (crypto.timingSafeEqual(digest, ghDigest)) {
// Valid webhook from github, proceed
const body = req.body;
if (
"action" in body &&
body.action == "published" &&
"release" in body &&
body.release.draft == true
) {
// Release event
if (
"repository" in body &&
body.repository.full_name == "PCSX2/pcsx2"
) {
this.releaseCache.refreshReleaseCache(cid);
} else if (
"repository" in body &&
body.repository.full_name == "PCSX2/archive"
) {
this.releaseCache.refreshLegacyReleaseCache(cid);
}
} else if (
"action" in body &&
body.action == "completed" &&
"check_suite" in body &&
body.check_suite.status == "completed" &&
body.check_suite.conclusion == "success"
) {
this.releaseCache.refreshPullRequestBuildCache(cid);
}
} else {
resp.send(403);
return;
}
resp.send(204);
}
}

View File

@@ -0,0 +1,116 @@
import { v4 as uuidv4 } from "uuid";
import { ReleaseCache } from "../models/ReleaseCache";
import { LogFactory } from "../utils/LogFactory";
import { Request, Response } from "express";
export class ReleaseCacheControllerV1 {
private releaseCache: ReleaseCache;
private log = new LogFactory("release-cache").getLogger();
private maxPageSize = 100;
constructor(releaseCache: ReleaseCache) {
this.releaseCache = releaseCache;
}
getLatestReleasesAndPullRequests(req: Request, resp: Response) {
const cid = uuidv4();
this.log.info("Fetching latest releases");
resp.status(200).send(this.releaseCache.getLatestReleases(cid));
}
getStableReleases(req: Request, resp: Response) {
const cid = uuidv4();
const offset = Number(req.query.offset) || 0;
const pageSize = Number(req.query.pageSize) || 30;
if (offset < 0) {
this.log.info("API error occurred - invalid offset", {
cid: cid,
offset: offset,
pageSize: pageSize,
});
resp.status(400).send("Invalid offset value");
return;
}
if (pageSize > this.maxPageSize) {
this.log.info("API error occurred - pageSize exceeded", {
cid: cid,
offset: offset,
pageSize: pageSize,
});
resp.status(400).send("pageSize exceeded maximum allowed '100'");
return;
}
this.log.info("Fetching stable releases", {
cid: cid,
offset: offset,
pageSize: pageSize,
});
resp
.status(200)
.send(this.releaseCache.getStableReleases(cid, offset, pageSize));
}
getNightlyReleases(req: Request, resp: Response) {
const cid = uuidv4();
const offset = Number(req.query.offset) || 0;
const pageSize = Number(req.query.pageSize) || 30;
if (offset < 0) {
this.log.info("API error occurred - invalid offset", {
cid: cid,
offset: offset,
pageSize: pageSize,
});
resp.status(400).send("Invalid offset value");
return;
}
if (pageSize > this.maxPageSize) {
this.log.info("API error occurred - pageSize exceeded", {
cid: cid,
offset: offset,
pageSize: pageSize,
});
resp.status(400).send("pageSize exceeded maximum allowed '100'");
return;
}
this.log.info("Fetching nightly releases", {
cid: cid,
offset: offset,
pageSize: pageSize,
});
resp
.status(200)
.send(this.releaseCache.getNightlyReleases(cid, offset, pageSize));
}
getPullRequests(req: Request, resp: Response) {
const cid = uuidv4();
const offset = Number(req.query.offset) || 0;
const pageSize = Number(req.query.pageSize) || 30;
if (offset < 0) {
this.log.info("API error occurred - invalid offset", {
cid: cid,
offset: offset,
pageSize: pageSize,
});
resp.status(400).send("Invalid offset value");
return;
}
if (pageSize > this.maxPageSize) {
this.log.info("API error occurred - pageSize exceeded", {
cid: cid,
offset: offset,
pageSize: pageSize,
});
resp.status(400).send("pageSize exceeded maximum allowed '100'");
return;
}
this.log.info("Fetching current pull requests", {
cid: cid,
offset: offset,
pageSize: pageSize,
});
resp
.status(200)
.send(this.releaseCache.getPullRequestBuilds(cid, offset, pageSize));
}
}

91
index.ts Normal file
View File

@@ -0,0 +1,91 @@
import { v4 as uuidv4 } from "uuid";
import express from "express";
import cors from "cors";
import { ReleaseCache } from "./models/ReleaseCache";
import { exit } from "process";
import { LogFactory } from "./utils/LogFactory";
import { RoutesV1 } from "./routes/RoutesV1";
import fs from "fs";
import https from "https";
const log = new LogFactory("app").getLogger();
const devEnv = process.env.NODE_ENV !== "production";
const ghWebhookSecret = process.env.GH_WEBHOOK_SECRET;
if (ghWebhookSecret == undefined) {
exit(1);
}
const corsOptions = {
origin: devEnv ? "http://localhost:8080" : process.env.CORS_FRONTEND_URL,
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
};
// eslint-disable-next-line @typescript-eslint/no-var-requires
const rateLimit = require("express-rate-limit");
const app = express();
app.use(cors(corsOptions));
app.use(express.json());
// Enable if you're behind a reverse proxy (Heroku, Bluemix, AWS ELB, Nginx, etc)
// see https://expressjs.com/en/guide/behind-proxies.html
app.set("trust proxy", 1);
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minutes
max: 30, // limit each IP to 30 requests per minute
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
onLimitReached: function (req: any, res: any, options: any) {
log.warn("rate limit hit", {
ip: req.ip,
url: req.url,
});
},
});
// apply to all requests
app.use(limiter);
const releaseCache = new ReleaseCache();
(async function () {
const cid = uuidv4();
log.info("Initializing Server Cache", { cid: cid });
await releaseCache.refreshReleaseCache(cid);
await releaseCache.refreshPullRequestBuildCache(cid);
// build up legacy releases in the background
// releaseCache.refreshLegacyReleaseCache(cid);
log.info("Initializing Server Cache", { cid: cid });
})();
// Init Routes
const v1Router = new RoutesV1(releaseCache);
app.use("/v1", v1Router.router);
// Default Route
app.use(function (req, res) {
log.warn("invalid route accessed", {
url: req.originalUrl,
});
res.send(404);
});
if (!devEnv) {
const key = fs.readFileSync(__dirname + "/../certs/ssl.key");
const cert = fs.readFileSync(__dirname + "/../certs/ssl.crt");
const sslOptions = { key: key, cert: cert };
const httpsServer = https.createServer(sslOptions, app);
httpsServer.listen(Number(process.env.PORT), async () => {
log.info("Cache Initialized, Serving...", {
port: Number(process.env.PORT),
});
});
} else {
app.listen(Number(process.env.PORT), async () => {
log.info("Cache Initialized, Serving...", {
port: Number(process.env.PORT),
});
});
}

517
models/ReleaseCache.ts Normal file
View File

@@ -0,0 +1,517 @@
import { Octokit } from "@octokit/rest";
import { throttling } from "@octokit/plugin-throttling";
import { retry } from "@octokit/plugin-retry";
import striptags from "striptags";
import * as path from "path";
import { LogFactory } from "../utils/LogFactory";
enum ReleaseType {
Stable = 1,
Nightly,
PullRequest,
}
enum ReleasePlatform {
Windows = "Windows",
Linux = "Linux",
MacOS = "MacOS",
}
class ReleaseAsset {
constructor(
readonly url: string,
readonly displayName: string,
readonly additionalTags: string[], // things like 32bit, AppImage, distro names, etc
readonly downloadCount: number
) {}
}
class Release {
constructor(
readonly version: string,
readonly url: string,
readonly semverMajor: number,
readonly semverMinor: number,
readonly semverPatch: number,
readonly description: string | undefined | null,
readonly assets: Record<ReleasePlatform, ReleaseAsset[]>,
readonly type: ReleaseType,
readonly prerelease: boolean,
readonly createdAt: Date,
readonly publishedAt: Date | undefined | null
) {}
}
class PullRequest {
constructor(
readonly number: number,
readonly link: string,
readonly githubUser: string,
readonly updatedAt: Date,
readonly body: string,
readonly title: string,
readonly additions: number,
readonly deletions: number
) {}
}
Octokit.plugin(throttling);
Octokit.plugin(retry);
const log = new LogFactory("release-cache").getLogger();
const semverRegex = /v?(\d+)\.(\d+)\.(\d+)/;
const octokit = new Octokit({
auth: process.env.GH_TOKEN,
userAgent: "PCSX2/PCSX2.github.io",
log: {
// eslint-disable-next-line @typescript-eslint/no-empty-function
debug: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
info: () => {},
warn: console.warn,
error: console.error,
},
throttle: {
onRateLimit: (retryAfter: any, options: any) => {
log.warn(
`Request quota exhausted for request ${options.method} ${options.url}`
);
// Retry twice after hitting a rate limit error, then give up
if (options.request.retryCount <= 2) {
log.warn(`Retrying after ${retryAfter} seconds!`);
return true;
}
},
onAbuseLimit: (retryAfter: any, options: any) => {
// does not retry, only logs a warning
log.warn(`Abuse detected for request ${options.method} ${options.url}`);
},
},
});
// NOTE - Depends on asset naming convention:
// pcsx2-<version>-windows-<arch>-<additional tags>.whatever
// In the case of macOS:
// pcsx2-<version>-macOS-<macOS version (ie. Mojave)>-<additional tags>.whatever
// In the case of linux:
// pcsx2-<version>-linux-<distro OR appimage>-<arch>-<additional tags>.whatever
function gatherReleaseAssets(
release: any,
legacy: boolean
): Record<ReleasePlatform, ReleaseAsset[]> {
const assets: Record<ReleasePlatform, ReleaseAsset[]> = {
Windows: [],
Linux: [],
MacOS: [],
};
if (!("assets" in release)) {
return assets;
}
// All legacy builds are only windows 32 bit, we'll find it to confirm
if (legacy) {
for (let i = 0; i < release.assets.length; i++) {
const asset = release.assets[i];
if (asset.name.includes("windows")) {
assets.Windows.push(
new ReleaseAsset(
asset.browser_download_url,
`Windows 32bit`,
[],
asset.download_count
)
);
}
}
return assets;
}
for (let i = 0; i < release.assets.length; i++) {
const asset = release.assets[i];
const assetComponents = path.parse(asset.name).name.split("-");
if (assetComponents.length < 4) {
log.warn("invalid release asset naming", {
isLegacy: legacy,
semver: release.tag_name,
assetName: asset.name,
});
continue;
}
const platform = assetComponents[2].toLowerCase();
if (platform == "windows") {
const arch = assetComponents[3];
const additionalTags = assetComponents.slice(4);
assets.Windows.push(
new ReleaseAsset(
asset.browser_download_url,
`Windows ${arch}`,
additionalTags,
asset.download_count
)
);
} else if (assetComponents[2].toLowerCase() == "linux") {
const distroOrAppImage = assetComponents[3];
const additionalTags = assetComponents.slice(4);
assets.Linux.push(
new ReleaseAsset(
asset.browser_download_url,
`Linux ${distroOrAppImage}`,
additionalTags,
asset.download_count
)
);
} else if (assetComponents[2].toLowerCase() == "macos") {
const osxVersion = assetComponents[3];
const additionalTags = assetComponents.slice(4);
assets.MacOS.push(
new ReleaseAsset(
asset.browser_download_url,
`MacOS ${osxVersion}`,
additionalTags,
asset.download_count
)
);
}
}
return assets;
}
export class ReleaseCache {
private stableReleases: Release[] = [];
private combinedNightlyReleases: Release[] = [];
private nightlyReleases: Release[] = [];
private legacyNightlyReleases: Release[] = [];
private pullRequestBuilds: PullRequest[] = [];
private initialized: boolean;
constructor() {
this.initialized = false;
}
public isInitialized(cid: string): boolean {
return this.initialized;
}
public async refreshReleaseCache(cid: string): Promise<void> {
log.info("refreshing main release cache", { cid: cid, cacheType: "main" });
const releases = await octokit.paginate(octokit.rest.repos.listReleases, {
owner: "PCSX2",
repo: "pcsx2",
per_page: 100,
});
const newStableReleases: Release[] = [];
const newNightlyReleases: Release[] = [];
for (let i = 0; i < releases.length; i++) {
const release = releases[i];
if (release.draft) {
continue;
}
const releaseAssets = gatherReleaseAssets(release, false);
const semverGroups = release.tag_name.match(semverRegex);
if (semverGroups != null && semverGroups.length == 4) {
const newRelease = new Release(
release.tag_name,
release.html_url,
Number(semverGroups[1]),
Number(semverGroups[2]),
Number(semverGroups[3]),
release.body == undefined || release.body == null
? release.body
: striptags(release.body),
releaseAssets,
release.prerelease ? ReleaseType.Nightly : ReleaseType.Stable,
release.prerelease,
new Date(release.created_at),
release.published_at == null
? undefined
: new Date(release.published_at)
);
if (newRelease.type == ReleaseType.Nightly) {
newNightlyReleases.push(newRelease);
} else {
newStableReleases.push(newRelease);
}
} else {
log.warn("invalid semantic version", {
cid: cid,
cacheType: "main",
semver: release.tag_name,
matches: semverGroups,
});
}
}
this.stableReleases = newStableReleases;
// Releases returned from github are not sorted by semantic version, but by published date -- this ensures consistency
this.stableReleases.sort(
(a, b) =>
b.semverMajor - a.semverMajor ||
b.semverMinor - a.semverMinor ||
b.semverPatch - a.semverPatch
);
this.nightlyReleases = newNightlyReleases;
this.nightlyReleases.sort(
(a, b) =>
b.semverMajor - a.semverMajor ||
b.semverMinor - a.semverMinor ||
b.semverPatch - a.semverPatch
);
this.combinedNightlyReleases = this.nightlyReleases.concat(
this.legacyNightlyReleases
);
log.info("main release cache refreshed", { cid: cid, cacheType: "main" });
}
public async refreshLegacyReleaseCache(cid: string): Promise<void> {
log.info("refreshing legacy release cache", {
cid: cid,
cacheType: "legacy",
});
// First pull down the legacy releases, these are OLD nightlys
const legacyReleases = await octokit.paginate(
octokit.rest.repos.listReleases,
{
owner: "PCSX2",
repo: "archive",
per_page: 100,
}
);
const newLegacyReleases: Release[] = [];
for (let i = 0; i < legacyReleases.length; i++) {
const release = legacyReleases[i];
if (release.draft) {
continue;
}
const releaseAssets = gatherReleaseAssets(release, true);
const semverGroups = release.tag_name.match(semverRegex);
if (semverGroups != null && semverGroups.length == 4) {
newLegacyReleases.push(
new Release(
release.tag_name,
release.html_url,
Number(semverGroups[1]),
Number(semverGroups[2]),
Number(semverGroups[3]),
release.body == undefined || release.body == null
? release.body
: striptags(release.body),
releaseAssets,
ReleaseType.Nightly,
release.prerelease,
new Date(release.created_at),
release.published_at == null
? undefined
: new Date(release.published_at)
)
);
} else {
log.warn("invalid semantic version", {
cid: cid,
cacheType: "main",
semver: release.tag_name,
matches: semverGroups,
});
}
}
this.legacyNightlyReleases = newLegacyReleases;
this.legacyNightlyReleases.sort(
(a, b) =>
b.semverMajor - a.semverMajor ||
b.semverMinor - a.semverMinor ||
b.semverPatch - a.semverPatch
);
this.combinedNightlyReleases = this.nightlyReleases.concat(
this.legacyNightlyReleases
);
log.info("legacy release cache refreshed", {
cid: cid,
cacheType: "legacy",
});
}
private async grabPullRequestInfo(cursor: string | null): Promise<any> {
const response: any = await octokit.graphql(
`
fragment pr on PullRequest {
number
author {
login
}
updatedAt
body
title
additions
deletions
isDraft
permalink
commits(last: 1) {
nodes {
commit {
statusCheckRollup {
state
}
}
}
}
}
query ($owner: String!, $repo: String!, $states: [PullRequestState!], $baseRefName: String, $headRefName: String, $orderField: IssueOrderField = UPDATED_AT, $orderDirection: OrderDirection = DESC, $perPage: Int!, $endCursor: String) {
repository(owner: $owner, name: $repo) {
pullRequests(states: $states, orderBy: {field: $orderField, direction: $orderDirection}, baseRefName: $baseRefName, headRefName: $headRefName, first: $perPage, after: $endCursor) {
nodes {
...pr
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`,
{
owner: "PCSX2",
repo: "pcsx2",
states: "OPEN",
baseRefName: "master",
perPage: 100,
endCursor: cursor,
}
);
return response;
}
public async refreshPullRequestBuildCache(cid: string): Promise<void> {
log.info("refreshing main release cache", {
cid: cid,
cacheType: "pullRequests",
});
try {
let paginate = true;
let cursor: string | null = null;
const newPullRequestCache: PullRequest[] = [];
while (paginate) {
const resp: any = await this.grabPullRequestInfo(cursor);
if (resp.repository.pullRequests.pageInfo.hasNextPage) {
cursor = resp.repository.pullRequests.pageInfo.endCursor;
} else {
paginate = false;
}
for (let i = 0; i < resp.repository.pullRequests.nodes.length; i++) {
// We only care about non-draft / successfully building PRs
const pr = resp.repository.pullRequests.nodes[i];
if (pr.isDraft) {
continue;
}
if (pr.commits.nodes[0].commit.statusCheckRollup.state == "SUCCESS") {
newPullRequestCache.push(
new PullRequest(
pr.number,
pr.permalink,
pr.author.login,
new Date(pr.updatedAt),
pr.body == undefined || pr.body == null
? pr.body
: striptags(pr.body),
pr.title == undefined || pr.title == null
? pr.title
: striptags(pr.title),
pr.additions,
pr.deletions
)
);
}
}
}
this.pullRequestBuilds = newPullRequestCache;
log.info("finished refreshing main release cache", {
cid: cid,
cacheType: "pullRequests",
});
} catch (error) {
log.error("error occurred when refreshing main release cache", error);
}
}
// Returns the first page of each release type in a single response
public getLatestReleases(cid: string) {
return {
stableReleases: this.getStableReleases(cid, 0, 30),
nightlyReleases: this.getNightlyReleases(cid, 0, 30),
pullRequestBuilds: this.getPullRequestBuilds(cid, 0, 30),
};
}
public getStableReleases(cid: string, offset: number, pageSize: number) {
if (offset >= this.stableReleases.length) {
return [];
}
const ret = [];
for (
let i = 0;
i < pageSize && i + offset < this.stableReleases.length;
i++
) {
ret.push(this.stableReleases[i + offset]);
}
return {
data: ret,
pageInfo: {
total: this.stableReleases.length,
},
};
}
public getNightlyReleases(cid: string, offset: number, pageSize: number) {
if (offset >= this.combinedNightlyReleases.length) {
return [];
}
const ret = [];
for (
let i = 0;
i < pageSize && i + offset < this.combinedNightlyReleases.length;
i++
) {
ret.push(this.combinedNightlyReleases[i + offset]);
}
return {
data: ret,
pageInfo: {
total: this.combinedNightlyReleases.length,
},
};
}
public getPullRequestBuilds(cid: string, offset: number, pageSize: number) {
if (offset >= this.pullRequestBuilds.length) {
return [];
}
const ret = [];
for (
let i = 0;
i < pageSize && i + offset < this.pullRequestBuilds.length;
i++
) {
ret.push(this.pullRequestBuilds[i + offset]);
}
return {
data: ret,
pageInfo: {
total: this.pullRequestBuilds.length,
},
};
}
}

6344
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "pcsx2-webapi",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"serve": "ts-node -r dotenv/config index.ts",
"build": "tsc -p .",
"start": "node -r dotenv/config ./dist/index.js dotenv_config_path=./.env",
"format": "npx prettier --write .",
"lint": "npx eslint ./"
},
"engines": {
"node": "16.x"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@octokit/graphql": "^4.8.0",
"@octokit/plugin-retry": "^3.0.9",
"@octokit/plugin-throttling": "^3.5.2",
"@octokit/rest": "^18.11.4",
"@octokit/types": "^6.31.3",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-rate-limit": "^5.4.0",
"striptags": "^3.2.0",
"uuid": "^8.3.2",
"winston": "^3.3.3",
"winston-loki": "^6.0.3"
},
"devDependencies": {
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/node": "^16.10.2",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^5.3.1",
"@typescript-eslint/parser": "^5.3.1",
"eslint": "^8.2.0",
"prettier": "2.4.1",
"ts-node": "^10.2.1",
"typescript": "^4.4.3"
}
}

46
routes/RoutesV1.ts Normal file
View File

@@ -0,0 +1,46 @@
import express from "express";
import { GithubController } from "../controllers/GithubController";
import { ReleaseCacheControllerV1 } from "../controllers/ReleaseCacheControllerV1";
import { ReleaseCache } from "../models/ReleaseCache";
export class RoutesV1 {
router: express.Router;
private githubController: GithubController;
private releaseCacheControllerV1: ReleaseCacheControllerV1;
constructor(releaseCache: ReleaseCache) {
this.router = express.Router();
this.githubController = new GithubController(releaseCache);
this.releaseCacheControllerV1 = new ReleaseCacheControllerV1(releaseCache);
// Init Routes
this.router
.route("/latestReleasesAndPullRequests")
.get((req, resp) =>
this.releaseCacheControllerV1.getLatestReleasesAndPullRequests(
req,
resp
)
);
this.router
.route("/stableReleases")
.get((req, resp) =>
this.releaseCacheControllerV1.getStableReleases(req, resp)
);
this.router
.route("/nightlyReleases")
.get((req, resp) =>
this.releaseCacheControllerV1.getNightlyReleases(req, resp)
);
this.router
.route("/pullRequests")
.get((req, resp) =>
this.releaseCacheControllerV1.getPullRequests(req, resp)
);
// Other Routes
this.router
.route("/github-webhook")
.post((req, resp) => this.githubController.webhookHandler(req, resp));
}
}

100
tsconfig.json Normal file
View File

@@ -0,0 +1,100 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "resolveJsonModule": true, /* Enable importing .json files */
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

37
utils/LogFactory.ts Normal file
View File

@@ -0,0 +1,37 @@
import LokiTransport from "winston-loki";
import winston from "winston";
export class LogFactory {
private devEnv = process.env.NODE_ENV !== "production";
private log: winston.Logger;
constructor(scope: string) {
this.log = winston.createLogger({
defaultMeta: { service: "pcsx2-api", scope: scope },
});
this.log.add(
new winston.transports.Console({
format: winston.format.simple(),
})
);
if (!this.devEnv) {
console.log("Piping logs to Grafana as well");
const lokiTransport = new LokiTransport({
host: `https://logs-prod-us-central1.grafana.net`,
batching: true,
basicAuth: `${process.env.GRAFANA_LOKI_USER}:${process.env.GRAFANA_LOKI_PASS}`,
labels: { app: "pcsx2-backend", env: this.devEnv ? "dev" : "prod" },
// remove color from log level label - loki really doesn't like it
format: winston.format.uncolorize({
message: false,
raw: false,
}),
});
this.log.add(lokiTransport);
}
}
public getLogger(): winston.Logger {
return this.log;
}
}