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 nunjucks from "nunjucks";
|
||||||
import { DateTime } from "ts-luxon";
|
import { DateTime } from "ts-luxon";
|
||||||
import { authRoutes } from "./auth/routes";
|
import { authRoutes } from "./auth/routes";
|
||||||
|
import { routes as basicRoutes } from "./basic/routes";
|
||||||
import { contentTypes } from "./content-types";
|
import { contentTypes } from "./content-types";
|
||||||
import { multiHandler } from "./handlers";
|
import { multiHandler } from "./handlers";
|
||||||
import { httpCodes } from "./http-codes";
|
import { httpCodes } from "./http-codes";
|
||||||
@@ -24,6 +25,7 @@ const okText = (result: string): Result => {
|
|||||||
|
|
||||||
const routes: Route[] = [
|
const routes: Route[] = [
|
||||||
...authRoutes,
|
...authRoutes,
|
||||||
|
basicRoutes.hello,
|
||||||
{
|
{
|
||||||
path: "/slow",
|
path: "/slow",
|
||||||
methods: ["GET"],
|
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"
|
cd "$DIR"
|
||||||
|
|
||||||
|
export diachron_root="$DIR/.."
|
||||||
|
|
||||||
./master-bin "$@"
|
./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