add growth rate feature and send email list
This commit is contained in:
@@ -276,10 +276,10 @@ export default async function AdminPage() {
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary
|
||||
fallback={<Skeleton className="h-[342px] w-full rounded-lg" />}
|
||||
fallback={<Skeleton className="min-h-[342px] w-full rounded-lg" />}
|
||||
>
|
||||
<Suspense
|
||||
fallback={<Skeleton className="h-[342px] w-full rounded-lg" />}
|
||||
fallback={<Skeleton className="min-h-[342px] w-full rounded-lg" />}
|
||||
>
|
||||
<RequestStatsSection />
|
||||
</Suspense>
|
||||
|
||||
@@ -105,7 +105,7 @@ export function DailyPVUVChart({ data }: { data: ScrapeMeta[] }) {
|
||||
const latestFrom = latestEntry.type;
|
||||
|
||||
return (
|
||||
<Card className="">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-2 sm:py-3">
|
||||
<CardTitle>Total Requests of APIs</CardTitle>
|
||||
|
||||
+154
-1
@@ -1,5 +1,6 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { TIME_RANGES } from "@/lib/enums";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { getStartDate } from "@/lib/utils";
|
||||
|
||||
@@ -20,6 +21,14 @@ export async function GET(req: Request) {
|
||||
const range = url.searchParams.get("range") || "7d";
|
||||
|
||||
const startDate = getStartDate(range);
|
||||
if (!startDate) {
|
||||
return Response.json({ statusText: "Invalid range" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Calculate previous period start and end dates
|
||||
const rangeDuration = TIME_RANGES[range];
|
||||
const prevStartDate = new Date(startDate.getTime() - rangeDuration);
|
||||
const prevEndDate = startDate;
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
@@ -86,12 +95,95 @@ export async function GET(req: Request) {
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
const sends = await prisma.userSendEmail.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startDate,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch previous period data
|
||||
const prevUsers = await prisma.user.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: prevStartDate,
|
||||
lt: prevEndDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
const prevRecords = await prisma.userRecord.findMany({
|
||||
where: {
|
||||
created_on: {
|
||||
gte: prevStartDate,
|
||||
lt: prevEndDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
created_on: true,
|
||||
},
|
||||
});
|
||||
const prevUrls = await prisma.userUrl.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: prevStartDate,
|
||||
lt: prevEndDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
const prevEmails = await prisma.userEmail.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: prevStartDate,
|
||||
lt: prevEndDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
const prevInbox = await prisma.forwardEmail.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: prevStartDate,
|
||||
lt: prevEndDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
const prevSends = await prisma.userSendEmail.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: prevStartDate,
|
||||
lt: prevEndDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Process current period data
|
||||
const userCountByDate: { [date: string]: number } = {};
|
||||
const recordCountByDate: { [date: string]: number } = {};
|
||||
const urlCountByDate: { [date: string]: number } = {};
|
||||
const emailCountByDate: { [date: string]: number } = {};
|
||||
const inboxCountByDate: { [date: string]: number } = {};
|
||||
const sendCountByDate: { [date: string]: number } = {};
|
||||
|
||||
users.forEach((user) => {
|
||||
const date = user.createdAt!.toISOString().split("T")[0];
|
||||
@@ -113,6 +205,10 @@ export async function GET(req: Request) {
|
||||
const date = email.createdAt.toISOString().split("T")[0];
|
||||
inboxCountByDate[date] = (inboxCountByDate[date] || 0) + 1;
|
||||
});
|
||||
sends.forEach((send) => {
|
||||
const date = send.createdAt.toISOString().split("T")[0];
|
||||
sendCountByDate[date] = (sendCountByDate[date] || 0) + 1;
|
||||
});
|
||||
|
||||
const allDates = Array.from(
|
||||
new Set([
|
||||
@@ -121,6 +217,7 @@ export async function GET(req: Request) {
|
||||
...Object.keys(urlCountByDate),
|
||||
...Object.keys(emailCountByDate),
|
||||
...Object.keys(inboxCountByDate),
|
||||
...Object.keys(sendCountByDate),
|
||||
]),
|
||||
);
|
||||
const combinedData = allDates.map((date) => ({
|
||||
@@ -130,6 +227,7 @@ export async function GET(req: Request) {
|
||||
users: userCountByDate[date] || 0,
|
||||
emails: emailCountByDate[date] || 0,
|
||||
inbox: inboxCountByDate[date] || 0,
|
||||
sends: sendCountByDate[date] || 0,
|
||||
}));
|
||||
|
||||
const total = {
|
||||
@@ -138,9 +236,64 @@ export async function GET(req: Request) {
|
||||
users: combinedData.reduce((acc, curr) => acc + curr.users, 0),
|
||||
emails: combinedData.reduce((acc, curr) => acc + curr.emails, 0),
|
||||
inbox: combinedData.reduce((acc, curr) => acc + curr.inbox, 0),
|
||||
sends: combinedData.reduce((acc, curr) => acc + curr.sends, 0),
|
||||
};
|
||||
|
||||
return Response.json({ list: combinedData.reverse(), total });
|
||||
// Calculate totals for previous period
|
||||
const prevTotal = {
|
||||
records: prevRecords.length,
|
||||
urls: prevUrls.length,
|
||||
users: prevUsers.length,
|
||||
emails: prevEmails.length,
|
||||
inbox: prevInbox.length,
|
||||
sends: prevSends.length,
|
||||
};
|
||||
|
||||
// Calculate growth rates
|
||||
const growthRates = {
|
||||
records:
|
||||
prevTotal.records === 0
|
||||
? total.records > 0
|
||||
? 100
|
||||
: 0
|
||||
: ((total.records - prevTotal.records) / prevTotal.records) * 100,
|
||||
urls:
|
||||
prevTotal.urls === 0
|
||||
? total.urls > 0
|
||||
? 100
|
||||
: 0
|
||||
: ((total.urls - prevTotal.urls) / prevTotal.urls) * 100,
|
||||
users:
|
||||
prevTotal.users === 0
|
||||
? total.users > 0
|
||||
? 100
|
||||
: 0
|
||||
: ((total.users - prevTotal.users) / prevTotal.users) * 100,
|
||||
emails:
|
||||
prevTotal.emails === 0
|
||||
? total.emails > 0
|
||||
? 100
|
||||
: 0
|
||||
: ((total.emails - prevTotal.emails) / prevTotal.emails) * 100,
|
||||
inbox:
|
||||
prevTotal.inbox === 0
|
||||
? total.inbox > 0
|
||||
? 100
|
||||
: 0
|
||||
: ((total.inbox - prevTotal.inbox) / prevTotal.inbox) * 100,
|
||||
sends:
|
||||
prevTotal.sends === 0
|
||||
? total.sends > 0
|
||||
? 100
|
||||
: 0
|
||||
: ((total.sends - prevTotal.sends) / prevTotal.sends) * 100,
|
||||
};
|
||||
|
||||
return Response.json({
|
||||
list: combinedData.reverse(),
|
||||
total,
|
||||
growthRates,
|
||||
});
|
||||
} catch (error) {
|
||||
return Response.json({ statusText: "Server error" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { getUserSendEmailList } from "@/lib/dto/email";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const user = checkUserStatus(await getCurrentUser());
|
||||
if (user instanceof Response) return user;
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const page = parseInt(searchParams.get("page") || "1", 10);
|
||||
const size = parseInt(searchParams.get("size") || "10", 10);
|
||||
const search = searchParams.get("search") || "";
|
||||
const all = searchParams.get("all") || "false";
|
||||
|
||||
const data = await getUserSendEmailList(
|
||||
user.id,
|
||||
user.role === "ADMIN" && all === "true",
|
||||
page,
|
||||
size,
|
||||
search,
|
||||
);
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
return NextResponse.json("Internal server error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { DATE_DIMENSION_ENUMS } from "@/lib/enums";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { cn, fetcher } from "@/lib/utils";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -54,6 +54,10 @@ const chartConfig = {
|
||||
label: "Inbox",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
sends: {
|
||||
label: "Sends",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function InteractiveBarChart() {
|
||||
@@ -69,6 +73,7 @@ export function InteractiveBarChart() {
|
||||
users: number;
|
||||
emails: number;
|
||||
inbox: number;
|
||||
sends: number;
|
||||
date: string;
|
||||
},
|
||||
];
|
||||
@@ -78,6 +83,15 @@ export function InteractiveBarChart() {
|
||||
users: number;
|
||||
emails: number;
|
||||
inbox: number;
|
||||
sends: number;
|
||||
};
|
||||
growthRates: {
|
||||
records: number;
|
||||
urls: number;
|
||||
users: number;
|
||||
emails: number;
|
||||
inbox: number;
|
||||
sends: number;
|
||||
};
|
||||
}>(`api/admin?range=${timeRange}`, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
@@ -94,7 +108,7 @@ export function InteractiveBarChart() {
|
||||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
|
||||
<CardTitle>Data Increase</CardTitle>
|
||||
<CardDescription>
|
||||
Showing data increase in
|
||||
Showing data increase in:
|
||||
<Select
|
||||
onValueChange={(value: string) => {
|
||||
setTimeRange(value);
|
||||
@@ -117,24 +131,41 @@ export function InteractiveBarChart() {
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
{["users", "records", "urls", "emails", "inbox"].map((key) => {
|
||||
const chart = key as keyof typeof chartConfig;
|
||||
return (
|
||||
<button
|
||||
key={chart}
|
||||
data-active={activeChart === chart}
|
||||
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-l border-t p-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:p-6"
|
||||
onClick={() => setActiveChart(chart)}
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{chartConfig[chart].label}
|
||||
</span>
|
||||
<span className="text-lg font-bold leading-none sm:text-3xl">
|
||||
{data.total[key as keyof typeof data.total].toLocaleString()}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{["users", "records", "urls", "emails", "inbox", "sends"].map(
|
||||
(key) => {
|
||||
const chart = key as keyof typeof chartConfig;
|
||||
const growthRate =
|
||||
data.growthRates[key as keyof typeof data.growthRates];
|
||||
return (
|
||||
<button
|
||||
key={chart}
|
||||
data-active={activeChart === chart}
|
||||
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-l border-t p-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:p-6"
|
||||
onClick={() => setActiveChart(chart)}
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{chartConfig[chart].label}
|
||||
</span>
|
||||
<span className="text-lg font-bold leading-none sm:text-3xl">
|
||||
{data.total[
|
||||
key as keyof typeof data.total
|
||||
].toLocaleString()}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded px-1.5 py-1 text-xs font-semibold leading-none",
|
||||
growthRate > 0 && "bg-green-200 text-green-700",
|
||||
growthRate < 0 && "bg-red-200 text-red-700",
|
||||
growthRate === 0 && "bg-neutral-100 text-neutral-700",
|
||||
)}
|
||||
>
|
||||
{growthRate >= 0 ? "+" : ""}
|
||||
{growthRate.toFixed(1)}%
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 sm:p-6">
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "../ui/tooltip";
|
||||
import { SendEmailModal } from "./SendEmailModal";
|
||||
import SendsEmailList from "./SendsEmailList";
|
||||
|
||||
interface EmailSidebarProps {
|
||||
user: User;
|
||||
@@ -85,6 +86,8 @@ export default function EmailSidebar({
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [onlyUnread, setOnlyUnread] = useState(false);
|
||||
|
||||
const [showSendsModal, setShowSendsModal] = useState(false);
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
const { data, isLoading, error, mutate } = useSWR<{
|
||||
@@ -355,7 +358,16 @@ export default function EmailSidebar({
|
||||
</div>
|
||||
|
||||
{/* Sent Emails */}
|
||||
<div className="flex cursor-pointer flex-col items-center gap-1 rounded-md bg-neutral-100 px-1 pb-1 pt-2 transition-colors hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-gray-700">
|
||||
<div
|
||||
onClick={() => setShowSendsModal(!showSendsModal)}
|
||||
className={cn(
|
||||
"flex cursor-pointer flex-col items-center gap-1 rounded-md bg-neutral-100 px-1 pb-1 pt-2 transition-colors hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-gray-700",
|
||||
{
|
||||
"bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-gray-700":
|
||||
showSendsModal,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icons.send className="size-3" />
|
||||
<p className="line-clamp-1 text-start font-medium">
|
||||
@@ -538,6 +550,16 @@ export default function EmailSidebar({
|
||||
/>
|
||||
)}
|
||||
|
||||
{showSendsModal && (
|
||||
<Modal
|
||||
className="md:max-w-2xl"
|
||||
showModal={showSendsModal}
|
||||
setShowModal={setShowSendsModal}
|
||||
>
|
||||
<SendsEmailList isAdminModel={isAdminModel} />
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* 创建\编辑邮箱的 Modal */}
|
||||
{showEmailModal && (
|
||||
<Modal showModal={showEmailModal} setShowModal={setShowEmailModal}>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { UserSendEmail } from "@prisma/client";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { cn, fetcher, formatDate, htmlToText } from "@/lib/utils";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
import { PaginationWrapper } from "../shared/pagination";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "../ui/collapsible";
|
||||
|
||||
export default function SendsEmailList({
|
||||
isAdminModel,
|
||||
}: {
|
||||
isAdminModel: boolean;
|
||||
}) {
|
||||
const pageSize = 10;
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const { data, isLoading, error } = useSWR<{
|
||||
list: UserSendEmail[];
|
||||
total: number;
|
||||
}>(
|
||||
`/api/email/send/list?page=${currentPage}&size=${pageSize}&search=${encodeURIComponent(searchQuery)}&all=${isAdminModel}`,
|
||||
fetcher,
|
||||
{ dedupingInterval: 5000 },
|
||||
);
|
||||
|
||||
const totalPages = data ? Math.ceil(data.total / pageSize) : 1;
|
||||
|
||||
const debouncedSearch = useCallback((value: string) => {
|
||||
setSearchQuery(value);
|
||||
setCurrentPage(1); // Reset to first page on search
|
||||
}, []);
|
||||
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
debouncedSearch(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-4xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Sent Emails</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-2 flex items-center justify-between gap-4">
|
||||
<Input
|
||||
placeholder="Search by send to email..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearch}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="space-y-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-16 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center text-red-500">
|
||||
Failed to load emails. Please try again.
|
||||
</div>
|
||||
) : !data || data.list.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground">
|
||||
No emails found.
|
||||
</div>
|
||||
) : (
|
||||
<div className="scrollbar-hidden max-h-[50vh] overflow-y-auto">
|
||||
<div className="space-y-1">
|
||||
{data.list.map((email) => (
|
||||
<Collapsible
|
||||
className="w-full rounded-lg border bg-white p-2 transition-all duration-200 hover:bg-gray-50"
|
||||
key={email.id}
|
||||
>
|
||||
<CollapsibleTrigger className="w-full">
|
||||
<div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div className="text-start">
|
||||
<div className="truncate text-xs font-semibold text-neutral-600 dark:text-neutral-200">
|
||||
<strong>From:</strong> {email.from}
|
||||
</div>
|
||||
<div className="truncate text-xs font-semibold text-neutral-600 dark:text-neutral-200">
|
||||
<strong>To:</strong> {email.to}
|
||||
</div>
|
||||
<div className="text-xs text-neutral-600 dark:text-neutral-400">
|
||||
<strong>Date:</strong>{" "}
|
||||
{formatDate(email.createdAt as any)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-start">
|
||||
<p className="line-clamp-1 truncate text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||
{email.subject || "No subject"}
|
||||
</p>
|
||||
<p className="line-clamp-2 break-all text-xs text-neutral-500">
|
||||
{htmlToText(email.html || "")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-2 animate-fade-in break-all text-sm text-neutral-500">
|
||||
{htmlToText(email.html || "")}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
))}
|
||||
</div>
|
||||
{data && totalPages > 1 && (
|
||||
<PaginationWrapper
|
||||
className="m-0 mt-6 scale-75 justify-center"
|
||||
total={totalPages}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -594,3 +594,47 @@ export async function getUserSendEmailCount(userId: string, admin: boolean) {
|
||||
}
|
||||
return prisma.userSendEmail.count({ where: { userId } });
|
||||
}
|
||||
|
||||
export async function getUserSendEmailList(
|
||||
userId: string,
|
||||
admin: boolean,
|
||||
page: number,
|
||||
size: number,
|
||||
search: string,
|
||||
) {
|
||||
const select = {
|
||||
from: true,
|
||||
to: true,
|
||||
subject: true,
|
||||
html: true,
|
||||
createdAt: true,
|
||||
};
|
||||
let where: any = {};
|
||||
|
||||
if (admin) {
|
||||
where = {
|
||||
to: { contains: search, mode: "insensitive" },
|
||||
};
|
||||
} else {
|
||||
where = {
|
||||
userId,
|
||||
to: { contains: search, mode: "insensitive" },
|
||||
};
|
||||
}
|
||||
|
||||
const listPromise = prisma.userSendEmail.findMany({
|
||||
where,
|
||||
select,
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
});
|
||||
const totalPromise = prisma.userSendEmail.count({
|
||||
where,
|
||||
});
|
||||
|
||||
const [list, total] = await Promise.all([listPromise, totalPromise]);
|
||||
return { list, total };
|
||||
}
|
||||
|
||||
@@ -277,4 +277,19 @@ CREATE TABLE "user_send_emails"
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
);
|
||||
|
||||
CREATE INDEX "user_send_emails_userId_idx" ON "user_send_emails" ("userId");
|
||||
CREATE INDEX "user_send_emails_userId_idx" ON "user_send_emails" ("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "users_createdAt_idx" ON "users"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_records_created_on_idx" ON "user_records"("created_on");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_urls_createdAt_idx" ON "user_urls"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_emails_createdAt_idx" ON "user_emails"("createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "forward_emails_createdAt_idx" ON "forward_emails"("createdAt");
|
||||
@@ -70,6 +70,7 @@ model User {
|
||||
UserEmail UserEmail[]
|
||||
UserSendEmail UserSendEmail[]
|
||||
|
||||
@@index([createdAt])
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
@@ -102,7 +103,7 @@ model UserRecord {
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([userId, created_on])
|
||||
@@map(name: "user_records")
|
||||
}
|
||||
|
||||
@@ -123,7 +124,7 @@ model UserUrl {
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
UrlMeta UrlMeta[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([userId, createdAt])
|
||||
@@map(name: "user_urls")
|
||||
}
|
||||
|
||||
@@ -205,6 +206,7 @@ model ForwardEmail {
|
||||
|
||||
UserEmail UserEmail? @relation(fields: [to], references: [emailAddress])
|
||||
|
||||
@@index([createdAt])
|
||||
@@map(name: "forward_emails")
|
||||
}
|
||||
|
||||
@@ -221,6 +223,7 @@ model UserEmail {
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
@@index([createdAt])
|
||||
@@map("user_emails")
|
||||
}
|
||||
|
||||
@@ -241,5 +244,6 @@ model UserSendEmail {
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([createdAt])
|
||||
@@map("user_send_emails")
|
||||
}
|
||||
|
||||
+101
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user