✨ feat: unify API options structure and enhance lookup handling across collections
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -1,2 +1,2 @@
|
|||||||
ADMIN_TOKEN=5bdfjc78hdxn338cuhSJ
|
ADMIN_TOKEN=5bdfjc78hdxn338cuhSJ
|
||||||
ADMIN_ASSET_VERSION=db968ab-dirty-1779027503096
|
ADMIN_ASSET_VERSION=db968ab-dirty-1779028656102
|
||||||
|
|||||||
@@ -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:..."
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Vendored
+1
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user