/** * Slug Validation Utilities * * Stellt sicher, dass Slugs innerhalb eines Tenants eindeutig sind. */ import type { Payload, Where } from 'payload' import type { Config } from '@/payload-types' type CollectionSlug = keyof Config['collections'] type LocaleType = 'de' | 'en' | 'all' | undefined export interface SlugValidationOptions { /** Collection slug */ collection: CollectionSlug /** Field name for slug (default: 'slug') */ slugField?: string /** Field name for tenant (default: 'tenant') */ tenantField?: string /** Whether to check per locale (default: false) */ perLocale?: boolean } /** * Validates that a slug is unique within a tenant * * @throws Error if slug already exists for this tenant */ export async function validateUniqueSlug( payload: Payload, data: Record, options: SlugValidationOptions & { existingId?: number | string locale?: string } ): Promise { const { collection, slugField = 'slug', tenantField = 'tenant', perLocale = false, existingId, locale, } = options const slug = data[slugField] const tenantId = data[tenantField] // Skip if no slug provided if (!slug || typeof slug !== 'string') { return } // Build where clause const conditions: Where[] = [{ [slugField]: { equals: slug } }] // Add tenant filter if tenant is set if (tenantId) { conditions.push({ [tenantField]: { equals: tenantId } }) } // Exclude current document when updating if (existingId) { conditions.push({ id: { not_equals: existingId } }) } const where: Where = conditions.length > 1 ? { and: conditions } : conditions[0] // Determine locale for query const queryLocale: LocaleType = perLocale && locale ? (locale as LocaleType) : undefined // Check for existing documents with same slug const existing = await payload.find({ collection, where, limit: 1, depth: 0, locale: queryLocale, }) if (existing.totalDocs > 0) { const tenantInfo = tenantId ? ` für diesen Tenant` : '' throw new Error(`Der Slug "${slug}" existiert bereits${tenantInfo}. Bitte wählen Sie einen anderen.`) } } /** * Creates a beforeValidate hook for slug uniqueness */ export function createSlugValidationHook(options: SlugValidationOptions) { return async ({ data, req, operation, originalDoc, }: { data?: Record req: { payload: Payload; locale?: string } operation: 'create' | 'update' originalDoc?: { id?: number | string } }) => { if (!data) return data await validateUniqueSlug(req.payload, data, { ...options, existingId: operation === 'update' ? originalDoc?.id : undefined, locale: req.locale, }) return data } } /** * Generates a unique slug by appending a number if necessary */ export async function generateUniqueSlug( payload: Payload, baseSlug: string, options: SlugValidationOptions & { existingId?: number | string tenantId?: number | string } ): Promise { const { collection, slugField = 'slug', tenantField = 'tenant', existingId, tenantId } = options let slug = baseSlug let counter = 1 let isUnique = false while (!isUnique && counter < 100) { const conditions: Where[] = [{ [slugField]: { equals: slug } }] if (tenantId) { conditions.push({ [tenantField]: { equals: tenantId } }) } if (existingId) { conditions.push({ id: { not_equals: existingId } }) } const where: Where = conditions.length > 1 ? { and: conditions } : conditions[0] const existing = await payload.find({ collection, where, limit: 1, depth: 0, }) if (existing.totalDocs === 0) { isUnique = true } else { slug = `${baseSlug}-${counter}` counter++ } } return slug }