feat: speed up admin clone import and enable Cache Components
- Clone import: defer FurnitureData.json writes and append all entries in a single batched write instead of one read-modify-write per item, removing the main serialization bottleneck for large batches. - Clone import: raise SSE batch concurrency cap from 5 to 10 and bump the clone client/route default from 2 to 6. - Add a flush hook to runSseBatch so callers can batch deferred work before batch_complete is emitted, and surface flush errors as an error event. - Enable Next.js Cache Components (instant: false opt-out) and silence the related build warnings in next.config.ts. - Switch isomorphic-dompurify to dompurify and refresh dependencies.
This commit is contained in:
1 parent
ac9b3e3cb5
commit
dc8fb8a6ed
254 files changed
+910
-551
No files matched your search
@@ -7,6 +7,10 @@ import {
|
||||
fetchSourceFurnidata,
|
||||
} from "@/lib/services/clone-import";
|
||||
import { getSource } from "@/lib/services/clone-sources";
|
||||
import {
|
||||
appendFurniEntriesBatch,
|
||||
appendFurniEntry,
|
||||
} from "@/lib/services/furni-data";
|
||||
import { runSseBatch } from "@/lib/services/import/core/sse-batch";
|
||||
|
||||
interface BatchItem {
|
||||
@@ -53,9 +57,17 @@ export const POST = withAdmin(
|
||||
if (items.length > 500)
|
||||
return apiError("Too many classnames (max 500)", 400);
|
||||
|
||||
// FurnitureData entries for successfully cloned items are appended once
|
||||
// at the end (single read-modify-write instead of one per item), which
|
||||
// removes the main serialization bottleneck when cloning hundreds of items.
|
||||
const deferredEntries: Array<{
|
||||
entry: Record<string, unknown>;
|
||||
itemType: string;
|
||||
}> = [];
|
||||
|
||||
return runSseBatch<BatchItem>({
|
||||
items,
|
||||
concurrency: body.concurrency || 2,
|
||||
concurrency: body.concurrency || 6,
|
||||
signal: request.signal,
|
||||
labelOf: (it) => it.classname,
|
||||
worker: async (it, _index, report) => {
|
||||
@@ -71,9 +83,13 @@ export const POST = withAdmin(
|
||||
source,
|
||||
entry,
|
||||
onProgress: report,
|
||||
deferFurniData: true,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
if (result.furniDataEntry) {
|
||||
deferredEntries.push(result.furniDataEntry);
|
||||
}
|
||||
logAudit({
|
||||
userId: ctx.session.user.id,
|
||||
action: "clone_furni",
|
||||
@@ -93,6 +109,24 @@ export const POST = withAdmin(
|
||||
error: result.error,
|
||||
};
|
||||
},
|
||||
flush: async () => {
|
||||
if (deferredEntries.length === 0) return;
|
||||
try {
|
||||
await appendFurniEntriesBatch(deferredEntries);
|
||||
} catch (err) {
|
||||
// Fallback so partial data survives: write each entry individually.
|
||||
for (const { entry, itemType } of deferredEntries) {
|
||||
try {
|
||||
await appendFurniEntry(entry, itemType);
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`FurnitureData batch write failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
fetchSourceFurnidata,
|
||||
} from "@/lib/services/clone-import";
|
||||
import { listSources } from "@/lib/services/clone-sources";
|
||||
import {
|
||||
appendFurniEntriesBatch,
|
||||
appendFurniEntry,
|
||||
} from "@/lib/services/furni-data";
|
||||
import { runSseBatch } from "@/lib/services/import/core/sse-batch";
|
||||
|
||||
function sseEvent(data: unknown): string {
|
||||
@@ -95,9 +99,16 @@ export const POST = withAdmin(
|
||||
);
|
||||
}
|
||||
|
||||
// FurnitureData entries are appended once at the end (single write) rather
|
||||
// than once per item, removing the main serialization bottleneck.
|
||||
const deferredEntries: Array<{
|
||||
entry: Record<string, unknown>;
|
||||
itemType: string;
|
||||
}> = [];
|
||||
|
||||
return runSseBatch({
|
||||
items: allItems,
|
||||
concurrency: 2,
|
||||
concurrency: 6,
|
||||
signal: request.signal,
|
||||
labelOf: (it) => `${it.sourceName}/${it.classname}`,
|
||||
worker: async (it, _index, report) => {
|
||||
@@ -129,9 +140,13 @@ export const POST = withAdmin(
|
||||
source,
|
||||
entry,
|
||||
onProgress: report,
|
||||
deferFurniData: true,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
if (result.furniDataEntry) {
|
||||
deferredEntries.push(result.furniDataEntry);
|
||||
}
|
||||
logAudit({
|
||||
userId: ctx.session.user.id,
|
||||
action: "auto_sync_furni",
|
||||
@@ -151,6 +166,23 @@ export const POST = withAdmin(
|
||||
error: result.error,
|
||||
};
|
||||
},
|
||||
flush: async () => {
|
||||
if (deferredEntries.length === 0) return;
|
||||
try {
|
||||
await appendFurniEntriesBatch(deferredEntries);
|
||||
} catch (err) {
|
||||
for (const { entry, itemType } of deferredEntries) {
|
||||
try {
|
||||
await appendFurniEntry(entry, itemType);
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`FurnitureData batch write failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -11,8 +11,6 @@ import { apiError, apiJson } from "@/lib/api";
|
||||
import { bearerUserId } from "@/lib/api-auth";
|
||||
import { db, WebsiteArticleComments, WebsiteArticles } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ slug: string }> },
|
||||
|
||||
@@ -2,8 +2,6 @@ import { eq } from "drizzle-orm";
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { db, WebsiteArticles } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* GET /api/articles/:slug — single website_article by slug, including the
|
||||
* fullStory body. Returns { error } (404) when the slug is unknown.
|
||||
|
||||
@@ -2,8 +2,6 @@ import { count, desc } from "drizzle-orm";
|
||||
import { apiJson, pagination } from "@/lib/api";
|
||||
import { db, WebsiteArticles } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* GET /api/articles — paginated list of website_articles, newest first.
|
||||
* Mirrors the /news page query. Returns list cards (shortStory only, never
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { connection } from "next/server";
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { bearerUserId } from "@/lib/api-auth";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db, User, UsersSettings } from "@/lib/db";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type BadgeRarityKey =
|
||||
| "common"
|
||||
| "rare"
|
||||
@@ -301,6 +300,7 @@ async function loadRarityBoard(
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
await connection();
|
||||
try {
|
||||
let userId: number | null = await bearerUserId(req);
|
||||
if (!userId) {
|
||||
|
||||
@@ -6,8 +6,6 @@ import { count, eq } from "drizzle-orm";
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { db, Guilds, GuildsMembers } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
|
||||
@@ -6,8 +6,6 @@ import { count, desc } from "drizzle-orm";
|
||||
import { apiJson, pagination } from "@/lib/api";
|
||||
import { db, Guilds } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const sp = new URL(req.url).searchParams;
|
||||
const { page, perPage, skip, take } = pagination(sp);
|
||||
|
||||
@@ -7,8 +7,6 @@ import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
import { redis } from "@/lib/redis";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Ops health probe: database reachability, Redis (when configured), emulator
|
||||
* RCON, SMTP (when configured), and runtime info. Returns HTTP 200 always
|
||||
|
||||
@@ -5,8 +5,6 @@ import { db, User, WebsiteArticles } from "@/lib/db";
|
||||
import { resolveHotelName } from "@/lib/hotel-name";
|
||||
import { apiCacheKey, redisCache } from "@/lib/redis-cache";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* GET /api/home — combined landing payload: the latest 4 website_articles and
|
||||
* the current online player count (users.online === "1"). Mirrors the queries
|
||||
|
||||
@@ -5,7 +5,6 @@ import { db, User, UsersCurrency } from "@/lib/db";
|
||||
// Public REST API — leaderboard. Mirrors src/app/leaderboard/page.tsx.
|
||||
// AtomCMS-faithful currency type ids (see UsersCurrency):
|
||||
// Credits = -1 (lives on users.credits), Duckets = 0, Diamonds = 5.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type LeaderboardType = "credits" | "diamonds" | "duckets";
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ import { auth } from "@/lib/auth";
|
||||
import { sessionUserId } from "@/lib/auth/session-user";
|
||||
import { db, User } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
const session = await auth();
|
||||
const id = sessionUserId(session?.user?.id);
|
||||
|
||||
@@ -16,8 +16,6 @@ import { personalTokenScope } from "@/lib/auth/personal-token-scope";
|
||||
import { sessionUserId } from "@/lib/auth/session-user";
|
||||
import { db, PersonalAccessTokens } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
async function currentUserId(): Promise<number | null> {
|
||||
const session = await auth();
|
||||
return sessionUserId(session?.user?.id);
|
||||
|
||||
@@ -4,8 +4,6 @@ import path from "node:path";
|
||||
import { NextResponse } from "next/server";
|
||||
import { MEDIA_ROOT, resolveMediaPath } from "@/lib/media-storage";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const ALLOWED_EXT = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"];
|
||||
|
||||
export async function GET(
|
||||
|
||||
@@ -3,8 +3,6 @@ import { resolve } from "node:path";
|
||||
import { NextResponse } from "next/server";
|
||||
import { MEDIA_ROOT } from "@/lib/media-storage";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
const dir = MEDIA_ROOT;
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
|
||||
@@ -8,8 +8,6 @@ import { apiJson } from "@/lib/api";
|
||||
import { cached } from "@/lib/cache";
|
||||
import { db, User } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
const result = await cached("online_count", 10_000, async () => {
|
||||
|
||||
@@ -8,8 +8,6 @@ import { apiJson } from "@/lib/api";
|
||||
import { cached } from "@/lib/cache";
|
||||
import { db, User } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
const users = await cached("online_users", 10_000, async () =>
|
||||
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { currencyDb, sendCurrency } from "@/lib/services/send-currency";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* POST /api/paypal/capture — capture an approved order and credit the buyer.
|
||||
* Body: { orderId: string } (PayPal order id from /api/paypal/create).
|
||||
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
} from "@/lib/services/paypal";
|
||||
import { recordCreatedTopup } from "@/lib/services/paypal-topup";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const MIN_AMOUNT = 1;
|
||||
const MAX_AMOUNT = 500;
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import { count, desc } from "drizzle-orm";
|
||||
import { apiJson, pagination } from "@/lib/api";
|
||||
import { CameraWeb, db } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* GET /api/photos — recent community photos (camera_web), newest first.
|
||||
* Mirrors the /photos page query. Returns id, userId, url and timestamp plus
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function truthy(value: string | null | undefined): boolean {
|
||||
if (value === undefined || value === null) return false;
|
||||
const s = value.trim().toLowerCase();
|
||||
|
||||
@@ -4,7 +4,6 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
// Radio player config: the subset of radio_* website_settings the front-end
|
||||
// player needs (stream URL, name, autoplay, enabled, widget visibility) as a
|
||||
// flat { key: value } map. No secrets live among these keys.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const CONFIG_KEYS = [
|
||||
"radio_enabled",
|
||||
|
||||
@@ -6,7 +6,6 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
// Current on-air DJ. The DJ is set manually via the radio_current_dj_id setting
|
||||
// (Manual DJ user ID). When set, resolve that user's username/look; otherwise
|
||||
// there is no DJ on air. Mirrors the AtomCMS radio "on air" widget.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
// Minimal radio_* settings an external page needs to embed the player: stream
|
||||
// URL, display name, whether the radio is enabled, and autoplay. Returned as a
|
||||
// flat { key: value } map. None of these keys are secrets.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const EMBED_KEYS = [
|
||||
"radio_enabled",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
// Proxy for the configured radio "listeners" provider. The provider URL is
|
||||
// stored in radio_listeners_api_url; we fetch it server-side with a short, hard
|
||||
// timeout and reduce the response to a single listener count.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const FETCH_TIMEOUT_MS = 4000;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
// Proxy for the configured radio "now playing" provider. The provider URL is
|
||||
// stored in radio_now_playing_api_url; we fetch it server-side (never exposing
|
||||
// the URL or any provider key to the browser) with a short, hard timeout.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const FETCH_TIMEOUT_MS = 4000;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { db, RadioListenerPoints, User } from "@/lib/db";
|
||||
// Radio listener-points leaderboard: the top 20 users by total points, summed
|
||||
// across radio_listener_points and joined to users for username/look. Public
|
||||
// (no auth) — mirrors the AtomCMS radio leaderboard widget.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { db, RadioListenerPoints } from "@/lib/db";
|
||||
|
||||
// The Bearer-authed user's total radio listener points: the sum of all
|
||||
// radio_listener_points.points rows for that user_id.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const uid = await bearerUserId(req);
|
||||
|
||||
@@ -6,7 +6,6 @@ import { db, RadioShouts, User } from "@/lib/db";
|
||||
// Latest 50 radio shouts with their author's username/look resolved. Mirrors the
|
||||
// query behind the public /radio/shouts page (radio_shouts ordered by created_at
|
||||
// desc, then joined to users by user_id).
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// Max shout length (radio_shouts.message is TEXT; cap to keep posts sane).
|
||||
const MAX_MESSAGE_LENGTH = 255;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { fetchListeners, fetchNowPlaying } from "@/lib/services/radio";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Server-Sent Events stream of live radio state (AtomCMS's radio SSE endpoint).
|
||||
* Pushes { nowPlaying, listeners } every ~10s so players/widgets get real-time
|
||||
|
||||
@@ -3,7 +3,6 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
// Public website settings as a flat { key: value } map. Secrets are stripped so
|
||||
// this can be served to the game client / external integrations.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// Any key containing one of these tokens is considered sensitive and never
|
||||
// exposed through the public API (mirrors AtomCMS's public settings filtering).
|
||||
|
||||
@@ -4,8 +4,6 @@ import { asc } from "drizzle-orm";
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { db, WebsiteShopCategories } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
const data = await db
|
||||
|
||||
@@ -5,8 +5,6 @@ import { asc, count, eq } from "drizzle-orm";
|
||||
import { apiError, apiJson, pagination, positiveBigInt } from "@/lib/api";
|
||||
import { db, WebsiteShopArticles } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const sp = new URL(req.url).searchParams;
|
||||
const { page, perPage, skip, take } = pagination(sp);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
// Public REST API — staff list. Mirrors src/app/staff/page.tsx: users whose
|
||||
// rank is >= min_staff_rank (default 7). Only safe fields are exposed.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { db, WebsiteTeams } from "@/lib/db";
|
||||
|
||||
// Public REST API — website teams (staff ranks). Mirrors src/app/staff/page.tsx:
|
||||
// visible ranks (hiddenRank=false) ordered by id.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
import { rateLimit } from "@/lib/rate-limit";
|
||||
import { createOwnedTicketReply } from "@/lib/services/ticket-replies";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// POST /api/tickets/:id/reply body: { content }
|
||||
export async function POST(
|
||||
req: Request,
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
WebsiteHelpCenterTickets,
|
||||
} from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// GET /api/tickets/:id
|
||||
export async function GET(
|
||||
req: Request,
|
||||
|
||||
@@ -11,8 +11,6 @@ import { bearerUserId } from "@/lib/api-auth";
|
||||
import { db, WebsiteHelpCenterTickets } from "@/lib/db";
|
||||
import { rateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// GET /api/tickets — the authed user's tickets (newest first).
|
||||
export async function GET(req: Request) {
|
||||
const uid = await bearerUserId(req);
|
||||
|
||||
@@ -10,8 +10,6 @@ import { issueToken } from "@/lib/api-auth";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { sessionUserId } from "@/lib/auth/session-user";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
const id = sessionUserId(session?.user?.id);
|
||||
|
||||
@@ -9,8 +9,6 @@ import { eq } from "drizzle-orm";
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { db, User } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ username: string }> },
|
||||
|
||||
@@ -5,8 +5,6 @@ import { eq } from "drizzle-orm";
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
|
||||
@@ -5,8 +5,6 @@ import { asc } from "drizzle-orm";
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { db, WebsiteRareValueCategories } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
const data = await db
|
||||
|
||||
@@ -5,8 +5,6 @@ import { asc, count, eq } from "drizzle-orm";
|
||||
import { apiError, apiJson, pagination, positiveBigInt } from "@/lib/api";
|
||||
import { db, WebsiteRareValues } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const sp = new URL(req.url).searchParams;
|
||||
const { page, perPage, skip, take } = pagination(sp);
|
||||
|
||||
Reference in new issue
Block a user