From ad1b2de8063ff617d0f4f4086d955765a68015df Mon Sep 17 00:00:00 2001 From: Michael Wolf Date: Sun, 8 Feb 2026 09:19:27 -0500 Subject: [PATCH] Get base files closer to being bootstrappable --- backend/app.ts | 17 +--- backend/diachron/app.ts | 113 +++++++----------------- backend/diachron/basic/routes.ts | 11 ++- backend/diachron/routing.ts | 93 +++++++++++++++++++ backend/{diachron => }/handlers.spec.ts | 10 +-- backend/{diachron => }/handlers.ts | 10 ++- backend/routes.ts | 10 ++- templates/basic/hello.html.njk | 2 +- 8 files changed, 154 insertions(+), 112 deletions(-) create mode 100644 backend/diachron/routing.ts rename backend/{diachron => }/handlers.spec.ts (89%) rename backend/{diachron => }/handlers.ts (54%) diff --git a/backend/app.ts b/backend/app.ts index 91d896d..683c817 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -1,14 +1,7 @@ -import{cli}from'./diachron/cli' -import { formatError } from './diachron/errors'; +// This is a sample file provided by diachron. You are encouraged to modify it. + -process.on('uncaughtException', (err) => { - console.error(formatError(err)); - process.exit(1); -}); -process.on('unhandledRejection', (reason) => { - console.error(formatError(reason)); -}); import { core } from "./diachron/core"; @@ -24,8 +17,4 @@ core.logging.log({ source: "logging", text: ["1"] }); - - -app.listen(cli.listen.port, cli.listen.host, () => { - console.log(`Listening on ${cli.listen.host}:${cli.listen.port}`); -}); +app.start() diff --git a/backend/diachron/app.ts b/backend/diachron/app.ts index 35d8fd0..5bad0cc 100644 --- a/backend/diachron/app.ts +++ b/backend/diachron/app.ts @@ -3,6 +3,7 @@ import{contentTypes} from './content-types' import{httpCodes}from'./http-codes' import express, { +type Express, type NextFunction, type Request as ExpressRequest, type Response as ExpressResponse, @@ -10,102 +11,39 @@ import express, { import { formatError, formatErrorHtml } from "./errors"; import {isRedirect, InternalHandler, AuthenticationRequired, AuthorizationDenied, Call,type Method, type ProcessedRoute,methodParser, type Result, type Route,massageMethod } from "./types"; -import { runWithContext } from "./context"; -import { Session } from "./auth";import { request } from "./request"; -import { match } from "path-to-regexp"; + + import { cli } from "./cli"; +import{processRoutes}from'./routing' -type ProcessedRoutes= {[K in Method]: ProcessedRoute[] } -const processRoutes=(routes:Route[]) :ProcessedRoutes => { - const retval:ProcessedRoutes= { - GET: [], - POST: [], - PUT: [], - PATCH: [], - DELETE: [], - }; - routes.forEach((route: Route, _idx: number, _allRoutes: Route[]) => { - // const pattern /*: URLPattern */ = new URLPattern({ pathname: route.path }); - const matcher = match>(route.path); - const methodList = route.methods; - - const handler: InternalHandler = async ( - expressRequest: ExpressRequest, - ): Promise => { - const method = massageMethod(expressRequest.method); - - console.log("method", method); - - if (!methodList.includes(method)) { - // XXX: Worth asserting this? - } - - console.log("request.originalUrl", expressRequest.originalUrl); - - // Authenticate the request - const auth = await request.auth.validateRequest(expressRequest); - - const req: Call = { - pattern: route.path, - path: expressRequest.originalUrl, - method, - parameters: { one: 1, two: 2 }, - request: expressRequest, - user: auth.user, - session: new Session(auth.session, auth.user), - }; - - try { - const retval = await runWithContext({ user: auth.user }, () => - route.handler(req), - ); - return retval; - } catch (error) { - // Handle authentication errors - if (error instanceof AuthenticationRequired) { - return { - code: httpCodes.clientErrors.Unauthorized, - contentType: contentTypes.application.json, - result: JSON.stringify({ - error: "Authentication required", - }), - }; - } - if (error instanceof AuthorizationDenied) { - return { - code: httpCodes.clientErrors.Forbidden, - contentType: contentTypes.application.json, - result: JSON.stringify({ error: "Access denied" }), - }; - } - throw error; - } - }; - - for (const [_idx, method] of methodList.entries()) { - const pr: ProcessedRoute = { matcher, method, handler }; - - retval[method].push(pr); - } - }); - - return retval; -} +process.on('uncaughtException', (err) => { + console.error(formatError(err)); + process.exit(1); +}); + +process.on('unhandledRejection', (reason) => { + console.error(formatError(reason)); +}); + type MakeAppArgs={routes:Route[], -processTitle?: string, + processTitle?: string, } +export interface DiachronApp extends Express { + start: () => void +} + const makeApp = ({routes, processTitle}: MakeAppArgs) => { if (process.title) { process.title = `diachron:${cli.listen.port}`; } -const processedRoutes = processRoutes(routes) + const processedRoutes = processRoutes(routes) -async function handler( + async function handler( req: ExpressRequest, _res: ExpressResponse, ): Promise { @@ -138,8 +76,17 @@ async function handler( return retval; } + // I don't like going around tsc but this is simple enough that it's probably OK. + const app = express() as DiachronApp + app.start = function() { + this.listen(cli.listen.port, cli.listen.host, () => { + console.log(`Listening on ${cli.listen.host}:${cli.listen.port}`); + }); + }; + + + - const app = express(); app.use(express.json()) diff --git a/backend/diachron/basic/routes.ts b/backend/diachron/basic/routes.ts index 4bd898e..2a7206a 100644 --- a/backend/diachron/basic/routes.ts +++ b/backend/diachron/basic/routes.ts @@ -10,9 +10,16 @@ const routes: Record = { hello: { path: "/hello", methods: ["GET"], - handler: async (_call: Call): Promise => { + handler: async (call: Call): Promise => { const now = DateTime.now(); - const c = await render("basic/hello", { now }); + const args: {now: DateTime,greeting?: string} = {now}; + + if (call.path !== '/hello') { + const greeting = call. path.replaceAll('/','').replaceAll('-', ' ') + args.greeting = greeting; + } + + const c = await render("basic/hello", args); return html(c); }, diff --git a/backend/diachron/routing.ts b/backend/diachron/routing.ts new file mode 100644 index 0000000..2535743 --- /dev/null +++ b/backend/diachron/routing.ts @@ -0,0 +1,93 @@ +import { contentTypes } from "./content-types"; +import { httpCodes } from "./http-codes"; +import express, { + type NextFunction, + type Request as ExpressRequest, + type Response as ExpressResponse, +} from "express"; + +import {isRedirect, InternalHandler, AuthenticationRequired, + AuthorizationDenied, Call,type Method, type ProcessedRoute,methodParser, type Result, type Route,massageMethod } from "./types"; +import { runWithContext } from "./context"; +import { Session } from "./auth";import { request } from "./request"; +import { match } from "path-to-regexp"; + +type ProcessedRoutes= {[K in Method]: ProcessedRoute[] } +const processRoutes=(routes:Route[]) :ProcessedRoutes => { + const retval:ProcessedRoutes= { + GET: [], + POST: [], + PUT: [], + PATCH: [], + DELETE: [], + }; + + routes.forEach((route: Route, _idx: number, _allRoutes: Route[]) => { + // const pattern /*: URLPattern */ = new URLPattern({ pathname: route.path }); + const matcher = match>(route.path); + const methodList = route.methods; + + const handler: InternalHandler = async ( + expressRequest: ExpressRequest, + ): Promise => { + const method = massageMethod(expressRequest.method); + + console.log("method", method); + + if (!methodList.includes(method)) { + // XXX: Worth asserting this? + } + + console.log("request.originalUrl", expressRequest.originalUrl); + + // Authenticate the request + const auth = await request.auth.validateRequest(expressRequest); + + const req: Call = { + pattern: route.path, + path: expressRequest.originalUrl, + method, + parameters: { one: 1, two: 2 }, + request: expressRequest, + user: auth.user, + session: new Session(auth.session, auth.user), + }; + + try { + const retval = await runWithContext({ user: auth.user }, () => + route.handler(req), + ); + return retval; + } catch (error) { + // Handle authentication errors + if (error instanceof AuthenticationRequired) { + return { + code: httpCodes.clientErrors.Unauthorized, + contentType: contentTypes.application.json, + result: JSON.stringify({ + error: "Authentication required", + }), + }; + } + if (error instanceof AuthorizationDenied) { + return { + code: httpCodes.clientErrors.Forbidden, + contentType: contentTypes.application.json, + result: JSON.stringify({ error: "Access denied" }), + }; + } + throw error; + } + }; + + for (const [_idx, method] of methodList.entries()) { + const pr: ProcessedRoute = { matcher, method, handler }; + + retval[method].push(pr); + } + }); + + return retval; +} + +export{processRoutes} diff --git a/backend/diachron/handlers.spec.ts b/backend/handlers.spec.ts similarity index 89% rename from backend/diachron/handlers.spec.ts rename to backend/handlers.spec.ts index 175b865..d1e4fa1 100644 --- a/backend/diachron/handlers.spec.ts +++ b/backend/handlers.spec.ts @@ -4,12 +4,12 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import type { Request as ExpressRequest } from "express"; -import { Session } from "./auth/types"; -import { contentTypes } from "./content-types"; +import { Session } from "./diachron/auth/types"; +import { contentTypes } from "./diachron/content-types"; import { multiHandler } from "./handlers"; -import { httpCodes } from "./http-codes"; -import type { Call } from "./types"; -import { anonymousUser } from "./user"; +import { httpCodes } from "./diachron/http-codes"; +import type { Call } from "./diachron/types"; +import { anonymousUser } from "./diachron/user"; // Helper to create a minimal mock Call function createMockCall(overrides: Partial = {}): Call { diff --git a/backend/diachron/handlers.ts b/backend/handlers.ts similarity index 54% rename from backend/diachron/handlers.ts rename to backend/handlers.ts index adbaee1..f9cc11a 100644 --- a/backend/diachron/handlers.ts +++ b/backend/handlers.ts @@ -1,7 +1,9 @@ -import { contentTypes } from "./content-types"; -import { core } from "./core"; -import { httpCodes } from "./http-codes"; -import type { Call, Handler, Result } from "./types"; +// This is a sample file provided by diachron. You are encouraged to modify it. + +import { contentTypes } from "./diachron/content-types"; +import { core } from "./diachron/core"; +import { httpCodes } from "./diachron/http-codes"; +import type { Call, Handler, Result } from "./diachron/types"; const multiHandler: Handler = async (call: Call): Promise => { const code = httpCodes.success.OK; diff --git a/backend/routes.ts b/backend/routes.ts index 0e7b76b..54eef80 100644 --- a/backend/routes.ts +++ b/backend/routes.ts @@ -1,3 +1,5 @@ +// This is a sample file provided by diachron. You are encouraged to modify it. + /// import nunjucks from "nunjucks"; @@ -6,7 +8,7 @@ import { authRoutes } from "./diachron/auth/routes"; import { routes as basicRoutes } from "./diachron/basic/routes"; import { contentTypes } from "./diachron/content-types"; import { core } from "./diachron/core"; -import { multiHandler } from "./diachron/handlers"; +import { multiHandler } from "./handlers"; import { httpCodes } from "./diachron/http-codes"; import type { Call, Result, Route } from "./diachron/types"; @@ -27,6 +29,9 @@ const routes: Route[] = [ ...authRoutes, basicRoutes.home, basicRoutes.hello, + {...basicRoutes.hello, + path: "/yo-whats-up" +}, basicRoutes.login, basicRoutes.logout, { @@ -35,7 +40,7 @@ const routes: Route[] = [ handler: async (_call: Call): Promise => { console.log("starting slow request"); - await core.misc.sleep(2); + await core.misc.sleep(5000); console.log("finishing slow request"); const retval = okText("that was slow"); @@ -72,7 +77,6 @@ const routes: Route[] = [ `; const result = nunjucks.renderString(template, { rrr }); - const _listing = lr(routes).join(", "); return { code, result, diff --git a/templates/basic/hello.html.njk b/templates/basic/hello.html.njk index bf2f094..58dee85 100644 --- a/templates/basic/hello.html.njk +++ b/templates/basic/hello.html.njk @@ -2,7 +2,7 @@

- Hello. + {{ greeting | default("hello")}}

The current time is {{ now }}.