Skip to content

feat(views): no-code REST endpoints and public embeds over document collections - #1060

Open
mmcintosh wants to merge 2 commits into
mainfrom
feat/views-plugin
Open

mmcintosh wants to merge 2 commits into
mainfrom
feat/views-plugin

Conversation

@mmcintosh

Copy link
Copy Markdown
Collaborator

Description

Views lets an admin turn any document-model collection into a filtered, sorted, paginated REST endpoint — no code, no migration, no deploy. Pick a collection, build filter rules through a visual builder (with an OR-group escape hatch for anything the builder can't express), choose columns, and the result is a live GET /api/views/:name JSON/CSV endpoint. Optionally, a view can also be published as a chromeless public page at /v/:path, or shared via an unguessable one-off token — a stakeholder-facing table or dashboard slice with zero admin exposure.

Today, exposing a filtered slice of content as an API or a public page means writing a route by hand — new endpoint, new query logic, new caching, new access-control wiring, each one a fresh place to get tenant-scoping or public-exposure gating wrong. Views collapses that into a form: build the filter once, and you have (a) a governed JSON/CSV API for internal tooling or partner integrations, and (b) an optional, cacheable public page for anything that used to mean "ask an engineer for a one-off report."

Changes

  • Storage. Two plugin-owned tables (views, view_displays), provisioned idempotently from the plugin's onBoot — no new numbered migration, matching this repo's collections-are-code-only convention. No FK into core tables.
  • Query engine. Views compile down to the same document-model substrate every other read in the app goes through (DocumentQueryProvider → DocumentRepository), so a view's read path gets the same ACL enforcement, indexed field lookups, and keyset pagination as any other document read.
  • Filter builder. Pick a field, pick a plain-English comparison (equals, contains, starts with, is between, is one of, is empty, greater/less than), enter a value — live preview updates as you type. The same grammar is validated at save time and enforced at read time, so a saved filter is guaranteed to be servable. Field names are strictly allowlisted against the collection's actual schema — never string-concatenated into SQL.
  • Tenant isolation. Every view and every published display is scoped to the tenant that created it — one tenant's views are invisible to another, and two different tenants can each use the same view name or the same public embed path without colliding.
  • Public exposure — opt-in and double-gated. A view is private by default. The public JSON/CSV endpoint and the public embed page both require the specific view/display to be marked public and the backing collection to grant public read. Drafts never leak: both public paths force published-only regardless of what a caller's request claims.
  • Per-document ACL still applies — anonymous public reads honor per-document deny overrides via the app's existing ACL resolver, rather than the plugin re-implementing access control.
  • Share tokens are 128-bit cryptographically random, namespaced apart from ordinary paths so the two can never collide.
  • Rate limiting — both public read paths are capped at 60 req/min/IP.
  • In-plugin documentation. The generic plugin "Information" tab (/admin/plugins/:id) had an About paragraph and an unused Routes-reference extension point. Added a second, also-reusable section — How to Use, a step-by-step walkthrough — and populated Views' own manifest with both a full route reference and a 6-step usage guide (screenshots below).

Testing

Unit Tests

  • Added/updated unit tests — 66 real-SQLite tests: tenant isolation for name/path/share-token uniqueness, admin routes end-to-end through a harness mimicking the real request-tenant resolution, the public embed route resolving the correct tenant when two tenants publish the same path, cache-key isolation.
  • All unit tests passing — 66/66 (views), 1796/1796 (full suite, since this PR also touches the shared plugin-info template).

E2E Tests

  • Added/updated E2E tests — 108-views-tenant-scoping.spec.ts (two tenants creating same-named views, cross-tenant edit rejection, two tenants publishing at the identical path both resolving correctly) and 109-views-plugin.spec.ts (admin builder, public API invariants, date-filter regression).
  • All E2E tests passing — not run locally per project policy; CI validates on this PR.

Security review: verified tenant isolation across all 12 admin routes, both public read endpoints (draft/private/non-public-collection leak checks, share-token unguessability), and SQL-injection safety in the filter engine. No vulnerabilities found.

Screenshots/Videos

Building a view — pick a collection, get a live preview immediately:
Building a view

A saved view, with its live API preview:
Saved view

The Views list:
Views list

The actual public REST response — GET /api/views/published-posts, unauthenticated:
Public JSON API

Publishing a view as a public embed — pick display columns, publish, get a share link:
Publish flow

The live public embed page — /v/published-posts, zero admin chrome:
Public embed page

The filter builder — plain-English operator labels, live-filtering price < 50 against 100 seeded products:
Filter operator labels

In-plugin "How to Use" guide (Plugins → Views → Information):
How to Use

In-plugin Routes reference (same tab, right below):
Routes reference

Checklist

  • Code follows project conventions
  • Tests added/updated and passing
  • Type checking passes
  • No console errors or warnings
  • Documentation updated — in-plugin How to Use + Routes reference (see screenshots)

@mmcintosh
mmcintosh marked this pull request as ready for review August 19, 2026 00:55
@mmcintosh
mmcintosh requested a review from lane711 as a code owner August 19, 2026 00:55
…over document collections

Lets an admin turn any document-model collection into a filtered, sorted,
paginated REST endpoint without writing code: pick a collection, build filter
rules through a visual builder (with a raw-JSON escape hatch for anything the
builder can't express), choose columns, and get a live
GET /api/views/:name JSON/CSV endpoint. A view can also be published as a
chromeless public page at /v/:path, or shared via an unguessable one-off
token.

Storage: two plugin-owned tables (views, view_displays), provisioned
idempotently from the plugin's onBoot — no new numbered migration, matching
the app's collections-are-code-only convention. No FK into core tables.

Query engine: views compile down to the same document-model substrate every
other read in the app goes through (DocumentQueryProvider -> DocumentRepository),
so a view's read path gets the same ACL enforcement, indexed field lookups,
and keyset pagination as any other document read. Filter field names are
strictly allowlisted against the collection's actual schema — never
string-concatenated into SQL.

Security:
- Every view and every published display is tenant-scoped end to end; two
  different tenants can use the same view name or the same embed path without
  colliding.
- Public exposure is opt-in and double-gated: the public JSON/CSV endpoint and
  the public embed page both require the specific view/display to be marked
  public AND the backing collection to grant public read. Drafts never leak —
  both public paths force published-only regardless of what a caller claims.
- Anonymous public reads honor per-document deny ACL overrides via the app's
  existing permissions service, rather than the plugin reimplementing access
  control.
- Share tokens are 128-bit cryptographically random and rate-limited (60
  req/min/IP) on both public read paths.

Testing: 66 real-SQLite tests (tenant isolation, public-exposure gating,
cache-key isolation), tsc clean, two E2E specs (108-views-tenant-scoping,
109-views-plugin) covering admin builder flows, public API invariants, and
cross-tenant isolation end to end.
@coderabbitai coderabbitai Bot added area: admin Issue concerns admin area: api Issue concerns api area: plugins Issue concerns plugins enhancement New feature or request priority: P2 Normal: actionable defects, reliability, documentation and core enhancements status: conflicts GitHub reports merge conflicts with the current PR base; refresh after resolving. labels Oct 23, 2026

This branch was previously deployed

1 inactive deployment
internal — e07c0ae5 Deployed Sep 12, 2026 by mmcintosh via authorize #1600
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: admin Issue concerns admin area: api Issue concerns api area: plugins Issue concerns plugins enhancement New feature or request priority: P2 Normal: actionable defects, reliability, documentation and core enhancements status: conflicts GitHub reports merge conflicts with the current PR base; refresh after resolving.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants