mirror of
https://github.com/supabase/supabase.git
synced 2026-06-01 02:14:43 +08:00
feat (docs): on-demand revalidation for graphql and wrappers (#35177)
Before: Revalidation was turned off entirely for wrappers pages, and done daily for GraphQL pages. After: Revalidation is via tag. There is an /api/revalidate endpoint that is used by GitHub Actions on the federated repos to trigger the revalidation. This decreases the number of unnecessary page generations by Vercel, and gives the maintainers of federated repos more fine-grained control over when to reploy their docs (see the docs deployment playbook for more info).
This commit is contained in:
@@ -40,6 +40,9 @@ describe('_handleRevalidateRequest', () => {
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:3000'
|
||||
process.env.SUPABASE_SECRET_KEY = 'secret_key'
|
||||
|
||||
// Silence intentional console errors for cleaner test output
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
// Mock current date
|
||||
mockDate = new Date('2023-01-01T12:00:00Z')
|
||||
vi.setSystemTime(mockDate)
|
||||
@@ -109,7 +112,7 @@ describe('_handleRevalidateRequest', () => {
|
||||
headers: {
|
||||
Authorization: 'Bearer basic_key',
|
||||
},
|
||||
body: JSON.stringify({ tags: ['tag1', 'tag2'] }),
|
||||
body: JSON.stringify({ tags: ['graphql', 'wrappers'] }),
|
||||
})
|
||||
|
||||
vi.mocked(headers).mockReturnValue(new Headers(request.headers))
|
||||
@@ -119,8 +122,8 @@ describe('_handleRevalidateRequest', () => {
|
||||
const response = await _handleRevalidateRequest(request)
|
||||
expect(response.status).toBe(204)
|
||||
expect(revalidateTag).toHaveBeenCalledTimes(2)
|
||||
expect(revalidateTag).toHaveBeenCalledWith('tag1')
|
||||
expect(revalidateTag).toHaveBeenCalledWith('tag2')
|
||||
expect(revalidateTag).toHaveBeenCalledWith('graphql')
|
||||
expect(revalidateTag).toHaveBeenCalledWith('wrappers')
|
||||
})
|
||||
|
||||
it('should return 429 if last revalidation was less than 6 hours ago with basic permissions', async () => {
|
||||
@@ -129,7 +132,7 @@ describe('_handleRevalidateRequest', () => {
|
||||
headers: {
|
||||
Authorization: 'Bearer basic_key',
|
||||
},
|
||||
body: JSON.stringify({ tags: ['tag1'] }),
|
||||
body: JSON.stringify({ tags: ['graphql'] }),
|
||||
})
|
||||
|
||||
vi.mocked(headers).mockReturnValue(new Headers(request.headers))
|
||||
@@ -150,7 +153,7 @@ describe('_handleRevalidateRequest', () => {
|
||||
headers: {
|
||||
Authorization: 'Bearer basic_key',
|
||||
},
|
||||
body: JSON.stringify({ tags: ['tag1'] }),
|
||||
body: JSON.stringify({ tags: ['graphql'] }),
|
||||
})
|
||||
|
||||
vi.mocked(headers).mockReturnValue(new Headers(request.headers))
|
||||
@@ -162,7 +165,7 @@ describe('_handleRevalidateRequest', () => {
|
||||
|
||||
const response = await _handleRevalidateRequest(request)
|
||||
expect(response.status).toBe(204)
|
||||
expect(revalidateTag).toHaveBeenCalledWith('tag1')
|
||||
expect(revalidateTag).toHaveBeenCalledWith('graphql')
|
||||
})
|
||||
|
||||
it('should revalidate regardless of last revalidation time with override permissions', async () => {
|
||||
@@ -171,7 +174,7 @@ describe('_handleRevalidateRequest', () => {
|
||||
headers: {
|
||||
Authorization: 'Bearer override_key',
|
||||
},
|
||||
body: JSON.stringify({ tags: ['tag1'] }),
|
||||
body: JSON.stringify({ tags: ['graphql'] }),
|
||||
})
|
||||
|
||||
vi.mocked(headers).mockReturnValue(new Headers(request.headers))
|
||||
@@ -183,6 +186,6 @@ describe('_handleRevalidateRequest', () => {
|
||||
|
||||
const response = await _handleRevalidateRequest(request)
|
||||
expect(response.status).toBe(204)
|
||||
expect(revalidateTag).toHaveBeenCalledWith('tag1')
|
||||
expect(revalidateTag).toHaveBeenCalledWith('graphql')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { type Database } from 'common'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
import { headers } from 'next/headers'
|
||||
import { type NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { type Database } from 'common'
|
||||
import { VALID_REVALIDATION_TAGS } from '~/features/helpers.fetch'
|
||||
|
||||
enum AuthorizationLevel {
|
||||
Unauthorized,
|
||||
@@ -13,7 +13,7 @@ enum AuthorizationLevel {
|
||||
}
|
||||
|
||||
const requestBodySchema = z.object({
|
||||
tags: z.array(z.string()),
|
||||
tags: z.array(z.enum(VALID_REVALIDATION_TAGS)),
|
||||
})
|
||||
|
||||
export const POST = handleError(_handleRevalidateRequest)
|
||||
@@ -35,14 +35,12 @@ export async function _handleRevalidateRequest(request: NextRequest) {
|
||||
}
|
||||
|
||||
let authorizationLevel = AuthorizationLevel.Unauthorized
|
||||
|
||||
const token = authorization.replace(/^Bearer\s+/, '')
|
||||
const token = authorization.replace(/^Bearer /, '')
|
||||
if (overrideKeys.includes(token)) {
|
||||
authorizationLevel = AuthorizationLevel.Override
|
||||
} else if (basicKeys.includes(token)) {
|
||||
authorizationLevel = AuthorizationLevel.Basic
|
||||
}
|
||||
|
||||
if (authorizationLevel === AuthorizationLevel.Unauthorized) {
|
||||
return new Response('Invalid Authorization header', { status: 401 })
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
removeRedundantH1,
|
||||
} from '~/features/docs/GuidesMdx.utils'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
||||
import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
import { REVALIDATION_TAGS } from '~/features/helpers.fetch'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
@@ -196,7 +196,9 @@ const getContent = async (params: Params) => {
|
||||
const repoPath = `${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
|
||||
editLink = `${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`
|
||||
|
||||
const response = await fetchRevalidatePerDay(`https://raw.githubusercontent.com/${repoPath}`)
|
||||
const response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, {
|
||||
next: { tags: [REVALIDATION_TAGS.WRAPPERS] },
|
||||
})
|
||||
const rawContent = await response.text()
|
||||
|
||||
const { content: contentWithoutFrontmatter } = matter(rawContent)
|
||||
|
||||
@@ -5,11 +5,11 @@ import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { fetchRevalidatePerDay_TEMP_TESTING } from '~/features/helpers.fetch'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
import { REVALIDATION_TAGS } from '~/features/helpers.fetch'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
@@ -135,8 +135,9 @@ const getContent = async ({ slug }: Params) => {
|
||||
|
||||
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
|
||||
|
||||
const response = await fetchRevalidatePerDay_TEMP_TESTING(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`,
|
||||
{ next: { tags: [REVALIDATION_TAGS.GRAPHQL] } }
|
||||
)
|
||||
|
||||
const content = await response.text()
|
||||
|
||||
@@ -8,6 +8,17 @@
|
||||
|
||||
import { ONE_DAY_IN_SECONDS } from './helpers.time'
|
||||
|
||||
export const REVALIDATION_TAGS = {
|
||||
GRAPHQL: 'graphql',
|
||||
WRAPPERS: 'wrappers',
|
||||
} as const
|
||||
// Casting to avoid problems with using this as a Zod enum, TypeScript does
|
||||
// not recognize the casted type as a supertype of the original type
|
||||
export const VALID_REVALIDATION_TAGS = Object.values(REVALIDATION_TAGS) as unknown as readonly [
|
||||
string,
|
||||
...string[],
|
||||
]
|
||||
|
||||
function fetchWithNextOptions({
|
||||
next,
|
||||
cache,
|
||||
|
||||
Reference in New Issue
Block a user