Skip to content

View

A View is a named read-only query over Schemas. It is the only atom that needs no Trigger: declaring surface mounts it. This page is the field-level contract; the concepts are in Views and The four atoms. Envelope rules are in Manifest envelope and conventions, and every diagnostic code named here is catalogued in Diagnostics.

Fields

FieldTypeRequiredDefaultRules
titleLocalizedTextnoTitle-Cased metadata.nameAdmin report label. Non-empty string or locale map.
uiSchemaobjectnoOnly on surface: staff; only the key list. Violations are VIEW_UI_INVALID.
fromstringexactly one of from / sqlName of a declared Schema (VIEW_FROM_UNKNOWN_SCHEMA). The declarative form.
sqlstringexactly one of from / sqlOne SQLite SELECT. See sql.
surfacepublic | staffyesDecides where the View mounts. See Surfaces.
cache{ sharedMaxAge }noAnonymous REST shared-cache hint. sharedMaxAge is an integer from 1 to 86400. Only an unguarded, declarative public View over a publishing Schema may declare it.
requiresAuthorizationRequirementsnoauth.all predicates plus one optional guard.procedure. See Authorization.
filterFilterAstnofrom form only. See Filter AST.
fieldsstring[]noevery columnfrom form only. Projection. Not shape-validated by the parser.
orderBy{ field, direction? }[]no[]from form only. direction defaults to asc.
limitnumberno50 at runtimefrom and sql. Not shape-validated by the parser; clamped at request time.
paramsJSON Schemanotype: object with properties. Reserved: page, show, cursor.

from counts as present when it is a non-empty string; sql when it is non-empty after trimming. Declaring both, or neither, is INVALID_MANIFEST_ENVELOPE at /spec with the message View.spec requires exactly one of from or sql. Combining sql with filter, fields or orderBy is rejected the same way at /spec/<key>.

Declarative example

yaml
apiVersion: cms.mantle.aotter.net/v1
kind: View
metadata:
  name: my-support-requests
spec:
  title: { en: My support requests, "zh-TW": 我的客服請求 }
  surface: public
  from: support-requests
  requires:
    auth:
      all: [ctx.user]
  fields: [id, ticketNumber, subject, requestStatus, submittedAt]
  filter:
    and:
      - eq: { field: submittedBy, value: { "$ctx.user": "id" } }
      - eq: { field: requestStatus, value: { $param: requestStatus } }
  orderBy:
    - { field: submittedAt, direction: desc }
  limit: 100
  params:
    type: object
    required: [requestStatus]
    properties:
      requestStatus: { type: string, enum: [open, waiting, closed] }

The Schema this reads must index submittedBy as the leftmost field of some tuple, otherwise the identity filter is rejected. See Schema indexes.

For caller-independent published data, a View may opt its anonymous REST response into the deployment cache:

yaml
spec:
  surface: public
  from: published-notes
  cache: { sharedMaxAge: 3600 }

This emits Cache-Control: public, max-age=0, s-maxage=3600 only when the request has no cookie or authorization header and the Cloudflare Worker has a valid cacheScope. MCP, WebMCP, staff, guarded, SQL and operational-schema reads remain uncached. Invalid combinations fail with VIEW_CACHE_INVALID.

SQL example

yaml
apiVersion: cms.mantle.aotter.net/v1
kind: View
metadata:
  name: order-lines-by-status
spec:
  title: Order lines by status
  surface: staff
  sql: |
    SELECT o._mantle_id AS orderId,
           o.orderNumber AS orderNumber,
           json_extract(line.value, '$.sku') AS sku,
           json_extract(line.value, '$.quantity') AS quantity
    FROM orders AS o
    JOIN json_each(o.lines) AS line
    WHERE o.orderStatus = :orderStatus
    ORDER BY o.orderNumber ASC
  params:
    type: object
    required: [orderStatus]
    properties:
      orderStatus: { type: string, enum: [paid, shipped, cancelled] }
  limit: 200
  uiSchema:
    list:
      columns: [orderNumber, sku, quantity]
      searchFields: [orderNumber, sku]
      filterFields: [sku]

Filter AST

filter is a tree. Every node is an object with exactly one key: a comparison operator, or and / or.

KeyShapeRules
eq, gt, gte, lt, lte{ field, value }Only those two keys. field is a non-empty string. value must be present; null is a legal value.
and, orarray of nodesNon-empty. Nests to any depth.

Any other key, a node with zero or several keys, an array node, or an empty and / or is INVALID_MANIFEST_ENVELOPE at the node's pointer.

Value forms

FormWritten asRules
Literalvalue: publishedAny JSON scalar, including null. Compared as written.
Param referencevalue: { $param: locale }The name must be declared under params.properties (VIEW_FILTER_PARAM_REF_UNKNOWN, also raised when no params is declared at all or the name is empty) and listed in params.required (VIEW_FILTER_PARAM_REF_NOT_REQUIRED).
Caller identityvalue: { "$ctx.user": "id" }The sentinel is closed: exactly one key, the literal string id, and only under eq. Anything else is VIEW_FILTER_CTX_USER_REF_INVALID.

The identity sentinel carries two further graph-level obligations, checked once from resolves:

RuleDiagnostic
The View declares ctx.user in requires.auth.all.VIEW_FILTER_CTX_USER_REF_REQUIRES_AUTH at /spec/requires/auth/all
The compared field is the leftmost field of some uniqueIndexes or indexes tuple on the source Schema.VIEW_FILTER_CTX_USER_REF_REQUIRES_INDEX

Provider claims and platform identities are deliberately out of reach; id is the only bindable identity value.

Graph-level field checks

For a from View, the valid field names are the top-level keys of the Schema's properties plus the reserved entry columns. Unknown names are rejected per site:

LocationDiagnostic
filter.<op>.fieldVIEW_FILTER_FIELD_NOT_IN_SCHEMA
fields[i], orderBy[i].fieldVIEW_FIELD_NOT_IN_SCHEMA
uiSchema.list.<key>[i]VIEW_UI_INVALID
from itselfVIEW_FROM_UNKNOWN_SCHEMA (no further field checks run)

None of these run for a sql View — its output columns are whatever the SELECT produces.

sql

One statement, read-only, compiled and bound by the runtime.

RuleEffect
The trimmed text matches /^select\b/i and contains no ;.Otherwise INVALID_MANIFEST_ENVELOPE at /spec/sql: View.spec.sql must be one SELECT statement without a semicolon.
Every :name occurrence is declared in params.properties.VIEW_FILTER_PARAM_REF_UNKNOWN
Every :name occurrence is listed in params.required.VIEW_FILTER_PARAM_REF_NOT_REQUIRED
filter, fields and orderBy are absent.INVALID_MANIFEST_ENVELOPE at /spec/<key>
Tables are Schema names.Each Schema is exposed as a logical table reconciled at boot. Names containing - must be double-quoted: FROM "post-translations".

Bound params are passed as positional values; caller input is never interpolated into the statement. SQLite JSON functions are available, so json_each and json_extract can unnest and project array or object members of data — the SQL example above does both.

Warningsql Views are native SQLite. Static validation never executes the statement, so a syntax or column error surfaces only when the View runs. On a storage adapter that does not support the native dialect the View fails at prepare time with VIEW_DIALECT_UNSUPPORTED, naming the dialects that adapter does support.

params

params declares the caller-supplied query shape and is walked by the JSON Schema subset validator.

RuleDiagnostic
A non-array object.VIEW_PARAMS_INVALID_SHAPE at /spec/params
type: "object".VIEW_PARAMS_INVALID_SHAPE at /spec/params/type
properties is declared and is an object.VIEW_PARAMS_INVALID_SHAPE at /spec/params/properties
No property named page, show or cursor.VIEW_PARAMS_RESERVED_NAME

The runtime owns those three names for pagination, which is why they cannot be redeclared. Rename the domain param (pageSize, showArchived).

orderBy, fields and limit

orderBy is an array of objects accepting only field and direction. A non-array value, a non-object entry, a missing or empty field, or a direction other than asc / desc is VIEW_ORDERBY_INVALID at the offending pointer; an unrecognized key inside an entry is INVALID_MANIFEST_ENVELOPE.

fields and limit receive no shape validation in the parser. A fields value that is not an array of strings, or a limit that is not a number, is not reported as a diagnostic — it fails later, at graph validation or at request time. Declare them as documented.

limit is a per-View cap, and the runtime clamps around it on every call:

InputResult
limit missing, non-numeric, non-finite or <= 0Cap is 50.
limit validCap is min(floor(limit), 500). 500 is the hard ceiling for any single round-trip.
?show= missing or not a positive finite numberPage size is the cap.
?show= validPage size is min(floor(show), cap).
?page= missing or below 1Page 1.

uiSchema.list

Admin presentation for surface: staff Views. Declaring uiSchema on a public View is VIEW_UI_INVALID, as is any root key other than list or any key inside list other than the three below.

KeyMeaning
columnsOrdered columns for the Admin report table and the default CSV column set.
searchFieldsOutput fields the Admin substring search box covers.
filterFieldsOutput fields offered as exact-match filters (?filter.<field>=).

Each is an array of non-empty strings with no duplicates within the key. The characters ", \ and NUL are rejected in a field name. The names are View output field names — SQL aliases for a sql View; for a from View they are additionally checked against the Schema's properties plus reserved columns.

Admin applies search and filters before pagination, rejecting a search term or filter value longer than 200 characters and any filter.<field> key that is not a declared filterFields entry. GET /admin/api/views/<name>/export streams the same query as CSV covering every matching row, not only the visible page; its columns come from uiSchema.list.columns, falling back to spec.fields and then to the union of keys in the returned rows.

Surfaces

surfaceRESTMCP toolAdmin
publicGET /api/views/<name>, plus a catalog at GET /api/viewsquery_view_<segment> on /mcpAlso mounted at GET /admin/api/views/<name> and /export behind the staff gate
staffGET /admin/api/views/<name> and /admin/api/views/<name>/export — not mounted publiclyquery_view_<segment> on /mcp/staffReport sidebar

<segment> is metadata.name lower-cased with - replaced by _. Two Views that mangle to the same segment collide with MCP_TOOL_NAME_COLLISION. Admin also serves the manifest listing GET /admin/api/views-manifest. Surface choice is visibility, not authorization: requires still gates every call on both transports. See Surfaces and MCP and agents.

The MCP inputSchema is params.properties plus page and show as optional numbers, carrying params.required through unchanged; the tool is annotated readOnlyHint: true.

REST contract

Pagination uses the two reserved knobs, ?page= (1-indexed) and ?show=. The response envelope is:

json
{ "ok": true, "data": { "rows": [], "page": 1, "show": 20, "hasMore": true } }

hasMore is the lazy form — rows.length === show. There is no COUNT query and no LIMIT n+1 probe, so a final page that exactly fills show reports hasMore: true and the next page comes back empty.

A failure returns { ok: false, diagnostic } with the diagnostic's mapped status. Static requires.auth runs before parameter validation, so an unauthorized caller never learns the parameter shape; a guard Procedure runs after validation and authorizes the whole query rather than filtering rows.

Param coercion

Query strings arrive as text. The runtime coerces each declared param by its type before validating against params; MCP callers send typed JSON and skip this step.

Declared typeCoercionRejected when
string, or type omittedUsed as-is.
integerparseInt(raw, 10)The round-trip does not equal the trimmed input, so "1.5" and "1abc" fail.
numberNumber(raw)The result is not finite.
boolean"true" / "false"Any other text.
enum (with any of the above)Coerced by type first, then checked for membership.The coerced value is not in enum.
anything elseUnsupported on the REST surface.

A missing required param or a failed coercion is INPUT_VALIDATION_FAILED (400). Unknown query keys are ignored.

Source