Type: reference · Imported reference (adopted from the clarity backend developer docs)
The metadata layer exposes generic LoopBack-3-style CRUD for every entity under /exactapi/<entity>. All routes require auth and are subject to row-level access scoping (non-admins see only their granted units/sites/orgs). See SQLite metadata API for the entity model.
meta_data-flatten conventionEvery entity table has an id, a meta_data JSON document, and zero or more real FK columns (orgsId, siteId, unitsId, equipmentId, dataTagId, …). On the wire the entity is flat: meta_data keys are hoisted to the top level and merged with id and the FK columns. A stored units row { id:7, meta_data:{"name":"Boiler 1","capacity":210}, orgsId:3, siteId:5 } serializes as:
{ "id": "7", "name": "Boiler 1", "capacity": 210, "orgsId": "3", "siteId": "5" }
On write you send the flat object; anything that is not a real column is packed into meta_data.
IDs are strings in responses. The top-level
idand any*Idfield are returned as strings (whole-number floats become strings;dataTagIdandnullare left as-is; values insidemeta_dataare untouched). Send them as numbers or strings on write.
Create returns HTTP 200 (not 201) in the generic layer.
/exactapi/<E>/exactapi/<E> — listReads the filter query param (see Filter operators), returns a JSON array of flattened entities.
GET /exactapi/units?filter={"where":{"orgsId":3},"limit":2,"order":"id DESC"}
[
{ "id": "7", "name": "Boiler 1", "capacity": 210, "orgsId": "3", "siteId": "5" },
{ "id": "6", "name": "Boiler 0", "capacity": 150, "orgsId": "3", "siteId": "5" }
]
/exactapi/<E> — createFlat object body; non-column keys fold into meta_data. Returns the created entity.
POST /exactapi/units
{ "name": "Boiler 2", "capacity": 300, "orgsId": 3, "siteId": 5 }
{ "id": "8", "name": "Boiler 2", "capacity": 300, "orgsId": "3", "siteId": "5" }
Creating a tagmeta with limLo/limHi also schedules monitor-rule creation:
POST /exactapi/tagmeta
{ "tagName": "Steam Temp", "unit": "degC", "unitsId": 8, "equipmentId": 12,
"dataTagId": "VTP_G1", "limLo": 100, "limHi": 540 }
{ "id": "45", "tagName": "Steam Temp", "unit": "degC", "limLo": 100, "limHi": 540,
"unitsId": "8", "equipmentId": "12", "dataTagId": "VTP_G1" }
/exactapi/<E>/{id} — read oneReturns the flat entity, or 404 { "error": "Item not found" }. Supports ?filter={"include":[...]} (see include).
/exactapi/<E>/{id} — updateFull JSON body; merges into the existing meta_data. Returns the updated entity.
PUT /exactapi/units/7
{ "capacity": 220 }
{ "id": "7", "name": "Boiler 1", "capacity": 220, "orgsId": "3", "siteId": "5" }
/exactapi/<E>/{id} — delete oneRecursively cascade-deletes FK children (and, for orgs/sites/units, the on-disk collections) then the row. Returns a count.
{ "count": 4 }
/exactapi/<E> — delete all matchingDeletes rows matching ?where=/?filter=. Returns { "count": N }.
/exactapi/<E> — upsertIf the body has an id it updates, else inserts. Returns the entity.
PATCH /exactapi/units
{ "id": 7, "capacity": 240 }
/exactapi/<E>/bulk — bulk createBody is a JSON array; returns the array of created entities.
POST /exactapi/units/bulk
[ { "name": "B3", "orgsId": 3 }, { "name": "B4", "orgsId": 3 } ]
[ { "id": "9", "name": "B3", "orgsId": "3" }, { "id": "10", "name": "B4", "orgsId": "3" } ]
/exactapi/<E>/countOptional ?where=/?filter=. Returns { "count": N }.
/exactapi/<E>/findOneReturns a single entity or 404 { "error": "No matching record" }.
/exactapi/<E>/{id}/existsReturns { "exists": true }.
/exactapi/<E>/update — updateAll by whereWHERE from the query, patch from the body; returns { "count": N }. For orgs/sites/units, changing name also renames the on-disk collection folder. For units, a body carrying limLo/limHi+dataTagId additionally spawns monitor rules.
POST /exactapi/units/update?where={"orgsId":3}
{ "status": "active" }
{ "count": 5 }
The filter object fields: where, fields, include, limit, skip, order.
fields — ["id","name"] or {"name":true} (projected in Rust).limit / skip — integers → SQL LIMIT/OFFSET.order — "name DESC" or a list of such strings.where operators (value under a field key is { "<op>": <val> }):
{"where": {"capacity": {"eq": 210}}} // =
{"where": {"capacity": {"neq": 210}}} // != (null-inclusive)
{"where": {"capacity": {"gt": 100}}} // >
{"where": {"capacity": {"lt": 500}}} // <
{"where": {"capacity": {"gte": 100}}} // >=
{"where": {"capacity": {"lte": 500}}} // <=
{"where": {"id": {"inq": [1,2,3]}}} // IN (alias "in")
{"where": {"id": {"nin": [4,5]}}} // NOT IN
{"where": {"name": {"like": "Boiler%"}}} // LIKE
{"where": {"name": {"nlike": "Test%"}}} // NOT LIKE
{"where": {"name": {"ilike": "boiler%"}}} // case-insensitive LIKE
{"where": {"name": {"nilike": "test%"}}} // case-insensitive NOT LIKE
{"where": {"capacity": {"between": [100,300]}}}
{"where": {"tags": {"contains": "steam"}}}
{"where": {"tags": {"ncontains": "spare"}}}
{"where": {"siteId": {"exists": true}}} // IS NOT NULL / IS NULL
{"where": {"description": {"regexp": "/steam/i"}}} // regex (evaluated in Rust)
Boolean grouping:
{"where": {"and": [ {"orgsId": 3}, {"capacity": {"gt": 100}} ]}}
{"where": {"or": [ {"name": {"like": "Boiler%"}}, {"name": {"like": "Furnace%"}} ]}}
{"where": {"nor": [ {"status": "retired"} ]}}
A bare scalar is equality: {"where":{"orgsId":3}} → orgsId = 3. Because meta_data is schemaless, each field is matched against both the real column and json_extract(meta_data,'$.field').
include (relation expansion)GET /exactapi/units/7?filter={"include":["equipment","tagmeta"]}
{
"id": "7", "name": "Boiler 1", "orgsId": "3",
"equipment": [ { "id": "12", "name": "SH Panel", "unitsId": "7" } ],
"tagmeta": [ { "id": "45", "tagName": "Steam Temp", "unitsId": "7", "dataTagId": "VTP_G1" } ]
}
Generated from the FK graph:
GET /exactapi/units/{id}/equipment # list children scoped to the parent
GET /exactapi/units/{id}/equipment/{eqId} # single, scoped
GET /exactapi/units/{id}/equipment/count
POST /exactapi/units/{id}/equipment # create (parent FK injected)
DELETE /exactapi/units/{id}/equipment/{eqId} # delete (WHERE id=? AND fk=? — cross-parent safe)
Two levels deep also exist, e.g. GET /exactapi/units/{id}/equipment/{eqId}/tagmeta and GET /exactapi/units/{id}/dashboards/{dashId}/dashboard-plots.
POST /exactapi/units/7/equipment
{ "name": "Economizer" }
{ "id": "13", "name": "Economizer", "unitsId": "7" }
URL names (discovered SQLite tables): orgs, sites, units, equipment, tagmeta, clients, ingestconfigs, statuses, deviations, faulttemplates, configs, configurations, faulttrees, incidents, calculations, dashboards, userprofiles, users, tags, activities, heatrates, dashboardplots, useractivities, modelpipelines, boilerassets, profiles_lookups, labels.
URL overrides (segment differs from table): faultTemplates (faulttemplates), dashboard-plots (dashboardplots), profiles-lookups (profiles_lookups).
URL aliases (same table, second URL): customers → orgs, Users → users.
Served by custom handlers, not this generic layer (see Special Resources): connections, tag_mappings, collections, attachments.
Special cases:
users — real columns email, password, username. The password is never returned and is Argon2id-hashed on write.configurations — uses a TEXT primary key, so {id} is a string (GET /exactapi/configurations/64a1b2c3d4e5f6a7b8c9d0e1).| Status | Body |
|---|---|
| 400 | { "error": "<filter/JSON parse error>" } |
| 403 | Row-level access denied. |
| 404 | { "error": "Item not found" } / { "error": "No matching record" } |
| 409 | Duplicate key on create (message prefixed DUPLICATE:). |
| 500 | { "error": "<message>" } |
GET /exactapi/_tables → array of discovered table names.GET /exactapi/_schema → per-table columns + FK relations.GET /exactapi/dyn/_schema → the discovered relation map (belongs_to / has_many).See Special Resources → Introspection.
Adopted from clarity backend developer docs (
docs/developer/api/entity-crud.md), imported reference.
Primary handlers:clarity:backend/src-tauri/src/sqlite_api/mod.rs(dynamic CRUD, relations, hooks).
Last updated: 2026-07-16 from clarity@bff451d