feat(api): Add cache revalidation after specific API operations
This commit is contained in:
@@ -2,8 +2,7 @@ name: Update Contributors
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every hour
|
||||
- cron: '0 * * * *'
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user