Compare commits
8 Commits
241d3e799e
...
7cecf5326d
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cecf5326d | |||
| 47f6bee75f | |||
| 6e96c33457 | |||
| 9e3329fa58 | |||
| 05eaf938fa | |||
| df2d4eea3f | |||
| b235a6be9a | |||
| 8cd4b42cc6 |
18
express/basic/routes.ts
Normal file
18
express/basic/routes.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { DateTime } from "ts-luxon";
|
||||
import type { Call, Result, Route } from "../types";
|
||||
import { html, render } from "../util";
|
||||
|
||||
const routes: Record<string, Route> = {
|
||||
hello: {
|
||||
path: "/hello",
|
||||
methods: ["GET"],
|
||||
handler: async (call: Call): Promise<Result> => {
|
||||
const now = DateTime.now();
|
||||
const c = await render("basic/hello", { now });
|
||||
|
||||
return html(c);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export { routes };
|
||||
13
express/execution-context-schema.ts
Normal file
13
express/execution-context-schema.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const executionContextSchema = z.object({
|
||||
diachron_root: z.string(),
|
||||
});
|
||||
|
||||
export type ExecutionContext = z.infer<typeof executionContextSchema>;
|
||||
|
||||
export function parseExecutionContext(
|
||||
env: Record<string, string | undefined>,
|
||||
): ExecutionContext {
|
||||
return executionContextSchema.parse(env);
|
||||
}
|
||||
38
express/execution-context.spec.ts
Normal file
38
express/execution-context.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import {
|
||||
executionContextSchema,
|
||||
parseExecutionContext,
|
||||
} from "./execution-context-schema";
|
||||
|
||||
describe("parseExecutionContext", () => {
|
||||
it("parses valid executionContext with diachron_root", () => {
|
||||
const env = { diachron_root: "/some/path" };
|
||||
const result = parseExecutionContext(env);
|
||||
assert.deepEqual(result, { diachron_root: "/some/path" });
|
||||
});
|
||||
|
||||
it("throws ZodError when diachron_root is missing", () => {
|
||||
const env = {};
|
||||
assert.throws(() => parseExecutionContext(env), ZodError);
|
||||
});
|
||||
|
||||
it("strips extra fields not in schema", () => {
|
||||
const env = {
|
||||
diachron_root: "/some/path",
|
||||
EXTRA_VAR: "should be stripped",
|
||||
};
|
||||
const result = parseExecutionContext(env);
|
||||
assert.deepEqual(result, { diachron_root: "/some/path" });
|
||||
assert.equal("EXTRA_VAR" in result, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("executionContextSchema", () => {
|
||||
it("requires diachron_root to be a string", () => {
|
||||
const result = executionContextSchema.safeParse({ diachron_root: 123 });
|
||||
assert.equal(result.success, false);
|
||||
});
|
||||
});
|
||||
5
express/execution-context.ts
Normal file
5
express/execution-context.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { parseExecutionContext } from "./execution-context-schema";
|
||||
|
||||
const executionContext = parseExecutionContext(process.env);
|
||||
|
||||
export { executionContext };
|
||||
45
express/migrate.ts
Normal file
45
express/migrate.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// migrate.ts
|
||||
// CLI script for running database migrations
|
||||
|
||||
import { migrate, migrationStatus, pool } from "./database";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const command = process.argv[2] || "run";
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case "run":
|
||||
await migrate();
|
||||
break;
|
||||
|
||||
case "status": {
|
||||
const status = await migrationStatus();
|
||||
console.log("Applied migrations:");
|
||||
for (const name of status.applied) {
|
||||
console.log(` ✓ ${name}`);
|
||||
}
|
||||
if (status.pending.length > 0) {
|
||||
console.log("\nPending migrations:");
|
||||
for (const name of status.pending) {
|
||||
console.log(` • ${name}`);
|
||||
}
|
||||
} else {
|
||||
console.log("\nNo pending migrations");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
console.error("Usage: migrate [run|status]");
|
||||
process.exit(1);
|
||||
}
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Migration failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
import nunjucks from "nunjucks";
|
||||
import { DateTime } from "ts-luxon";
|
||||
import { authRoutes } from "./auth/routes";
|
||||
import { routes as basicRoutes } from "./basic/routes";
|
||||
import { contentTypes } from "./content-types";
|
||||
import { multiHandler } from "./handlers";
|
||||
import { httpCodes } from "./http-codes";
|
||||
@@ -24,6 +25,7 @@ const okText = (result: string): Result => {
|
||||
|
||||
const routes: Route[] = [
|
||||
...authRoutes,
|
||||
basicRoutes.hello,
|
||||
{
|
||||
path: "/slow",
|
||||
methods: ["GET"],
|
||||
|
||||
36
express/util.ts
Normal file
36
express/util.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import nunjucks from "nunjucks";
|
||||
import { contentTypes } from "./content-types";
|
||||
import { executionContext } from "./execution-context";
|
||||
import { httpCodes } from "./http-codes";
|
||||
import type { Result } from "./types";
|
||||
|
||||
// FIXME: Handle the error here
|
||||
const loadFile = async (path: string): Promise<string> => {
|
||||
// Specifying 'utf8' returns a string; otherwise, it returns a Buffer
|
||||
const data = await readFile(path, "utf8");
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const render = async (path: string, ctx: object): Promise<string> => {
|
||||
const fullPath = `${executionContext.diachron_root}/templates/${path}.html.njk`;
|
||||
|
||||
const template = await loadFile(fullPath);
|
||||
|
||||
const retval = nunjucks.renderString(template, ctx);
|
||||
|
||||
return retval;
|
||||
};
|
||||
|
||||
const html = (payload: string): Result => {
|
||||
const retval: Result = {
|
||||
code: httpCodes.success.OK,
|
||||
result: payload,
|
||||
contentType: contentTypes.text.html,
|
||||
};
|
||||
|
||||
return retval;
|
||||
};
|
||||
|
||||
export { render, html };
|
||||
9
framework/cmd.d/db
Executable file
9
framework/cmd.d/db
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$DIR/../.."
|
||||
|
||||
# FIXME: don't hard code this of course
|
||||
PGPASSWORD=diachron psql -U diachron -h localhost diachron
|
||||
9
framework/cmd.d/migrate
Executable file
9
framework/cmd.d/migrate
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$DIR/../.."
|
||||
|
||||
cd "$ROOT/express"
|
||||
"$DIR"/tsx migrate.ts "$@"
|
||||
15
framework/cmd.d/test
Executable file
15
framework/cmd.d/test
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
shopt -s globstar nullglob
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
cd "$DIR/../../express"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
"$DIR"/../shims/pnpm tsx --test ./**/*.spec.ts ./**/*.test.ts
|
||||
else
|
||||
"$DIR"/../shims/pnpm tsx --test "$@"
|
||||
fi
|
||||
@@ -4,4 +4,6 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
cd "$DIR"
|
||||
|
||||
export diachron_root="$DIR/.."
|
||||
|
||||
./master-bin "$@"
|
||||
|
||||
11
templates/basic/hello.html.njk
Normal file
11
templates/basic/hello.html.njk
Normal file
@@ -0,0 +1,11 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<p>
|
||||
Hello.
|
||||
</p>
|
||||
<p>
|
||||
The current time is {{ now }}.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user