feat: unify API options structure and enhance lookup handling across collections

This commit is contained in:
2026-05-17 14:53:52 +00:00
parent f332c707b7
commit bd8d413850
10 changed files with 58 additions and 112 deletions
+6 -11
View File
@@ -522,19 +522,14 @@ Test setup:
## API lookup für aufgelöste Referenzen
Beim Laden von Collections können Fremdschlüssel via `lookup`-Parameter automatisch aufgelöst werden. Der `lookup`-Parameter wird als 8. Argument an `getCachedEntries` übergeben:
Beim Laden von Collections können Fremdschlüssel via `lookup`-Parameter automatisch aufgelöst werden. Der `lookup`-Parameter wird einfach im Optionen-Objekt an `getCachedEntries` übergeben:
```ts
const products = await getCachedEntries<"machines">(
"machines",
{ active: true, category: catId },
"sortOrder",
undefined,
undefined,
undefined,
undefined,
"images:medialib" // lookup: "feld:collection"
)
const products = await getCachedEntries<"machines">("machines", {
filter: { active: true, category: catId },
sort: "sortOrder",
lookup: "images:medialib", // lookup: "feld:collection"
})
```
Das Format ist `"feldname:zielcollection"` (z.B. `"images:medialib"`). Die aufgelösten Daten landen in `entry._lookup.feldname` als Array der Ziel-Collection-Objekte. Ohne lookup bleiben `string[]`-Felder reine ID-Arrays.
+6 -11
View File
@@ -126,19 +126,14 @@ Typical usage:
For repeated collection data such as galleries, teaser lists, or detail-page image arrays, also request the lookup instead of rendering from raw ID strings:
```ts
const entries = await getCachedEntries<CollectionName>(
"your-collection",
{ active: true },
"sortOrder",
undefined,
undefined,
undefined,
undefined,
"imageField:medialib"
)
const entries = await getCachedEntries<CollectionName>("your-collection", {
filter: { active: true },
sort: "sortOrder",
lookup: "imageField:medialib",
})
```
`getCachedEntries()` expects `lookup` as the 8th argument and as a string, not as part of the `params` object.
`getCachedEntries()` expects `lookup` as an option in the second arguments object.
Then consume the resolved entry from `_lookup`, for example `_lookup.imageField` or `_lookup.imageField?.[0]` depending on whether the schema stores one image or an array.
+6 -11
View File
@@ -136,20 +136,15 @@ Beim Laden von Collections können Fremdschlüssel via `lookup`-Parameter automa
```ts
// entries mit aufgelösten medialib-Bildern laden
const entries = await getCachedEntries<"content">(
"content",
{ active: true },
"sort",
undefined,
undefined,
undefined,
undefined,
"blocks.heroImage.image:medialib"
)
const entries = await getCachedEntries<"content">("content", {
filter: { active: true },
sort: "sort",
lookup: "blocks.heroImage.image:medialib"
})
// Ergebnis: entry._lookup enthält die aufgelösten Referenzen
```
Der `lookup`-Parameter muss als 8. Argument an `getCachedEntries` übergeben werden. Ohne lookup bleiben Referenzfelder reine ID-Werte ohne `_lookup`.
Der `lookup`-Parameter wird im Optionen-Objekt an `getCachedEntries` übergeben. Ohne lookup bleiben Referenzfelder reine ID-Werte ohne `_lookup`.
## Tailwind CSS 4
+1 -1
View File
@@ -1,2 +1,2 @@
ADMIN_TOKEN=5bdfjc78hdxn338cuhSJ
ADMIN_ASSET_VERSION=db968ab-dirty-1779027503096
ADMIN_ASSET_VERSION=db968ab-dirty-1779028656102
+4 -3
View File
@@ -104,9 +104,10 @@ function ssrRequest(cacheKey, endpoint, query, options) {
}
}
}
if (options && options.params && options.params.aggregate) {
const aggregates = typeof options.params.aggregate === "string"
? options.params.aggregate.split(",")
const rawAggregate = (options && options.aggregate) || (options && options.params && options.params.aggregate);
if (rawAggregate) {
const aggregates = typeof rawAggregate === "string"
? rawAggregate.split(",")
: [];
for (const a of aggregates) {
// simple format: "collectionName:foreignField:..."
+1
View File
@@ -72,6 +72,7 @@ function apiRequest(endpoint, options, body, sentry) {
if (options?.offset) query += "&offset=" + options.offset
if (options?.projection) query += "&projection=" + options.projection
if (options?.lookup) query += "&lookup=" + options.lookup
if (options?.aggregate) query += "&aggregate=" + (typeof options.aggregate === 'string' ? options.aggregate : encodeURIComponent(JSON.stringify(options.aggregate)))
if (options?.params) {
Object.keys(options.params).forEach((p) => {
+23 -39
View File
@@ -159,45 +159,30 @@
try {
// Load navigation
const [headerEntries, footerEntries] = await Promise.all([
getCachedEntries<"navigation">(
"navigation",
{ type: "header", language: lang },
"sort",
1,
undefined,
undefined,
undefined,
NAVIGATION_CONTENT_LOOKUP
),
getCachedEntries<"navigation">(
"navigation",
{ type: "footer", language: lang },
"sort",
1,
undefined,
undefined,
undefined,
NAVIGATION_CONTENT_LOOKUP
),
getCachedEntries<"navigation">("navigation", {
filter: { type: "header", language: lang },
sort: "sort",
limit: 1,
lookup: NAVIGATION_CONTENT_LOOKUP
}),
getCachedEntries<"navigation">("navigation", {
filter: { type: "footer", language: lang },
sort: "sort",
limit: 1,
lookup: NAVIGATION_CONTENT_LOOKUP
}),
])
headerNav = headerEntries[0] || null
footerNav = footerEntries[0] || null
// Load content for current path. Limit 1 so SSR tracks content:<id> instead of content:*.
const contentEntries = await getCachedEntries<"content">(
"content",
{
lang,
path: routePath,
active: true,
},
"sort",
1,
undefined,
undefined,
{ aggregate: "comments:contentId:count" },
CONTENT_MEDIA_LOOKUP
)
const contentEntries = await getCachedEntries<"content">("content", {
filter: { lang, path: routePath, active: true },
sort: "sort",
limit: 1,
aggregate: "comments:contentId:count",
lookup: CONTENT_MEDIA_LOOKUP
})
if (contentEntries.length > 0) {
contentEntry = contentEntries[0]
@@ -208,11 +193,10 @@
}
try {
comments = await getCachedEntries(
"comments",
{ active: true, contentId: contentEntry.id as string },
"sort"
)
comments = await getCachedEntries("comments", {
filter: { active: true, contentId: contentEntry.id as string },
sort: "sort"
})
} catch (e) {
console.error("Failed to load comments", e)
comments = []
+9 -35
View File
@@ -149,63 +149,37 @@ type EntryTypeSwitch<T extends string> = T extends "medialib"
export async function getDBEntries<T extends CollectionNameT>(
collectionName: T,
filter?: MongoFilter,
sort: string = "sort",
limit?: number,
offset?: number,
projection?: string,
params?: Record<string, string>,
lookup?: string
options?: ApiOptions
): Promise<EntryTypeSwitch<T>[]> {
const c = await api<EntryTypeSwitch<T>[]>(collectionName, {
filter,
sort,
limit,
offset,
projection,
params,
lookup,
})
const c = await api<EntryTypeSwitch<T>[]>(collectionName, options)
return c.data
}
export async function getCachedEntries<T extends CollectionNameT>(
collectionName: T,
filter?: MongoFilter,
sort: string = "sort",
limit?: number,
offset?: number,
projection?: string,
params?: Record<string, string>,
lookup?: string
options?: ApiOptions
): Promise<EntryTypeSwitch<T>[]> {
const filterStr = obj2str({ collectionName, filter, sort, limit, offset, projection, params, lookup })
const filterStr = obj2str({ collectionName, options })
if (cache[filterStr] && cache[filterStr].expire >= Date.now()) {
return cache[filterStr].data as EntryTypeSwitch<T>[]
}
const entries = await getDBEntries<T>(collectionName, filter, sort, limit, offset, projection, params, lookup)
const entries = await getDBEntries<T>(collectionName, options)
cache[filterStr] = { expire: Date.now() + CACHE_TTL, data: entries }
return entries
}
export async function getDBEntry<T extends CollectionNameT>(
collectionName: T,
filter: MongoFilter,
projection?: string,
params?: Record<string, string>,
lookup?: string
options?: ApiOptions
): Promise<EntryTypeSwitch<T> | undefined> {
return (await getDBEntries<T>(collectionName, filter, "_id", 1, undefined, projection, params, lookup))?.[0]
return (await getDBEntries<T>(collectionName, { ...options, limit: 1 }))?.[0]
}
export async function getCachedEntry<T extends CollectionNameT>(
collectionName: T,
filter: MongoFilter,
projection?: string,
params?: Record<string, string>,
lookup?: string
options?: ApiOptions
): Promise<EntryTypeSwitch<T> | undefined> {
return (await getCachedEntries<T>(collectionName, filter, "_id", 1, undefined, projection, params, lookup))?.[0]
return (await getCachedEntries<T>(collectionName, { ...options, limit: 1 }))?.[0]
}
export async function postDBEntry<T extends CollectionNameT>(
+1 -1
View File
@@ -242,7 +242,7 @@ function cloneEntry<T>(entry: T): T {
}
function applyAggregate(entry: Record<string, unknown>, options?: ApiOptions): Record<string, unknown> {
const rawAggregate = options?.params?.aggregate
const rawAggregate = options?.aggregate || options?.params?.aggregate
if (!rawAggregate) return entry
const aggregates = Array.isArray(rawAggregate)
+1
View File
@@ -15,6 +15,7 @@ interface ApiOptions {
filter?: MongoFilter
sort?: string
lookup?: string
aggregate?: string | Record<string, any>
limit?: number
offset?: number
projection?: string