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.
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:
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | - | Free-text search; matches id, name, and description |
sort | string | name | Sort field |
limit | integer | - | Maximum number of items to return |
start | integer | 0 | Offset 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.
{
"_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
}
]
}
}
POST /api/saved-query-filter
Create a saved query filter.
Authentication: Any authenticated session (tenant RLS only; no permission check)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Display name. When omitted it defaults to a generated name (see below) |
description | string | No | Description |
mappingId | string | Yes | Id of the Mapping this filter attaches to |
filter | object | No | Query-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.
{
"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"
]
}
]
}
}
{
"_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
}
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:
| Parameter | Type | Description |
|---|---|---|
id | string | The 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.
{
"_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": {}
}
]
}
}
}
}
PUT /api/saved-query-filter/{id}
Update a saved query filter.
Authentication: Any authenticated session (tenant RLS only; no permission check)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | The slug primary key |
Request Body:
| Field | Type | Description |
|---|---|---|
name | string | Update display name |
description | string | Update description |
filter | object | Update 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.
{
"name": "Active Beverages (Updated)",
"description": "Beverage categories, refined",
"filter": {
"op": "and",
"junctions": [
{
"field": "CategoryName",
"op": "contains",
"values": [
"bev"
]
},
{
"field": "Description",
"op": "isNotNull",
"values": []
}
]
}
}
{
"_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
}
DELETE /api/saved-query-filter/{id}
Delete a saved query filter.
Authentication: Any authenticated session (tenant RLS only; no permission check)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | The slug primary key |
Response:
Responds 200 with an empty body (Informer's db.remove convention), not 204.