2 Commits

Author SHA1 Message Date
cf04ecc78a Add todo item 2026-02-08 09:42:33 -05:00
82e87577cc Get base files closer to being bootstrappable 2026-02-08 09:42:23 -05:00
11 changed files with 180 additions and 114 deletions

View File

@@ -60,6 +60,9 @@ CREATE TABLE app.customer_metadata (...);
- A lot of other stuff should probably be logfmt too but maybe we can get to - A lot of other stuff should probably be logfmt too but maybe we can get to
that later that later
- [ ] master rebuilds (or tries to) too many times; need some sort of debounce
or whatever it's called
## medium importance ## medium importance
- [ ] Add a log viewer - [ ] Add a log viewer

1
backend/.npmrc Normal file
View File

@@ -0,0 +1 @@
shamefully-hoist=true

View File

@@ -1,14 +1,7 @@
import{cli}from'./diachron/cli' // This is a sample file provided by diachron. You are encouraged to modify it.
import { formatError } from './diachron/errors';
process.on('uncaughtException', (err) => {
console.error(formatError(err));
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error(formatError(reason));
});
import { core } from "./diachron/core"; import { core } from "./diachron/core";
@@ -24,8 +17,4 @@ core.logging.log({ source: "logging", text: ["1"] });
app.start()
app.listen(cli.listen.port, cli.listen.host, () => {
console.log(`Listening on ${cli.listen.host}:${cli.listen.port}`);
});

View File

@@ -3,6 +3,7 @@
import{contentTypes} from './content-types' import{contentTypes} from './content-types'
import{httpCodes}from'./http-codes' import{httpCodes}from'./http-codes'
import express, { import express, {
type Express,
type NextFunction, type NextFunction,
type Request as ExpressRequest, type Request as ExpressRequest,
type Response as ExpressResponse, type Response as ExpressResponse,
@@ -10,94 +11,31 @@ import express, {
import { formatError, formatErrorHtml } from "./errors"; import { formatError, formatErrorHtml } from "./errors";
import {isRedirect, InternalHandler, AuthenticationRequired, import {isRedirect, InternalHandler, AuthenticationRequired,
AuthorizationDenied, Call,type Method, type ProcessedRoute,methodParser, type Result, type Route,massageMethod } from "./types"; 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 { 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[]) => { process.on('uncaughtException', (err) => {
// const pattern /*: URLPattern */ = new URLPattern({ pathname: route.path }); console.error(formatError(err));
const matcher = match<Record<string, string>>(route.path); process.exit(1);
const methodList = route.methods; });
const handler: InternalHandler = async ( process.on('unhandledRejection', (reason) => {
expressRequest: ExpressRequest, console.error(formatError(reason));
): Promise<Result> => {
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;
}
type MakeAppArgs={routes:Route[], type MakeAppArgs={routes:Route[],
processTitle?: string, processTitle?: string,
} }
export interface DiachronApp extends Express {
start: () => void
}
const makeApp = ({routes, processTitle}: MakeAppArgs) => { const makeApp = ({routes, processTitle}: MakeAppArgs) => {
if (process.title) { if (process.title) {
process.title = `diachron:${cli.listen.port}`; process.title = `diachron:${cli.listen.port}`;
@@ -138,8 +76,17 @@ async function handler(
return retval; 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()) app.use(express.json())

View File

@@ -10,9 +10,16 @@ const routes: Record<string, Route> = {
hello: { hello: {
path: "/hello", path: "/hello",
methods: ["GET"], methods: ["GET"],
handler: async (_call: Call): Promise<Result> => { handler: async (call: Call): Promise<Result> => {
const now = DateTime.now(); 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); return html(c);
}, },

View File

@@ -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<Record<string, string>>(route.path);
const methodList = route.methods;
const handler: InternalHandler = async (
expressRequest: ExpressRequest,
): Promise<Result> => {
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}

View File

@@ -4,12 +4,12 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, it } from "node:test"; import { describe, it } from "node:test";
import type { Request as ExpressRequest } from "express"; import type { Request as ExpressRequest } from "express";
import { Session } from "./auth/types"; import { Session } from "./diachron/auth/types";
import { contentTypes } from "./content-types"; import { contentTypes } from "./diachron/content-types";
import { multiHandler } from "./handlers"; import { multiHandler } from "./handlers";
import { httpCodes } from "./http-codes"; import { httpCodes } from "./diachron/http-codes";
import type { Call } from "./types"; import type { Call } from "./diachron/types";
import { anonymousUser } from "./user"; import { anonymousUser } from "./diachron/user";
// Helper to create a minimal mock Call // Helper to create a minimal mock Call
function createMockCall(overrides: Partial<Call> = {}): Call { function createMockCall(overrides: Partial<Call> = {}): Call {

View File

@@ -1,7 +1,9 @@
import { contentTypes } from "./content-types"; // This is a sample file provided by diachron. You are encouraged to modify it.
import { core } from "./core";
import { httpCodes } from "./http-codes"; import { contentTypes } from "./diachron/content-types";
import type { Call, Handler, Result } from "./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<Result> => { const multiHandler: Handler = async (call: Call): Promise<Result> => {
const code = httpCodes.success.OK; const code = httpCodes.success.OK;

View File

@@ -1,3 +1,5 @@
// This is a sample file provided by diachron. You are encouraged to modify it.
/// <reference lib="dom" /> /// <reference lib="dom" />
import nunjucks from "nunjucks"; import nunjucks from "nunjucks";
@@ -6,7 +8,7 @@ import { authRoutes } from "./diachron/auth/routes";
import { routes as basicRoutes } from "./diachron/basic/routes"; import { routes as basicRoutes } from "./diachron/basic/routes";
import { contentTypes } from "./diachron/content-types"; import { contentTypes } from "./diachron/content-types";
import { core } from "./diachron/core"; import { core } from "./diachron/core";
import { multiHandler } from "./diachron/handlers"; import { multiHandler } from "./handlers";
import { httpCodes } from "./diachron/http-codes"; import { httpCodes } from "./diachron/http-codes";
import type { Call, Result, Route } from "./diachron/types"; import type { Call, Result, Route } from "./diachron/types";
@@ -27,6 +29,9 @@ const routes: Route[] = [
...authRoutes, ...authRoutes,
basicRoutes.home, basicRoutes.home,
basicRoutes.hello, basicRoutes.hello,
{...basicRoutes.hello,
path: "/yo-whats-up"
},
basicRoutes.login, basicRoutes.login,
basicRoutes.logout, basicRoutes.logout,
{ {
@@ -35,7 +40,7 @@ const routes: Route[] = [
handler: async (_call: Call): Promise<Result> => { handler: async (_call: Call): Promise<Result> => {
console.log("starting slow request"); console.log("starting slow request");
await core.misc.sleep(2); await core.misc.sleep(5000);
console.log("finishing slow request"); console.log("finishing slow request");
const retval = okText("that was slow"); const retval = okText("that was slow");
@@ -72,7 +77,6 @@ const routes: Route[] = [
`; `;
const result = nunjucks.renderString(template, { rrr }); const result = nunjucks.renderString(template, { rrr });
const _listing = lr(routes).join(", ");
return { return {
code, code,
result, result,

View File

@@ -1,16 +1,36 @@
# please keep this file sorted alphabetically # please keep this file sorted alphabetically
.gitignore
.go-version
backend/.gitignore
backend/.npmrc
backend/app.ts
backend/build.sh
backend/check-deps.ts
backend/check.sh
backend/diachron backend/diachron
backend/generated
backend/group.ts
backend/handlers.spec.ts
backend/handlers.ts
backend/package.json backend/package.json
backend/pnpm-workspace.yaml backend/pnpm-workspace.yaml
# express/framework backend/routes.ts
backend/run.sh
backend/show-config.sh
backend/tsconfig.json
backend/watch.sh
bootstrap.sh
cmd cmd
file-list
develop develop
diachron diachron
file-list
logger logger
master master
mgmt mgmt
sync.sh sync.sh
templates templates
update-cached-repository.sh
upgrade.sh upgrade.sh

View File

@@ -2,7 +2,7 @@
<head></head> <head></head>
<body> <body>
<p> <p>
Hello. {{ greeting | default("hello")}}
</p> </p>
<p> <p>
The current time is {{ now }}. The current time is {{ now }}.