upd: routing dev
This commit is contained in:
45
src/lib/applog.ts
Normal file
45
src/lib/applog.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { redis } from './redis'
|
||||
|
||||
export type LogLevel = 'info' | 'warn' | 'error'
|
||||
|
||||
export interface AppLogEntry {
|
||||
id: number
|
||||
level: LogLevel
|
||||
message: string
|
||||
detail?: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
const REDIS_KEY = 'app:logs'
|
||||
const MAX_ENTRIES = 500
|
||||
const ID_KEY = 'app:logs:next_id'
|
||||
|
||||
export async function appLog(level: LogLevel, message: string, detail?: string) {
|
||||
if (!redis) return
|
||||
const id = await redis.incr(ID_KEY)
|
||||
const entry: AppLogEntry = { id, level, message, detail, timestamp: new Date().toISOString() }
|
||||
await redis.lpush(REDIS_KEY, JSON.stringify(entry))
|
||||
await redis.ltrim(REDIS_KEY, 0, MAX_ENTRIES - 1)
|
||||
}
|
||||
|
||||
export async function getAppLogs(options?: {
|
||||
level?: LogLevel
|
||||
limit?: number
|
||||
afterId?: number
|
||||
}): Promise<AppLogEntry[]> {
|
||||
if (!redis) return []
|
||||
const limit = options?.limit ?? 100
|
||||
const fetchCount = options?.level || options?.afterId ? MAX_ENTRIES : limit
|
||||
const raw = await redis.lrange(REDIS_KEY, 0, fetchCount - 1)
|
||||
let logs: AppLogEntry[] = raw.map((s: string) => JSON.parse(s))
|
||||
if (options?.afterId) logs = logs.filter((l) => l.id > options.afterId!)
|
||||
if (options?.level) logs = logs.filter((l) => l.level === options.level)
|
||||
logs.reverse()
|
||||
return logs.slice(-limit)
|
||||
}
|
||||
|
||||
export async function clearAppLogs() {
|
||||
if (!redis) return
|
||||
await redis.del(REDIS_KEY)
|
||||
await redis.del(ID_KEY)
|
||||
}
|
||||
@@ -25,4 +25,5 @@ export const env = {
|
||||
MINIO_SECRET_KEY: required('MINIO_SECRET_KEY'),
|
||||
MINIO_BUCKET: required('MINIO_BUCKET'),
|
||||
MINIO_UPLOAD_DIR: optional('MINIO_UPLOAD_DIR', 'bug-reports'),
|
||||
REDIS_URL: optional('REDIS_URL', ''),
|
||||
} as const
|
||||
|
||||
44
src/lib/presence.ts
Normal file
44
src/lib/presence.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { ServerWebSocket } from 'bun'
|
||||
|
||||
const connections = new Map<string, Set<ServerWebSocket<{ userId: string }>>>()
|
||||
const adminSubs = new Set<ServerWebSocket<{ userId: string }>>()
|
||||
|
||||
export function getOnlineUserIds(): string[] {
|
||||
return Array.from(connections.keys())
|
||||
}
|
||||
|
||||
function broadcast() {
|
||||
const online = getOnlineUserIds()
|
||||
const msg = JSON.stringify({ type: 'presence', online })
|
||||
for (const ws of adminSubs) ws.send(msg)
|
||||
}
|
||||
|
||||
export function addConnection(ws: ServerWebSocket<{ userId: string }>, userId: string, isAdmin: boolean) {
|
||||
let set = connections.get(userId)
|
||||
if (!set) {
|
||||
set = new Set()
|
||||
connections.set(userId, set)
|
||||
}
|
||||
set.add(ws)
|
||||
if (isAdmin) {
|
||||
adminSubs.add(ws)
|
||||
ws.send(JSON.stringify({ type: 'presence', online: getOnlineUserIds() }))
|
||||
}
|
||||
broadcast()
|
||||
}
|
||||
|
||||
export function broadcastToAdmins(message: object) {
|
||||
const msg = JSON.stringify(message)
|
||||
for (const ws of adminSubs) ws.send(msg)
|
||||
}
|
||||
|
||||
export function removeConnection(ws: ServerWebSocket<{ userId: string }>) {
|
||||
const userId = ws.data.userId
|
||||
const set = connections.get(userId)
|
||||
if (set) {
|
||||
set.delete(ws)
|
||||
if (set.size === 0) connections.delete(userId)
|
||||
}
|
||||
adminSubs.delete(ws)
|
||||
broadcast()
|
||||
}
|
||||
3
src/lib/redis.ts
Normal file
3
src/lib/redis.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { env } from './env'
|
||||
|
||||
export const redis = env.REDIS_URL ? new Bun.RedisClient(env.REDIS_URL) : null
|
||||
104
src/lib/schema-parser.ts
Normal file
104
src/lib/schema-parser.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
export interface SchemaField {
|
||||
name: string
|
||||
type: string
|
||||
isId: boolean
|
||||
isUnique: boolean
|
||||
isOptional: boolean
|
||||
isList: boolean
|
||||
isRelation: boolean
|
||||
default?: string
|
||||
}
|
||||
|
||||
export interface SchemaRelation {
|
||||
from: string
|
||||
fromField: string
|
||||
to: string
|
||||
toField: string
|
||||
onDelete?: string
|
||||
}
|
||||
|
||||
export interface SchemaModel {
|
||||
name: string
|
||||
tableName: string
|
||||
fields: SchemaField[]
|
||||
}
|
||||
|
||||
export interface SchemaEnum {
|
||||
name: string
|
||||
values: string[]
|
||||
}
|
||||
|
||||
export interface ParsedSchema {
|
||||
models: SchemaModel[]
|
||||
enums: SchemaEnum[]
|
||||
relations: SchemaRelation[]
|
||||
}
|
||||
|
||||
export function parseSchema(raw: string): ParsedSchema {
|
||||
const models: SchemaModel[] = []
|
||||
const enums: SchemaEnum[] = []
|
||||
const relations: SchemaRelation[] = []
|
||||
|
||||
const blocks = raw.match(/(model|enum)\s+(\w+)\s*\{([^}]*)}/gs) ?? []
|
||||
|
||||
for (const block of blocks) {
|
||||
const match = block.match(/(model|enum)\s+(\w+)\s*\{([^}]*)}/s)
|
||||
if (!match) continue
|
||||
const [, type, name, body] = match
|
||||
const lines = body
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l && !l.startsWith('//'))
|
||||
|
||||
if (type === 'enum') {
|
||||
enums.push({ name, values: lines })
|
||||
continue
|
||||
}
|
||||
|
||||
let tableName = name
|
||||
const fields: SchemaField[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
const mapMatch = line.match(/@@map\("(\w+)"\)/)
|
||||
if (mapMatch) { tableName = mapMatch[1]; continue }
|
||||
if (line.startsWith('@@')) continue
|
||||
|
||||
const fieldMatch = line.match(/^(\w+)\s+(\w+)(\?)?(\[\])?\s*(.*)$/)
|
||||
if (!fieldMatch) continue
|
||||
const [, fName, fType, optional, list, attrs] = fieldMatch
|
||||
|
||||
const isId = attrs.includes('@id')
|
||||
const isUnique = attrs.includes('@unique')
|
||||
const isRelation = attrs.includes('@relation')
|
||||
const defaultMatch = attrs.match(/@default\(([^)]+)\)/)
|
||||
|
||||
const isModelRef =
|
||||
/^[A-Z]/.test(fType) &&
|
||||
!enums.some((e) => e.name === fType) &&
|
||||
!['String', 'Int', 'Float', 'Boolean', 'DateTime', 'BigInt', 'Decimal', 'Bytes', 'Json'].includes(fType)
|
||||
|
||||
if (isRelation) {
|
||||
const relMatch = attrs.match(
|
||||
/@relation\(fields:\s*\[(\w+)],\s*references:\s*\[(\w+)](?:,\s*onDelete:\s*(\w+))?\)/,
|
||||
)
|
||||
if (relMatch) {
|
||||
relations.push({ from: name, fromField: relMatch[1], to: fType, toField: relMatch[2], onDelete: relMatch[3] })
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({
|
||||
name: fName,
|
||||
type: fType + (list ? '[]' : ''),
|
||||
isId, isUnique,
|
||||
isOptional: !!optional,
|
||||
isList: !!list,
|
||||
isRelation: isModelRef,
|
||||
default: defaultMatch?.[1],
|
||||
})
|
||||
}
|
||||
|
||||
models.push({ name, tableName, fields })
|
||||
}
|
||||
|
||||
return { models, enums, relations }
|
||||
}
|
||||
Reference in New Issue
Block a user