feat: move perf,logs and user into sqlite
This commit is contained in:
parent
1937ef55bb
commit
bf7d88a588
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,5 +6,6 @@
|
|||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
data/
|
data/
|
||||||
|
data-dev/
|
||||||
_fresh/
|
_fresh/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
@ -14,3 +14,9 @@ deno task start
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will watch the project directory and restart as necessary.
|
This will watch the project directory and restart as necessary.
|
||||||
|
|
||||||
|
## FIX
|
||||||
|
|
||||||
|
```
|
||||||
|
sed -i -e 's/"deno"/"no-deno"/' node_modules/@libsql/client/package.json
|
||||||
|
```
|
||||||
|
12
deno.json
12
deno.json
@ -3,7 +3,8 @@
|
|||||||
"nodeModulesDir": "auto",
|
"nodeModulesDir": "auto",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
||||||
"start": "deno run -A --watch=static/,routes/ dev.ts",
|
"start": "deno run --env-file -A --watch=static/,routes/ dev.ts",
|
||||||
|
"db": "deno run --env-file -A npm:drizzle-kit",
|
||||||
"build": "deno run -A dev.ts build",
|
"build": "deno run -A dev.ts build",
|
||||||
"preview": "deno run -A main.ts",
|
"preview": "deno run -A main.ts",
|
||||||
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
||||||
@ -25,11 +26,13 @@
|
|||||||
"@islands/": "./islands/",
|
"@islands/": "./islands/",
|
||||||
"@lib": "./lib",
|
"@lib": "./lib",
|
||||||
"@lib/": "./lib/",
|
"@lib/": "./lib/",
|
||||||
|
"@libsql/client": "npm:@libsql/client@^0.14.0",
|
||||||
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
|
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
|
||||||
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
|
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
|
||||||
"@std/dotenv": "jsr:@std/dotenv@^0.225.3",
|
|
||||||
"@std/http": "jsr:@std/http@^1.0.12",
|
"@std/http": "jsr:@std/http@^1.0.12",
|
||||||
"@std/yaml": "jsr:@std/yaml@^1.0.5",
|
"@std/yaml": "jsr:@std/yaml@^1.0.5",
|
||||||
|
"drizzle-kit": "npm:drizzle-kit@^0.30.1",
|
||||||
|
"drizzle-orm": "npm:drizzle-orm@^0.38.3",
|
||||||
"preact": "https://esm.sh/preact@10.22.0",
|
"preact": "https://esm.sh/preact@10.22.0",
|
||||||
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2",
|
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2",
|
||||||
"preact/": "https://esm.sh/preact@10.22.0/",
|
"preact/": "https://esm.sh/preact@10.22.0/",
|
||||||
@ -38,9 +41,12 @@
|
|||||||
"tailwindcss/": "npm:/tailwindcss@^3.4.17/",
|
"tailwindcss/": "npm:/tailwindcss@^3.4.17/",
|
||||||
"tailwindcss/plugin": "npm:/tailwindcss@^3.4.17/plugin.js",
|
"tailwindcss/plugin": "npm:/tailwindcss@^3.4.17/plugin.js",
|
||||||
"camelcase-css": "npm:camelcase-css",
|
"camelcase-css": "npm:camelcase-css",
|
||||||
|
"tsx": "npm:tsx@^4.19.2",
|
||||||
"typesense": "https://raw.githubusercontent.com/bradenmacdonald/typesense-deno/main/mod.ts",
|
"typesense": "https://raw.githubusercontent.com/bradenmacdonald/typesense-deno/main/mod.ts",
|
||||||
"yaml": "https://deno.land/std@0.197.0/yaml/mod.ts",
|
"yaml": "https://deno.land/std@0.197.0/yaml/mod.ts",
|
||||||
"zod": "https://deno.land/x/zod@v3.21.4/mod.ts"
|
"zod": "https://deno.land/x/zod@v3.21.4/mod.ts",
|
||||||
|
"fs": "https://deno.land/std/fs/mod.ts",
|
||||||
|
"imagemagick": "https://deno.land/x/imagemagick_deno@0.0.31/mod.ts"
|
||||||
},
|
},
|
||||||
"scopes": {
|
"scopes": {
|
||||||
"https://deno.land/x/emoji/": {
|
"https://deno.land/x/emoji/": {
|
||||||
|
18
drizzle.config.ts
Normal file
18
drizzle.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
const DATA_DIR = Deno.env.has("DATA_DIR")
|
||||||
|
? path.resolve(Deno.env.get("DATA_DIR")!)
|
||||||
|
: path.resolve(Deno.cwd(), "data");
|
||||||
|
|
||||||
|
// const DB_FILE = "file://" + path.resolve(DATA_DIR, "db.sqlite");
|
||||||
|
const DB_FILE = "file:data-dev/db.sqlite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: "./drizzle",
|
||||||
|
schema: "./lib/sqlite/schema.ts",
|
||||||
|
dialect: "turso",
|
||||||
|
dbCredentials: {
|
||||||
|
url: DB_FILE,
|
||||||
|
},
|
||||||
|
});
|
20
drizzle/0000_dashing_sunspot.sql
Normal file
20
drizzle/0000_dashing_sunspot.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE `performance` (
|
||||||
|
`path` text NOT NULL,
|
||||||
|
`search` text,
|
||||||
|
`time` integer NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (current_timestamp)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `session` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (current_timestamp),
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`user_id` text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `user` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (current_timestamp) NOT NULL,
|
||||||
|
`email` text NOT NULL,
|
||||||
|
`name` text NOT NULL
|
||||||
|
);
|
1
drizzle/0001_classy_justin_hammer.sql
Normal file
1
drizzle/0001_classy_justin_hammer.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `performance` ALTER COLUMN "created_at" TO "created_at" integer DEFAULT (STRFTIME('%s', 'now') * 1000);
|
135
drizzle/meta/0000_snapshot.json
Normal file
135
drizzle/meta/0000_snapshot.json
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "49f64916-b687-40c5-a37f-de031c1641f1",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"performance": {
|
||||||
|
"name": "performance",
|
||||||
|
"columns": {
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"name": "search",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"name": "time",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(current_timestamp)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"name": "session",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(current_timestamp)"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(current_timestamp)"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
135
drizzle/meta/0001_snapshot.json
Normal file
135
drizzle/meta/0001_snapshot.json
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "3890bdbe-0c06-4619-a35f-e1380ac8f42f",
|
||||||
|
"prevId": "49f64916-b687-40c5-a37f-de031c1641f1",
|
||||||
|
"tables": {
|
||||||
|
"performance": {
|
||||||
|
"name": "performance",
|
||||||
|
"columns": {
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"name": "search",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"name": "time",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(STRFTIME('%s', 'now') * 1000)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"name": "session",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(current_timestamp)"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(current_timestamp)"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
20
drizzle/meta/_journal.json
Normal file
20
drizzle/meta/_journal.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1736093081790,
|
||||||
|
"tag": "0000_dashing_sunspot",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1736093851600,
|
||||||
|
"tag": "0001_classy_justin_hammer",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
37
lib/cache/logs.ts
vendored
37
lib/cache/logs.ts
vendored
@ -1,37 +0,0 @@
|
|||||||
import { createLogger } from "@lib/log.ts";
|
|
||||||
import * as cache from "@lib/cache/cache.ts";
|
|
||||||
import { getTimeCacheKey, parseTimeCacheKey } from "@lib/string.ts";
|
|
||||||
|
|
||||||
const log = createLogger("");
|
|
||||||
log.addEventListener("log", (data) => {
|
|
||||||
cache.set(`log:${getTimeCacheKey()}`, JSON.stringify(data.detail), {
|
|
||||||
noLog: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Log = {
|
|
||||||
scope: string;
|
|
||||||
level: number;
|
|
||||||
date: Date;
|
|
||||||
args: unknown[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getLogs() {
|
|
||||||
const d = new Date();
|
|
||||||
const year = d.getFullYear();
|
|
||||||
const month = d.getMonth().toString();
|
|
||||||
const day = d.getDate().toString();
|
|
||||||
|
|
||||||
const keys = await cache.keys(
|
|
||||||
`log:${year}:${month}:${day}:*`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const logs = await Promise.all(
|
|
||||||
keys.map(async (key) => {
|
|
||||||
const date = parseTimeCacheKey(key);
|
|
||||||
return { ...JSON.parse(await cache.get<string>(key)), date } as Log;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return logs.sort((a, b) => a.date.getTime() > b.date.getTime() ? -1 : 1);
|
|
||||||
}
|
|
18
lib/db.ts
18
lib/db.ts
@ -1,18 +0,0 @@
|
|||||||
import z from "https://deno.land/x/zod@v3.21.4/index.ts";
|
|
||||||
import { createSchema } from "@lib/db/createSchema.ts";
|
|
||||||
|
|
||||||
const UserSchema = z.object({
|
|
||||||
id: z.string().optional().default(() => crypto.randomUUID()),
|
|
||||||
createdAt: z.date().default(() => new Date()),
|
|
||||||
email: z.string().email(),
|
|
||||||
name: z.string(),
|
|
||||||
});
|
|
||||||
export const userDB = createSchema("user", UserSchema);
|
|
||||||
|
|
||||||
const SessionSchema = z.object({
|
|
||||||
id: z.string().default(() => crypto.randomUUID()),
|
|
||||||
createdAt: z.date().default(() => new Date()),
|
|
||||||
expiresAt: z.date().default(() => new Date()),
|
|
||||||
userId: z.string(),
|
|
||||||
});
|
|
||||||
export const sessionDB = createSchema("session", SessionSchema);
|
|
@ -1,32 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
import * as cache from "@lib/cache/cache.ts";
|
|
||||||
|
|
||||||
export function createSchema<T extends z.ZodSchema>(name: string, schema: T) {
|
|
||||||
type Data = z.infer<T>;
|
|
||||||
return {
|
|
||||||
async create(input: Omit<Data, "id">): Promise<Data> {
|
|
||||||
const data = schema.safeParse(input);
|
|
||||||
if (data.success) {
|
|
||||||
const d = data.data;
|
|
||||||
const id = d["id"];
|
|
||||||
if (!id) return d;
|
|
||||||
await cache.set(`${name}:${id}`, JSON.stringify(d));
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
async findAll(): Promise<Data[]> {
|
|
||||||
const keys = await cache.keys(`${name}:*`);
|
|
||||||
return Promise.all(keys.map((k) => {
|
|
||||||
return cache.get<string>(k);
|
|
||||||
})).then((values) => values.map((v) => JSON.parse(v || "null")));
|
|
||||||
},
|
|
||||||
async find(id: string) {
|
|
||||||
const k = await cache.get<string>(`${name}:${id}`);
|
|
||||||
return JSON.parse(k || "null") as Data | null;
|
|
||||||
},
|
|
||||||
delete(id: string) {
|
|
||||||
return cache.del(`${name}:${id}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
11
lib/env.ts
11
lib/env.ts
@ -1,4 +1,4 @@
|
|||||||
import "@std/dotenv/load";
|
import path from "node:path";
|
||||||
|
|
||||||
export const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER");
|
export const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER");
|
||||||
export const REDIS_HOST = Deno.env.get("REDIS_HOST");
|
export const REDIS_HOST = Deno.env.get("REDIS_HOST");
|
||||||
@ -8,7 +8,10 @@ export const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
|||||||
export const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY");
|
export const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY");
|
||||||
|
|
||||||
export const GITEA_SERVER = Deno.env.get("GITEA_SERVER");
|
export const GITEA_SERVER = Deno.env.get("GITEA_SERVER");
|
||||||
export const GITEA_CLIENT_ID = Deno.env.get("GITEA_CLIENT_ID");
|
export const GITEA_CLIENT_ID = Deno.env.get("GITEA_CLIENT_ID")!;
|
||||||
|
if (!GITEA_CLIENT_ID) {
|
||||||
|
throw new Error("GITEA_CLIENT_ID is required");
|
||||||
|
}
|
||||||
export const GITEA_CLIENT_SECRET = Deno.env.get("GITEA_CLIENT_SECRET");
|
export const GITEA_CLIENT_SECRET = Deno.env.get("GITEA_CLIENT_SECRET");
|
||||||
export const GITEA_REDIRECT_URL = Deno.env.get("GITEA_REDIRECT_URL");
|
export const GITEA_REDIRECT_URL = Deno.env.get("GITEA_REDIRECT_URL");
|
||||||
|
|
||||||
@ -23,5 +26,9 @@ export const TYPESENSE_URL = Deno.env.get("TYPESENSE_URL") ||
|
|||||||
"http://localhost:8108";
|
"http://localhost:8108";
|
||||||
export const TYPESENSE_API_KEY = Deno.env.get("TYPESENSE_API_KEY");
|
export const TYPESENSE_API_KEY = Deno.env.get("TYPESENSE_API_KEY");
|
||||||
|
|
||||||
|
export const DATA_DIR = Deno.env.has("DATA_DIR")
|
||||||
|
? path.resolve(Deno.env.get("DATA_DIR")!)
|
||||||
|
: path.resolve(Deno.cwd(), "data");
|
||||||
|
|
||||||
export const LOG_LEVEL: string = Deno.env.get("LOG_LEVEL") ||
|
export const LOG_LEVEL: string = Deno.env.get("LOG_LEVEL") ||
|
||||||
"warn";
|
"warn";
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
import { MiddlewareHandlerContext } from "$fresh/server.ts";
|
import { FreshContext } from "$fresh/server.ts";
|
||||||
|
|
||||||
class DomainError extends Error {
|
class DomainError extends Error {
|
||||||
status = 500;
|
status = 500;
|
||||||
render?: (ctx: MiddlewareHandlerContext) => void;
|
render?: (ctx: FreshContext) => void;
|
||||||
constructor(public statusText = "Internal Server Error") {
|
constructor(public statusText = "Internal Server Error") {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotFoundError extends DomainError {
|
class NotFoundError extends DomainError {
|
||||||
status = 404;
|
override status = 404;
|
||||||
constructor(public statusText = "Not Found") {
|
constructor(public override statusText = "Not Found") {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BadRequestError extends DomainError {
|
class BadRequestError extends DomainError {
|
||||||
status = 400;
|
override status = 400;
|
||||||
constructor(public statusText = "Bad Request") {
|
constructor(public override statusText = "Bad Request") {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccessDeniedError extends DomainError {
|
class AccessDeniedError extends DomainError {
|
||||||
status = 403;
|
override status = 403;
|
||||||
constructor(public statusText = "Access Denied") {
|
constructor(public override statusText = "Access Denied") {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { useEffect, useRef } from "preact/hooks";
|
|||||||
export function useEventListener<T extends Event>(
|
export function useEventListener<T extends Event>(
|
||||||
eventName: string,
|
eventName: string,
|
||||||
handler: (event: T) => void,
|
handler: (event: T) => void,
|
||||||
element: Window | HTMLElement = globalThis,
|
element: typeof globalThis | HTMLElement = globalThis,
|
||||||
) {
|
) {
|
||||||
// Create a ref that stores handler
|
// Create a ref that stores handler
|
||||||
const savedHandler = useRef<(event: Event) => void>();
|
const savedHandler = useRef<(event: Event) => void>();
|
||||||
|
@ -23,9 +23,9 @@ const logFuncs = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
let longestScope = 0;
|
let longestScope = 0;
|
||||||
let logLevel = (_LOG_LEVEL && _LOG_LEVEL in logMap && logMap[_LOG_LEVEL]) ??
|
let logLevel = _LOG_LEVEL && _LOG_LEVEL in logMap && _LOG_LEVEL in logMap
|
||||||
LOG_LEVEL.WARN;
|
? logMap[_LOG_LEVEL]
|
||||||
|
: LOG_LEVEL.WARN;
|
||||||
const ee = new EventEmitter<{
|
const ee = new EventEmitter<{
|
||||||
log: { level: LOG_LEVEL; scope: string; args: unknown[] };
|
log: { level: LOG_LEVEL; scope: string; args: unknown[] };
|
||||||
}>();
|
}>();
|
||||||
|
66
lib/logs.ts
Normal file
66
lib/logs.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { createLogger } from "@lib/log.ts";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { DATA_DIR } from "@lib/env.ts";
|
||||||
|
import { ensureDir } from "fs";
|
||||||
|
|
||||||
|
function getLogFileName() {
|
||||||
|
const d = new Date();
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = (d.getMonth() + 1).toString().padStart(2, "0"); // Ensure two digits
|
||||||
|
const day = d.getDate().toString().padStart(2, "0"); // Ensure two digits
|
||||||
|
return `${year}-${month}-${day}.log`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_DIR = join(DATA_DIR, "logs");
|
||||||
|
|
||||||
|
// Ensure the log directory exists
|
||||||
|
await ensureDir(LOG_DIR);
|
||||||
|
|
||||||
|
const log = createLogger("");
|
||||||
|
log.addEventListener("log", async (data) => {
|
||||||
|
const logEntry = JSON.stringify(data.detail);
|
||||||
|
const logFilePath = join(LOG_DIR, getLogFileName());
|
||||||
|
|
||||||
|
// Append the log entry to the file (creating it if it doesn't exist)
|
||||||
|
await Deno.writeTextFile(
|
||||||
|
logFilePath,
|
||||||
|
new Date().toISOString() + " | " + logEntry + "\n",
|
||||||
|
{ append: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Log = {
|
||||||
|
scope: string;
|
||||||
|
level: number;
|
||||||
|
date: Date;
|
||||||
|
args: unknown[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getLogs() {
|
||||||
|
const logFilePath = join(LOG_DIR, getLogFileName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the log file content
|
||||||
|
const logFileContent = await Deno.readTextFile(logFilePath);
|
||||||
|
|
||||||
|
// Split by lines and parse logs
|
||||||
|
const logs: Log[] = logFileContent
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => line.trim() !== "")
|
||||||
|
.map((line) => {
|
||||||
|
const [date, ...rest] = line.split(" | ");
|
||||||
|
const parsed = JSON.parse(rest.join(" | ")) as Log;
|
||||||
|
return {
|
||||||
|
...parsed,
|
||||||
|
date: new Date(date),
|
||||||
|
} as Log;
|
||||||
|
});
|
||||||
|
console.log(logs);
|
||||||
|
|
||||||
|
// Return the logs sorted by date
|
||||||
|
return logs.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||||
|
} catch (error) {
|
||||||
|
// If file does not exist, return an empty array
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import * as cache from "@lib/cache/cache.ts";
|
import { db } from "@lib/sqlite/sqlite.ts";
|
||||||
import { getTimeCacheKey } from "@lib/string.ts";
|
import { performanceTable } from "@lib/sqlite/schema.ts";
|
||||||
|
import { between } from "drizzle-orm/sql";
|
||||||
|
|
||||||
export type PerformancePoint = {
|
export type PerformancePoint = {
|
||||||
path: string;
|
path: string;
|
||||||
@ -16,40 +17,29 @@ export type PerformanceRes = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const savePerformance = (url: string, milliseconds: number) => {
|
export const savePerformance = async (url: string, seconds: number) => {
|
||||||
const cacheKey = getTimeCacheKey();
|
|
||||||
|
|
||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
if (u.pathname.includes("_frsh/")) return;
|
if (u.pathname.includes("_frsh/")) return;
|
||||||
u.searchParams.delete("__frsh_c");
|
u.searchParams.delete("__frsh_c");
|
||||||
|
|
||||||
cache.set(
|
console.log("Saving performance", u.pathname, u.search, seconds);
|
||||||
`performance:${cacheKey}`,
|
const res = await db.insert(performanceTable).values({
|
||||||
JSON.stringify({
|
|
||||||
path: decodeURIComponent(u.pathname),
|
path: decodeURIComponent(u.pathname),
|
||||||
search: u.search,
|
search: u.search,
|
||||||
time: Math.floor(milliseconds * 1000),
|
time: Math.floor(seconds * 1000),
|
||||||
}),
|
});
|
||||||
);
|
console.log({ res });
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getPerformances(): Promise<PerformanceRes> {
|
export async function getPerformances(): Promise<PerformanceRes> {
|
||||||
const d = new Date();
|
const now = new Date();
|
||||||
const year = d.getFullYear();
|
const startOfDay = new Date(now.setHours(0, 0, 0, 0));
|
||||||
const month = d.getMonth().toString();
|
const endOfDay = new Date(now.setHours(23, 59, 59, 999));
|
||||||
const day = d.getDay().toString();
|
|
||||||
|
|
||||||
const keys = await cache.keys(
|
const performances = await db.select().from(performanceTable).where(
|
||||||
`performance:${year}:${month}:${day}:*`,
|
between(performanceTable.createdAt, startOfDay, endOfDay),
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`performance:${year}:${month}:${day}:*`);
|
|
||||||
|
|
||||||
const performances = await Promise.all(
|
|
||||||
keys.map(async (key) =>
|
|
||||||
JSON.parse(await cache.get<string>(key)) as PerformancePoint
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
console.log({ performances });
|
||||||
|
|
||||||
let maximum = 0;
|
let maximum = 0;
|
||||||
|
|
45
lib/sqlite/schema.ts
Normal file
45
lib/sqlite/schema.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { int, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
import { sql } from "drizzle-orm/sql";
|
||||||
|
|
||||||
|
export const userTable = sqliteTable("user", {
|
||||||
|
id: text()
|
||||||
|
.primaryKey(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.default(sql`(current_timestamp)`)
|
||||||
|
.notNull(),
|
||||||
|
email: text()
|
||||||
|
.notNull(),
|
||||||
|
name: text()
|
||||||
|
.notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sessionTable = sqliteTable("session", {
|
||||||
|
id: text("id")
|
||||||
|
.primaryKey(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).default(
|
||||||
|
sql`(current_timestamp)`,
|
||||||
|
),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp" })
|
||||||
|
.notNull(),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const performanceTable = sqliteTable("performance", {
|
||||||
|
path: text().notNull(),
|
||||||
|
search: text(),
|
||||||
|
time: int().notNull(),
|
||||||
|
createdAt: integer("created_at", {
|
||||||
|
mode: "timestamp_ms",
|
||||||
|
}).default(sql`(STRFTIME('%s', 'now') * 1000)`),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const imageTable = sqliteTable("image", {
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" }).default(
|
||||||
|
sql`(current_timestamp)`,
|
||||||
|
),
|
||||||
|
path: text().notNull(),
|
||||||
|
average: text().notNull(),
|
||||||
|
blurhash: text().notNull(),
|
||||||
|
mime: text().notNull(),
|
||||||
|
});
|
14
lib/sqlite/sqlite.ts
Normal file
14
lib/sqlite/sqlite.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/libsql/node";
|
||||||
|
import { DATA_DIR } from "@lib/env.ts";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
// const DB_FILE = "file://" + path.resolve(DATA_DIR, "db.sqlite");
|
||||||
|
const DB_FILE = "file:data-dev/db.sqlite";
|
||||||
|
|
||||||
|
// You can specify any property from the libsql connection options
|
||||||
|
export const db = drizzle({
|
||||||
|
logger: true,
|
||||||
|
connection: {
|
||||||
|
url: DB_FILE,
|
||||||
|
},
|
||||||
|
});
|
2
main.ts
2
main.ts
@ -4,8 +4,6 @@
|
|||||||
/// <reference lib="dom.asynciterable" />
|
/// <reference lib="dom.asynciterable" />
|
||||||
/// <reference lib="deno.ns" />
|
/// <reference lib="deno.ns" />
|
||||||
|
|
||||||
import "@std/dotenv/load";
|
|
||||||
|
|
||||||
import { start } from "$fresh/server.ts";
|
import { start } from "$fresh/server.ts";
|
||||||
import manifest from "./fresh.gen.ts";
|
import manifest from "./fresh.gen.ts";
|
||||||
import config from "./fresh.config.ts";
|
import config from "./fresh.config.ts";
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
//routes/middleware-error-handler/_middleware.ts
|
//routes/middleware-error-handler/_middleware.ts
|
||||||
import { MiddlewareHandlerContext } from "$fresh/server.ts";
|
import { FreshContext } from "$fresh/server.ts";
|
||||||
import { DomainError } from "@lib/errors.ts";
|
import { DomainError } from "@lib/errors.ts";
|
||||||
import { getCookies } from "@std/http/cookie";
|
import { getCookies } from "@std/http/cookie";
|
||||||
import { verify } from "https://deno.land/x/djwt@v2.2/mod.ts";
|
import { verify } from "https://deno.land/x/djwt@v2.2/mod.ts";
|
||||||
import * as cache from "@lib/cache/performance.ts";
|
import * as perf from "@lib/performance.ts";
|
||||||
import { JWT_SECRET } from "@lib/env.ts";
|
import { JWT_SECRET } from "@lib/env.ts";
|
||||||
|
|
||||||
export async function handler(
|
export async function handler(
|
||||||
req: Request,
|
req: Request,
|
||||||
ctx: MiddlewareHandlerContext,
|
ctx: FreshContext,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
performance.mark("a");
|
performance.mark("a");
|
||||||
@ -29,7 +29,7 @@ export async function handler(
|
|||||||
const resp = await ctx.next();
|
const resp = await ctx.next();
|
||||||
performance.mark("b");
|
performance.mark("b");
|
||||||
const b = performance.measure("a->b", "a", "b");
|
const b = performance.measure("a->b", "a", "b");
|
||||||
cache.savePerformance(req.url, b.duration);
|
perf.savePerformance(req.url, b.duration);
|
||||||
return resp;
|
return resp;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error", error);
|
console.error("Error", error);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { AccessDeniedError } from "@lib/errors.ts";
|
import { AccessDeniedError } from "@lib/errors.ts";
|
||||||
import { getLogs, Log } from "@lib/cache/logs.ts";
|
import { getLogs, Log } from "@lib/logs.ts";
|
||||||
import { formatDate } from "@lib/string.ts";
|
import { formatDate } from "@lib/string.ts";
|
||||||
import { renderMarkdown } from "@lib/documents.ts";
|
import { renderMarkdown } from "@lib/documents.ts";
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { getPerformances, PerformanceRes } from "@lib/cache/performance.ts";
|
import { getPerformances, PerformanceRes } from "@lib/performance.ts";
|
||||||
import { AccessDeniedError } from "@lib/errors.ts";
|
import { AccessDeniedError } from "@lib/errors.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
|
@ -4,9 +4,11 @@ import { oauth2Client } from "@lib/auth.ts";
|
|||||||
import { getCookies, setCookie } from "@std/http/cookie";
|
import { getCookies, setCookie } from "@std/http/cookie";
|
||||||
import { codeChallengeMap } from "./login.ts";
|
import { codeChallengeMap } from "./login.ts";
|
||||||
import { GITEA_SERVER, JWT_SECRET, SESSION_DURATION } from "@lib/env.ts";
|
import { GITEA_SERVER, JWT_SECRET, SESSION_DURATION } from "@lib/env.ts";
|
||||||
import { userDB } from "@lib/db.ts";
|
|
||||||
import { GiteaOauthUser } from "@lib/types.ts";
|
import { GiteaOauthUser } from "@lib/types.ts";
|
||||||
import { BadRequestError } from "@lib/errors.ts";
|
import { BadRequestError } from "@lib/errors.ts";
|
||||||
|
import { db } from "@lib/sqlite/sqlite.ts";
|
||||||
|
import { userTable } from "@lib/sqlite/schema.ts";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET(request) {
|
async GET(request) {
|
||||||
@ -38,15 +40,17 @@ export const handler: Handlers = {
|
|||||||
|
|
||||||
const oauthUser = await userResponse.json() as GiteaOauthUser;
|
const oauthUser = await userResponse.json() as GiteaOauthUser;
|
||||||
|
|
||||||
const allUsers = await userDB.findAll();
|
let user = await db.select().from(userTable).where(
|
||||||
let user = allUsers.find((u) => u.name === oauthUser.name);
|
eq(userTable.name, oauthUser.name),
|
||||||
|
).limit(1).then((users) => users[0]);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await userDB.create({
|
const res = await db.insert(userTable).values({
|
||||||
createdAt: new Date(),
|
id: crypto.randomUUID(),
|
||||||
email: oauthUser.email,
|
email: oauthUser.email,
|
||||||
name: oauthUser.name,
|
name: oauthUser.name,
|
||||||
});
|
}).returning();
|
||||||
|
user = res[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const jwt = await create({ alg: "HS512", type: "JWT" }, {
|
const jwt = await create({ alg: "HS512", type: "JWT" }, {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user