diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cac0e10 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c8eac36..fed47c3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,19 +1,19 @@ { - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "dev", - "problemMatcher": [], - "label": "npm: dev", - "detail": "Watch code files to auto-rebuild the library and example." - }, - { - "type": "npm", - "script": "build", - "problemMatcher": [], - "label": "npm: build", - "detail": "Get an optimized build of the library." - } - ] -} \ No newline at end of file + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "dev", + "problemMatcher": [], + "label": "npm: dev", + "detail": "Watch code files to auto-rebuild the library and example." + }, + { + "type": "npm", + "script": "build", + "problemMatcher": [], + "label": "npm: build", + "detail": "Get an optimized build of the library." + } + ] +} diff --git a/example/index.ts b/example/index.ts index 64524d5..7c327cb 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,3 +1,17 @@ -import { foo } from "../src/utils"; +import { endpoint } from "../src"; +import { startServer } from "../src/core/server"; -console.log(foo()); \ No newline at end of file +type Data = { + name: string; +}; + +startServer( + endpoint(async (req) => { + return { + status: 200, + body: { + name: "foo", + }, + }; + }), +); diff --git a/package.json b/package.json index ff70532..b6d5a21 100644 --- a/package.json +++ b/package.json @@ -13,5 +13,8 @@ "devDependencies": { "@types/node": "^17.0.35", "esbuild": "^0.14.39" + }, + "dependencies": { + "tslog": "^3.3.3" } } diff --git a/src/compose.ts b/src/compose.ts new file mode 100644 index 0000000..a5f0e5f --- /dev/null +++ b/src/compose.ts @@ -0,0 +1 @@ +export type Server = (request: Request) => Promise; diff --git a/src/core/bufferedServer.ts b/src/core/bufferedServer.ts new file mode 100644 index 0000000..da0caea --- /dev/null +++ b/src/core/bufferedServer.ts @@ -0,0 +1,52 @@ +import { Logger } from "tslog"; +import type { Server } from "../compose"; +import type { CoreRequest } from "./server"; + +export type Request = Omit; + +export type BufferedResponse = { + status: number; + body: string; +}; + +export function bufferedResponse( + app: Server, +): Server { + return async function bufferedResp(request) { + const out = await app(request); + request.response.statusCode = out.status; + request.response.write(out.body); + }; +} + +export type BufferedRequest = Request & { + body: string; +}; + +export function bufferedRequest( + app: Server, +): Server { + return function bufferedReq(request) { + const logger = new Logger(); + let body = ""; + const response = new Promise((resolve) => { + request.request.on("data", (chunk) => { + body += chunk.toString(); + }); + request.request.on("end", () => { + app({ + ...request, + body, + }) + .then((out) => { + resolve(out); + }) + .catch((err) => { + logger.error("App did not return a response", err); + resolve(undefined); + }); + }); + }); + return response; + }; +} diff --git a/src/core/server.ts b/src/core/server.ts new file mode 100644 index 0000000..40614f4 --- /dev/null +++ b/src/core/server.ts @@ -0,0 +1,48 @@ +import { createServer } from "http"; +import type { IncomingMessage, ServerResponse } from "http"; +import { Logger } from "tslog"; +import { Server } from "../compose"; + +/** A basic request. */ +export type CoreRequest = { + /** The full URL for the request. */ + url: string; + /** The HTTP method for the request. */ + method: string; + /** The headers associated with the request. */ + headers: { [key: string]: string | string[] }; + /** The request body. */ + request: IncomingMessage; + /** The response. */ + response: ServerResponse; +}; + +export function startServer(app: Server) { + const logger = new Logger(); + const server = createServer((req, res) => { + logger.info(req.method, req.url); + app({ + url: req.url, + method: req.method, + headers: req.headers, + request: req, + response: res, + }) + .then(() => { + logger.debug("request processed successfully"); + }) + .catch((error) => { + res.statusCode = 500; + logger.error("server throw an unexpected error", error); + res.write("Error!\n"); + res.write(JSON.stringify(error)); + }) + .finally(() => { + res.end(); + }); + }); + server.on("clientError", (err, socket) => { + socket.end("HTTP/1.1 400 Bad CoreRequest\r\n\r\n"); + }); + server.listen(8000); +} diff --git a/src/index.ts b/src/index.ts index b76b709..d787901 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,10 @@ -import http from "http"; +import type { Server } from "./compose"; +import { bufferedRequest, bufferedResponse } from "./core/bufferedServer"; +import type { CoreRequest } from "./core/server"; +import { jsonRequest, JSONRequest, jsonResponse, JSONResponse } from "./json"; -const server = http.createServer((req, res) => { - res.end(); -}); -server.on('clientError', (err, socket) => { - socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); -}); -server.listen(8000); +export function endpoint( + app: Server, JSONResponse>, +): Server { + return bufferedResponse(bufferedRequest(jsonResponse(jsonRequest(app)))); +} diff --git a/src/json.ts b/src/json.ts new file mode 100644 index 0000000..12a26fc --- /dev/null +++ b/src/json.ts @@ -0,0 +1,32 @@ +import { Server } from "./compose"; +import type { BufferedRequest, BufferedResponse } from "./core/bufferedServer"; + +export type JSONRequest = Omit & { body: Data }; + +export type JSONResponse = Omit & { + body: Data; +}; + +export function jsonResponse( + app: Server>, +): Server { + return async function jsonResp(request) { + const out = await app(request); + return { + ...out, + body: JSON.stringify(out.body), + }; + }; +} + +export function jsonRequest( + app: Server, Response>, +): Server { + return async function jsonReq(request) { + let body: any | undefined; + if (request.body.length > 0) { + body = JSON.parse(request.body); + } + return app({ ...request, body }); + }; +} diff --git a/src/routing/router.ts b/src/routing/router.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 1cc4358..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function foo() { - return "bar"; -} - -export function baz() { - return "baz"; -} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7eb5ca9..ab9aba0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz" integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + esbuild-android-64@0.14.39: version "0.14.39" resolved "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.39.tgz#09f12a372eed9743fd77ff6d889ac14f7b340c21" @@ -132,3 +137,23 @@ esbuild@^0.14.39: esbuild-windows-32 "0.14.39" esbuild-windows-64 "0.14.39" esbuild-windows-arm64 "0.14.39" + +source-map-support@^0.5.21: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +tslog@^3.3.3: + version "3.3.3" + resolved "https://registry.npmjs.org/tslog/-/tslog-3.3.3.tgz#751a469e0d36841bd7e03676c27e53e7ffe9bc3d" + integrity sha512-lGrkndwpAohZ9ntQpT+xtUw5k9YFV1DjsksiWQlBSf82TTqsSAWBARPRD9juI730r8o3Awpkjp2aXy9k+6vr+g== + dependencies: + source-map-support "^0.5.21"