Last active
April 9, 2020 14:17
-
-
Save phiresky/f55d3c6b1d3282a1846c0e33504dcd5a to your computer and use it in GitHub Desktop.
koa example of a somewhat sane typed http server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { makeClient } from "./makeTypedApi"; | |
import { Api } from "./common"; | |
const api = makeClient(Api); | |
// has all the HTTP methods like normal methods, e.g. | |
const results = await api.byDate() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
array, | |
number, | |
boolean, | |
Integer as int, | |
string, | |
TypeOf, | |
union, | |
interface as object, | |
null as nul, | |
} from "io-ts"; | |
import { ApiType } from "./makeTypedApi"; | |
import * as tbl from "../tables"; | |
import * as aapi from "../autoload-api"; | |
const existsById = array( | |
object({ | |
min: int, | |
max: int, | |
exists: int, | |
total: int, | |
}), | |
); | |
const date = string; //todo | |
export const Api = { | |
getHighrollers: { | |
request: object({ limit: int }), | |
response: array( | |
object({ | |
id: string, | |
transactions: int, | |
total: string, | |
}), | |
), | |
}, | |
existsById: { | |
request: object({ width: int }), | |
response: array( | |
object({ | |
max: int, | |
min: int, | |
exists: int, | |
total: int, | |
}), | |
), | |
}, | |
userCounts: { | |
request: nul, | |
response: array( | |
object({ | |
seemsToExist: string, | |
count: int, | |
}), | |
), | |
}, | |
byDate: { | |
request: nul, | |
response: array( | |
object({ | |
date, | |
incoming: number, | |
incomingCount: int, | |
outgoing: number, | |
outgoingCount: int, | |
}), | |
), | |
}, | |
}; | |
export type Api = { [k in keyof typeof Api]: Api[k] }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as t from "io-ts"; | |
import * as Koa from "koa"; | |
import { ThrowReporter } from "io-ts/lib/ThrowReporter"; | |
import * as util from "../common-util"; | |
export type ApiType = { | |
[name: string]: { | |
request: t.Type<any>; | |
response: t.Type<any>; | |
}; | |
}; | |
export type ApiServer<Api extends ApiType> = { | |
[k in keyof Api]: ( | |
request: t.TypeOf<Api[k]["request"]>, | |
) => Promise<t.TypeOf<Api[k]["response"]>> | |
}; | |
export function makeServer<Api extends ApiType>( | |
api: Api, | |
server: Partial<ApiServer<Api>>, | |
): Koa.Middleware { | |
return async function typedApi( | |
context: Koa.Context, | |
next: () => Promise<any>, | |
) { | |
const parts = context.path.split("/"); | |
if (parts[0] === "" && parts[1] === "api" && parts.length === 3) { | |
const method = parts[2]; | |
const methodType = api[method]; | |
if (!methodType) { | |
throw new Error(`Unknown method ${methodType}`); | |
} | |
if (!context.request.body) { | |
throw new Error( | |
"body is undefined. do you have bodyparser activated?", | |
); | |
} | |
const body = context.request.body as { data: any }; | |
if (!("data" in body)) { | |
throw new Error(`undefined arguments`); | |
} | |
const args = body.data; | |
const validation = methodType.request.decode(args); | |
try { | |
ThrowReporter.report(validation); | |
} catch (e) { | |
throw new Error("Type Error: " + e); | |
} | |
const meth = server[method]; | |
if (!meth) throw new Error(`Method not implemented: ${method}`); | |
context.body = await meth(args); | |
} else return next(); | |
}; | |
} | |
export function makeClient<Api extends ApiType>(api: Api): ApiServer<Api> { | |
const obj: ApiServer<Api> = {} as any; | |
for (const [k, v] of util.objectEntries(api)) { | |
obj[k] = async (args: t.TypeOf<typeof v.request>) => { | |
const res = await fetch(`/api/${k}`, { | |
method: "POST", | |
headers: { | |
"Content-Type": "application/json", | |
}, | |
credentials: "include", | |
body: JSON.stringify({ data: args }), | |
}); | |
if (!res.ok) | |
throw new Error( | |
`${res.status}: ${res.statusText}: ${await res.text()}`, | |
); | |
else { | |
return (await res.json()) as t.TypeOf<typeof v.response>; | |
} | |
}; | |
} | |
return obj; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Api } from "./common"; | |
import { makeServer } from "./makeTypedApi"; | |
import * as Koa from "koa"; | |
import * as compress from "koa-compress"; | |
import db from "../db"; | |
import { | |
users, | |
trans_pos, | |
trans, | |
login_response, | |
timestamptz, | |
FromItemToInRow, | |
tTime, | |
tDate, | |
} from "../tables"; | |
import * as s from "@phiresky/typed-sql"; | |
import { distinct, min, max, nullif, abs, date_trunc } from "../util"; | |
import * as webpackConfig from "../../webpack.config"; | |
import * as webpack from "webpack"; | |
import { devMiddleware } from "koa-webpack-middleware"; | |
import * as bodyparser from "koa-bodyparser"; | |
import auth = require("koa-basic-auth"); | |
const app = new Koa(); | |
app.use(compress()); | |
app.use(async (ctx, next) => { | |
try { | |
await next(); | |
} catch (err) { | |
if (401 === err.status) { | |
ctx.status = 401; | |
ctx.set("WWW-Authenticate", "Basic"); | |
ctx.body = "cant haz that"; | |
} else { | |
throw err; | |
} | |
} | |
}); | |
app.use(auth({ name: "test", pass: "ing" })); | |
const config = { | |
...webpackConfig, | |
entry: "./src/stats/client", | |
mode: "production", | |
}; | |
app.use(async (ctx, next) => { | |
try { | |
await next(); | |
} catch (err) { | |
// console.error(err); | |
ctx.status = err.status || 400; | |
ctx.body = err.message; | |
ctx.app.emit("error", err, ctx); | |
} | |
}); | |
app.use(bodyparser()); | |
const server = makeServer(Api, { | |
getHighrollers: async ({ limit }) => | |
await db.exec( | |
s | |
.from(users) | |
.innerJoin(trans) | |
.on({ _userId: users.id }) | |
// .where({ seemsToExist: true }) | |
.groupBy(users.id) | |
.orderBy( | |
abs(trans.value) | |
.sum() | |
.desc(), | |
) | |
//[...] | |
.limit(limit), | |
), | |
byDate: async _ => { | |
// [...] | |
}, | |
}); | |
app.use(server); | |
app.use((ctx, next) => { | |
if ( | |
!ctx.path.endsWith(".js") && | |
!ctx.path.endsWith(".css") && | |
!ctx.path.endsWith(".map") | |
) | |
ctx.path = "/"; | |
// just handle everything client side, don't care | |
return next(); | |
}); | |
app.use( | |
devMiddleware(webpack([config as any]), { | |
watchOptions: { | |
aggregateTimeout: 300, | |
poll: true, | |
}, | |
historyApiFallback: true, | |
}), | |
); | |
if (!module.parent) { | |
const port = 8000; | |
app.listen(port); | |
console.log(`listening on ${port}`); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment