Visibility
Visibility decides whether an entity exists for a request. A Hidden entity is left out of listings and search, including their totals and facets, and a direct request for it gets the same 404 as a missing entity. Visibility vs Access explains how visibility and access combine.
| Option | Required | Type | Default | Purpose |
|---|---|---|---|---|
entityVisibilityFilter |
yes | EntityVisibilityFilter |
none | Prisma condition matching the entities visible to a request. |
searchVisibilityFilter |
yes | SearchVisibilityFilter |
none | Condition over the declared search fields matching the same entities. |
promptLoginForHidden |
no | boolean |
false |
Answer anonymous requests for Hidden entities with 401. |
Both filters receive the request context and may be async. They must match the same entities, and any field they use must be in both the database and the search index; see why the two filters must agree.
entityVisibilityFilter
Section titled “entityVisibilityFilter”Returns a Prisma EntityWhereInput. arocapi combines it with AND into every database query: /entities, /files
(through each file’s entity), /entity/:id, /entity/:id/metadata, /file/:id and the reload of search hits.
searchVisibilityFilter
Section titled “searchVisibilityFilter”Returns a SearchVisibilityWhere: a condition over id and the fields declared in your
search backend, in one language every backend applies, so it carries over unchanged when
you switch backends. POST /search calls it on every search and passes the result to the backend as the request’s
visibility, and the backend applies it to the hits, the total and the facets. OpenSearchBackend translates it to a
bool filter beside the query.
Type it as SearchVisibilityFilter<typeof fields>, with fields your backend’s declarations as const, so an
undeclared field or a value of the wrong type fails to compile.
Conditions
Section titled “Conditions”Each key is id or a declared field, including fields with no role, which are stored only for this filter. geo
fields can’t be used. Several keys in one object must all match.
| Condition | Fields | Matches when |
|---|---|---|
a bare value, or { equals } |
every type but geo |
any value equals it |
{ not } |
every type but geo |
no value equals it, including when there are none |
{ in: [...] } |
every type but geo |
any value is in the list; an empty list matches nothing |
{ notIn: [...] } |
every type but geo |
no value is in the list, including when there are none |
{ lt, lte, gt, gte } |
date and number |
any value is within every bound given |
null, or { equals: null } |
every type but geo |
the field has no value |
{ not: null } |
every type but geo |
the field has a value |
Operators in one condition must all match, so { gte: 5, not: 10 } is a value from 5 that isn’t 10. A text field
compares its exact value, not its words. Date values are a Date or a YYYY, YYYY-MM, YYYY-MM-DD or RFC 3339
string, read like the dates in search documents: a partial date is its
first UTC instant, and a date-time with no offset is UTC.
AND, OR and NOT combine filters, each taking an array of them. AND matches when every filter does, OR when
any does (an empty OR matches nothing) and NOT when none does.
Semantics
Section titled “Semantics”A field with several values matches when any of them does, and not and notIn match when none does. So
draft: { not: true } keeps entities that have no draft value at all, where Prisma’s not on a nullable column
leaves out its nulls. null means the field has no value, not an SQL null.
AllVisibleSearchFilter returns {}, which matches every entity.
Invalid filters
Section titled “Invalid filters”The backend checks the filter on every search. An undeclared field, an operator that doesn’t fit the field’s type,
such as lt on a keyword, or a bad value, such as a string for a number, fails the search with a 500
INTERNAL_ERROR, so a mistake never shows hidden entities. The log has the error, naming the field.
If the two filters disagree, search drops any hit the entity filter hides and logs a warning. The page can then be
shorter than limit, and total still counts the dropped hits.
For a public repository, use the exported filters that make every entity visible, with AllPublicAccess:
import { AllPublicAccess, AllVisibleEntityFilter, AllVisibleSearchFilter, type Options } from 'arocapi';
export const guards = { entityVisibilityFilter: AllVisibleEntityFilter, searchVisibilityFilter: AllVisibleSearchFilter, access: AllPublicAccess,} satisfies Partial<Options>;To hide whole collections, such as ones withdrawn from publication, build both filters from the same list. id is
always indexed, and rootCollection needs declaring as a field with no role:
import type { EntityVisibilityFilter, SearchVisibilityFilter } from 'arocapi';import type { FastifyRequest } from 'fastify';
// Your app's lookup of the top-level collections a request may not see, such as withdrawn onesdeclare function hiddenCollections(request: FastifyRequest): Promise<string[]>;
// Your search backend's field declarations, with rootCollection stored for visibility onlyconst fields = { name: { type: 'text' }, rootCollection: { type: 'keyword' },} as const;
// Hides each collection and, through rootCollection, everything in itexport const entityVisibilityFilter: EntityVisibilityFilter = async ({ request }) => { const hidden = await hiddenCollections(request);
return { id: { notIn: hidden }, OR: [{ rootCollection: null }, { rootCollection: { notIn: hidden } }] };};
export const searchVisibilityFilter: SearchVisibilityFilter<typeof fields> = async ({ request }) => { const hidden = await hiddenCollections(request);
return { id: { notIn: hidden }, rootCollection: { notIn: hidden } };};Visibility doesn’t pass down from collections. A member of a Hidden collection is only Hidden if your filters hide it,
which is why both filters also check rootCollection. In Prisma, notIn leaves out rows where the column is null, so
the entity filter keeps top-level collections with an explicit rootCollection: null. The search filter needs no such
clause, as its notIn already matches entities with no rootCollection.
Hide only what shouldn’t be published. An entity a request lacks the licence for should stay Visible and be Denied, so its Record still says where to apply; see Licence checks against an external authority.
promptLoginForHidden
Section titled “promptLoginForHidden”When true, an Anonymous request for a Hidden entity gets 401 UNAUTHORIZED instead of 404, in case logging in makes
it visible. Authenticated requests still get 404, and missing entities always do. It applies to /entity/:id,
/entity/:id/metadata and /file/:id. The trade-off is described under
Hidden is 404, unless you prompt a login.
It requires isAuthenticated; registration throws
promptLoginForHidden requires isAuthenticated without it. See Error responses for the
full set of outcomes, and Logged-in only for using it to hide a whole
repository from anonymous visitors.