feat(api): Add cache revalidation after specific API operations

This commit is contained in:
Fatih Kadir Akın
2025-12-20 12:30:14 +03:00
parent f95056fabe
commit aea14c9071
9 changed files with 173 additions and 69 deletions
+1 -2
View File
@@ -2,8 +2,7 @@ name: Update Contributors
on:
schedule:
# Run every hour
- cron: '0 * * * *'
- cron: '0 3 * * *'
workflow_dispatch: # Allow manual trigger
jobs:
+11
View File
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { revalidatePrompts } from "@/lib/cache";
import fs from "fs/promises";
import path from "path";
@@ -246,6 +247,11 @@ export async function POST(request: NextRequest) {
}
}
// Revalidate prompts cache after import
if (imported > 0) {
revalidatePrompts();
}
return NextResponse.json({
success: true,
imported,
@@ -291,6 +297,11 @@ export async function DELETE(request: NextRequest) {
},
});
// Revalidate prompts cache after deletion
if (result.count > 0) {
revalidatePrompts();
}
return NextResponse.json({
success: true,
deleted: result.count,
+4
View File
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { revalidatePrompts } from "@/lib/cache";
// DELETE - Hard delete a prompt (admin only)
export async function DELETE(
@@ -54,6 +55,9 @@ export async function DELETE(
where: { id },
});
// Revalidate prompts cache
revalidatePrompts();
return NextResponse.json({
success: true,
message: "Prompt deleted successfully",
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { revalidatePrompts } from "@/lib/cache";
// POST /api/prompts/[id]/feature - Toggle featured status (admin only)
export async function POST(
@@ -45,6 +46,9 @@ export async function POST(
},
});
// Revalidate prompts cache
revalidatePrompts();
return NextResponse.json({
success: true,
isFeatured: updatedPrompt.isFeatured,
+9
View File
@@ -5,6 +5,7 @@ import { db } from "@/lib/db";
import { generatePromptEmbedding } from "@/lib/ai/embeddings";
import { generatePromptSlug } from "@/lib/slug";
import { checkPromptQuality } from "@/lib/ai/quality-check";
import { revalidatePrompts } from "@/lib/cache";
const updatePromptSchema = z.object({
title: z.string().min(1).max(200).optional(),
@@ -233,12 +234,17 @@ export async function PATCH(
},
});
console.log(`[Quality Check] Prompt ${id} delisted successfully`);
// Revalidate cache after auto-delist
revalidatePrompts();
}
}).catch((err) => {
console.error("[Quality Check] Failed to run quality check for prompt:", id, err);
});
}
// Revalidate prompts cache
revalidatePrompts();
return NextResponse.json(prompt);
} catch (error) {
console.error("Update prompt error:", error);
@@ -317,6 +323,9 @@ export async function DELETE(
data: { deletedAt: new Date() },
});
// Revalidate prompts cache
revalidatePrompts();
return NextResponse.json({
success: true,
message: isOwner && isDelisted
+4
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { revalidatePrompts } from "@/lib/cache";
// Toggle unlist status (admin only)
export async function POST(
@@ -50,6 +51,9 @@ export async function POST(
},
});
// Revalidate prompts cache
revalidatePrompts();
return NextResponse.json({
success: true,
isUnlisted: newUnlistedStatus,
+6
View File
@@ -7,6 +7,7 @@ import { generatePromptEmbedding } from "@/lib/ai/embeddings";
import { generatePromptSlug } from "@/lib/slug";
import { checkPromptQuality } from "@/lib/ai/quality-check";
import { isSimilarContent, normalizeContent } from "@/lib/similarity";
import { revalidatePrompts } from "@/lib/cache";
const promptSchema = z.object({
title: z.string().min(1).max(200),
@@ -234,6 +235,8 @@ export async function POST(request: Request) {
},
});
console.log(`[Quality Check] Prompt ${prompt.id} delisted successfully`);
// Revalidate cache after auto-delist
revalidatePrompts();
}
}).catch((err) => {
console.error("[Quality Check] Failed to run quality check for prompt:", prompt.id, err);
@@ -242,6 +245,9 @@ export async function POST(request: Request) {
console.log(`[Quality Check] Skipped - prompt ${prompt.id} is private`);
}
// Revalidate prompts cache
revalidatePrompts();
return NextResponse.json(prompt);
} catch (error) {
console.error("Create prompt error:", error);
+105 -67
View File
@@ -2,6 +2,7 @@ import { Metadata } from "next";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { Plus } from "lucide-react";
import { unstable_cache } from "next/cache";
import { Button } from "@/components/ui/button";
import { InfinitePromptList } from "@/components/prompts/infinite-prompt-list";
import { PromptFilters } from "@/components/prompts/prompt-filters";
@@ -11,6 +12,7 @@ import { McpServerPopup, McpIcon } from "@/components/mcp/mcp-server-popup";
import { db } from "@/lib/db";
import { isAISearchEnabled, semanticSearch } from "@/lib/ai/embeddings";
import { isAIGenerationEnabled } from "@/lib/ai/generation";
import { CACHE_TAGS } from "@/lib/cache";
import config from "@/../prompts.config";
export const metadata: Metadata = {
@@ -18,6 +20,100 @@ export const metadata: Metadata = {
description: "Browse and discover AI prompts",
};
// Cached query for categories
const getCachedCategories = unstable_cache(
async () => {
return db.category.findMany({
orderBy: [{ order: "asc" }, { name: "asc" }],
select: {
id: true,
name: true,
slug: true,
parentId: true,
},
});
},
["prompts-categories"],
{ tags: [CACHE_TAGS.CATEGORIES, CACHE_TAGS.PROMPTS], revalidate: 3600 }
);
// Cached query for tags
const getCachedTags = unstable_cache(
async () => {
return db.tag.findMany({
orderBy: { name: "asc" },
});
},
["prompts-tags"],
{ tags: [CACHE_TAGS.TAGS, CACHE_TAGS.PROMPTS], revalidate: 3600 }
);
// Cached query for prompts list
const getCachedPrompts = unstable_cache(
async (
where: Record<string, unknown>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
orderBy: any,
perPage: number
) => {
const [promptsRaw, totalCount] = await Promise.all([
db.prompt.findMany({
where,
orderBy,
skip: 0,
take: perPage,
include: {
author: {
select: {
id: true,
name: true,
username: true,
avatar: true,
},
},
category: {
include: {
parent: {
select: { id: true, name: true, slug: true },
},
},
},
tags: {
include: {
tag: true,
},
},
contributors: {
select: {
id: true,
username: true,
name: true,
avatar: true,
},
},
_count: {
select: { votes: true, contributors: true },
},
},
}),
db.prompt.count({ where }),
]);
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
prompts: promptsRaw.map((p: any) => ({
...p,
voteCount: p._count.votes,
contributorCount: p._count.contributors,
contributors: p.contributors,
})),
total: totalCount,
};
},
["prompts-list"],
{ tags: [CACHE_TAGS.PROMPTS_LIST, CACHE_TAGS.PROMPTS], revalidate: 60 }
);
interface PromptsPageProps {
searchParams: Promise<{
q?: string;
@@ -102,75 +198,17 @@ export default async function PromptsPage({ searchParams }: PromptsPageProps) {
orderBy = { votes: { _count: "desc" } };
}
// Fetch initial prompts (first page)
const [promptsRaw, totalCount] = await Promise.all([
db.prompt.findMany({
where,
orderBy,
skip: 0,
take: perPage,
include: {
author: {
select: {
id: true,
name: true,
username: true,
avatar: true,
},
},
category: {
include: {
parent: {
select: { id: true, name: true, slug: true },
},
},
},
tags: {
include: {
tag: true,
},
},
contributors: {
select: {
id: true,
username: true,
name: true,
avatar: true,
},
},
_count: {
select: { votes: true, contributors: true },
},
},
}),
db.prompt.count({ where }),
]);
// Transform to include voteCount and contributorCount
prompts = promptsRaw.map((p) => ({
...p,
voteCount: p._count.votes,
contributorCount: p._count.contributors,
contributors: p.contributors,
}));
total = totalCount;
// Fetch initial prompts (first page) - using cached query
const result = await getCachedPrompts(where, orderBy, perPage);
prompts = result.prompts;
total = result.total;
}
// Fetch categories for filter (with parent info for nesting)
const categories = await db.category.findMany({
orderBy: [{ order: "asc" }, { name: "asc" }],
select: {
id: true,
name: true,
slug: true,
parentId: true,
},
});
// Fetch tags for filter
const tags = await db.tag.findMany({
orderBy: { name: "asc" },
});
// Fetch categories and tags for filter - using cached queries
const [categories, tags] = await Promise.all([
getCachedCategories(),
getCachedTags(),
]);
return (
<div className="container py-6">
+29
View File
@@ -0,0 +1,29 @@
import { unstable_cache } from "next/cache";
import { revalidateTag } from "next/cache";
// Cache tags for prompts
export const CACHE_TAGS = {
PROMPTS: "prompts",
PROMPTS_LIST: "prompts-list",
CATEGORIES: "categories",
TAGS: "tags",
} as const;
// Revalidate prompts cache - call this when prompts are created, updated, deleted, or unlisted
export function revalidatePrompts() {
revalidateTag(CACHE_TAGS.PROMPTS, "max");
revalidateTag(CACHE_TAGS.PROMPTS_LIST, "max");
}
// Helper to create cached database queries with prompts tag
export function createCachedQuery<T, Args extends unknown[]>(
queryFn: (...args: Args) => Promise<T>,
keyParts: string[],
tags: string[] = [CACHE_TAGS.PROMPTS],
revalidateSeconds: number = 60
) {
return unstable_cache(queryFn, keyParts, {
tags,
revalidate: revalidateSeconds,
});
}