Merged in feat/rework-contentstack (pull request #3493)
Feat(SW-3708): refactor contentstack fetching (removing all refs) and cache invalidation * Remove all REFS * Revalidate correct language * PR fixes * PR fixes * Throw when errors from contentstack api Approved-by: Joakim Jäderberg
This commit is contained in:
@@ -3,6 +3,7 @@ CMS_ACCESS_TOKEN=""
|
||||
CMS_BRANCH="development"
|
||||
CMS_API_KEY=""
|
||||
CMS_ENVIRONMENT="development"
|
||||
CMS_MANAGEMENT_TOKEN="csddab1f1520ce77a1312af5d3"
|
||||
CMS_PREVIEW_TOKEN=""
|
||||
CMS_PREVIEW_URL=""
|
||||
CMS_URL="https://eu-graphql.contentstack.com/stacks/${CMS_API_KEY}?environment=${CMS_ENVIRONMENT}"
|
||||
|
||||
@@ -11,13 +11,10 @@ import { languageSwitcherAffix } from "@scandic-hotels/trpc/routers/contentstack
|
||||
import { affix as metadataAffix } from "@scandic-hotels/trpc/routers/contentstack/metadata/utils"
|
||||
import { affix as pageSettingsAffix } from "@scandic-hotels/trpc/routers/contentstack/pageSettings/utils"
|
||||
import { resolveEntryCacheKey } from "@scandic-hotels/trpc/utils/entry"
|
||||
import {
|
||||
generateRefsResponseTag,
|
||||
generateRefTag,
|
||||
generateTag,
|
||||
} from "@scandic-hotels/trpc/utils/generateTag"
|
||||
import { generateTag } from "@scandic-hotels/trpc/utils/generateTag"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { fetchEntryReferences } from "@/lib/contentstack/fetchEntryReferences"
|
||||
import { badRequest, internalServerError } from "@/server/errors/next"
|
||||
|
||||
import type { NextRequest } from "next/server"
|
||||
@@ -28,11 +25,6 @@ const validateJsonBody = z.object({
|
||||
uid: z.string(),
|
||||
}),
|
||||
entry: z.object({
|
||||
breadcrumbs: z
|
||||
.object({
|
||||
title: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
locale: z.nativeEnum(Lang),
|
||||
publish_details: z.object({ locale: z.nativeEnum(Lang) }).optional(),
|
||||
uid: z.string(),
|
||||
@@ -44,6 +36,15 @@ const validateJsonBody = z.object({
|
||||
.optional(),
|
||||
destination_settings:
|
||||
destinationCityPageDestinationSettingsSchema.optional(),
|
||||
web: z
|
||||
.object({
|
||||
breadcrumbs: z
|
||||
.object({
|
||||
title: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
@@ -81,12 +82,6 @@ export async function POST(request: NextRequest) {
|
||||
// The publish_details.locale is the locale that the entry is published in, regardless if it is "localized" or not
|
||||
const entryLocale = entry.publish_details?.locale ?? entry.locale
|
||||
|
||||
const refsTag = generateRefsResponseTag(entryLocale, entry.uid)
|
||||
const contentEntryTag = generateRefsResponseTag(
|
||||
entryLocale,
|
||||
content_type.uid
|
||||
)
|
||||
const refTag = generateRefTag(entryLocale, content_type.uid, entry.uid)
|
||||
const tag = generateTag(entryLocale, entry.uid)
|
||||
const languageSwitcherTag = generateTag(
|
||||
"" as Lang, // We want to clear all languages when unpublishing a page
|
||||
@@ -106,14 +101,6 @@ export async function POST(request: NextRequest) {
|
||||
revalidateTag(contentTypeUidTag, { expire: 0 })
|
||||
keysToDelete.push(contentTypeUidTag)
|
||||
|
||||
revalidateLogger.debug(`Revalidating refsTag: ${refsTag}`)
|
||||
revalidateTag(refsTag, { expire: 0 })
|
||||
keysToDelete.push(refsTag)
|
||||
|
||||
revalidateLogger.debug(`Revalidating refTag: ${refTag}`)
|
||||
revalidateTag(refTag, { expire: 0 })
|
||||
keysToDelete.push(refTag)
|
||||
|
||||
revalidateLogger.debug(`Revalidating tag: ${tag}`)
|
||||
revalidateTag(tag, { expire: 0 })
|
||||
keysToDelete.push(tag)
|
||||
@@ -126,9 +113,6 @@ export async function POST(request: NextRequest) {
|
||||
revalidateLogger.debug(`Revalidating metadataTag: ${metadataTag}`)
|
||||
revalidateTag(metadataTag, { expire: 0 })
|
||||
keysToDelete.push(metadataTag)
|
||||
revalidateLogger.debug(`Revalidating contentEntryTag: ${contentEntryTag}`)
|
||||
revalidateTag(contentEntryTag, { expire: 0 })
|
||||
keysToDelete.push(contentEntryTag)
|
||||
|
||||
if (entry.url) {
|
||||
const resolveEntryTag = resolveEntryCacheKey(entryLocale, entry.url)
|
||||
@@ -136,24 +120,13 @@ export async function POST(request: NextRequest) {
|
||||
keysToDelete.push(resolveEntryTag)
|
||||
}
|
||||
|
||||
if (entry.breadcrumbs) {
|
||||
const breadcrumbsRefsTag = generateRefsResponseTag(
|
||||
entryLocale,
|
||||
entry.uid,
|
||||
breadcrumbsAffix
|
||||
)
|
||||
if (entry.web?.breadcrumbs) {
|
||||
const breadcrumbsTag = generateTag(
|
||||
entryLocale,
|
||||
entry.uid,
|
||||
breadcrumbsAffix
|
||||
)
|
||||
|
||||
revalidateLogger.debug(
|
||||
`Revalidating breadcrumbsRefsTag: ${breadcrumbsRefsTag}`
|
||||
)
|
||||
revalidateTag(breadcrumbsRefsTag, { expire: 0 })
|
||||
keysToDelete.push(breadcrumbsRefsTag)
|
||||
|
||||
revalidateLogger.debug(`Revalidating breadcrumbsTag: ${breadcrumbsTag}`)
|
||||
revalidateTag(breadcrumbsTag, { expire: 0 })
|
||||
keysToDelete.push(breadcrumbsTag)
|
||||
@@ -183,6 +156,26 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch entries that REFERENCE this entry (parent pages that embed this content)
|
||||
// This allows us to invalidate parent page caches when a referenced child is updated
|
||||
const references = await fetchEntryReferences(content_type.uid, entry.uid)
|
||||
|
||||
for (const ref of references) {
|
||||
// Use the entry's locale (from the webhook payload), not the reference's locale
|
||||
// The API returns references with their base locale, but we only want to
|
||||
// invalidate caches for the locale that was actually published
|
||||
const parentTag = generateTag(entryLocale, ref.entry_uid)
|
||||
revalidateLogger.debug(
|
||||
`Revalidating parent reference tag: ${parentTag} (from ${ref.content_type_uid})`
|
||||
)
|
||||
revalidateTag(parentTag, { expire: 0 })
|
||||
keysToDelete.push(parentTag)
|
||||
}
|
||||
|
||||
revalidateLogger.debug(
|
||||
`Deleting ${keysToDelete.length} keys (including ${references.length} parent references)`
|
||||
)
|
||||
|
||||
await cacheClient.deleteKeys(keysToDelete, { fuzzy: true })
|
||||
|
||||
return Response.json({ revalidated: true, now: Date.now() })
|
||||
|
||||
2
apps/scandic-web/env/server.ts
vendored
2
apps/scandic-web/env/server.ts
vendored
@@ -24,6 +24,7 @@ export const env = createEnv({
|
||||
CMS_API_KEY: z.string(),
|
||||
CMS_ENVIRONMENT: z.enum(["development", "production", "stage", "test"]),
|
||||
CMS_BRANCH: z.enum(["development", "production"]),
|
||||
CMS_MANAGEMENT_TOKEN: z.string().optional(),
|
||||
CURITY_CLIENT_ID_USER: z.string().default("scandichotels-web"),
|
||||
CURITY_CLIENT_ID_SERVICE: z.string().default("scandichotels-web-backend"),
|
||||
CURITY_CLIENT_SECRET_SERVICE: z.string(),
|
||||
@@ -123,6 +124,7 @@ export const env = createEnv({
|
||||
CMS_API_KEY: process.env.CMS_API_KEY,
|
||||
CMS_ENVIRONMENT: process.env.CMS_ENVIRONMENT,
|
||||
CMS_BRANCH: process.env.CMS_BRANCH,
|
||||
CMS_MANAGEMENT_TOKEN: process.env.CMS_MANAGEMENT_TOKEN,
|
||||
CURITY_CLIENT_ID_USER: process.env.CURITY_CLIENT_ID_USER,
|
||||
CURITY_CLIENT_ID_SERVICE: process.env.CURITY_CLIENT_ID_SERVICE,
|
||||
CURITY_CLIENT_SECRET_SERVICE: process.env.CURITY_CLIENT_SECRET_SERVICE,
|
||||
|
||||
86
apps/scandic-web/lib/contentstack/fetchEntryReferences.ts
Normal file
86
apps/scandic-web/lib/contentstack/fetchEntryReferences.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
const logger = createLogger("contentstack-references")
|
||||
|
||||
/**
|
||||
* Schema for validating Contentstack Management API references response
|
||||
*/
|
||||
const entryReferenceSchema = z.object({
|
||||
uid: z.string().optional(),
|
||||
entry_uid: z.string(),
|
||||
content_type_uid: z.string(),
|
||||
locale: z.string(),
|
||||
title: z.string(),
|
||||
_version: z.number().optional(),
|
||||
content_type_title: z.string().optional(),
|
||||
})
|
||||
|
||||
const referencesResponseSchema = z.object({
|
||||
references: z.array(entryReferenceSchema).optional().default([]),
|
||||
})
|
||||
|
||||
export type EntryReference = z.infer<typeof entryReferenceSchema>
|
||||
|
||||
/**
|
||||
* Fetches all entries that reference the given entry from Contentstack Management API.
|
||||
*
|
||||
* This is used during cache invalidation to find parent pages that embed/reference
|
||||
* the changed entry, so we can invalidate their caches as well.
|
||||
*
|
||||
* @see https://www.contentstack.com/docs/developers/apis/content-management-api#entry-references
|
||||
*/
|
||||
export async function fetchEntryReferences(
|
||||
contentTypeUid: string,
|
||||
entryUid: string
|
||||
): Promise<EntryReference[]> {
|
||||
const managementToken = env.CMS_MANAGEMENT_TOKEN
|
||||
|
||||
if (!managementToken) {
|
||||
throw new Error("CMS_MANAGEMENT_TOKEN not configured")
|
||||
}
|
||||
|
||||
// Contentstack EU region base URL
|
||||
const baseUrl = "https://eu-api.contentstack.com"
|
||||
const url = `${baseUrl}/v3/content_types/${contentTypeUid}/entries/${entryUid}/references`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
api_key: env.CMS_API_KEY,
|
||||
authorization: managementToken,
|
||||
branch: env.CMS_BRANCH,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
// Don't cache this request - we need fresh data
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(
|
||||
`Failed to fetch entry references: ${response.status} ${errorText}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const parsed = referencesResponseSchema.safeParse(data)
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error("Invalid response from Contentstack references API", {
|
||||
cause: parsed.error,
|
||||
})
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Found ${parsed.data.references.length} references for ${contentTypeUid}/${entryUid}`
|
||||
)
|
||||
|
||||
return parsed.data.references
|
||||
}
|
||||
Reference in New Issue
Block a user