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
60 changes: 60 additions & 0 deletions packages/payload/src/versions/buildCollectionFields.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest'

import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'

import { buildVersionCollectionFields } from './buildCollectionFields.js'

const makeCollection = (slug: string, drafts = true): SanitizedCollectionConfig =>
({
slug,
fields: [
{ name: 'id', type: 'text' },
{ name: 'title', type: 'text' },
],
flattenedFields: [
{ name: 'id', type: 'text' },
{ name: 'title', type: 'text' },
],
versions: { drafts },
}) as unknown as SanitizedCollectionConfig

const config = { localization: false } as unknown as SanitizedConfig

describe('buildVersionCollectionFields', () => {
it('returns a referentially stable array across calls for the same collection', () => {
const collection = makeCollection('posts')
const first = buildVersionCollectionFields(config, collection, true)
const second = buildVersionCollectionFields(config, collection, true)
// Same reference: the schema is built once and memoized, so identity-keyed
// downstream caches (flattenedFieldsCache) hit instead of growing per call.
expect(second).toBe(first)
})

it('caches flattened and unflattened variants separately', () => {
const collection = makeCollection('pages')
const flattened = buildVersionCollectionFields(config, collection, true)
const unflattened = buildVersionCollectionFields(config, collection, false)
expect(flattened).not.toBe(unflattened)
expect(buildVersionCollectionFields(config, collection, true)).toBe(flattened)
expect(buildVersionCollectionFields(config, collection, false)).toBe(unflattened)
})

it('memoizes per collection (distinct references for distinct collections)', () => {
const a = buildVersionCollectionFields(config, makeCollection('a'), true)
const b = buildVersionCollectionFields(config, makeCollection('b'), true)
expect(a).not.toBe(b)
})

it('still produces the expected version schema', () => {
const fields = buildVersionCollectionFields(config, makeCollection('shape'), true)
const names = fields.map((field) => ('name' in field ? field.name : undefined))
expect(names).toEqual(expect.arrayContaining(['parent', 'version', 'createdAt', 'updatedAt', 'latest']))
const version = fields.find((field) => 'name' in field && field.name === 'version') as {
fields: { name?: string }[]
}
// the `version` group mirrors the collection fields but drops the base `id`
expect(version.fields.some((field) => field.name === 'id')).toBe(false)
expect(version.fields.some((field) => field.name === 'title')).toBe(true)
})
})
32 changes: 32 additions & 0 deletions packages/payload/src/versions/buildCollectionFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,37 @@ import type { Field, FlattenedField } from '../fields/config/types.js'

import { hasAutosaveEnabled, hasDraftsEnabled } from '../utilities/getVersionsConfig.js'

// Cache the version schema per collection. It derives only from the (stable)
// collection config, so rebuilding a fresh array on every call is wasted work
// and — since `flattenAllFields` keys `flattenedFieldsCache` by array identity —
// leaks one cache entry per request. WeakMap so discarded configs are reclaimed;
// flattened/unflattened are cached separately (flatten changes the version group).
type VersionCollectionFieldsCacheEntry = {
flattened?: FlattenedField[]
unflattened?: Field[]
}

const versionCollectionFieldsCache = new WeakMap<
SanitizedCollectionConfig,
VersionCollectionFieldsCacheEntry
>()

export const buildVersionCollectionFields = <T extends boolean = false>(
config: SanitizedConfig,
collection: SanitizedCollectionConfig,
flatten?: T,
): true extends T ? FlattenedField[] : Field[] => {
let cacheEntry = versionCollectionFieldsCache.get(collection)
if (!cacheEntry) {
cacheEntry = {}
versionCollectionFieldsCache.set(collection, cacheEntry)
}

const cached = flatten ? cacheEntry.flattened : cacheEntry.unflattened
if (cached) {
return cached as true extends T ? FlattenedField[] : Field[]
}

const fields: FlattenedField[] = [
{
name: 'parent',
Expand Down Expand Up @@ -79,5 +105,11 @@ export const buildVersionCollectionFields = <T extends boolean = false>(
}
}

if (flatten) {
cacheEntry.flattened = fields
} else {
cacheEntry.unflattened = fields as Field[]
}

return fields as true extends T ? FlattenedField[] : Field[]
}
Loading