Digging Deeper

Details

Describe a resource to clients with a thin base, then ask for fields, relations, actions, metrics and authorizations with the with parameter.

The details endpoint, GET /{resource}, describes a resource for the current user: its fields, relations, scopes, actions, metrics, rules, text search, gates and authorizations. A frontend reads it once to build a list, a form or the dialog of an action.

The endpoint answers a thin base. Ask for the rest with the with query parameter: what is not asked for is not computed.

The base

GET /api/artworks
{"data": {
  "fields": [
    {"name": "id", "label": null, "type": "id", "nullable": false},
    {"name": "title", "label": "Title", "type": "text", "nullable": false},
    {"name": "medium", "label": null, "type": "enum", "nullable": false, "values": ["oil", "ink", "watercolor"]}
  ],
  "limits": [10, 25, 50],
  "meta": {}
}}
  • fields has one entry per field, in declared order: name, label (the field's label(), or null), type and nullable. An Enum field adds values, the backing values of the enum, and offered, the values that writes accept, when its offered() leaves cases out:
{"name": "status", "label": null, "type": "enum", "nullable": false, "values": ["planned", "on_sale", "sold_out"], "offered": ["planned", "on_sale"]}
  • limits lists the page sizes that search.limit accepts.
  • meta holds what the resource's detailsMeta() returns, see below.
  • The endpoint checks viewAny on the resource. Without authorizations, actions.authorized or metrics.authorized in with, it calls no other policy.

Asking for more with with

with is a comma-separated list of sections, such as actions, and parts of a section, such as actions.authorized:

GET /api/artworks?with=fields.capabilities,actions.authorized

A part adds its section with the base keys of that section. A repeated value counts once, and an empty with answers the base.

withAdds
fields.capabilitiesselect, sort, create and update on each field
fields.filterfilter.operators on each field
fields.rulesrules on each field: all, create and update
fields.metameta on each field
relationseach relation: resource, relation and name, plus types and morph on a MorphTo
relations.constraintsconstraints on each relation
relations.operationsoperations on each relation
relations.pivot_fieldspivot_fields on each many-to-many relation
actionseach action: name, uriKey, target, destructive, confirm and confirmation
actions.fieldsthe input entries of each action
actions.authorizedauthorized on each action (policy calls)
actions.metameta on each action
metricseach metric: name, uriKey, type and ranges
metrics.authorizedauthorized on each metric (policy calls)
metrics.metameta on each metric
instructionseach instruction: name, uriKey and text
instructions.fieldsthe input entries of each instruction
instructions.metameta on each instruction
scopeseach scope: name
scopes.fieldsthe input entries of each scope
scopes.metameta on each scope
rulesthe rules of the resource
textthe text search, null without one
gatesthe names that search.gates accepts
confirmationsthe writes that ask for a confirmation
authorizationsthe class-level abilities for the user (policy calls)

Any other value is a 422 on with that lists the accepted values:

{
  "message": "The with field accepts these values, separated by commas: fields.capabilities, fields.filter, fields.rules, fields.meta, relations, relations.constraints, relations.operations, relations.pivot_fields, actions, actions.fields, actions.authorized, actions.meta, metrics, metrics.authorized, metrics.meta, instructions, instructions.fields, instructions.meta, scopes, scopes.fields, scopes.meta, rules, text, gates, confirmations, authorizations.",
  "errors": {"with": ["The with field accepts these values, separated by commas: fields.capabilities, fields.filter, fields.rules, fields.meta, relations, relations.constraints, relations.operations, relations.pivot_fields, actions, actions.fields, actions.authorized, actions.meta, metrics, metrics.authorized, metrics.meta, instructions, instructions.fields, instructions.meta, scopes, scopes.fields, scopes.meta, rules, text, gates, confirmations, authorizations."]}
}

The parsing is strict:

  • fields and limits are always in the base: with=fields is a 422.
  • There is no third level (actions.fields.rules) and no wildcard (*).
  • Values are case sensitive, take no space after a comma, and no empty value between two commas.
  • A list, with[]=gates, is a 422: The with field must be a string.

One request per screen

Ask each screen for what it shows:

GET /api/orders?with=fields.capabilities,fields.filter,scopes.fields,actions.authorized
GET /api/orders?with=fields.capabilities,fields.rules,relations.operations,relations.constraints,authorizations
GET /api/orders?with=actions.fields,actions.authorized

The first one serves a list (sortable and filterable columns, scopes, bulk action buttons), the second a create or edit form, the third the dialog of an action.

Fields

GET /api/artworks?with=fields.capabilities,fields.filter,fields.rules,fields.meta
{"name": "title", "label": "Title", "type": "text", "nullable": false, "select": true, "sort": true, "filter": {"operators": ["=", "!=", "like", "not like", "ilike", "not ilike", "in", "not in"]}, "create": true, "update": true, "rules": {"all": ["string", "max:120"], "create": ["required"], "update": ["filled"]}, "meta": {}}
  • select: accepted in search.selects and returned. sort: accepted in search.sorts.
  • filter.operators: the operators that search.filters accept on the field, [] when it is not filterable.
  • create and update: writable on that operation.
  • rules.all holds the type rule, nullable and rules(). rules.create and rules.update hold creationRules() and updateRules().
  • Rules are strings: a pipe string is split, a rule object that casts to a string gives that string, an enum rule gives enum, another rule object gives its snake class name, and closures are left out.

See Fields.

Relations

GET /api/artworks?with=relations.constraints,relations.operations
{"resource": "App\\Rest\\Resources\\ExhibitionResource", "relation": "exhibitions", "constraints": {"required_on_creation": false, "prohibited_on_creation": false, "required_on_update": false, "prohibited_on_update": false}, "operations": ["create", "update", "attach", "detach", "sync", "toggle"], "name": "BelongsToMany"}
  • resource is the class of the related resource. A MorphTo with several targets has no resource key.
  • operations lists the operations that the entries of the relation accept in mutate, delete last on a deletable() relation, [] on HasOneThrough and HasManyThrough. Read the operations from it, not from name.
  • A MorphTo adds types, each morph type mapped to its resource class, and morph, its type_column and key_column.
  • relations.pivot_fields adds pivot_fields to each many-to-many relation, with every key of a field entry.

Detect pivot_fields, types and morph by their presence, not by name. See Relations.

Actions and metrics

GET /api/artworks?with=actions.fields,actions.authorized,metrics
{"data": {
  "actions": [{"name": "Retire Artworks", "uriKey": "retire-artworks", "target": "search",
    "fields": [{"name": "notify", "label": null, "type": "boolean", "nullable": false, "default": false, "rules": ["boolean"], "meta": {}}],
    "destructive": false, "confirm": null, "confirmation": false, "authorized": true}],
  "metrics": [{"name": "Artworks By Medium", "uriKey": "artworks-by-medium", "type": "partition", "ranges": []}],
  "fields": [...],
  "limits": [10, 25, 50],
  "meta": {}
}}
  • target is search, resources or none, see Actions.
  • destructive and confirm are hints for clients. confirmation is true when the action declares a server-checked confirmation.
  • An input entry holds name, label, type, nullable, default, rules and meta, plus values for an Enum, and offered when its offered() leaves cases out. A frontend can render it as a form.
  • authorized is true when the user passes every ability that the route of the action or the metric checks before it reads the request. A refused one stays in the list with false, and its route answers 403.

Instructions and scopes

GET /api/artworks?with=instructions,scopes
{"instructions": [{"name": "By Medium", "uriKey": "by-medium", "text": true}], "scopes": [{"name": "pricedUnder"}]}

An instruction's text says whether it can be sent next to a search.text. instructions.fields and scopes.fields add their input entries. See Scopes and instructions.

Rules, text and gates

GET /api/artworks?with=rules,text,gates
{
  "rules": {"all": {}, "create": {"price": ["required", "min:0"]}, "update": {}},
  "text": {"fields": ["title"], "modes": ["words", "phrase"], "relevance": false, "trashed": false, "max_length": 255, "engine": null},
  "gates": ["view", "create", "update", "delete"]
}
  • rules holds rules(), createRules() and updateRules() of the resource, keyed by attribute.
  • text describes the text search. On ScoutEngine, engine describes the filters and sorts of the index, see Search engines.
  • gates lists the abilities that search.gates accepts, then the gates that the resource declares. restore and forceDelete come after delete when the model uses SoftDeletes. It is [] when the gates are off. See Gates.

Confirmations

GET /api/documents?with=confirmations
{"confirmations": {"mutate": false, "destroy": true, "restore": false, "forceDestroy": false}}

confirmations says, for each write endpoint, whether the resource declares its confirmation hook, such as destroyConfirmation(). restore and forceDestroy appear when the model uses SoftDeletes. A true endpoint can answer 428: see Confirmation.

Authorizations

GET /api/artworks?with=authorizations
{"authorizations": {"viewAny": {"allowed": true, "message": null}, "create": {"allowed": false, "message": "Only curators add artworks."}}}

authorizations answers each class-level ability that the routes of the resource check: viewAny, create, the abilities that abilities() returns for each endpoint (restore and forceDestroy only when the model uses SoftDeletes), then those of the actions and the metrics. Row abilities are not here: list them in the gates of a search. A resource whose isAuthorizingEnabled() returns false answers "allowed": true to every ability.

The answers go through the decisions of the request, and an ability that authorizationCache() declares is read from the cache.

Meta

detailsMeta() adds keys under meta. It runs on each details request, for its user:

public function detailsMeta(): array
{
    return [
        'label' => 'Artworks',
        'default_sort' => ['field' => 'title', 'direction' => 'desc'],
        'curator' => $this->request()->user()?->name,
    ];
}
{"meta": {"label": "Artworks", "default_sort": {"field": "title", "direction": "desc"}, "curator": "Ada"}}

Behaviour

  • The details list what the declarations of the resource return for the current request, so a list that depends on $this->request()->user() changes with the user.
  • actions, instructions and relations are lists. Maps are objects, also when empty: "meta": {}.
  • The describing() event runs after viewAny, and described() receives the details as the request asked for them. See Events and hooks.
  • A declaration error throws when the request reads that declaration, for example InvalidTextSearchException with with=text.
  • Clients ignore the keys that they do not know: later releases can add keys.
  • The OpenAPI document gives each resource a {Resource}Details schema, where the base keys are required and the sections and parts optional, and documents the with parameter. See OpenAPI.