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
that later
- [ ] master rebuilds (or tries to) too many times; need some sort of debounce
or whatever it's called
## medium importance
- [ ] 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'
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()

View File

@@ -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,94 +11,31 @@ 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<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);
}
process.on('uncaughtException', (err) => {
console.error(formatError(err));
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error(formatError(reason));
});
return retval;
}
type MakeAppArgs={routes:Route[],
processTitle?: string,
}
export interface DiachronApp extends Express {
start: () => void
}
const makeApp = ({routes, processTitle}: MakeAppArgs) => {
if (process.title) {
process.title = `diachron:${cli.listen.port}`;
@@ -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())

View File

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

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 { 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> = {}): Call {

View File

@@ -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<Result> => {
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" />
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<Result> => {
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,

View File

@@ -1,16 +1,36 @@
# 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/generated
backend/group.ts
backend/handlers.spec.ts
backend/handlers.ts
backend/package.json
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
file-list
develop
diachron
file-list
logger
master
mgmt
sync.sh
templates
update-cached-repository.sh
upgrade.sh

View File

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