Problem
Plugin server entrypoints can read content via api.cms.content.table(slug).list() and getPublishedSnapshot(), but media-typed cells (e.g. featuredMedia) come back as bare media asset ids. There is no way for a plugin to turn an id into its public URL:
api.cms.media.* only covers storage adapters / URL transformers / variant delegates — no read/resolve.
content.list() and getPublishedSnapshot() return raw cells (ids, not paths).
- The public path (
/uploads/<storage_path>) is not derivable from the id, and there is no serve-by-id route.
The built-in data.rows loop source resolves featuredMediaPath via internal SQL (resolveMediaIdsToPaths), but a sandboxed plugin loop source cannot run SQL — so a plugin-provided source can return titles/links but not thumbnails.
Concretely this blocks a fairly common plugin: a loop source that filters rows by a field/tag value (the built-in data.rows only filters by tableId) and renders an image grid.
Proposed solution
Add a read-only batch resolver to the plugin server API:
api.cms.media.resolve(ids: string[]):
Promise<Record<string, { url: string; width: number | null; height: number | null }>>
- Absent/deleted ids omitted from the result.
- Gated by a light read permission (or the existing content-read grant).
- Can reuse the exported
resolveMediaIdsToPaths(db, ids) in src/core/loops/sources/dataRows.ts, wired through the existing plugin RPC dispatch (server/plugins/host/handlers/media.ts, server/plugins/protocol/schemas/media.ts, apiDispatch.ts).
Alternative shape: resolve media-typed cells directly in content.list() / getPublishedSnapshot() results (a resolvedMedia: Record<fieldId, { url, width, height }> map per entry).
Happy to send a PR for whichever shape you prefer.
Problem
Plugin server entrypoints can read content via
api.cms.content.table(slug).list()andgetPublishedSnapshot(), but media-typed cells (e.g.featuredMedia) come back as bare media asset ids. There is no way for a plugin to turn an id into its public URL:api.cms.media.*only covers storage adapters / URL transformers / variant delegates — no read/resolve.content.list()andgetPublishedSnapshot()return rawcells(ids, not paths)./uploads/<storage_path>) is not derivable from the id, and there is no serve-by-id route.The built-in
data.rowsloop source resolvesfeaturedMediaPathvia internal SQL (resolveMediaIdsToPaths), but a sandboxed plugin loop source cannot run SQL — so a plugin-provided source can return titles/links but not thumbnails.Concretely this blocks a fairly common plugin: a loop source that filters rows by a field/tag value (the built-in
data.rowsonly filters bytableId) and renders an image grid.Proposed solution
Add a read-only batch resolver to the plugin server API:
resolveMediaIdsToPaths(db, ids)insrc/core/loops/sources/dataRows.ts, wired through the existing plugin RPC dispatch (server/plugins/host/handlers/media.ts,server/plugins/protocol/schemas/media.ts,apiDispatch.ts).Alternative shape: resolve media-typed cells directly in
content.list()/getPublishedSnapshot()results (aresolvedMedia: Record<fieldId, { url, width, height }>map per entry).Happy to send a PR for whichever shape you prefer.