Skip to content

Commit de15f1f

Browse files
authored
Refactor To Fastify (#413)
1 parent 5ef3caa commit de15f1f

24 files changed

+706
-1237
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.snap

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ module.exports = {
1111
"plugin:@beequeue/prettier",
1212
],
1313
rules: {
14+
"no-console": "off",
1415
"import/no-named-as-default": "off",
16+
"@typescript-eslint/require-await": "off",
1517
},
1618
}

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
This app uses data from [`anime-offline-database`](https://github.com/manami-project/anime-offline-database/) - fetching
77
and updating itself every 24 hours.
88

9-
[![Deploy to DigitalOcean](https://mp-assets1.sfo2.digitaloceanspaces.com/deploy-to-do/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/BeeeQueue/arm-server/tree/master&refcode=52b251df60e7)
9+
#### Get notifications on important API changes
10+
11+
Subscribe to new releases in this repo:
12+
13+
![image](https://user-images.githubusercontent.com/472500/121041611-c116fc00-c767-11eb-9aaa-64a894a1598a.png)
1014

1115
### Missing or duplicate entries
1216

@@ -31,12 +35,12 @@ enum Source {
3135

3236
### Get IDS:
3337

34-
`GET/POST` `/api/ids`
38+
`POST` `/api/ids`
3539

36-
Either use GET query parameters:
40+
Either use GET with query parameters:
3741
`?source={Source}&id={number}`
3842

39-
or send the query as a POST JSON body:
43+
or use POST with a JSON body:
4044

4145
`{ "anilist": 1337 }`
4246

@@ -52,7 +56,9 @@ interface Entry {
5256
kitsu: number | null
5357
}
5458

59+
// If JSON body is a single object
5560
// { "anilist": 1337 } => Entry | null
61+
// // If JSON body is an array of objects
5662
// [{ ... }] => Array<Entry | null>
5763
```
5864

package.json

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
"version": "1.0.0",
55
"private": true,
66
"license": "AGPL-3.0-only",
7+
"homepage": "https://github.com/BeeeQueue/arm-server",
78
"engines": {
8-
"node": ">=14.17"
9+
"node": "^14.17"
910
},
1011
"husky": {
1112
"hooks": {
@@ -34,37 +35,30 @@
3435
"@sentry/tracing": "6.3.6",
3536
"cross-env": "7.0.3",
3637
"envsafe": "2.0.3",
38+
"fastify": "3.17.0",
39+
"fastify-cors": "6.0.1",
40+
"fastify-helmet": "5.3.1",
3741
"got": "11.8.2",
38-
"joi": "17.4.0",
3942
"knex": "0.95.6",
40-
"koa": "2.13.1",
41-
"koa-bodyparser": "4.3.0",
42-
"koa-connect": "2.1.0",
43-
"koa-pino-logger": "3.0.0",
44-
"koa-router": "10.0.0",
45-
"pino": "6.11.3",
46-
"pino-pretty": "4.8.0",
43+
"nanoid": "3.1.23",
4744
"sqlite3": "5.0.2",
45+
"ts-json-validator": "0.7.1",
4846
"ts-node": "9.1.1",
49-
"typescript": "4.2.4"
47+
"type-fest": "1.2.0",
48+
"typescript": "4.3.2"
5049
},
5150
"devDependencies": {
5251
"@beequeue/eslint-plugin": "0.2.0",
5352
"@tsconfig/node14": "1.0.0",
5453
"@types/jest": "26.0.23",
55-
"@types/koa": "2.13.1",
56-
"@types/koa-bodyparser": "4.3.0",
57-
"@types/koa-pino-logger": "3.0.0",
58-
"@types/koa-router": "7.4.2",
5954
"@types/node": "14.17.2",
60-
"@types/supertest": "2.0.11",
61-
"dotenv": "9.0.2",
55+
"dotenv": "10.0.0",
6256
"eslint": "7.27.0",
6357
"husky": "4.3.8",
6458
"jest": "26.6.3",
6559
"lint-staged": "11.0.0",
60+
"pino-pretty": "5.0.2",
6661
"prettier": "2.3.0",
67-
"supertest": "6.1.3",
6862
"ts-jest": "26.5.6",
6963
"ts-node-dev": "1.1.6",
7064
"tsconfig-paths": "3.9.0"

src/app.ts

Lines changed: 36 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,50 @@
1-
import Koa, { Context } from "koa"
2-
import BodyParser from "koa-bodyparser"
3-
import RequestLogger from "koa-pino-logger"
4-
import Router from "koa-router"
1+
import Fastify from "fastify"
2+
import Cors from "fastify-cors"
3+
import Helmet from "fastify-helmet"
4+
import { customAlphabet, urlAlphabet } from "nanoid"
55

66
import { config } from "@/config"
7-
import { Logger } from "@/lib/logger"
8-
import { requestHandler, sendErrorToSentry, tracingMiddleWare } from "@/lib/sentry"
7+
import { sendErrorToSentry } from "@/lib/sentry"
8+
import { apiPlugin } from "@/routes/ids"
99

10-
import { routes } from "./routes"
10+
import pkgJson from "../package.json"
1111

12-
export const App = new Koa()
13-
const router = new Router()
12+
const isProd = config.NODE_ENV === "production"
1413

15-
App.use(
16-
RequestLogger({
17-
prettyPrint: config.NODE_ENV === "development",
18-
}),
19-
)
14+
const nanoid = customAlphabet(urlAlphabet, 16)
2015

21-
App.use(requestHandler)
22-
App.use(tracingMiddleWare)
16+
export const buildApp = async () => {
17+
const App = Fastify({
18+
ignoreTrailingSlash: true,
19+
onProtoPoisoning: "remove",
20+
onConstructorPoisoning: "remove",
21+
trustProxy: isProd,
22+
genReqId: nanoid,
23+
disableRequestLogging: process.env.NODE_ENV === "test",
24+
logger: {
25+
level: config.LOG_LEVEL,
26+
prettyPrint: !isProd,
27+
},
28+
})
2329

24-
App.on("error", (err, ctx) => {
25-
Logger.error(err)
30+
await App.register(Cors, {
31+
origin: true,
32+
})
2633

27-
sendErrorToSentry(err, ctx)
28-
})
34+
await App.register(Helmet, {
35+
hsts: false,
36+
contentSecurityPolicy: false,
37+
})
2938

30-
App.use(BodyParser())
39+
App.addHook("onError", (request, _reply, error, next) => {
40+
sendErrorToSentry(error, request as any)
3141

32-
router.get("/", (ctx: Context) => {
33-
ctx.body = `
34-
<pre>
35-
<b>Get IDs:</b>
36-
<b>GET/POST /api/ids</b>
42+
next()
43+
})
3744

38-
enum Source {
39-
anilist,
40-
anidb,
41-
myanimelist,
42-
kitsu,
43-
}
44-
45-
<b>Either use GET query parameters:</b>
46-
?source={Source}&id={number}
47-
48-
<b>or send the query as a POST JSON body:</b>
45+
await App.register(apiPlugin, { prefix: "/api" })
4946

50-
{ "anilist": 1337 }
47+
App.get("/", async (_request, reply) => reply.redirect(301, pkgJson.homepage))
5148

52-
[{ "anilist": 1337 }, { "anilist": 69 }, { "anidb": 420 }]
53-
54-
interface Entry {
55-
anilist: number | null
56-
anidb: number | null
57-
myanimelist: number | null
58-
kitsu: number | null
49+
return App
5950
}
60-
61-
{ "anilist": 1337 } => Entry | null
62-
[{ ... }] => Array<Entry | null>
63-
64-
<b>The response code will always be 200 (OK).
65-
If an entry is not found null is returned instead.</b>
66-
67-
Source code is available on GitHub at <a href="https://github.com/BeeeQueue/arm-server">https://github.com/BeeeQueue/arm-server</a>
68-
</pre>
69-
`
70-
})
71-
72-
router.use(routes)
73-
74-
App.use(router.routes())
75-
App.use(router.allowedMethods())

src/index.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
import { captureException } from "@sentry/node"
22

3-
import { Logger } from "@/lib/logger"
3+
import { config } from "@/config"
44

5-
import { App } from "./app"
5+
import { buildApp } from "./app"
66
import { updateRelations } from "./update"
77

8-
const { NODE_ENV, PORT } = process.env
9-
const port = PORT ?? 3000
8+
const { NODE_ENV, PORT } = config
109

1110
const runUpdateScript = () => updateRelations().catch(captureException)
1211

13-
const listen = () => {
12+
const listen = async () => {
1413
if (NODE_ENV === "production") {
1514
void runUpdateScript()
1615

1716
// eslint-disable-next-line @typescript-eslint/no-misused-promises
1817
setInterval(runUpdateScript, 1000 * 60 * 60 * 24)
1918
}
2019

21-
App.listen(port, () => {
22-
Logger.info(`Listening on ${port}`)
23-
})
20+
await (
21+
await buildApp()
22+
).listen(PORT, process.env.NODE_ENV === "production" ? "0.0.0.0" : undefined)
2423
}
2524

26-
listen()
25+
void listen()

src/lib/logger.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/lib/sentry.ts

Lines changed: 8 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,22 @@
1-
// eslint-disable-next-line node/no-deprecated-api
2-
import domain from "domain"
3-
4-
import { Context, Next } from "koa"
1+
import { FastifyRequest } from "fastify"
52

63
import * as Sentry from "@sentry/node"
7-
import { extractTraceparentData, stripUrlQueryAndFragment } from "@sentry/tracing"
84

95
const { NODE_ENV, TRACES_SAMPLERATE, SENTRY_DSN } = process.env
106

117
Sentry.init({
128
dsn: SENTRY_DSN,
139
enabled: NODE_ENV === "production",
14-
tracesSampleRate: Number(TRACES_SAMPLERATE ?? 1),
10+
tracesSampleRate: Number(TRACES_SAMPLERATE ?? 0.25),
1511
})
1612

17-
export const requestHandler = (ctx: Context, next: Next) =>
18-
new Promise<void>((resolve) => {
19-
const local = domain.create()
20-
21-
local.add(ctx as any) // TODO
22-
23-
local.on("error", (err) => {
24-
ctx.status = err.status || 500
25-
ctx.body = err.message
26-
ctx.app.emit("error", err, ctx)
27-
})
28-
29-
void local.run(async () => {
30-
Sentry.getCurrentHub().configureScope((scope) =>
31-
scope.addEventProcessor((event) =>
32-
Sentry.Handlers.parseRequest(event, ctx.request as any, {
33-
user: false,
34-
}),
35-
),
36-
)
37-
38-
await next()
39-
40-
resolve()
41-
})
42-
})
43-
44-
export const tracingMiddleWare = async (ctx: Context, next: Next) => {
45-
const reqMethod = (ctx.method || "").toUpperCase()
46-
const reqUrl = ctx.url && stripUrlQueryAndFragment(ctx.url)
47-
48-
// connect to trace of upstream app
49-
let traceparentData = null
50-
51-
if (ctx.request.get("sentry-trace")) {
52-
traceparentData = extractTraceparentData(ctx.request.get("sentry-trace"))
53-
}
54-
55-
const transaction = Sentry.startTransaction({
56-
name: `${reqMethod} ${reqUrl}`,
57-
op: "http.server",
58-
...traceparentData,
59-
})
60-
61-
ctx.__sentry_transaction = transaction
62-
63-
await next()
64-
65-
// if using koa router, a nicer way to capture transaction using the matched route
66-
if (ctx._matchedRoute) {
67-
const mountPath = (ctx.mountPath as string) || ""
68-
transaction.setName(`${reqMethod} ${mountPath}${ctx._matchedRoute as string}`)
69-
}
70-
71-
transaction.setHttpStatus(ctx.status)
72-
transaction.finish()
73-
}
74-
75-
export const sendErrorToSentry = (err: Error, ctx: Context) => {
13+
export const sendErrorToSentry = (
14+
err: Error,
15+
// eslint-disable-next-line @typescript-eslint/naming-convention
16+
request: FastifyRequest<{ Querystring: Record<string, unknown> }>,
17+
) => {
7618
Sentry.withScope((scope) => {
77-
scope.addEventProcessor((event) =>
78-
Sentry.Handlers.parseRequest(event, ctx.request as any),
79-
)
19+
scope.addEventProcessor((event) => Sentry.Handlers.parseRequest(event, request))
8020

8121
Sentry.captureException(err)
8222
})

src/manual-rules.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { knex, Relation } from "./db"
2-
import { Logger } from "./lib/logger"
32

43
const rules = {
54
// Kaguya-sama
@@ -37,7 +36,7 @@ export const updateBasedOnManualRules = async () => {
3736
knex("relations").update(fromWhere).where(toWhere).transacting(trx),
3837
),
3938
)
40-
.catch(Logger.error)
39+
.catch(console.error)
4140
})
4241

4342
await Promise.all(promises)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`json body array input requires at least one source 1`] = `
4+
Object {
5+
"error": "Bad Request",
6+
"message": "body should be object, body[0] should NOT have fewer than 1 properties, body should match exactly one schema in oneOf",
7+
"statusCode": 400,
8+
}
9+
`;
10+
11+
exports[`json body object input GET fails with json body 1`] = `
12+
Object {
13+
"error": "Bad Request",
14+
"message": "querystring should have required property 'source'",
15+
"statusCode": 400,
16+
}
17+
`;
18+
19+
exports[`json body object input errors correctly on an empty object 1`] = `
20+
Object {
21+
"error": "Bad Request",
22+
"message": "body should NOT have fewer than 1 properties, body should be array, body should match exactly one schema in oneOf",
23+
"statusCode": 400,
24+
}
25+
`;

0 commit comments

Comments
 (0)