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

feat(views): no-code REST endpoints and public embeds over document collections#1060
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 (DocumentQueryProviderDocumentRepository), 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)

…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.
@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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant