import { Collections } from "../shared/payload/constants"; import { SDKEndpointNames, getSDKEndpoint } from "../shared/payload/sdk"; import { EndpointChange } from "../shared/payload/webhooks"; import { Audio, ChronologyEvent, Collectible, File, Folder, Image, Page, Recorder, Relationship, Video, } from "../types/collections"; import { isDefined, isPayloadType } from "../utils/asserts"; import { AfterChangeHook, AfterDeleteHook, BeforeChangeHook, BeforeDeleteHook, } from "payload/dist/collections/config/types"; import { GeneratedTypes } from "payload"; import { uniqueBy } from "../utils/array"; import { GlobalAfterChangeHook } from "payload/types"; import { findRelationByID } from "payloadcms-relationships/dist/utils"; export const beforeChangePrepareChanges: BeforeChangeHook = async ({ collection, originalDoc, context, data, }) => { if ("_status" in data && data._status === "draft") return data; if (!originalDoc) return data; context.beforeChangeChanges = await getChanges( collection.slug as keyof GeneratedTypes["collections"], originalDoc ); return data; }; export const afterChangeSendChangesWebhook: AfterChangeHook = async ({ doc, collection, context, }) => { if ("_status" in doc && doc._status === "draft") return doc; const changes = await getChanges(collection.slug as keyof GeneratedTypes["collections"], doc); const previousChanges = context.beforeChangeChanges as EndpointChange[] | undefined; if (isDefined(previousChanges)) { await sendWebhookMessage(uniqueBy([...previousChanges, ...changes], ({ url }) => url)); } else { await sendWebhookMessage(changes); } return doc; }; export const beforeDeletePrepareChanges: BeforeDeleteHook = async ({ id, collection, context }) => { context.beforeDeleteChanges = await getChanges( collection.slug as keyof GeneratedTypes["collections"], { id } ); }; export const afterDeleteSendChangesWebhook: AfterDeleteHook = async ({ doc, context }) => { const changes = context.beforeDeleteChanges as EndpointChange[] | undefined; if (isDefined(changes)) { await sendWebhookMessage(changes); } return doc; }; export const globalAfterChangeSendChangesWebhook: GlobalAfterChangeHook = async ({ doc, global, }) => { const changes: EndpointChange[] = []; switch (global.slug as keyof GeneratedTypes["globals"]) { case Collections.WebsiteConfig: changes.push(...getEndpointChangesForWebsiteConfig()); break; default: break; } await sendWebhookMessage(uniqueBy(changes, ({ url }) => url)); return doc; }; const getChanges = async ( slug: keyof GeneratedTypes["collections"], doc: any ): Promise => { if (slug === "relationships") return []; if (slug === "payload-migrations") return []; if (slug === "payload-preferences") return []; let relation: Relationship; try { relation = await findRelationByID(slug, doc.id); } catch (e) { relation = { id: "", document: { relationTo: slug, value: doc, }, outgoingRelations: [], }; } const changes: EndpointChange[] = getEndpointChangesFromDocument(relation.document); relation.incomingRelations?.forEach((relation) => changes.push(...getEndpointChangesFromIncomingRelation(relation)) ); relation.outgoingRelations?.forEach((relation) => changes.push(...getEndpointChangesFromOutgoingRelation(relation)) ); return uniqueBy(changes, ({ url }) => url); }; // ------------------------------------------------------------------------------------------------- const getEndpointChangesFromDocument = ({ relationTo, value, }: NonNullable): EndpointChange[] => { if (!isPayloadType(value)) return []; switch (relationTo) { case Collections.Folders: return getEndpointChangesForFolder(value); case Collections.Pages: return getEndpointChangesForPage(value); case Collections.Collectibles: return getEndpointChangesForCollectible(value); case Collections.Audios: return getEndpointChangesForAudio(value); case Collections.Images: return getEndpointChangesForImage(value); case Collections.Videos: return getEndpointChangesForVideo(value); case Collections.Files: return getEndpointChangesForFile(value); case Collections.Recorders: return getEndpointChangesForRecorder(value); case Collections.ChronologyEvents: return getEndpointChangesForChronologyEvent(value); case Collections.Languages: return getEndpointChangesForLanguage(); case Collections.Currencies: return getEndpointChangesForCurrency(); case Collections.Wordings: return getEndpointChangesForWording(); case Collections.Attributes: case Collections.CreditsRole: case Collections.GenericContents: case Collections.MediaThumbnails: case Collections.Scans: case Collections.Tags: case Collections.VideosChannels: case Collections.VideosSubtitles: default: return []; } }; const getEndpointChangesFromIncomingRelation = ({ relationTo, value, }: NonNullable[number]): EndpointChange[] => { if (!isPayloadType(value)) return []; switch (relationTo) { case Collections.Folders: return getEndpointChangesForFolder(value); case Collections.Pages: return getEndpointChangesForPage(value); case Collections.Collectibles: return getEndpointChangesForCollectible(value); case Collections.Audios: return getEndpointChangesForAudio(value); case Collections.Images: return getEndpointChangesForImage(value); case Collections.Videos: return getEndpointChangesForVideo(value); case Collections.Files: return getEndpointChangesForFile(value); case Collections.Recorders: return getEndpointChangesForRecorder(value); case Collections.ChronologyEvents: return getEndpointChangesForChronologyEvent(value); case Collections.Languages: case Collections.Currencies: case Collections.Wordings: case Collections.Attributes: case Collections.CreditsRole: case Collections.GenericContents: case Collections.MediaThumbnails: case Collections.Scans: case Collections.Tags: case Collections.VideosChannels: case Collections.VideosSubtitles: default: return []; } }; const getEndpointChangesFromOutgoingRelation = ({ relationTo, value, }: NonNullable[number]): EndpointChange[] => { if (!isPayloadType(value)) return []; switch (relationTo) { case Collections.Folders: return getEndpointChangesForFolder(value); case Collections.Pages: return getEndpointChangesForPage(value); case Collections.Collectibles: return getEndpointChangesForCollectible(value); case Collections.Audios: return getEndpointChangesForAudio(value); case Collections.Images: return getEndpointChangesForImage(value); case Collections.Videos: return getEndpointChangesForVideo(value); case Collections.Files: return getEndpointChangesForFile(value); case Collections.Languages: case Collections.Currencies: case Collections.Wordings: case Collections.Attributes: case Collections.CreditsRole: case Collections.GenericContents: case Collections.MediaThumbnails: case Collections.Scans: case Collections.Tags: case Collections.VideosChannels: case Collections.VideosSubtitles: case Collections.ChronologyEvents: case Collections.Recorders: default: return []; } }; export const getEndpointChangesForWebsiteConfig = (): EndpointChange[] => [ { type: SDKEndpointNames.getWebsiteConfig, url: getSDKEndpoint.getWebsiteConfig(), }, ]; export const getEndpointChangesForFolder = ({ slug }: Folder): EndpointChange[] => [ { type: SDKEndpointNames.getFolder, slug, url: getSDKEndpoint.getFolder(slug) }, ]; export const getEndpointChangesForLanguage = (): EndpointChange[] => [ { type: SDKEndpointNames.getLanguages, url: getSDKEndpoint.getLanguages() }, ]; export const getEndpointChangesForCurrency = (): EndpointChange[] => [ { type: SDKEndpointNames.getCurrencies, url: getSDKEndpoint.getCurrencies() }, ]; export const getEndpointChangesForWording = (): EndpointChange[] => [ { type: SDKEndpointNames.getWordings, url: getSDKEndpoint.getWordings() }, ]; export const getEndpointChangesForPage = ({ slug }: Page): EndpointChange[] => [ { type: SDKEndpointNames.getPage, slug, url: getSDKEndpoint.getPage(slug) }, ]; export const getEndpointChangesForCollectible = ({ slug, gallery, scans, scansEnabled, }: Collectible): EndpointChange[] => { const changes: EndpointChange[] = []; changes.push({ type: SDKEndpointNames.getCollectible, slug, url: getSDKEndpoint.getCollectible(slug), }); if (gallery && gallery.length > 0) { changes.push({ type: SDKEndpointNames.getCollectibleGallery, slug, url: getSDKEndpoint.getCollectibleGallery(slug), }); gallery.forEach((_, indexNumber) => { const index = indexNumber.toString(); changes.push({ type: SDKEndpointNames.getCollectibleGalleryImage, slug, index: index, url: getSDKEndpoint.getCollectibleGalleryImage(slug, index), }); }); } if (scans && scansEnabled) { changes.push({ type: SDKEndpointNames.getCollectibleScans, slug, url: getSDKEndpoint.getCollectibleScans(slug), }); // TODO: Add other changes for cover, obi, dustjacket... scans.pages?.forEach(({ page }) => { const index = page.toString(); changes.push({ type: SDKEndpointNames.getCollectibleScanPage, slug, index: index, url: getSDKEndpoint.getCollectibleScanPage(slug, index), }); }); } return changes; }; export const getEndpointChangesForAudio = ({ id }: Audio): EndpointChange[] => [ { type: SDKEndpointNames.getAudioByID, id, url: getSDKEndpoint.getAudioByID(id) }, ]; export const getEndpointChangesForImage = ({ id }: Image): EndpointChange[] => [ { type: SDKEndpointNames.getImageByID, id, url: getSDKEndpoint.getImageByID(id) }, ]; export const getEndpointChangesForVideo = ({ id }: Video): EndpointChange[] => [ { type: SDKEndpointNames.getVideoByID, id, url: getSDKEndpoint.getVideoByID(id) }, ]; export const getEndpointChangesForFile = ({ id }: File): EndpointChange[] => [ { type: SDKEndpointNames.getFileByID, id, url: getSDKEndpoint.getFileByID(id) }, ]; export const getEndpointChangesForRecorder = ({ id }: Recorder): EndpointChange[] => [ { type: SDKEndpointNames.getRecorderByID, id, url: getSDKEndpoint.getRecorderByID(id) }, ]; export const getEndpointChangesForChronologyEvent = ({ id }: ChronologyEvent): EndpointChange[] => [ { type: SDKEndpointNames.getChronologyEventByID, id, url: getSDKEndpoint.getChronologyEventByID(id), }, { type: SDKEndpointNames.getChronologyEvents, url: getSDKEndpoint.getChronologyEvents(), }, ]; // ------------------------------------------------------------------------------------------------- const webhookTargets: { url: string; token: string }[] = [ { url: process.env.WEB_SERVER_HOOK_URL ?? "", token: process.env.WEB_SERVER_HOOK_TOKEN ?? "", }, { url: process.env.MEILISEARCH_HOOK_URL ?? "", token: process.env.MEILISEARCH_HOOK_TOKEN ?? "", }, ]; const sendWebhookMessage = async (changes: EndpointChange[]) => { if (changes.length === 0) return; try { await Promise.all( webhookTargets.flatMap(({ url, token }) => { if (!url) return; return fetch(url, { headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(changes), method: "POST", }); }) ); } catch (e) { if (e instanceof Error) { console.warn("Error while sending webhook", e.message); } else { console.warn("Error while sending webhook", e); } } };