Skip to content

Instantly share code, notes, and snippets.

@phiresky
Last active April 9, 2020 14:17
Show Gist options
  • Save phiresky/f55d3c6b1d3282a1846c0e33504dcd5a to your computer and use it in GitHub Desktop.
Save phiresky/f55d3c6b1d3282a1846c0e33504dcd5a to your computer and use it in GitHub Desktop.
koa example of a somewhat sane typed http server
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()
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] };
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;
}
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