Skip to main content

Saved Query Filters

A saved query filter is a named, reusable filter (a "saved junction") attached to a single Mapping (a query-able table or view). Its filter JSONB blob is query-language specific: for the informer Query language it is a subset of a Query's payload structure, treated as a parenthesized saved junction. The primary key id is a slug derived from name (not a UUID), mappingId is required and ties the filter to a Mapping, and an optional ownerId and sourceId round out the record.

This collection is governed only by tenant row-level security (RLS). None of its routes carry a permission check or a license-feature gate, and the model has no read_access scope, so any authenticated session in the tenant can list, create, read, update, or delete every saved query filter, regardless of the caller's access to the underlying datasources or Mappings.

Distinct from the datasource-scoped variant

A parallel, datasource-scoped set of saved-query-filter routes lives under /api/datasources/{id}/mappings/{schemaId}+{mappingId}/saved-query-filters and does gate on permission.datasource.writeStrict. The top-level /api/saved-query-filter collection documented here does not. Choose the top-level collection when you want tenant-wide management and the datasource-scoped routes when you want per-datasource permission enforcement.

GET /api/saved-query-filter

List the tenant's saved query filters, sorted by name.

Authentication: Any authenticated session (tenant RLS only; no permission check and no read_access scope)

Query Parameters:

ParameterTypeDefaultDescription
qstring-Free-text search; matches id, name, and description
sortstringnameSort field
limitinteger-Maximum number of items to return
startinteger0Offset for pagination

Response:

A HAL collection (rel inf:saved-query-filters) with items embedded under _embedded["inf:saved-query-filter"]. It is the { start, total, count, items } collection envelope. Because there is no where clause and no scope, the result includes every saved query filter in the tenant.

List saved query filtersGET /api/saved-query-filter
GET /api/saved-query-filter is a HAL collection (rel inf:saved-query-filters) of the tenant’s saved query filters, items embedded under inf:saved-query-filter, sorted by name. It is the {start,total,count,items} collection envelope. There is NO permission check and NO read_access scope: it is filtered by tenant RLS only, so it returns every saved query filter in the tenant regardless of the caller’s access to the underlying datasources/mappings. Free-text `q` matches id/name/description; supports {?sort,limit,start,q}.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/saved-query-filter?sort=name&limit=0&start=0"
}
},
"start": 0,
"count": 2,
"total": 2,
"_embedded": {
"inf:saved-query-filter": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/saved-query-filter/active-beverages"
}
},
"id": "active-beverages",
"name": "Active Beverages",
"description": "Categories whose name contains \"bev\"",
"mappingId": "00000000-0000-4000-8000-000000000001:public+categories",
"sourceId": null,
"filter": {
"op": "and",
"junctions": [
{
"op": "contains",
"field": "CategoryName",
"values": [
"bev"
]
}
]
},
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"ownerId": null
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/saved-query-filter/scratch-filter"
}
},
"id": "scratch-filter",
"name": "Scratch Filter",
"description": null,
"mappingId": "00000000-0000-4000-8000-000000000001:public+categories",
"sourceId": null,
"filter": {},
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"ownerId": null
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/saved-query-filter

Create a saved query filter.

Authentication: Any authenticated session (tenant RLS only; no permission check)

Request Body:

FieldTypeRequiredDescription
namestringNoDisplay name. When omitted it defaults to a generated name (see below)
descriptionstringNoDescription
mappingIdstringYesId of the Mapping this filter attaches to
filterobjectNoQuery-language-specific filter blob. Defaults to {}

The filter blob is query-language specific: for the informer Query language it is a subset of a Query payload, treated as a parenthesized saved junction.

When name is omitted it defaults to the joined value of mappingId, the literal Saved, the literal Filter, and the count of existing filters for that mappingId. The id is then the slug of the final name. mappingId is not validated against a real Mapping at create time, and ownerId is left null unless explicitly supplied.

Example Request:

{
"name": "Seafood and Produce",
"description": "Perishable categories",
"mappingId": "00000000-0000-4000-8000-000000000001:public+categories",
"filter": {
"op": "or",
"junctions": [
{ "field": "CategoryName", "op": "equals", "values": ["Seafood"] },
{ "field": "CategoryName", "op": "equals", "values": ["Produce"] }
]
}
}

Response:

Responds 201 Created with the saved query filter and a Location header.

Create a saved query filterPOST /api/saved-query-filter
POST /api/saved-query-filter has NO permission check (tenant RLS only). Payload is { name?, description?, mappingId (required), filter? (object, default {}) }. The `filter` blob is query-language specific — for the informer query language it is a subset of a query payload, treated as a parenthesized saved junction. When `name` is omitted it defaults to `[mappingId, "Saved", "Filter", <existing-count>].join(" ")`; `id` is the slug of the final name. `mappingId` is not validated against a real Mapping at create time. Responds 201 with a Location header.
Request body
{
"name": "Seafood and Produce",
"description": "Perishable categories",
"mappingId": "00000000-0000-4000-8000-000000000001:public+categories",
"filter": {
"op": "or",
"junctions": [
{
"field": "CategoryName",
"op": "equals",
"values": [
"Seafood"
]
},
{
"field": "CategoryName",
"op": "equals",
"values": [
"Produce"
]
}
]
}
}
Response · 201
{
"_links": {
"self": {
"href": "https://informer.example.com/api/saved-query-filter/seafood-and-produce"
}
},
"tenant": "acme",
"id": "seafood-and-produce",
"name": "Seafood and Produce",
"description": "Perishable categories",
"mappingId": "00000000-0000-4000-8000-000000000001:public+categories",
"filter": {
"op": "or",
"junctions": [
{
"op": "equals",
"field": "CategoryName",
"values": [
"Seafood"
]
},
{
"op": "equals",
"field": "CategoryName",
"values": [
"Produce"
]
}
]
},
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"sourceId": null,
"ownerId": null
}
Captured from the API examples test suite; ids and timestamps are normalized.
mappingId is not validated at create time

Because mappingId is a plain required string, attaching a filter to a nonexistent Mapping succeeds here. The mismatch only surfaces later at GET /api/saved-query-filter/{id}, where the embed href is derived from the owning Mapping's datasource.


GET /api/saved-query-filter/{id}

Retrieve a single saved query filter.

Authentication: Any authenticated session (tenant RLS only; no permission check and no read_access scope)

Path Parameters:

ParameterTypeDescription
idstringThe slug primary key

Response:

The single saved query filter (HAL rel inf:saved-query-filter). It eager-loads the owning Mapping and that Mapping's Datasource, and embeds inf:mapping, whose href is built from the Datasource naturalId plus schemaId plus mappingId. Answers 404 when the id is unknown in the tenant.

Get a saved query filterGET /api/saved-query-filter/active-beverages
GET /api/saved-query-filter/{id} returns the single saved query filter (HAL rel inf:saved-query-filter). `{id}` is the slug primary key. It eager-loads the owning `mapping` and its `datasource`, and embeds `inf:mapping` (an href built from the datasource naturalId + schemaId + mappingId). No permission check and no read_access scope: tenant RLS only. 404 if the id is unknown in the tenant.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/saved-query-filter/active-beverages"
}
},
"id": "active-beverages",
"name": "Active Beverages",
"description": "Categories whose name contains \"bev\"",
"mappingId": "00000000-0000-4000-8000-000000000001:public+categories",
"sourceId": null,
"filter": {
"op": "and",
"junctions": [
{
"op": "contains",
"field": "CategoryName",
"values": [
"bev"
]
}
]
},
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"ownerId": null,
"_embedded": {
"inf:mapping": {
"_links": {
"self": {
"href": "https://informer.example.com/api/datasources/admin%3Anorthwind/mappings/public+categories"
}
},
"id": "00000000-0000-4000-8000-000000000001:public+categories",
"name": "Categories",
"permissions": {},
"datasourceId": "00000000-0000-4000-8000-000000000001",
"schemaId": "public",
"mappingId": "categories",
"source": "pg",
"sourceId": null,
"description": null,
"hidden": false,
"records": 8,
"size": "524288",
"data": {
"size": "524288",
"records": "8",
"schemaId": "public",
"mappingId": "categories"
},
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"_changes": null,
"tenant": "acme",
"datasource": {
"languages": [],
"naturalId": "admin:northwind",
"compatibility": [],
"permissions": {
"assignTags": false,
"edit": false,
"delete": true,
"share": false,
"write": false,
"changeOwner": false,
"ping": false,
"scan": false,
"downloadSchema": false,
"importData": false,
"importSchema": false,
"rename": false,
"editConnection": false,
"createLink": false,
"setWrite": false,
"createSuite": false,
"createFieldExpression": false,
"createMapping": false,
"bundle": true,
"query": false
},
"id": "00000000-0000-4000-8000-000000000001",
"slug": "northwind",
"ownerId": "admin",
"name": "Northwind",
"features": [
{
"datasourceId": "00000000-0000-4000-8000-000000000001",
"featureId": "northwind-datasets",
"version": null,
"data": {}
},
{
"datasourceId": "00000000-0000-4000-8000-000000000001",
"featureId": "northwind-schema",
"version": null,
"data": {}
}
]
}
}
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/saved-query-filter/{id}

Update a saved query filter.

Authentication: Any authenticated session (tenant RLS only; no permission check)

Path Parameters:

ParameterTypeDescription
idstringThe slug primary key

Request Body:

FieldTypeDescription
namestringUpdate display name
descriptionstringUpdate description
filterobjectUpdate the filter blob

mappingId is not part of the update schema, so the Mapping a filter is attached to is effectively immutable through this route. Editing name does not re-slug the id: the primary key is unchanged (the slug hook only fills a missing id), so name and id can diverge after an update.

Example Request:

{
"name": "Active Beverages (Updated)",
"description": "Beverage categories, refined",
"filter": {
"op": "and",
"junctions": [
{ "field": "CategoryName", "op": "contains", "values": ["bev"] },
{ "field": "Description", "op": "isNotNull", "values": [] }
]
}
}

Response:

Responds 200 with the updated saved query filter. Answers 404 when the id is unknown.

Update a saved query filterPUT /api/saved-query-filter/active-beverages
PUT /api/saved-query-filter/{id} has NO permission check (tenant RLS only). Payload is { name?, description?, filter? }. `mappingId` is NOT updatable here, so the mapping a filter is attached to is effectively immutable through this route. Editing `name` does NOT re-slug the `id` (the PK is unchanged), so name and id can diverge after an update. Returns the updated entity (200); 404 if the id is unknown.
Request body
{
"name": "Active Beverages (Updated)",
"description": "Beverage categories, refined",
"filter": {
"op": "and",
"junctions": [
{
"field": "CategoryName",
"op": "contains",
"values": [
"bev"
]
},
{
"field": "Description",
"op": "isNotNull",
"values": []
}
]
}
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/saved-query-filter/active-beverages"
}
},
"id": "active-beverages",
"name": "Active Beverages (Updated)",
"description": "Beverage categories, refined",
"mappingId": "00000000-0000-4000-8000-000000000001:public+categories",
"sourceId": null,
"filter": {
"op": "and",
"junctions": [
{
"field": "CategoryName",
"op": "contains",
"values": [
"bev"
]
},
{
"field": "Description",
"op": "isNotNull",
"values": []
}
]
},
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"ownerId": null
}
Captured from the API examples test suite; ids and timestamps are normalized.

DELETE /api/saved-query-filter/{id}

Delete a saved query filter.

Authentication: Any authenticated session (tenant RLS only; no permission check)

Path Parameters:

ParameterTypeDescription
idstringThe slug primary key

Response:

Responds 200 with an empty body (Informer's db.remove convention), not 204.

Delete a saved query filterDELETE /api/saved-query-filter/scratch-filter
DELETE /api/saved-query-filter/{id} has NO permission check (tenant RLS only) and removes the saved query filter. Returns 200 with an EMPTY body (Informer db.remove convention), NOT 204.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.