349fb9b2da
- Improved parsing of `lookup` and `aggregate` options to support JSON strings and arrays. - Added support for object format in `lookup` and `aggregate` to specify collections. - Simplified dependency tracking for SSR cache invalidation based on new formats.
179 lines
7.0 KiB
JavaScript
179 lines
7.0 KiB
JavaScript
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<any>}
|
||
*/
|
||
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,
|
||
}
|