church-website/src/fetch/eventOccurrences.ts
2026-07-16 09:39:52 +02:00

207 lines
5.1 KiB
TypeScript

import { getPayload, PaginatedDocs } from 'payload'
import { unstable_cache } from 'next/cache'
import config from '@/payload.config'
import { EventOccurrence } from '@/payload-types'
const CACHE_TTL = 300 // 5 minutes
type ListArgs = {
parishId?: string
groupId?: string
eventId?: string
limit?: number
page?: number
fromDate?: Date
toDate?: Date
}
// Resolved args use ISO strings so they serialize into a stable cache key.
type ResolvedArgs = {
parishId?: string
groupId?: string
eventId?: string
limit: number
page: number
fromDate?: string
toDate?: string
}
// Round down to the cache window so a default `new Date()` (millisecond
// precision) produces a stable key for the duration of the TTL.
function roundDownToTtl(date: Date): Date {
const ms = CACHE_TTL * 1000
return new Date(Math.floor(date.getTime() / ms) * ms)
}
/**
* Fetch upcoming event occurrences, joined to their parent event. Always
* filters to occurrences whose parent event is published.
*/
const getUpcomingOccurrences = unstable_cache(
async (args: ResolvedArgs): Promise<PaginatedDocs<EventOccurrence>> => {
const { parishId, groupId, eventId, limit, page, fromDate, toDate } = args
const query: any = {
and: [
// Overlap semantics: an occurrence stays "upcoming" while it is still
// running, not only before it starts. endDateTime is null for
// occurrences without an end; those fall back to the start date.
{
or: [
{ date: { greater_than_equal: fromDate } },
{ endDateTime: { greater_than_equal: fromDate } },
],
},
{ 'event._status': { equals: 'published' } },
],
}
if (toDate) {
query.and.push({ date: { less_than: toDate } })
}
if (eventId) {
query.and.push({ event: { equals: eventId } })
}
if (parishId) {
query.and.push({ 'event.parish': { equals: parishId } })
}
if (groupId) {
query.and.push({ 'event.group': { equals: groupId } })
}
const payload = await getPayload({ config })
return payload.find({
collection: 'eventOccurrence',
sort: 'date',
where: query,
depth: 2,
limit,
page,
}) as Promise<PaginatedDocs<EventOccurrence>>
},
['fetchUpcomingOccurrences'],
{ revalidate: CACHE_TTL },
)
export async function fetchUpcomingOccurrences(
args?: ListArgs,
): Promise<PaginatedDocs<EventOccurrence>> {
const {
parishId,
groupId,
eventId,
limit = 30,
page = 0,
fromDate = new Date(),
toDate,
} = args || {}
return getUpcomingOccurrences({
parishId,
groupId,
eventId,
limit,
page,
fromDate: roundDownToTtl(fromDate).toISOString(),
toDate: toDate?.toISOString(),
})
}
/**
* Fetch past event occurrences (most recent first), joined to their parent
* event. Always filters to occurrences whose parent event is published.
*/
const getPastOccurrences = unstable_cache(
async (args: ResolvedArgs): Promise<PaginatedDocs<EventOccurrence>> => {
const { parishId, groupId, eventId, limit, page, fromDate, toDate } = args
const query: any = {
and: [
{ date: { less_than: toDate } },
{ 'event._status': { equals: 'published' } },
],
}
if (fromDate) {
query.and.push({ date: { greater_than_equal: fromDate } })
}
if (eventId) {
query.and.push({ event: { equals: eventId } })
}
if (parishId) {
query.and.push({ 'event.parish': { equals: parishId } })
}
if (groupId) {
query.and.push({ 'event.group': { equals: groupId } })
}
const payload = await getPayload({ config })
return payload.find({
collection: 'eventOccurrence',
sort: '-date',
where: query,
depth: 2,
limit,
page,
}) as Promise<PaginatedDocs<EventOccurrence>>
},
['fetchPastOccurrences'],
{ revalidate: CACHE_TTL },
)
export async function fetchPastOccurrences(
args?: ListArgs,
): Promise<PaginatedDocs<EventOccurrence>> {
const {
parishId,
groupId,
eventId,
limit = 30,
page = 0,
fromDate,
toDate = new Date(),
} = args || {}
return getPastOccurrences({
parishId,
groupId,
eventId,
limit,
page,
fromDate: fromDate?.toISOString(),
toDate: roundDownToTtl(toDate).toISOString(),
})
}
/**
* Fetch upcoming occurrences, falling back to past occurrences if there
* are no upcoming ones.
*/
export async function fetchUpcomingOrPastOccurrences(
args?: ListArgs,
): Promise<PaginatedDocs<EventOccurrence>> {
const upcoming = await fetchUpcomingOccurrences(args)
if (upcoming.docs.length > 0) return upcoming
return fetchPastOccurrences(args)
}
/**
* Fetch a single occurrence by id with its parent event populated.
* Returns undefined if not found or if the id is malformed.
*/
export async function fetchOccurrenceById(
id: string,
draft: boolean = false,
): Promise<EventOccurrence | undefined> {
try {
const payload = await getPayload({ config })
return await payload.findByID({
collection: 'eventOccurrence',
id,
depth: 2,
draft,
})
} catch {
return undefined
}
}