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
|
||||
|
||||
data/
|
||||
data-dev/
|
||||
_fresh/
|
||||
node_modules/
|
||||
|
@ -14,3 +14,9 @@ deno task start
|
||||
```
|
||||
|
||||
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",
|
||||
"tasks": {
|
||||
"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",
|
||||
"preview": "deno run -A main.ts",
|
||||
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
||||
@ -25,11 +26,13 @@
|
||||
"@islands/": "./islands/",
|
||||
"@lib": "./lib",
|
||||
"@lib/": "./lib/",
|
||||
"@libsql/client": "npm:@libsql/client@^0.14.0",
|
||||
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
|
||||
"@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/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-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2",
|
||||
"preact/": "https://esm.sh/preact@10.22.0/",
|
||||
@ -38,9 +41,12 @@
|
||||
"tailwindcss/": "npm:/tailwindcss@^3.4.17/",
|
||||
"tailwindcss/plugin": "npm:/tailwindcss@^3.4.17/plugin.js",
|
||||
"camelcase-css": "npm:camelcase-css",
|
||||
"tsx": "npm:tsx@^4.19.2",
|
||||
"typesense": "https://raw.githubusercontent.com/bradenmacdonald/typesense-deno/main/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": {
|
||||
"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 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 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_REDIRECT_URL = Deno.env.get("GITEA_REDIRECT_URL");
|
||||
|
||||
@ -23,5 +26,9 @@ export const TYPESENSE_URL = Deno.env.get("TYPESENSE_URL") ||
|
||||
"http://localhost:8108";
|
||||
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") ||
|
||||
"warn";
|
||||
|
@ -1,30 +1,30 @@
|
||||
import { MiddlewareHandlerContext } from "$fresh/server.ts";
|
||||
import { FreshContext } from "$fresh/server.ts";
|
||||
|
||||
class DomainError extends Error {
|
||||
status = 500;
|
||||
render?: (ctx: MiddlewareHandlerContext) => void;
|
||||
render?: (ctx: FreshContext) => void;
|
||||
constructor(public statusText = "Internal Server Error") {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends DomainError {
|
||||
status = 404;
|
||||
constructor(public statusText = "Not Found") {
|
||||
override status = 404;
|
||||
constructor(public override statusText = "Not Found") {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class BadRequestError extends DomainError {
|
||||
status = 400;
|
||||
constructor(public statusText = "Bad Request") {
|
||||
override status = 400;
|
||||
constructor(public override statusText = "Bad Request") {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class AccessDeniedError extends DomainError {
|
||||
status = 403;
|
||||
constructor(public statusText = "Access Denied") {
|
||||
override status = 403;
|
||||
constructor(public override statusText = "Access Denied") {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { useEffect, useRef } from "preact/hooks";
|
||||
export function useEventListener<T extends Event>(
|
||||
eventName: string,
|
||||
handler: (event: T) => void,
|
||||
element: Window | HTMLElement = globalThis,
|
||||
element: typeof globalThis | HTMLElement = globalThis,
|
||||
) {
|
||||
// Create a ref that stores handler
|
||||
const savedHandler = useRef<(event: Event) => void>();
|
||||
|
@ -23,9 +23,9 @@ const logFuncs = {
|
||||
} as const;
|
||||
|
||||
let longestScope = 0;
|
||||
let logLevel = (_LOG_LEVEL && _LOG_LEVEL in logMap && logMap[_LOG_LEVEL]) ??
|
||||
LOG_LEVEL.WARN;
|
||||
|
||||
let logLevel = _LOG_LEVEL && _LOG_LEVEL in logMap && _LOG_LEVEL in logMap
|
||||
? logMap[_LOG_LEVEL]
|
||||
: LOG_LEVEL.WARN;
|
||||
const ee = new EventEmitter<{
|
||||
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 { getTimeCacheKey } from "@lib/string.ts";
|
||||
import { db } from "@lib/sqlite/sqlite.ts";
|
||||
import { performanceTable } from "@lib/sqlite/schema.ts";
|
||||
import { between } from "drizzle-orm/sql";
|
||||
|
||||
export type PerformancePoint = {
|
||||
path: string;
|
||||
@ -16,40 +17,29 @@ export type PerformanceRes = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export const savePerformance = (url: string, milliseconds: number) => {
|
||||
const cacheKey = getTimeCacheKey();
|
||||
|
||||
export const savePerformance = async (url: string, seconds: number) => {
|
||||
const u = new URL(url);
|
||||
if (u.pathname.includes("_frsh/")) return;
|
||||
u.searchParams.delete("__frsh_c");
|
||||
|
||||
cache.set(
|
||||
`performance:${cacheKey}`,
|
||||
JSON.stringify({
|
||||
path: decodeURIComponent(u.pathname),
|
||||
search: u.search,
|
||||
time: Math.floor(milliseconds * 1000),
|
||||
}),
|
||||
);
|
||||
console.log("Saving performance", u.pathname, u.search, seconds);
|
||||
const res = await db.insert(performanceTable).values({
|
||||
path: decodeURIComponent(u.pathname),
|
||||
search: u.search,
|
||||
time: Math.floor(seconds * 1000),
|
||||
});
|
||||
console.log({ res });
|
||||
};
|
||||
|
||||
export async function getPerformances(): Promise<PerformanceRes> {
|
||||
const d = new Date();
|
||||
const year = d.getFullYear();
|
||||
const month = d.getMonth().toString();
|
||||
const day = d.getDay().toString();
|
||||
const now = new Date();
|
||||
const startOfDay = new Date(now.setHours(0, 0, 0, 0));
|
||||
const endOfDay = new Date(now.setHours(23, 59, 59, 999));
|
||||
|
||||
const keys = await cache.keys(
|
||||
`performance:${year}:${month}:${day}:*`,
|
||||
);
|
||||
|
||||
console.log(`performance:${year}:${month}:${day}:*`);
|
||||
|
||||
const performances = await Promise.all(
|
||||
keys.map(async (key) =>
|
||||
JSON.parse(await cache.get<string>(key)) as PerformancePoint
|
||||
),
|
||||
const performances = await db.select().from(performanceTable).where(
|
||||
between(performanceTable.createdAt, startOfDay, endOfDay),
|
||||
);
|
||||
console.log({ performances });
|
||||
|
||||
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="deno.ns" />
|
||||
|
||||
import "@std/dotenv/load";
|
||||
|
||||
import { start } from "$fresh/server.ts";
|
||||
import manifest from "./fresh.gen.ts";
|
||||
import config from "./fresh.config.ts";
|
||||
|
@ -1,14 +1,14 @@
|
||||
//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 { getCookies } from "@std/http/cookie";
|
||||
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";
|
||||
|
||||
export async function handler(
|
||||
req: Request,
|
||||
ctx: MiddlewareHandlerContext,
|
||||
ctx: FreshContext,
|
||||
) {
|
||||
try {
|
||||
performance.mark("a");
|
||||
@ -29,7 +29,7 @@ export async function handler(
|
||||
const resp = await ctx.next();
|
||||
performance.mark("b");
|
||||
const b = performance.measure("a->b", "a", "b");
|
||||
cache.savePerformance(req.url, b.duration);
|
||||
perf.savePerformance(req.url, b.duration);
|
||||
return resp;
|
||||
} catch (error) {
|
||||
console.error("Error", error);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { MainLayout } from "@components/layouts/main.tsx";
|
||||
import { Handlers, PageProps } from "$fresh/server.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 { renderMarkdown } from "@lib/documents.ts";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { MainLayout } from "@components/layouts/main.tsx";
|
||||
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";
|
||||
|
||||
export const handler: Handlers = {
|
||||
|
@ -4,9 +4,11 @@ import { oauth2Client } from "@lib/auth.ts";
|
||||
import { getCookies, setCookie } from "@std/http/cookie";
|
||||
import { codeChallengeMap } from "./login.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 { 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 = {
|
||||
async GET(request) {
|
||||
@ -38,15 +40,17 @@ export const handler: Handlers = {
|
||||
|
||||
const oauthUser = await userResponse.json() as GiteaOauthUser;
|
||||
|
||||
const allUsers = await userDB.findAll();
|
||||
let user = allUsers.find((u) => u.name === oauthUser.name);
|
||||
let user = await db.select().from(userTable).where(
|
||||
eq(userTable.name, oauthUser.name),
|
||||
).limit(1).then((users) => users[0]);
|
||||
|
||||
if (!user) {
|
||||
user = await userDB.create({
|
||||
createdAt: new Date(),
|
||||
const res = await db.insert(userTable).values({
|
||||
id: crypto.randomUUID(),
|
||||
email: oauthUser.email,
|
||||
name: oauthUser.name,
|
||||
});
|
||||
}).returning();
|
||||
user = res[0];
|
||||
}
|
||||
|
||||
const jwt = await create({ alg: "HS512", type: "JWT" }, {
|
||||
|
Loading…
x
Reference in New Issue
Block a user