♻️ refactor: remove unused code

This commit is contained in:
2026-08-13 02:32:29 +07:00
parent 5a290fcfa5
commit 3df22125dd
29 changed files with 60 additions and 588 deletions
@@ -2,11 +2,8 @@ import type { UpdateSuspensionInput, UpdateUserInput, User } from "@minikura/db"
export interface IUserService {
getUserById(id: string): Promise<User>;
getUserByEmail(email: string): Promise<User | null>;
getAllUsers(): Promise<User[]>;
updateUser(id: string, input: UpdateUserInput): Promise<User>;
updateSuspension(id: string, input: UpdateSuspensionInput): Promise<User>;
suspendUser(id: string, suspendedUntil?: Date | null): Promise<User>;
unsuspendUser(id: string): Promise<User>;
deleteUser(requestingUserId: string, targetUserId: string): Promise<void>;
}
@@ -3,7 +3,7 @@ import { BusinessRuleError, NotFoundError } from "../../domain/errors/base.error
import {
UserSuspendedEvent,
UserUnsuspendedEvent,
} from "../../domain/events/server-lifecycle.events";
} from "../../domain/events/user-lifecycle.events";
import type { UserRepository } from "../../domain/repositories/user.repository";
import { eventBus } from "../../infrastructure/event-bus";
import type { IUserService } from "../interfaces/user.service.interface";
@@ -19,10 +19,6 @@ export class UserService implements IUserService {
return user;
}
async getUserByEmail(email: string): Promise<User | null> {
return this.userRepo.findByEmail(email);
}
async getAllUsers(): Promise<User[]> {
return this.userRepo.findAll();
}
@@ -42,20 +38,6 @@ export class UserService implements IUserService {
return user;
}
async suspendUser(id: string, suspendedUntil?: Date | null): Promise<User> {
return this.updateSuspension(id, {
isSuspended: true,
suspendedUntil: suspendedUntil ?? null,
});
}
async unsuspendUser(id: string): Promise<User> {
return this.updateSuspension(id, {
isSuspended: false,
suspendedUntil: null,
});
}
async deleteUser(requestingUserId: string, targetUserId: string): Promise<void> {
if (requestingUserId === targetUserId) {
throw new BusinessRuleError("Cannot delete yourself");
-36
View File
@@ -1,36 +0,0 @@
export const API_KEY_PREFIXES = {
SERVER: "minikura_server_api_key_",
REVERSE_PROXY: "minikura_reverse_proxy_server_api_key_",
} as const;
export const DEFAULT_PORTS = {
MINECRAFT: 25565,
} as const;
export const DEFAULT_MEMORY = {
SERVER: 2048,
REVERSE_PROXY: 512,
} as const;
export const DEFAULT_MEMORY_REQUEST = {
SERVER: 1024,
REVERSE_PROXY: 512,
} as const;
export const DEFAULT_CPU = {
SERVER: {
REQUEST: "500m",
LIMIT: "2",
},
REVERSE_PROXY: {
REQUEST: "250m",
LIMIT: "500m",
},
} as const;
export const VALIDATION = {
ID_PATTERN: /^[a-zA-Z0-9-_]+$/,
ID_ERROR_MESSAGE: "ID must be alphanumeric with - or _",
PORT_MIN: 1,
PORT_MAX: 65535,
} as const;
@@ -26,18 +26,3 @@ export class ServerDeletedEvent extends DomainEvent {
super();
}
}
export class UserSuspendedEvent extends DomainEvent {
constructor(
public readonly userId: string,
public readonly suspendedUntil: Date | null
) {
super();
}
}
export class UserUnsuspendedEvent extends DomainEvent {
constructor(public readonly userId: string) {
super();
}
}
@@ -0,0 +1,16 @@
import { DomainEvent } from "./domain-event";
export class UserSuspendedEvent extends DomainEvent {
constructor(
public readonly userId: string,
public readonly suspendedUntil: Date | null
) {
super();
}
}
export class UserUnsuspendedEvent extends DomainEvent {
constructor(public readonly userId: string) {
super();
}
}
@@ -2,10 +2,8 @@ import type { UpdateSuspensionInput, UpdateUserInput, User } from "@minikura/db"
export interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
findAll(): Promise<User[]>;
update(id: string, input: UpdateUserInput): Promise<User>;
updateSuspension(id: string, input: UpdateSuspensionInput): Promise<User>;
delete(id: string): Promise<void>;
count(): Promise<number>;
}
@@ -1,33 +0,0 @@
export class ApiKey {
private static readonly SERVER_PREFIX = "minikura_srv_";
private static readonly REVERSE_PROXY_PREFIX = "minikura_proxy_";
private static readonly TOKEN_BYTES = 32;
private constructor(private readonly value: string) {}
static generate(type: "server" | "reverse-proxy"): ApiKey {
const prefix = type === "server" ? ApiKey.SERVER_PREFIX : ApiKey.REVERSE_PROXY_PREFIX;
const token = Buffer.from(crypto.randomUUID())
.toString("base64")
.replace(/[^a-zA-Z0-9]/g, "")
.substring(0, ApiKey.TOKEN_BYTES);
return new ApiKey(`${prefix}${token}`);
}
static validate(value: string): boolean {
const patterns = [
new RegExp(`^${ApiKey.SERVER_PREFIX}[a-zA-Z0-9]{32}$`),
new RegExp(`^${ApiKey.REVERSE_PROXY_PREFIX}[a-zA-Z0-9]{32}$`),
];
return patterns.some((pattern) => pattern.test(value));
}
toString(): string {
return this.value;
}
getType(): "server" | "reverse-proxy" {
if (this.value.startsWith(ApiKey.SERVER_PREFIX)) return "server";
return "reverse-proxy";
}
}
@@ -1,15 +0,0 @@
export class K8sConnectionInfo {
constructor(
public readonly host: string,
public readonly port: number,
public readonly namespace: string
) {}
toUrl(): string {
return `${this.host}:${this.port}`;
}
toConnectionString(): string {
return `Host: ${this.host}, Port: ${this.port}, Namespace: ${this.namespace}`;
}
}
@@ -1,39 +0,0 @@
export class ServerConfig {
constructor(
public readonly memory: number,
public readonly memoryRequest: number,
public readonly cpuRequest: string,
public readonly cpuLimit: string,
public readonly jvmOpts: string | null
) {}
static fromDefaults(): ServerConfig {
return new ServerConfig(2048, 1024, "250m", "500m", null);
}
static fromInput(input: {
memory?: number;
memoryRequest?: number;
cpuRequest?: string;
cpuLimit?: string;
jvmOpts?: string;
}): ServerConfig {
return new ServerConfig(
input.memory ?? 2048,
input.memoryRequest ?? 1024,
input.cpuRequest ?? "250m",
input.cpuLimit ?? "500m",
input.jvmOpts ?? null
);
}
getJvmArgs(): string {
const args: string[] = [`-Xmx${this.memory}M`];
if (this.jvmOpts) {
args.push(this.jvmOpts);
}
return args.join(" ");
}
}
@@ -5,7 +5,6 @@ type EventHandler<T extends DomainEvent = DomainEvent> = (event: T) => void | Pr
export class EventBus {
private handlers = new Map<string, Set<EventHandler>>();
private eventHistory: DomainEvent[] = [];
subscribe<T extends DomainEvent>(
eventClass: { new (...args: any[]): T },
@@ -22,7 +21,6 @@ export class EventBus {
}
async publish<T extends DomainEvent>(event: T): Promise<void> {
this.eventHistory.push(event);
const eventName = event.constructor.name;
const handlers = this.handlers.get(eventName) || [];
for (const handler of handlers) {
@@ -33,14 +31,6 @@ export class EventBus {
}
}
}
getHistory(): DomainEvent[] {
return [...this.eventHistory];
}
clearHistory(): void {
this.eventHistory = [];
}
}
export const eventBus = new EventBus();
@@ -1,7 +1,7 @@
import {
UserSuspendedEvent,
UserUnsuspendedEvent,
} from "../../domain/events/server-lifecycle.events";
} from "../../domain/events/user-lifecycle.events";
import { eventBus } from "../event-bus";
import { logger } from "../logger";
@@ -8,12 +8,6 @@ export class PrismaUserRepository implements UserRepository {
});
}
async findByEmail(email: string): Promise<User | null> {
return await prisma.user.findUnique({
where: { email },
});
}
async findAll(): Promise<User[]> {
return await prisma.user.findMany({
orderBy: { createdAt: "desc" },
@@ -39,8 +33,4 @@ export class PrismaUserRepository implements UserRepository {
where: { id },
});
}
async count(): Promise<number> {
return await prisma.user.count();
}
}
+26 -42
View File
@@ -1,57 +1,41 @@
import type { User } from "@minikura/db";
import type { Elysia } from "elysia";
import { ForbiddenError, UnauthorizedError } from "../domain/errors/base.error";
import { bearerToken, findApiKeyOwner } from "./api-key";
function authenticatedUser(ctx: { user?: User | null; isSuspended?: boolean }) {
const { user, isSuspended } = ctx;
if (!user) {
throw new UnauthorizedError();
}
if (isSuspended) {
throw new ForbiddenError("Account is suspended");
}
return { user };
}
export const requireAuth = (app: Elysia) => {
return app.derive((ctx: any) => {
const { user, isSuspended } = ctx as {
user: User | null;
isSuspended: boolean;
};
if (!user) {
throw new UnauthorizedError();
}
if (isSuspended) {
throw new ForbiddenError("Account is suspended");
}
return { user };
});
};
export const requireAdmin = (app: Elysia) => {
return app.derive((ctx: any) => {
const { user, isSuspended } = ctx as {
user: User | null;
isSuspended: boolean;
};
if (!user) {
throw new UnauthorizedError();
}
if (isSuspended) {
throw new ForbiddenError("Account is suspended");
}
if (user.role !== "admin") {
throw new ForbiddenError("Admin access required");
}
return { user };
});
return app.derive((ctx: any) => authenticatedUser(ctx));
};
export const requireRole = (role: string) => (app: Elysia) => {
return app.derive((ctx: any) => {
const { user, isSuspended } = ctx as {
user: User | null;
isSuspended: boolean;
};
if (!user) {
throw new UnauthorizedError();
}
if (isSuspended) {
throw new ForbiddenError("Account is suspended");
}
const { user } = authenticatedUser(ctx);
if (user.role !== role) {
throw new ForbiddenError(`${role} access required`);
}
return { user };
});
};
export const requireAdmin = requireRole("admin");
export const requirePluginApiKey = (app: Elysia) => {
return app.derive(async ({ request }) => {
const owner = await findApiKeyOwner(bearerToken(request.headers.get("authorization")));
if (!owner) {
throw new UnauthorizedError();
}
return { pluginAuth: owner };
});
};
@@ -1,25 +0,0 @@
import type { z } from "zod";
type ErrorHandler = (code: number, value: unknown) => never;
export function validateBody<T extends z.ZodType>(
schema: T,
body: unknown,
error: ErrorHandler
): z.infer<T> {
const result = schema.safeParse(body);
if (!result.success) {
const firstError = result.error.issues[0];
const message = `${firstError.path.join(".")}: ${firstError.message}`;
throw error(400, { message });
}
return result.data;
}
export function zodValidate<T extends z.ZodType>(schema: T) {
return (context: { body: unknown; error: ErrorHandler }) => {
return validateBody(schema, context.body, context.error);
};
}
-2
View File
@@ -2,10 +2,8 @@ import { labelKeys } from "@minikura/api";
import { Elysia } from "elysia";
import { k8sService } from "../application/di-container";
import { requireAuth } from "../middleware/auth-guards";
import { authPlugin } from "../middleware/auth-plugin";
export const k8sRoutes = new Elysia({ prefix: "/k8s" })
.use(authPlugin)
.use(requireAuth)
.get("/status", async () => {
return k8sService.getConnectionInfo();
+4 -55
View File
@@ -54,49 +54,7 @@ export const createServerSchema = z.object({
level_type: z.string().optional(),
});
export const updateServerSchema = z.object({
description: z.string().nullable().optional(),
listen_port: z.number().int().min(1).max(65535).optional(),
service_type: z.nativeEnum(ServiceType).optional(),
node_port: z
.union([
z
.number()
.int()
.min(30000, "Node port must be at least 30000")
.max(32767, "Node port must be at most 32767"),
z.null(),
])
.optional(),
env_variables: z
.array(
z.object({
key: z.string().min(1),
value: z.string(),
})
)
.optional(),
memory: z.number().int().min(256).optional(),
memory_request: z.number().int().min(256).optional(),
cpu_request: z.string().optional(),
cpu_limit: z.string().optional(),
jar_type: z.nativeEnum(MinecraftServerJarType).optional(),
minecraft_version: z.string().optional(),
jvm_opts: z.string().optional(),
use_aikar_flags: z.boolean().optional(),
use_meowice_flags: z.boolean().optional(),
difficulty: z.nativeEnum(ServerDifficulty).optional(),
game_mode: z.nativeEnum(GameMode).optional(),
max_players: z.number().int().min(1).max(1000).optional(),
pvp: z.boolean().optional(),
online_mode: z.boolean().optional(),
motd: z.string().optional(),
level_seed: z.string().optional(),
level_type: z.string().optional(),
});
export const updateServerSchema = createServerSchema.omit({ id: true, type: true }).partial();
export const createReverseProxySchema = z.object({
id: z
@@ -123,18 +81,9 @@ export const createReverseProxySchema = z.object({
cpu_limit: z.string().optional(),
});
export const updateReverseProxySchema = z.object({
description: z.string().nullable().optional(),
external_address: z.string().optional(),
external_port: z.number().int().min(1).max(65535).optional(),
listen_port: z.number().int().min(1).max(65535).optional(),
type: z.nativeEnum(ReverseProxyServerType).optional(),
service_type: z.nativeEnum(ServiceType).optional(),
node_port: z.union([z.number().int().min(30000).max(32767), z.null()]).optional(),
memory: z.number().int().min(256).optional(),
cpu_request: z.string().optional(),
cpu_limit: z.string().optional(),
});
export const updateReverseProxySchema = createReverseProxySchema
.omit({ id: true, env_variables: true })
.partial();
export const envVariableSchema = z.object({
key: z.string().min(1, "Key is required"),
-5
View File
@@ -10,10 +10,5 @@ export const updateSuspensionSchema = z.object({
suspendedUntil: z.string().nullable().optional(),
});
export const suspendUserSchema = z.object({
suspendedUntil: z.string().nullable().optional(),
});
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
export type UpdateSuspensionInput = z.infer<typeof updateSuspensionSchema>;
export type SuspendUserInput = z.infer<typeof suspendUserSchema>;
+1 -1
View File
@@ -54,7 +54,7 @@ export class K8sService implements IK8sService {
private initializeOperations(): void {
this.podOps = new PodOperations(this.coreApi, this.namespace);
this.clusterOps = new ClusterOperations(this.coreApi, this.customObjectsApi, this.namespace);
this.clusterOps = new ClusterOperations(this.customObjectsApi, this.namespace);
this.customResourceOps = new CustomResourceOperations(this.customObjectsApi, this.namespace);
}
@@ -3,20 +3,12 @@ import { BaseK8sOperations } from "./base.operations";
export class ClusterOperations extends BaseK8sOperations {
constructor(
private coreApi: k8s.CoreV1Api,
private customObjectsApi: k8s.CustomObjectsApi,
namespace: string
) {
super(namespace);
}
async listNodes() {
return this.executeOperation(
() => this.coreApi.listNode().then((r) => r.items),
"Failed to fetch nodes"
);
}
async getNodeMetrics() {
return this.executeOperation(
() =>
@@ -28,12 +20,4 @@ export class ClusterOperations extends BaseK8sOperations {
"Failed to fetch node metrics"
);
}
async listConfigMaps(namespace?: string) {
const ns = namespace || this.namespace;
return this.executeOperation(
() => this.coreApi.listNamespacedConfigMap({ namespace: ns }).then((r) => r.items),
"Failed to fetch configmaps"
);
}
}
@@ -1,6 +1,4 @@
export { BaseK8sOperations } from "./base.operations";
export { ClusterOperations } from "./cluster.operations";
export { CustomResourceOperations } from "./custom-resource.operations";
export { NetworkOperations } from "./network.operations";
export { PodOperations } from "./pod.operations";
export { WorkloadOperations } from "./workload.operations";
@@ -1,89 +0,0 @@
import type * as k8s from "@kubernetes/client-node";
import { BaseK8sOperations } from "./base.operations";
export class NetworkOperations extends BaseK8sOperations {
constructor(
private coreApi: k8s.CoreV1Api,
private networkingApi: k8s.NetworkingV1Api,
namespace: string
) {
super(namespace);
}
async listServices() {
return this.executeOperation(
() => this.coreApi.listNamespacedService({ namespace: this.namespace }).then((r) => r.items),
"Failed to fetch services"
);
}
async listIngresses() {
return this.executeOperation(
() =>
this.networkingApi
.listNamespacedIngress({ namespace: this.namespace })
.then((r) => r.items),
"Failed to fetch ingresses"
);
}
async getServiceInfo(serviceName: string) {
return this.executeOperation(
() => this.coreApi.readNamespacedService({ name: serviceName, namespace: this.namespace }),
`Failed to fetch service info for ${serviceName}`
);
}
async getServerConnectionInfo(serviceName: string) {
return this.executeOperation(async () => {
const service = await this.coreApi.readNamespacedService({
name: serviceName,
namespace: this.namespace,
});
const serviceType = service.spec?.type || "ClusterIP";
const ports = service.spec?.ports || [];
let host: string;
let externalHost: string | undefined;
switch (serviceType) {
case "LoadBalancer": {
const ingress = service.status?.loadBalancer?.ingress?.[0];
host =
ingress?.hostname ||
ingress?.ip ||
`${serviceName}.${this.namespace}.svc.cluster.local`;
externalHost = ingress?.hostname || ingress?.ip;
break;
}
case "NodePort":
host = `${serviceName}.${this.namespace}.svc.cluster.local`;
externalHost = "<node-ip>";
break;
default:
host = `${serviceName}.${this.namespace}.svc.cluster.local`;
externalHost = undefined;
}
const portMappings = ports.map((port) => ({
name: port.name,
port: port.port,
targetPort: port.targetPort,
nodePort: port.nodePort,
protocol: port.protocol || "TCP",
}));
return {
serviceName,
namespace: this.namespace,
serviceType,
internalHost: host,
externalHost,
ports: portMappings,
};
}, `Failed to fetch connection info for service ${serviceName}`);
}
}
@@ -9,30 +9,6 @@ export class PodOperations extends BaseK8sOperations {
super(namespace);
}
async listPods() {
return this.executeOperation(
() => this.coreApi.listNamespacedPod({ namespace: this.namespace }).then((r) => r.items),
"Failed to fetch pods"
);
}
async listPodsByLabel(labelSelector: string) {
return this.executeOperation(
() =>
this.coreApi
.listNamespacedPod({ namespace: this.namespace, labelSelector })
.then((r) => r.items),
"Failed to fetch pods by label"
);
}
async getPodInfo(podName: string) {
return this.executeOperation(
() => this.coreApi.readNamespacedPod({ name: podName, namespace: this.namespace }),
`Failed to fetch pod info for ${podName}`
);
}
async getPodLogs(
podName: string,
options?: {
@@ -1,27 +0,0 @@
import type * as k8s from "@kubernetes/client-node";
import { BaseK8sOperations } from "./base.operations";
export class WorkloadOperations extends BaseK8sOperations {
constructor(
private appsApi: k8s.AppsV1Api,
namespace: string
) {
super(namespace);
}
async listDeployments() {
return this.executeOperation(
() =>
this.appsApi.listNamespacedDeployment({ namespace: this.namespace }).then((r) => r.items),
"Failed to fetch deployments"
);
}
async listStatefulSets() {
return this.executeOperation(
() =>
this.appsApi.listNamespacedStatefulSet({ namespace: this.namespace }).then((r) => r.items),
"Failed to fetch statefulsets"
);
}
}
@@ -1,9 +1,8 @@
import * as k8s from "@kubernetes/client-node";
import { API_GROUP } from "@minikura/api";
import { prisma, type ReverseProxyWithEnvVars, type ServerWithEnvVars } from "@minikura/db";
import { buildKubeConfig } from "@minikura/shared/kube-auth";
import { logger } from "../infrastructure/logger";
const API_GROUP = "minikura.kirameki.cafe";
const API_VERSION = "v1alpha1";
const FIELD_MANAGER = "minikura-backend";
const SYNC_INTERVAL_MS = 30_000;
@@ -1,83 +0,0 @@
import type { TopologyEdge, TopologyNode } from "@/lib/topology-types";
const LAYOUT_CONFIG = {
TIER_SPACING: 300,
NODE_SPACING: 250,
START_X: 150,
START_Y: 100,
} as const;
export function applyHierarchicalLayout(
nodes: TopologyNode[],
edges: TopologyEdge[]
): { nodes: TopologyNode[]; edges: TopologyEdge[] } {
const proxyNodes = nodes.filter((n) => n.data.type === "proxy");
const serverNodes = nodes.filter((n) => n.data.type === "server");
const layoutedNodes: TopologyNode[] = [];
const maxNodesInTier = Math.max(proxyNodes.length, serverNodes.length);
const tierWidth = maxNodesInTier * LAYOUT_CONFIG.NODE_SPACING;
const proxyOffsetX = (tierWidth - proxyNodes.length * LAYOUT_CONFIG.NODE_SPACING) / 2;
proxyNodes.forEach((node, index) => {
const x = LAYOUT_CONFIG.START_X + proxyOffsetX + index * LAYOUT_CONFIG.NODE_SPACING;
const y = LAYOUT_CONFIG.START_Y;
layoutedNodes.push({
...node,
position: { x, y },
});
});
const serverOffsetX = (tierWidth - serverNodes.length * LAYOUT_CONFIG.NODE_SPACING) / 2;
serverNodes.forEach((node, index) => {
const x = LAYOUT_CONFIG.START_X + serverOffsetX + index * LAYOUT_CONFIG.NODE_SPACING;
const y = LAYOUT_CONFIG.START_Y + LAYOUT_CONFIG.TIER_SPACING;
layoutedNodes.push({
...node,
position: { x, y },
});
});
return {
nodes: layoutedNodes,
edges,
};
}
export function applyGridLayout(
nodes: TopologyNode[],
edges: TopologyEdge[]
): { nodes: TopologyNode[]; edges: TopologyEdge[] } {
const columns = Math.ceil(Math.sqrt(nodes.length));
const layoutedNodes = nodes.map((node, index) => ({
...node,
position: {
x: LAYOUT_CONFIG.START_X + (index % columns) * LAYOUT_CONFIG.NODE_SPACING,
y: LAYOUT_CONFIG.START_Y + Math.floor(index / columns) * LAYOUT_CONFIG.TIER_SPACING,
},
}));
return {
nodes: layoutedNodes,
edges,
};
}
export function layoutTopologyGraph(
nodes: TopologyNode[],
edges: TopologyEdge[]
): { nodes: TopologyNode[]; edges: TopologyEdge[] } {
if (nodes.length === 0) {
return { nodes: [], edges: [] };
}
if (nodes.length < 20) {
return applyHierarchicalLayout(nodes, edges);
}
return applyGridLayout(nodes, edges);
}
+1 -15
View File
@@ -9,24 +9,10 @@ import type {
PodInfo,
StatefulSetInfo,
} from "@minikura/api";
import { getErrorMessage } from "@minikura/shared/errors";
import { useCallback, useEffect, useRef, useState } from "react";
import { api } from "@/lib/api-client";
function getErrorMessage(error: unknown) {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "object" && error) {
const value = "value" in error ? error.value : error;
if (typeof value === "object" && value && "message" in value) {
return String(value.message);
}
}
return "Failed to fetch Kubernetes resources";
}
export function useK8sResources() {
const [status, setStatus] = useState<K8sStatus | null>(null);
const [pods, setPods] = useState<PodInfo[]>([]);
+3 -7
View File
@@ -6,6 +6,9 @@ import type {
ReverseProxyServer,
} from "@minikura/api";
import type { Edge, Node } from "@xyflow/react";
import type { ResourceMetrics } from "./k8s-metrics";
export type { ResourceMetrics };
export type HealthStatus = "healthy" | "degraded" | "unhealthy" | "unknown";
@@ -13,13 +16,6 @@ export type NodeType = "server" | "proxy" | "k8s-node";
export type EdgeType = "proxy-to-server" | "pod-to-node";
export interface ResourceMetrics {
cpuUsage?: string;
memoryUsage?: string;
cpuUsagePercent?: number;
memoryUsagePercent?: number;
}
export interface K8sNodeMetadata {
node: K8sNodeSummary;
podCount: number;
-10
View File
@@ -1,10 +0,0 @@
import type { Config } from "drizzle-kit";
export default {
schema: "./packages/db/src/schema.ts",
out: "./packages/db/src/drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL || "",
},
} satisfies Config;
+6
View File
@@ -5,6 +5,12 @@ export function getErrorMessage(error: unknown): string {
if (typeof error === "string") {
return error;
}
if (typeof error === "object" && error) {
const value = "value" in error ? error.value : error;
if (typeof value === "object" && value && "message" in value) {
return String(value.message);
}
}
return String(error);
}
export function getAge(timestamp: Date | string | undefined): string {