Initial scaffold: web API (Bun/Elysia) + monitor (Rust/Tokio)

This commit is contained in:
M1
2026-03-16 11:40:24 +04:00
commit 570222c7a9
21 changed files with 837 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
+106
View File
@@ -0,0 +1,106 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
import { createRoot } from "react-dom/client";
// import .css files directly and it works
import './index.css';
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
+15
View File
@@ -0,0 +1,15 @@
# web
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.3.10. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+1
View File
@@ -0,0 +1 @@
console.log("Hello via Bun!");
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@pingql/web",
"version": "0.1.0",
"scripts": {
"dev": "bun run --hot src/index.ts",
"start": "bun run src/index.ts",
"build": "bun build src/index.ts --outdir dist"
},
"dependencies": {
"elysia": "^1.4.27",
"@elysiajs/cors": "^1.4.1",
"@elysiajs/swagger": "^1.3.1",
"postgres": "^3.4.8"
},
"devDependencies": {
"@types/bun": "^1.3.10",
"typescript": "^5.9.3"
}
}
+46
View File
@@ -0,0 +1,46 @@
import postgres from "postgres";
const sql = postgres(process.env.DATABASE_URL ?? "postgres://pingql:pingql@localhost:5432/pingql");
export default sql;
// Run migrations on startup
export async function migrate() {
await sql`
CREATE TABLE IF NOT EXISTS accounts (
id TEXT PRIMARY KEY, -- random 16-digit key
email_hash TEXT, -- optional, for recovery only
created_at TIMESTAMPTZ DEFAULT now()
)
`;
await sql`
CREATE TABLE IF NOT EXISTS monitors (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
name TEXT NOT NULL,
url TEXT NOT NULL,
interval_s INTEGER NOT NULL DEFAULT 60, -- check interval in seconds
query JSONB, -- pingql query filter
enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ DEFAULT now()
)
`;
await sql`
CREATE TABLE IF NOT EXISTS check_results (
id BIGSERIAL PRIMARY KEY,
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
checked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
status_code INTEGER,
latency_ms INTEGER,
up BOOLEAN NOT NULL,
error TEXT,
meta JSONB -- headers, body snippet, etc.
)
`;
await sql`CREATE INDEX IF NOT EXISTS idx_results_monitor ON check_results(monitor_id, checked_at DESC)`;
console.log("DB ready");
}
+22
View File
@@ -0,0 +1,22 @@
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { swagger } from "@elysiajs/swagger";
import { checks } from "./routes/checks";
import { monitors } from "./routes/monitors";
import { auth } from "./routes/auth";
import { internal } from "./routes/internal";
import { migrate } from "./db";
await migrate();
const app = new Elysia()
.use(cors())
.use(swagger({ path: "/docs", documentation: { info: { title: "PingQL API", version: "0.1.0" } } }))
.get("/", () => ({ name: "PingQL", version: "0.1.0", docs: "/docs" }))
.use(auth)
.use(monitors)
.use(checks)
.use(internal)
.listen(3000);
console.log(`PingQL running at http://localhost:${app.server?.port}`);
+45
View File
@@ -0,0 +1,45 @@
import { Elysia, t } from "elysia";
import { randomBytes, createHash } from "crypto";
import sql from "../db";
// Generate a memorable 16-digit account key: XXXX-XXXX-XXXX-XXXX
function generateAccountKey(): string {
const bytes = randomBytes(8);
const hex = bytes.toString("hex").toUpperCase();
return `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
}
// Middleware: validate account key from Authorization header
export function requireAuth(app: Elysia) {
return app.derive(async ({ headers, error }) => {
const key = headers["authorization"]?.replace("Bearer ", "").trim();
if (!key) return error(401, { error: "Missing account key. Use: Authorization: Bearer <key>" });
const [account] = await sql`SELECT id FROM accounts WHERE id = ${key}`;
if (!account) return error(401, { error: "Invalid account key" });
return { accountId: account.id };
});
}
export const auth = new Elysia({ prefix: "/auth" })
// Create a new account — no email required
.post("/register", async ({ body }) => {
const key = generateAccountKey();
const emailHash = body.email
? createHash("sha256").update(body.email.toLowerCase().trim()).digest("hex")
: null;
await sql`INSERT INTO accounts (id, email_hash) VALUES (${key}, ${emailHash})`;
return {
key,
message: "Save this key — it's your only credential. We don't store it.",
...(body.email ? { email_registered: true } : { email_registered: false }),
};
}, {
body: t.Object({
email: t.Optional(t.String({ format: "email", description: "Optional. Only used for account recovery." })),
}),
detail: { summary: "Create account", tags: ["auth"] },
});
+44
View File
@@ -0,0 +1,44 @@
import { Elysia } from "elysia";
import { requireAuth } from "./auth";
import sql from "../db";
export const checks = new Elysia({ prefix: "/checks" })
.use(requireAuth)
// Get recent results for a monitor
.get("/:monitorId", async ({ accountId, params, query, error }) => {
// Verify ownership
const [monitor] = await sql`
SELECT id FROM monitors WHERE id = ${params.monitorId} AND account_id = ${accountId}
`;
if (!monitor) return error(404, { error: "Not found" });
const limit = Math.min(Number(query.limit ?? 100), 1000);
return sql`
SELECT * FROM check_results
WHERE monitor_id = ${params.monitorId}
ORDER BY checked_at DESC
LIMIT ${limit}
`;
}, { detail: { summary: "Get check history", tags: ["checks"] } })
// Internal endpoint: monitor runner posts results here
.post("/ingest", async ({ body, headers, error }) => {
const token = headers["x-monitor-token"];
if (token !== process.env.MONITOR_TOKEN) return error(401, { error: "Unauthorized" });
await sql`
INSERT INTO check_results (monitor_id, status_code, latency_ms, up, error, meta)
VALUES (
${body.monitor_id},
${body.status_code ?? null},
${body.latency_ms ?? null},
${body.up},
${body.error ?? null},
${body.meta ? sql.json(body.meta) : null}
)
`;
return { ok: true };
}, {
detail: { summary: "Ingest check result (monitor runner only)", tags: ["internal"] },
});
+28
View File
@@ -0,0 +1,28 @@
/// Internal endpoints used by the Rust monitor runner.
/// Protected by MONITOR_TOKEN — not exposed to users.
import { Elysia } from "elysia";
import sql from "../db";
export const internal = new Elysia({ prefix: "/internal" })
.derive(({ headers, error }) => {
if (headers["x-monitor-token"] !== process.env.MONITOR_TOKEN)
return error(401, { error: "Unauthorized" });
return {};
})
// Returns monitors that are due for a check
.get("/due", async () => {
return sql`
SELECT m.id, m.url, m.interval_s, m.query
FROM monitors m
LEFT JOIN LATERAL (
SELECT checked_at FROM check_results
WHERE monitor_id = m.id
ORDER BY checked_at DESC LIMIT 1
) last ON true
WHERE m.enabled = true
AND (last.checked_at IS NULL
OR last.checked_at < now() - (m.interval_s || ' seconds')::interval)
`;
});
+77
View File
@@ -0,0 +1,77 @@
import { Elysia, t } from "elysia";
import { requireAuth } from "./auth";
import sql from "../db";
const MonitorBody = t.Object({
name: t.String({ description: "Human-readable name" }),
url: t.String({ format: "uri", description: "URL to check" }),
interval_s: t.Optional(t.Number({ minimum: 10, default: 60, description: "Check interval in seconds" })),
query: t.Optional(t.Any({ description: "PingQL query — filter conditions for up/down" })),
});
export const monitors = new Elysia({ prefix: "/monitors" })
.use(requireAuth)
// List monitors
.get("/", async ({ accountId }) => {
return sql`SELECT * FROM monitors WHERE account_id = ${accountId} ORDER BY created_at DESC`;
}, { detail: { summary: "List monitors", tags: ["monitors"] } })
// Create monitor
.post("/", async ({ accountId, body }) => {
const [monitor] = await sql`
INSERT INTO monitors (account_id, name, url, interval_s, query)
VALUES (${accountId}, ${body.name}, ${body.url}, ${body.interval_s ?? 60}, ${body.query ? sql.json(body.query) : null})
RETURNING *
`;
return monitor;
}, { body: MonitorBody, detail: { summary: "Create monitor", tags: ["monitors"] } })
// Get monitor + recent status
.get("/:id", async ({ accountId, params, error }) => {
const [monitor] = await sql`
SELECT * FROM monitors WHERE id = ${params.id} AND account_id = ${accountId}
`;
if (!monitor) return error(404, { error: "Not found" });
const results = await sql`
SELECT * FROM check_results WHERE monitor_id = ${params.id}
ORDER BY checked_at DESC LIMIT 100
`;
return { ...monitor, results };
}, { detail: { summary: "Get monitor with results", tags: ["monitors"] } })
// Update monitor
.patch("/:id", async ({ accountId, params, body, error }) => {
const [monitor] = await sql`
UPDATE monitors SET
name = COALESCE(${body.name ?? null}, name),
url = COALESCE(${body.url ?? null}, url),
interval_s = COALESCE(${body.interval_s ?? null}, interval_s),
query = COALESCE(${body.query ? sql.json(body.query) : null}, query)
WHERE id = ${params.id} AND account_id = ${accountId}
RETURNING *
`;
if (!monitor) return error(404, { error: "Not found" });
return monitor;
}, { body: t.Partial(MonitorBody), detail: { summary: "Update monitor", tags: ["monitors"] } })
// Delete monitor
.delete("/:id", async ({ accountId, params, error }) => {
const [deleted] = await sql`
DELETE FROM monitors WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id
`;
if (!deleted) return error(404, { error: "Not found" });
return { deleted: true };
}, { detail: { summary: "Delete monitor", tags: ["monitors"] } })
// Toggle enabled
.post("/:id/toggle", async ({ accountId, params, error }) => {
const [monitor] = await sql`
UPDATE monitors SET enabled = NOT enabled
WHERE id = ${params.id} AND account_id = ${accountId}
RETURNING id, enabled
`;
if (!monitor) return error(404, { error: "Not found" });
return monitor;
}, { detail: { summary: "Toggle monitor on/off", tags: ["monitors"] } });
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}