@lbu/server
Advanced tools
Comparing version 0.0.8 to 0.0.9
{ | ||
"name": "@lbu/server", | ||
"version": "0.0.8", | ||
"version": "0.0.9", | ||
"description": "Koa server and common middleware", | ||
@@ -17,4 +17,4 @@ "main": "index.js", | ||
"dependencies": { | ||
"@lbu/insight": "^0.0.8", | ||
"@lbu/stdlib": "^0.0.8", | ||
"@lbu/insight": "^0.0.9", | ||
"@lbu/stdlib": "^0.0.9", | ||
"koa": "2.11.0", | ||
@@ -38,3 +38,3 @@ "koa-body": "4.1.1", | ||
}, | ||
"gitHead": "85feb582684f93714a7aad81f2a47883e72bd363" | ||
"gitHead": "d847630e049071c7c2385eef8377ba976ddd0e2a" | ||
} |
@@ -12,30 +12,13 @@ # @lbu/server | ||
## Versioning and first release | ||
For internal testing we stay on v0.0.x. To reach v0.1.0 the following features | ||
will be supported: | ||
- Flexible code generation (validators, router, queries, openapi) | ||
- Usable documentation | ||
- Test coverage (either e2e or unit, but enough to be considered somewhat | ||
stable) | ||
- Used in a medium size project @ Lightbase | ||
- Implement a [Realworld project](https://github.com/gothinkster/realworld) | ||
## Features | ||
- @lbu/cli: Project template, and simple script runner | ||
- @lbu/code-gen: Flexible code generators. Supports generating router, validator | ||
- @lbu/insight: Opinionated logger | ||
- @lbu/server: Wrap around Koa and some useful middleware | ||
- @lbu/stdlib: Growing library of various common utilities like uuid & a basic | ||
templating system | ||
- Minimal API project boilerplate | ||
- Script runner, can watch & reload almost anything (via nodemon) | ||
- Flexible code generators supporting routers, validators, api clients, mocks | ||
and more in the future. | ||
- Opinionated structured logging | ||
- Common Koa middleware wrapped in a single function | ||
- Various utilities like loading .env files, executing other processes and a | ||
basic template system | ||
## Roadmap | ||
- [ ] @lbu/code-gen: OpenAPI importer | ||
- [ ] @lbu/features: Feature flag implementation based on @lbu/store & support | ||
for code-gen | ||
- [ ] @lbu/code-gen: Postgres query generator | ||
## Docs | ||
@@ -42,0 +25,0 @@ |
@@ -20,3 +20,3 @@ import Koa from "koa"; | ||
*/ | ||
export const getApp = (opts = {}) => { | ||
export function getApp(opts = {}) { | ||
const app = new Koa(); | ||
@@ -41,2 +41,2 @@ app.proxy = | ||
return app; | ||
}; | ||
} |
@@ -12,3 +12,3 @@ import { merge } from "@lbu/stdlib"; | ||
*/ | ||
export const createBodyParsers = (opts = {}) => { | ||
export function createBodyParsers(opts = {}) { | ||
const multiPartOpts = merge({}, opts); | ||
@@ -21,3 +21,3 @@ | ||
memoizeMultipartBodyParser = koaBody(multiPartOpts); | ||
}; | ||
} | ||
@@ -29,3 +29,3 @@ /** | ||
*/ | ||
export const getBodyParser = () => { | ||
export function getBodyParser() { | ||
if (memoizeBodyParser === undefined) { | ||
@@ -38,3 +38,3 @@ throw new Error( | ||
return memoizeBodyParser; | ||
}; | ||
} | ||
@@ -47,3 +47,3 @@ /** | ||
*/ | ||
export const getMultipartBodyParser = () => { | ||
export function getMultipartBodyParser() { | ||
if (memoizeMultipartBodyParser === undefined) { | ||
@@ -56,2 +56,2 @@ throw new Error( | ||
return memoizeMultipartBodyParser; | ||
}; | ||
} |
@@ -35,3 +35,4 @@ /* | ||
* @typedef {Object} CorsOptions | ||
* @property {string|function(ctx)} [origin] `Access-Control-Allow-Origin`, default is request Origin header | ||
* @property {string|function(ctx)} [origin] `Access-Control-Allow-Origin`, default is | ||
* request Origin header | ||
* @property {string[]} [exposeHeaders] `Access-Control-Expose-Headers` | ||
@@ -53,6 +54,6 @@ * @property {string|number} [maxAge] `Access-Control-Max-Age` in seconds | ||
*/ | ||
export const cors = (options = {}) => { | ||
export function cors(options = {}) { | ||
const opts = Object.assign({}, defaultOptions, options); | ||
let originFn = ctx => options.origin || ctx.get("Origin") || "*"; | ||
let originFn = (ctx) => options.origin || ctx.get("Origin") || "*"; | ||
if (typeof options.origin === "function") { | ||
@@ -132,2 +133,2 @@ originFn = options.origin; | ||
}; | ||
}; | ||
} |
@@ -122,3 +122,3 @@ import { isNil } from "@lbu/stdlib"; | ||
*/ | ||
export const errorHandler = ({ onAppError, onError, leakError }) => { | ||
export function errorHandler({ onAppError, onError, leakError }) { | ||
onAppError = onAppError || defaultOnAppError; | ||
@@ -137,6 +137,6 @@ onError = onError || defaultOnError; | ||
let err = error; | ||
let log = ctx.log.info; | ||
let log = ctx.log.info.bind(ctx.log); | ||
if (!(error instanceof AppError)) { | ||
log = ctx.log.error; | ||
log = ctx.log.error.bind(ctx.log); | ||
err = new AppError("error.server.internal", 500, {}, error); | ||
@@ -171,2 +171,2 @@ } | ||
}; | ||
}; | ||
} |
@@ -7,3 +7,3 @@ import { cors } from "./cors.js"; | ||
*/ | ||
export const defaultHeaders = (opts = {}) => { | ||
export function defaultHeaders(opts = {}) { | ||
// Excerpt from default helmet headers | ||
@@ -26,2 +26,2 @@ // When serving static files, some extra headers should be added | ||
}; | ||
}; | ||
} |
/** | ||
* Middleware that immediately returns on ANY /_health | ||
*/ | ||
export const healthHandler = () => (ctx, next) => { | ||
if (ctx.path === "/_health") { | ||
ctx.body = ""; | ||
} else { | ||
return next(); | ||
} | ||
}; | ||
export function healthHandler() { | ||
return (ctx, next) => { | ||
if (ctx.path === "/_health") { | ||
ctx.body = ""; | ||
} else { | ||
return next(); | ||
} | ||
}; | ||
} |
@@ -6,23 +6,64 @@ import { newLogger } from "@lbu/insight"; | ||
/** | ||
* Wait for the ctx.body stream to finish before resolving | ||
* @param ctx | ||
* @returns {Promise<void>} | ||
* Log basic request and response information | ||
*/ | ||
const bodyCloseOrFinish = async ctx => { | ||
return new Promise(resolve => { | ||
const onFinish = done.bind(null, "finish"); | ||
const onClose = done.bind(null, "close"); | ||
export function logMiddleware() { | ||
return async (ctx, next) => { | ||
const startTime = process.hrtime.bigint(); | ||
ctx.body.once("finish", onFinish); | ||
ctx.body.once("close", onClose); | ||
let requestId = ctx.get("X-Request-Id"); | ||
if (isNil(requestId) || requestId.length === 0) { | ||
requestId = uuid(); | ||
} | ||
ctx.set("X-Request-Id", requestId); | ||
function done() { | ||
ctx.body.removeListener("finish", onFinish); | ||
ctx.body.removeListener("close", onClose); | ||
resolve(); | ||
ctx.log = newLogger({ | ||
depth: 5, | ||
ctx: { | ||
type: "HTTP", | ||
requestId, | ||
}, | ||
}); | ||
await next(); | ||
let counter; | ||
if (!isNil(ctx.response.length)) { | ||
logInfo(ctx, startTime, ctx.response.length); | ||
return; | ||
} else if (ctx.body && ctx.body.readable) { | ||
const body = ctx.body; | ||
counter = new StreamLength(); | ||
ctx.body = body.pipe(counter).on("error", ctx.onerror); | ||
await bodyCloseOrFinish(ctx); | ||
} | ||
}); | ||
}; | ||
logInfo(ctx, startTime, isNil(counter) ? 0 : counter.length); | ||
}; | ||
} | ||
/** | ||
* Basic http log counters | ||
* @param store | ||
* @return {function(...[*]=)} | ||
*/ | ||
export function logParser(store) { | ||
store.requestCount = 0; | ||
store.totalDuration = 0; | ||
store.totalResponseLength = 0; | ||
store.methodSummary = {}; | ||
store.statusCodeSummary = {}; | ||
return (obj) => { | ||
if (!obj.type || obj.type !== "HTTP") { | ||
return; | ||
} | ||
if (!obj.message || !obj.message.request || !obj.message.response) { | ||
return; | ||
} | ||
handleRequestLog(store, obj); | ||
}; | ||
} | ||
/** | ||
* Get the size of data that goes through a stream | ||
@@ -49,3 +90,3 @@ */ | ||
*/ | ||
const logInfo = (ctx, startTime, length) => { | ||
function logInfo(ctx, startTime, length) { | ||
const duration = Math.round( | ||
@@ -67,78 +108,39 @@ Number(process.hrtime.bigint() - startTime) / 1000000, | ||
}); | ||
}; | ||
} | ||
/** | ||
* Log basic request and response information | ||
*/ | ||
export const logMiddleware = () => async (ctx, next) => { | ||
const startTime = process.hrtime.bigint(); | ||
function handleRequestLog(store, obj) { | ||
store.requestCount++; | ||
store.totalDuration += Number(obj.message.response.duration); | ||
store.totalResponseLength += obj.message.response.length; | ||
let requestId = ctx.get("X-Request-Id"); | ||
if (isNil(requestId) || requestId.length === 0) { | ||
requestId = uuid(); | ||
if (!store.methodSummary[obj.message.request.method]) { | ||
store.methodSummary[obj.message.request.method] = 0; | ||
} | ||
ctx.set("X-Request-Id", requestId); | ||
store.methodSummary[obj.message.request.method]++; | ||
ctx.log = newLogger({ | ||
depth: 5, | ||
ctx: { | ||
type: "HTTP", | ||
requestId, | ||
}, | ||
}); | ||
await next(); | ||
let counter; | ||
if (!isNil(ctx.response.length)) { | ||
logInfo(ctx, startTime, ctx.response.length); | ||
return; | ||
} else if (ctx.body && ctx.body.readable) { | ||
const body = ctx.body; | ||
counter = new StreamLength(); | ||
ctx.body = body.pipe(counter).on("error", ctx.onerror); | ||
await bodyCloseOrFinish(ctx); | ||
if (!store.statusCodeSummary[obj.message.response.status]) { | ||
store.statusCodeSummary[obj.message.response.status] = 0; | ||
} | ||
store.statusCodeSummary[obj.message.response.status]++; | ||
} | ||
logInfo(ctx, startTime, isNil(counter) ? 0 : counter.length); | ||
}; | ||
/** | ||
* Basic http log counters | ||
* @param store | ||
* @return {function(...[*]=)} | ||
* Wait for the ctx.body stream to finish before resolving | ||
* @param ctx | ||
* @returns {Promise<void>} | ||
*/ | ||
export const logParser = store => { | ||
store.requestCount = 0; | ||
store.totalDuration = 0; | ||
store.totalResponseLength = 0; | ||
store.methodSummary = {}; | ||
store.statusCodeSummary = {}; | ||
async function bodyCloseOrFinish(ctx) { | ||
return new Promise((resolve) => { | ||
const onFinish = done.bind(null, "finish"); | ||
const onClose = done.bind(null, "close"); | ||
return obj => { | ||
if (!obj.type || obj.type !== "HTTP") { | ||
return; | ||
} | ||
if (!obj.message || !obj.message.request || !obj.message.response) { | ||
return; | ||
} | ||
ctx.body.once("finish", onFinish); | ||
ctx.body.once("close", onClose); | ||
handleRequestLog(obj); | ||
}; | ||
function handleRequestLog(obj) { | ||
store.requestCount++; | ||
store.totalDuration += Number(obj.message.response.duration); | ||
store.totalResponseLength += obj.message.response.length; | ||
if (!store.methodSummary[obj.message.request.method]) { | ||
store.methodSummary[obj.message.request.method] = 0; | ||
function done() { | ||
ctx.body.removeListener("finish", onFinish); | ||
ctx.body.removeListener("close", onClose); | ||
resolve(); | ||
} | ||
store.methodSummary[obj.message.request.method]++; | ||
if (!store.statusCodeSummary[obj.message.response.status]) { | ||
store.statusCodeSummary[obj.message.response.status] = 0; | ||
} | ||
store.statusCodeSummary[obj.message.response.status]++; | ||
} | ||
}; | ||
}); | ||
} |
@@ -6,8 +6,10 @@ import { AppError } from "./error.js"; | ||
*/ | ||
export const notFoundHandler = () => async (ctx, next) => { | ||
await next(); | ||
ctx.status = ctx.status || 404; | ||
if (ctx.status === 404) { | ||
throw AppError.notFound(); | ||
} | ||
}; | ||
export function notFoundHandler() { | ||
return async (ctx, next) => { | ||
await next(); | ||
ctx.status = ctx.status || 404; | ||
if (ctx.status === 404) { | ||
throw AppError.notFound(); | ||
} | ||
}; | ||
} |
527
18991
30
+ Added@lbu/insight@0.0.9(transitive)
+ Added@lbu/stdlib@0.0.9(transitive)
+ Addeduuid@7.0.2(transitive)
- Removed@lbu/insight@0.0.8(transitive)
- Removed@lbu/stdlib@0.0.8(transitive)
- Removeduuid@3.4.0(transitive)
Updated@lbu/insight@^0.0.9
Updated@lbu/stdlib@^0.0.9