Appearance
Table View Settings — API Analysis
Five endpoints on the tenant API (apps/backend). Three manage the user's own column arrangement for one overview; two are the pilot overviews' exports, which take the arrangement from the request so that what is exported is what the user sees at that moment, saved or not.
The column arrangement endpoints never take a user identifier: the user is always the authenticated one, so there is no way to address another user's preference.
The project has no shared API-conventions document yet, so the envelope shapes are stated once here. Success:
json
{
"data": {},
"status": 200,
"message": "OK",
"requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21"
}Error:
json
{
"errorCode": "ERR_TABLE_VIEW_UNKNOWN",
"errorMessage": "Unknown table view.",
"status": 404,
"requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21"
}Authentication, tenant resolution and permission failures are answered by the tenant API's shared handling and are not restated per endpoint.
📥 GET /v1/table-views/{viewKey}/columns
Returns the overview's column catalogue for the current user, merged with the user's saved arrangement: every column the user may see, in display order, each with its visibility and whether it can be hidden.
Authorization
Requires the read permission of the overview the key names — tenant.buildings.read for buildings, tenant.gauges.read for gauges. A user who cannot open the overview cannot read its arrangement.
Request Headers
None beyond the tenant API's standard bearer authentication and tenant selection.
Request Body
None.
Request Parameters
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
viewKey | Path | Enum | Yes | - | The overview. | A value of Enum - TableViewKey. | Suffix of userPreference.key |
Request Logic
- Resolves the overview's column catalogue for this user: the columns the overview defines, minus those the user's permissions, the tenant's modules or the tenant's configuration exclude.
- Reads the user's row, if any:
sql
SELECT value, updated_at
FROM user_preference
WHERE user_id = @currentUserId
AND key = 'table.' || @viewKey || '.columns';- No row → the catalogue's default selection and order,
customised: false. - A row → reconciled against the catalogue as described in JSON-DAT: userPreference.value,
customised: true. The reconciled result is not written back. - Writes nothing.
Transactional Operations
N/A — read-only.
✅ Success Response (200)
json
{
"data": {
"viewKey": "gauges",
"customised": true,
"columns": [
{ "key": "name", "mandatory": true, "visible": true, "labelKey": "gauge.list.columns.name" },
{ "key": "medium", "mandatory": false, "visible": true, "labelKey": "gauge.list.columns.medium" },
{ "key": "building", "mandatory": false, "visible": true, "labelKey": "gauge.list.columns.building" },
{ "key": "purpose", "mandatory": false, "visible": false, "labelKey": "gauge.list.columns.purpose" },
{ "key": "actions", "mandatory": true, "visible": true, "labelKey": "gauge.list.columns.actions" }
]
},
"status": 200,
"message": "OK",
"requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}Response Data Mapping
| Response Field | Source / Value |
|---|---|
viewKey | Echo of the path parameter |
customised | Derived: a userPreference row exists for this user and key |
columns[].key | Column catalogue; order from userPreference.value.columns after reconciliation, else catalogue default order |
columns[].mandatory | Column catalogue |
columns[].visible | userPreference.value.columns[].visible after reconciliation; true for a mandatory column; catalogue default when no row |
columns[].labelKey | Column catalogue — a GUI label key resolved by the frontend |
❌ Error Responses
404 ERR_TABLE_VIEW_UNKNOWN
Thrown when viewKey is not a value of the enum, or is a retired value.
json
{
"errorCode": "ERR_TABLE_VIEW_UNKNOWN",
"errorMessage": "Unknown table view.",
"status": 404,
"requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}📝 PUT /v1/table-views/{viewKey}/columns
Saves the user's arrangement of one overview — the full ordered list of columns with their visibility — replacing any earlier one.
Authorization
Same as the GET: the overview's read permission. Arranging one's own view needs nothing more than being allowed to open it.
Request Headers
None beyond the tenant API's standard bearer authentication and tenant selection.
Request Body
json
{
"columns": [
{ "key": "name", "visible": true },
{ "key": "building", "visible": true },
{ "key": "medium", "visible": true },
{ "key": "purpose", "visible": false },
{ "key": "actions", "visible": true }
]
}| Field | Type | Required | Validation | DB Mapping |
|---|---|---|---|---|
columns | Array | Yes | 1–100 items; no duplicate key. | userPreference.value.columns |
columns[].key | String | Yes | A column of this overview's catalogue that the user may see. | userPreference.value.columns[].key |
columns[].visible | Boolean | Yes | false is rejected for a mandatory column. | userPreference.value.columns[].visible |
Request Parameters
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
viewKey | Path | Enum | Yes | - | The overview. | A value of Enum - TableViewKey. | Suffix of userPreference.key |
Request Logic
- Validates every column against the user's catalogue. A column the user may not see is rejected, not silently dropped: a client that sends one is out of date or tampered with.
- Stores the arrangement with
version: 1. Catalogue columns absent from the request are stored as hidden at the end, so the stored value always covers the whole catalogue as of the save. - Upsert on the unique key:
sql
INSERT INTO user_preference (id, tenant_id, user_id, key, value, created_at, updated_at, created_by, updated_by)
VALUES (@id, @tenantId, @currentUserId, 'table.' || @viewKey || '.columns', @value, now(), now(), @currentUserId, @currentUserId)
ON CONFLICT (user_id, key)
DO UPDATE SET value = EXCLUDED.value, updated_at = now(), updated_by = EXCLUDED.updated_by;- Returns the reconciled arrangement, the same shape as the GET, so the client renders what the server now holds.
Transactional Operations
A single upsert; last write wins. Two tabs of the same user saving at once leave the later arrangement, which is the expected outcome for a personal display setting.
✅ Success Response (200)
Same body as GET /v1/table-views/{viewKey}/columns, with customised: true.
Response Data Mapping
As for GET /v1/table-views/{viewKey}/columns.
❌ Error Responses
400 ERR_TABLE_VIEW_INVALID_COLUMNS
Thrown when the body is malformed: empty or over-long list, duplicate key, missing field.
json
{
"errorCode": "ERR_TABLE_VIEW_INVALID_COLUMNS",
"errorMessage": "The column list is not valid.",
"status": 400,
"requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}400 ERR_TABLE_VIEW_COLUMN_NOT_AVAILABLE
Thrown when a column is not in the user's catalogue for this overview.
json
{
"errorCode": "ERR_TABLE_VIEW_COLUMN_NOT_AVAILABLE",
"errorMessage": "Column 'contractPrice' is not available in this view.",
"status": 400,
"requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}400 ERR_TABLE_VIEW_MANDATORY_COLUMN_HIDDEN
Thrown when a mandatory column is sent with visible: false.
json
{
"errorCode": "ERR_TABLE_VIEW_MANDATORY_COLUMN_HIDDEN",
"errorMessage": "Column 'name' cannot be hidden.",
"status": 400,
"requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}404 ERR_TABLE_VIEW_UNKNOWN
As for the GET.
🗑️ DELETE /v1/table-views/{viewKey}/columns
Resets the user's arrangement of one overview to the default by removing the saved one.
Authorization
Same as the GET.
Request Headers
None beyond the tenant API's standard bearer authentication and tenant selection.
Request Body
None.
Request Parameters
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
viewKey | Path | Enum | Yes | - | The overview. | A value of Enum - TableViewKey. | Suffix of userPreference.key |
Request Logic
sql
DELETE FROM user_preference
WHERE user_id = @currentUserId
AND key = 'table.' || @viewKey || '.columns';Idempotent: resetting a view that was never customised succeeds. Returns the default arrangement, so the client needs no second call.
Transactional Operations
A single delete.
✅ Success Response (200)
Same body as GET /v1/table-views/{viewKey}/columns, with customised: false.
Response Data Mapping
As for GET /v1/table-views/{viewKey}/columns.
❌ Error Responses
404 ERR_TABLE_VIEW_UNKNOWN
As for the GET.
📤 POST /v1/buildings/export
Downloads the building overview as XLSX: the rows matching the current search and filters (or the rows the user selected), with the columns the user currently shows, in their current order.
Authorization
Requires permission code tenant.buildings.read.
Request Headers
Accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, beyond the standard authentication and tenant selection.
Request Body
json
{
"filters": { "type": "building", "sector": "018f9c4d-0000-7a3e-9b77-4f0d3a5e6b21", "q": "škola", "state": "active" },
"columns": ["name", "address", "city", "sector", "status"],
"ids": null
}| Field | Type | Required | Validation | DB Mapping |
|---|---|---|---|---|
filters | Object | Yes | Same fields and rules as the GET /v1/buildings list query, without page / pageSize. {} means no filter. | The list's filter mapping on building |
columns | Array of String | Yes | 1–100 unique keys from the buildings column catalogue the user may see. Columns the catalogue marks as not exported (row actions, display-only values) are ignored. | Column catalogue → building attributes |
ids | Array of UUID | No | 1–5 000 unique ids when present. null or absent exports every row matching filters. | building.id |
Request Parameters
None.
Request Logic
- Validates
columnsagainst the user'sbuildingscatalogue. A column the user may not see is rejected — the export never widens what the screen shows. - Row set: with
ids, those ids intersected withfiltersand with what the user may read (an id the user cannot see, or that no longer matches, is left out without error); withoutids, every row matchingfilters. Never limited to the current page. - Counts the row set first; above the export limit (50 000 rows, a configuration value) the request is refused before any row is read.
- Orders rows as the list does, writes one sheet with a header row of the columns' labels in the requested order, and streams the workbook.
- Writes nothing.
Transactional Operations
N/A — read-only. Count and read run in one read-only transaction so the limit check and the file describe the same data.
✅ Success Response (200)
Binary XLSX body; Content-Disposition: attachment; filename="buildings-YYYY-MM-DD.xlsx". Not wrapped in the envelope.
Response Data Mapping
| Response Field | Source / Value |
|---|---|
| Sheet header row | Labels of the requested columns, in the user's language, in request order |
| Sheet data rows | One per building row in the set; one cell per requested column, formatted as the overview formats it |
❌ Error Responses
400 ERR_EXPORT_INVALID_REQUEST
Thrown when filters, columns or ids break the rules above.
json
{
"errorCode": "ERR_EXPORT_INVALID_REQUEST",
"errorMessage": "The export request is not valid.",
"status": 400,
"requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}400 ERR_TABLE_VIEW_COLUMN_NOT_AVAILABLE
As for the PUT.
422 ERR_EXPORT_TOO_LARGE
Thrown when the row set exceeds the export limit. The message names the count and the limit so the user knows how far to narrow the filter.
json
{
"errorCode": "ERR_EXPORT_TOO_LARGE",
"errorMessage": "The export would contain 61 240 rows; the limit is 50 000. Narrow the filter.",
"status": 422,
"requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}📤 POST /v1/gauges/export
Downloads the gauge overview as XLSX on the same terms as the building export.
Authorization
Requires permission code tenant.gauges.read.
Request Headers
Accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, beyond the standard authentication and tenant selection.
Request Body
json
{
"filters": { "building": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21", "medium": "electricity", "archived": "false" },
"columns": ["name", "medium", "building", "purpose"],
"ids": ["018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b40", "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b41"]
}| Field | Type | Required | Validation | DB Mapping |
|---|---|---|---|---|
filters | Object | Yes | Same fields and rules as the GET /v1/gauges list query (building, kind, medium, archived, isPartOfBulkPurchase, q), without paging. The building tab sends its building here. | The list's filter mapping on gauge |
columns | Array of String | Yes | 1–100 unique keys from the gauges column catalogue the user may see. Columns the catalogue marks as not exported are ignored. | Column catalogue → gauge attributes and resolved names |
ids | Array of UUID | No | 1–5 000 unique ids when present. | gauge.id |
Request Parameters
None.
Request Logic
As for POST /v1/buildings/export, over gauges. Names shown in the overview (building, subscriber, purpose) are resolved in batch for the exported rows only, as the list does, so the export stays free of per-row lookups.
Transactional Operations
N/A — read-only; count and read in one read-only transaction.
✅ Success Response (200)
Binary XLSX body; Content-Disposition: attachment; filename="gauges-YYYY-MM-DD.xlsx".
Response Data Mapping
| Response Field | Source / Value |
|---|---|
| Sheet header row | Labels of the requested columns, in the user's language, in request order |
| Sheet data rows | One per gauge row in the set; one cell per requested column, formatted as the overview formats it |
❌ Error Responses
As for POST /v1/buildings/export: 400 ERR_EXPORT_INVALID_REQUEST, 400 ERR_TABLE_VIEW_COLUMN_NOT_AVAILABLE, 422 ERR_EXPORT_TOO_LARGE.