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 ## 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 ```ts
const products = await getCachedEntries<"machines">( const products = await getCachedEntries<"machines">("machines", {
"machines", filter: { active: true, category: catId },
{ active: true, category: catId }, sort: "sortOrder",
"sortOrder", lookup: "images:medialib", // lookup: "feld:collection"
undefined, })
undefined,
undefined,
undefined,
"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. 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: 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 ```ts
const entries = await getCachedEntries<CollectionName>( const entries = await getCachedEntries<CollectionName>("your-collection", {
"your-collection", filter: { active: true },
{ active: true }, sort: "sortOrder",
"sortOrder", lookup: "imageField:medialib",
undefined, })
undefined,
undefined,
undefined,
"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. 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 ```ts
// entries mit aufgelösten medialib-Bildern laden // entries mit aufgelösten medialib-Bildern laden
const entries = await getCachedEntries<"content">( const entries = await getCachedEntries<"content">("content", {
"content", filter: { active: true },
{ active: true }, sort: "sort",
"sort", lookup: "blocks.heroImage.image:medialib"
undefined, })
undefined,
undefined,
undefined,
"blocks.heroImage.image:medialib"
)
// Ergebnis: entry._lookup enthält die aufgelösten Referenzen // 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 ## Tailwind CSS 4
+1 -1
View File
@@ -1,2 +1,2 @@
ADMIN_TOKEN=5bdfjc78hdxn338cuhSJ 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 rawAggregate = (options && options.aggregate) || (options && options.params && options.params.aggregate);
const aggregates = typeof options.params.aggregate === "string" if (rawAggregate) {
? options.params.aggregate.split(",") const aggregates = typeof rawAggregate === "string"
? rawAggregate.split(",")
: []; : [];
for (const a of aggregates) { for (const a of aggregates) {
// simple format: "collectionName:foreignField:..." // 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?.offset) query += "&offset=" + options.offset
if (options?.projection) query += "&projection=" + options.projection if (options?.projection) query += "&projection=" + options.projection
if (options?.lookup) query += "&lookup=" + options.lookup 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) { if (options?.params) {
Object.keys(options.params).forEach((p) => { Object.keys(options.params).forEach((p) => {
+23 -39
View File
@@ -159,45 +159,30 @@
try { try {
// Load navigation // Load navigation
const [headerEntries, footerEntries] = await Promise.all([ const [headerEntries, footerEntries] = await Promise.all([
getCachedEntries<"navigation">( getCachedEntries<"navigation">("navigation", {
"navigation", filter: { type: "header", language: lang },
{ type: "header", language: lang }, sort: "sort",
"sort", limit: 1,
1, lookup: NAVIGATION_CONTENT_LOOKUP
undefined, }),
undefined, getCachedEntries<"navigation">("navigation", {
undefined, filter: { type: "footer", language: lang },
NAVIGATION_CONTENT_LOOKUP sort: "sort",
), limit: 1,
getCachedEntries<"navigation">( lookup: NAVIGATION_CONTENT_LOOKUP
"navigation", }),
{ type: "footer", language: lang },
"sort",
1,
undefined,
undefined,
undefined,
NAVIGATION_CONTENT_LOOKUP
),
]) ])
headerNav = headerEntries[0] || null headerNav = headerEntries[0] || null
footerNav = footerEntries[0] || null footerNav = footerEntries[0] || null
// Load content for current path. Limit 1 so SSR tracks content:<id> instead of content:*. // Load content for current path. Limit 1 so SSR tracks content:<id> instead of content:*.
const contentEntries = await getCachedEntries<"content">( const contentEntries = await getCachedEntries<"content">("content", {
"content", filter: { lang, path: routePath, active: true },
{ sort: "sort",
lang, limit: 1,
path: routePath, aggregate: "comments:contentId:count",
active: true, lookup: CONTENT_MEDIA_LOOKUP
}, })
"sort",
1,
undefined,
undefined,
{ aggregate: "comments:contentId:count" },
CONTENT_MEDIA_LOOKUP
)
if (contentEntries.length > 0) { if (contentEntries.length > 0) {
contentEntry = contentEntries[0] contentEntry = contentEntries[0]
@@ -208,11 +193,10 @@
} }
try { try {
comments = await getCachedEntries( comments = await getCachedEntries("comments", {
"comments", filter: { active: true, contentId: contentEntry.id as string },
{ active: true, contentId: contentEntry.id as string }, sort: "sort"
"sort" })
)
} catch (e) { } catch (e) {
console.error("Failed to load comments", e) console.error("Failed to load comments", e)
comments = [] comments = []
+9 -35
View File
@@ -149,63 +149,37 @@ type EntryTypeSwitch<T extends string> = T extends "medialib"
export async function getDBEntries<T extends CollectionNameT>( export async function getDBEntries<T extends CollectionNameT>(
collectionName: T, collectionName: T,
filter?: MongoFilter, options?: ApiOptions
sort: string = "sort",
limit?: number,
offset?: number,
projection?: string,
params?: Record<string, string>,
lookup?: string
): Promise<EntryTypeSwitch<T>[]> { ): Promise<EntryTypeSwitch<T>[]> {
const c = await api<EntryTypeSwitch<T>[]>(collectionName, { const c = await api<EntryTypeSwitch<T>[]>(collectionName, options)
filter,
sort,
limit,
offset,
projection,
params,
lookup,
})
return c.data return c.data
} }
export async function getCachedEntries<T extends CollectionNameT>( export async function getCachedEntries<T extends CollectionNameT>(
collectionName: T, collectionName: T,
filter?: MongoFilter, options?: ApiOptions
sort: string = "sort",
limit?: number,
offset?: number,
projection?: string,
params?: Record<string, string>,
lookup?: string
): Promise<EntryTypeSwitch<T>[]> { ): 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()) { if (cache[filterStr] && cache[filterStr].expire >= Date.now()) {
return cache[filterStr].data as EntryTypeSwitch<T>[] 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 } cache[filterStr] = { expire: Date.now() + CACHE_TTL, data: entries }
return entries return entries
} }
export async function getDBEntry<T extends CollectionNameT>( export async function getDBEntry<T extends CollectionNameT>(
collectionName: T, collectionName: T,
filter: MongoFilter, options?: ApiOptions
projection?: string,
params?: Record<string, string>,
lookup?: string
): Promise<EntryTypeSwitch<T> | undefined> { ): 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>( export async function getCachedEntry<T extends CollectionNameT>(
collectionName: T, collectionName: T,
filter: MongoFilter, options?: ApiOptions
projection?: string,
params?: Record<string, string>,
lookup?: string
): Promise<EntryTypeSwitch<T> | undefined> { ): 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>( 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> { 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 if (!rawAggregate) return entry
const aggregates = Array.isArray(rawAggregate) const aggregates = Array.isArray(rawAggregate)
+1
View File
@@ -15,6 +15,7 @@ interface ApiOptions {
filter?: MongoFilter filter?: MongoFilter
sort?: string sort?: string
lookup?: string lookup?: string
aggregate?: string | Record<string, any>
limit?: number limit?: number
offset?: number offset?: number
projection?: string projection?: string