Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions dev/payload.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Config } from 'payload'

import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
Expand Down Expand Up @@ -25,7 +27,7 @@ if (!process.env.ROOT_DIR) {
process.env.ROOT_DIR = dirname
}

const config = buildConfig({
export const configOptions: Config = {
admin: {
importMap: {
baseDir: path.resolve(dirname),
Expand All @@ -47,15 +49,17 @@ const config = buildConfig({
},
plugins: [
payloadRevalidate({
defaultDepth: undefined,
enable: true,
maxDepth: undefined,
}),
],
secret: env.PAYLOAD_SECRET,
sharp,
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}

const config = buildConfig(configOptions)

export default config
188 changes: 188 additions & 0 deletions dev/test/int/depth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import type { Payload } from 'payload'

import config, { configOptions } from '@payload-config'
import { getAssetsPath } from 'helpers/file.js'
import { buildConfig, getPayload } from 'payload'
import { payloadRevalidate } from 'payload-revalidate'
import { mockRevalidateTag, payload } from 'test/setup.js'
import { beforeAll, expect, test } from 'vitest'

let mediaId: number

beforeAll(async () => {
const media = await payload.create({
collection: 'media',
data: {
id: 1,
alt: 'image',
},
filePath: getAssetsPath() + '/placeholder.png',
})

mediaId = media.id
})

test('should not revalidate any relation when defaultDepth = 0', async () => {
const config = buildConfig({
...configOptions,
plugins: [payloadRevalidate({ defaultDepth: 0 })],
})

// Use custom payload instance, with modified config
const payload = await getPayload({ config })

let author = await payload.create({
collection: 'authors',
data: {
name: 'added by plugin',
},
})

// Verify revalidateTag was called for author creation
expect(mockRevalidateTag).toHaveBeenCalledTimes(2)

Check failure on line 42 in dev/test/int/depth.spec.ts

View workflow job for this annotation

GitHub Actions / test

dev/test/int/depth.spec.ts > should not revalidate any relation when defaultDepth = 0

AssertionError: expected "spy" to be called 2 times, but got 4 times ❯ dev/test/int/depth.spec.ts:42:29
expect(mockRevalidateTag).toHaveBeenCalledWith('authors')
expect(mockRevalidateTag).toHaveBeenCalledWith(`authors.${author.id}`)

mockRevalidateTag.mockClear()

const post = await payload.create({
collection: 'posts',
data: {
author: author.id,
image: mediaId,
title: 'added by plugin',
},
depth: 2,
})

if (typeof post.author === 'number' || !post.author?.name) {
throw new Error('Wrong depth')
}

expect(post.author?.name).toBe('added by plugin')

// Verify revalidateTag was called for post creation
expect(mockRevalidateTag).toHaveBeenCalledTimes(2)
expect(mockRevalidateTag).toHaveBeenCalledWith('posts')
expect(mockRevalidateTag).toHaveBeenCalledWith(`posts.${post.id}`)

mockRevalidateTag.mockClear()

author = await payload.update({
id: author.id,
collection: 'authors',
data: {
name: 'updated by plugin',
},
})

// Verify that revalidateTag was called for related collections
expect(mockRevalidateTag).toHaveBeenCalledTimes(2)
expect(mockRevalidateTag).toHaveBeenCalledWith('authors')
expect(mockRevalidateTag).toHaveBeenCalledWith(`authors.${author.id}`)

mockRevalidateTag.mockClear()

await payload.delete({
collection: 'authors',
where: { name: { equals: 'updated by plugin' } },
})

expect(mockRevalidateTag).toHaveBeenCalledTimes(2)
expect(mockRevalidateTag).toHaveBeenCalledWith('authors')
expect(mockRevalidateTag).toHaveBeenCalledWith(`authors.${author.id}`)

mockRevalidateTag.mockClear()

await payload.delete({ collection: 'posts', where: { title: { equals: 'added by plugin' } } })

expect(mockRevalidateTag).toHaveBeenCalledTimes(2)
expect(mockRevalidateTag).toHaveBeenCalledWith('posts')
expect(mockRevalidateTag).toHaveBeenCalledWith(`posts.${post.id}`)
})

test('should revalidate only 1 level of relations when defaultDepth = 1', async () => {
const config = buildConfig({
...configOptions,
plugins: [payloadRevalidate({ defaultDepth: 1 })],
})

// Use custom payload instance, with modified config
const payload = await getPayload({ config })

// Create an author first
const author = await payload.create({
collection: 'authors',
data: {
name: 'authorName',
},
})

// Create a post with the author (depth 1) - without media to avoid upload issues
const post = await payload.create({
collection: 'posts',
data: {
author: author.id,
image: mediaId,
title: 'postTitle',
},
})

// Create a category with the post (depth 0 -> depth 1 -> depth 2)
const category = await payload.create({
collection: 'categories',
data: {
name: 'categoryName',
description: 'categoryDescription',
featuredPost: post.id,
posts: [post.id],
},
})

mockRevalidateTag.mockClear()

// Update the author (depth 2) - this should trigger revalidation for:
// - authors collection and specific author
// - posts collection and specific post (depth 1)
await payload.update({
id: author.id,
collection: 'authors',
data: {
name: 'authorNameUpdated',
},
})

// Verify revalidateTag was called for all related collections in the chain
expect(mockRevalidateTag).toHaveBeenCalledTimes(4)

Check failure on line 156 in dev/test/int/depth.spec.ts

View workflow job for this annotation

GitHub Actions / test

dev/test/int/depth.spec.ts > should revalidate only 1 level of relations when defaultDepth = 1

AssertionError: expected "spy" to be called 4 times, but got 16 times ❯ dev/test/int/depth.spec.ts:156:29
expect(mockRevalidateTag).toHaveBeenCalledWith('authors')
expect(mockRevalidateTag).toHaveBeenCalledWith(`authors.${author.id}`)
expect(mockRevalidateTag).toHaveBeenCalledWith('posts')
expect(mockRevalidateTag).toHaveBeenCalledWith(`posts.${post.id}`)

mockRevalidateTag.mockClear()

// Delete the author (depth 2) - this should trigger revalidation for:
// - authors collection and specific author
// - posts collection and specific post (depth 1)
await payload.delete({
id: author.id,
collection: 'authors',
})

expect(mockRevalidateTag).toHaveBeenCalledTimes(4)
expect(mockRevalidateTag).toHaveBeenCalledWith('authors')
expect(mockRevalidateTag).toHaveBeenCalledWith(`authors.${author.id}`)
expect(mockRevalidateTag).toHaveBeenCalledWith('posts')
expect(mockRevalidateTag).toHaveBeenCalledWith(`posts.${post.id}`)

mockRevalidateTag.mockClear()

await payload.delete({
id: post.id,
collection: 'posts',
})
await payload.delete({
id: category.id,
collection: 'categories',
})
})
3 changes: 3 additions & 0 deletions dev/test/int/relations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ beforeAll(async () => {
mediaId = media.id
})

// Tests here are using the default config, where defaultDepth = undefined
// Check depth.spec.ts for tests using a custom defaultDepth

test('revalidates correctly relations for depth 1', async () => {
let author = await payload.create({
collection: 'authors',
Expand Down
95 changes: 55 additions & 40 deletions src/hooks/revalidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,59 @@ import { revalidateCollectionItem, revalidateGlobalItem } from '../lib/revalidat
* revalidate the page in the background, so the user doesn't have to wait
* notice that the hook itself is not async and we are not awaiting `revalidate`
*/
export const revalidateCollectionDelete: CollectionAfterDeleteHook = async (params) => {
if (process.env.SEED_RUN === 'true') {
return
export const getRevalidateCollectionDeleteHook = (depth?: number): CollectionAfterDeleteHook => {
return async (params) => {
if (process.env.SEED_RUN === 'true') {
return
}
/**
* TOWONDER : await here is needed, in order to ensure data is
* correctly invalidaded INSTANTANEOUSLY, and avoid querying
* old data if we use a cached query directly after modifying
* the data. However, this could reduces performances a LOT with
* complex data structures or big data sets.
* Also fixes the error :
* "Error: Route /xxxxxx used "revalidateTag xxxxxxx" during render
* which is unsupported. To ensure revalidation is performed
* consistently it must always happen outside of renders
* and cached functions."
*/
await revalidateCollectionItem({
...params,
depth,
})
}
/**
* TOWONDER : await here is needed, in order to ensure data is
* correctly invalidaded INSTANTANEOUSLY, and avoid querying
* old data if we use a cached query directly after modifying
* the data. However, this could reduces performances a LOT with
* complex data structures or big data sets.
* Also fixes the error :
* "Error: Route /xxxxxx used "revalidateTag xxxxxxx" during render
* which is unsupported. To ensure revalidation is performed
* consistently it must always happen outside of renders
* and cached functions."
*/
await revalidateCollectionItem(params)
}

/**x
/**
* revalidate the page in the background, so the user doesn't have to wait
* notice that the hook itself is not async and we are not awaiting `revalidate`
*/
export const revalidateCollectionChange: CollectionAfterChangeHook = async (params) => {
if (process.env.SEED_RUN === 'true') {
return
}
if (params.req.query.draft) {
return
export const getRevalidateCollectionChangeHook = (depth?: number): CollectionAfterChangeHook => {
return async (params) => {
if (process.env.SEED_RUN === 'true') {
return
}
if (params.req.query.draft) {
return
}
/**
* TOWONDER : await here is needed, in order to ensure data is
* correctly invalidaded INSTANTANEOUSLY, and avoid querying
* old data if we use a cached query directly after modifying
* the data. However, this could reduces performances a LOT with
* complex data structures or big data sets.
* Also fixes the error :
* "Error: Route /xxxxxx used "revalidateTag xxxxxxx" during render
* which is unsupported. To ensure revalidation is performed
* consistently it must always happen outside of renders
* and cached functions."
*/
await revalidateCollectionItem({
...params,
depth,
})
}
/**
* TOWONDER : await here is needed, in order to ensure data is
* correctly invalidaded INSTANTANEOUSLY, and avoid querying
* old data if we use a cached query directly after modifying
* the data. However, this could reduces performances a LOT with
* complex data structures or big data sets.
* Also fixes the error :
* "Error: Route /xxxxxx used "revalidateTag xxxxxxx" during render
* which is unsupported. To ensure revalidation is performed
* consistently it must always happen outside of renders
* and cached functions."
*/
await revalidateCollectionItem(params)
}

/**
Expand All @@ -62,9 +72,14 @@ export const revalidateCollectionChange: CollectionAfterChangeHook = async (para
* send `revalidatePath`, `collection`, and `slug` to the frontend to use in its revalidate route
* frameworks may have different ways of doing this, but the idea is the same
*/
export const revalidateGlobal: GlobalAfterChangeHook = (params) => {
if (process.env.SEED_RUN === 'true') {
return
export const getRevalidateGlobalHook = (depth?: number): GlobalAfterChangeHook => {
return (params) => {
if (process.env.SEED_RUN === 'true') {
return
}
void revalidateGlobalItem({
...params,
depth,
})
}
void revalidateGlobalItem(params)
}
20 changes: 8 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import type { Config } from 'payload'

import {
revalidateCollectionChange,
revalidateCollectionDelete,
revalidateGlobal,
getRevalidateCollectionChangeHook,
getRevalidateCollectionDeleteHook,
getRevalidateGlobalHook,
} from './hooks/revalidate.js'

export type PayloadRevalidateConfig = {
defaultDepth?: number
enable?: boolean
/**
* Maximum depth of relations to follow
* If undefined, will follow all relations
*/
maxDepth?: number
}

export const payloadRevalidate =
(pluginOptions: PayloadRevalidateConfig) =>
(config: Config): Config => {
const { enable = true } = pluginOptions
const { defaultDepth, enable = true } = pluginOptions

if (!enable) {
return config
Expand All @@ -36,8 +32,8 @@ export const payloadRevalidate =
collection.hooks.afterDelete = []
}
// Revalidation hooks should be trigger at the end of the hooks chain
collection.hooks.afterChange.push(revalidateCollectionChange)
collection.hooks.afterDelete.push(revalidateCollectionDelete)
collection.hooks.afterChange.push(getRevalidateCollectionChangeHook(defaultDepth))
collection.hooks.afterDelete.push(getRevalidateCollectionDeleteHook(defaultDepth))
}
}

Expand All @@ -50,7 +46,7 @@ export const payloadRevalidate =
global.hooks.afterChange = []
}

global.hooks.afterChange.push(revalidateGlobal)
global.hooks.afterChange.push(getRevalidateGlobalHook(defaultDepth))
}
}

Expand Down
Loading
Loading