const { apiSsrBaseURL, ssrPublishCheckCollections } = require("../config") /** * api request via server, cache result in context.ssrCache * should be elimated in client code via tree shaking * * @param {string} cacheKey * @param {string} endpoint * @param {string} query * @param {ApiOptions} options * @returns {ApiResult} */ function ssrRequest(cacheKey, endpoint, query, options) { let url = endpoint + (query ? "?" + query : "") // track which collections/entries contribute to this SSR render // endpoint may contain path segments (e.g. "content/abc123") or query strings const collectionName = endpoint.split("?")[0].split("/")[0] if (ssrPublishCheckCollections?.includes(endpoint)) { // @ts-ignore let validUntil = context.ssrCacheValidUntil // check in db for publish date to invalidate cache const _optionsPublishSearch = Object.assign( {}, { filter: options?.filter }, { selector: { publication: 1 }, projection: null, } ) const publishSearch = context.db.find(endpoint, _optionsPublishSearch) publishSearch?.forEach((item) => { const publicationFrom = item.publication?.from ? new Date(item.publication.from.unixMilli()) : null const publicationTo = item.publication?.to ? new Date(item.publication.to.unixMilli()) : null if (publicationFrom && publicationFrom > new Date()) { // entry has a publish date that is further in in the future than current, set global validUntil if (validUntil == null || validUntil > publicationFrom) { validUntil = publicationFrom } } if (publicationTo && publicationTo > new Date()) { // entry has a unpublish date that is further in in the future than current, set global validUntil if (validUntil == null || validUntil > publicationTo) { validUntil = publicationTo } } }) // @ts-ignore context.ssrCacheValidUntil = validUntil } // console.log("############ FETCHING ", apiSsrBaseURL + url) const response = context.http.fetch(apiSsrBaseURL + url, { method: options.method, headers: options.headers, }) // console.log(JSON.stringify(response.headers, null, 2)) const json = response.body.json() const count = parseInt(response.headers["X-Results-Count"] || "0") // json is go data structure and incompatible with js, so we need to convert it const r = { data: JSON.parse(JSON.stringify(json)), count: count } // track dependencies: "col:id" for single-entry, "col:*" for list queries // @ts-ignore – dynamic property set by get_read.js if (context.ssrDeps) { let entryId = null if (!Array.isArray(r.data) && r.data && r.data.id) { // direct ID lookup (COLLECTION/ID) – API returned single object entryId = r.data.id } else if (options?.limit === 1 && Array.isArray(r.data) && r.data.length === 1 && r.data[0] && r.data[0].id) { // filter-based detail query with limit:1 entryId = r.data[0].id } if (entryId) { // @ts-ignore context.ssrDeps[collectionName + ":" + entryId] = true } else { // list query – any change to this collection affects this page // @ts-ignore context.ssrDeps[collectionName + ":*"] = true } // --- EXTENSION: Track dependencies from lookup and aggregate --- // Both `lookup` and `aggregate` parameters can inject data from other collections. // We must invalidate the SSR cache if any of those referenced collections change. if (options && options.lookup) { /** @type {any[]} */ let lookups = [] if (typeof options.lookup === "string") { const trimmed = options.lookup.trim() if (trimmed.startsWith("[") || trimmed.startsWith("{")) { try { const parsed = JSON.parse(trimmed) lookups = Array.isArray(parsed) ? parsed : [parsed] } catch (e) { lookups = options.lookup.split(",") } } else { lookups = options.lookup.split(",") } } else if (Array.isArray(options.lookup)) { lookups = options.lookup } else if (typeof options.lookup === "object" && options.lookup !== null) { lookups = [options.lookup] } for (const l of lookups) { if (typeof l === "object" && l !== null && l.collection) { // @ts-ignore context.ssrDeps[l.collection + ":*"] = true } else if (typeof l === "string") { // format: "fieldPath:collectionName" const parts = l.split(":") if (parts.length > 1) { const targetCollection = parts[parts.length - 1] // @ts-ignore context.ssrDeps[targetCollection + ":*"] = true } } } } const rawAggregate = (options && options.aggregate) || (options && options.params && options.params.aggregate) if (rawAggregate) { /** @type {any[]} */ let aggregates = [] if (typeof rawAggregate === "string") { const trimmed = rawAggregate.trim() if (trimmed.startsWith("[") || trimmed.startsWith("{")) { try { const parsed = JSON.parse(trimmed) aggregates = Array.isArray(parsed) ? parsed : [parsed] } catch (e) { aggregates = rawAggregate.split(",") } } else { aggregates = rawAggregate.split(",") } } else if (Array.isArray(rawAggregate)) { aggregates = rawAggregate } else if (typeof rawAggregate === "object" && rawAggregate !== null) { aggregates = [rawAggregate] } for (const a of aggregates) { if (typeof a === "object" && a !== null && a.collection) { // @ts-ignore context.ssrDeps[a.collection + ":*"] = true } else if (typeof a === "string") { const parts = a.split(":") if (parts.length > 0) { const targetCollection = parts[0] // @ts-ignore context.ssrDeps[targetCollection + ":*"] = true } } } } // --- END EXTENSION --- } // @ts-ignore context.ssrCache[cacheKey] = r return r } module.exports = { ssrRequest, }