What this is about
With * you get a reference only as { "id": … }. If you want to see more of the referenced object, you expand the reference: instead of a field name, you put an object into the response.
{ "field": "company", "response": ["companyname", "city"] }
field names the reference, response says which fields of the referenced object you want. This works for single references and for lists.
The example model
| Model | Fields | References |
|---|---|---|
company | companyname, city | employees (list of employee) |
employee | firstname, lastname | company (single), department (single) |
department | name | – |
employee.company and company.employees are the two sides of the same relation. company is the back reference of employees.
From request to response
flowchart LR
subgraph R["response"]
direction TB
r1["companyname"]
r2["{ field: employees }"]
r3["firstname"]
r4["{ field: department }"]
r5["name"]
r2 --> r3
r2 --> r4
r4 --> r5
end
subgraph A["Response"]
direction TB
a1["company<br/>companyname: Codamic AG"]
a2["employees[0]<br/>firstname: Daniel"]
a3["department<br/>name: Development"]
a4["employees[1]<br/>firstname: Anna"]
a5["department<br/>name: Sales"]
a1 --> a2
a1 --> a4
a2 --> a3
a4 --> a5
end
R ==> A
POST /api/rest/hr/company/read/a1…
{
"response": [
"companyname",
{
"field": "employees",
"response": [
"firstname",
{ "field": "department", "response": ["name"] }
]
}
]
}{
"data": {
"id": "a1…",
"companyname": "Codamic AG",
"employees": [
{ "id": "7f3…", "firstname": "Daniel",
"department": { "id": "d4…", "name": "Development" } },
{ "id": "8b1…", "firstname": "Anna",
"department": { "id": "d7…", "name": "Sales" } }
]
},
"meta": { "error": false }
}The response is shortened: fields you did not request are actually in it as null, and every object has its system fields _createdOn and _updatedOn.
Single reference and list work differently
When: The reference points to one object, e.g. employee.company.
-
1CDMS→Databasereads the
employeeand attachescompanywith a LEFT JOIN, taking only theidand the type -
2CDMSchecks the read role of
company -
3CDMS→Databasereads the
companyobject with thisidlike a read of its own: with its row filters and only with the fields from the innerresponse -
4CDMSputs the result into the field
company
Result: company is an object with the requested fields.
When: The employee has no company, or the user may not see this company (own data, attribute filters).
In the first case the LEFT JOIN returns no id, in the second the read of the company finds nothing because of the row filters. In both cases the field stays empty. The employee itself still comes back.
Result: 200 with "company": null.
When: The reference is a list, e.g. company.employees.
-
1CDMS→Databasereads the
company -
2CDMSchecks the read role of
employee -
3CDMSbuilds a search of its own on
employeewith the filtercompany.id = <id of the company>CDMS adds this filter over the back reference itself. You do not write it. -
4CDMS→Databasesearches the matching
employeeobjects, with their row filters and only with the fields from the innerresponse -
5CDMSputs the matches as a list into
employees
Result: employees is a list. Without matches it is empty.
When: You want only part of the list, sorted.
The object may contain a parameter, just like a search: query, order, limit, page. CDMS combines your filter with the filter on the back reference using AND. Without limit you get all entries.
Result: The list contains only the entries that match your filter, in your sorting and at most limit of them.
When: The inner response contains an object again.
Every level works exactly like the first one. There is no fixed depth limit, the limit is your response: CDMS never goes deeper than you write it.
Result: A tree that has exactly the shape of your response.
When: The response reaches into employees, the user lacks employee-read.
-
1CDMSchecks the read role of
employee -
2CDMS→Client403
missing-permission|employee-read
Result: The whole request fails. The same request without the employees object works with the same role.
A list with its own filter
POST /api/rest/hr/company/read/a1…
{
"response": [
"companyname",
{
"field": "employees",
"response": ["firstname", "lastname"],
"parameter": {
"limit": 5,
"order": [{ "field": "_createdOn", "order": "DESC" }],
"query": {
"type": "AND",
"filter": [{ "key": "lastname", "value": "M%", "param": "LIKE" }]
}
}
}
]
}{
"data": {
"id": "a1…",
"companyname": "Codamic AG",
"employees": [
{ "id": "7f3…", "firstname": "Daniel", "lastname": "Mertins" },
{ "id": "2c9…", "firstname": "Eva", "lastname": "Meier" }
]
},
"meta": { "error": false }
}What CDMS turns this into as a search on employee:
flowchart TB
subgraph W["WHERE for employee"]
direction TB
w1["company.id = 'a1…'<br/>(added by CDMS)"]
w2["AND lastname LIKE 'M%'<br/>(your filter)"]
w3["AND row filters of employee<br/>(own data, attribute filters)"]
end
W --> O["ORDER BY _createdOn DESC"]
O --> L["at most 5"]
How filtering, sorting and paging work in detail is described in the search chapter, among others under Filters in nested lists and Paging and match count.
In a search: one sub-query per match
If you expand a list in a search, CDMS runs the sub-query for each match separately:
sequenceDiagram
participant C as Client
participant D as CDMS
participant DB as Database
C->>D: POST /hr/company/query (limit 25) with { field: employees }
D->>DB: search on company
DB-->>D: 25 companies
loop for each of the 25 companies
D->>DB: search on employee with company.id = …
DB-->>D: employees of this company
end
D-->>C: 25 companies, each with its list employees
With 25 companies these are 25 additional searches. If every company has 500 employees and you set no limit, 12,500 employees come back. More on this under What happens in the database when reading.
What a nested list does not have
dataplusmetameta.totalCounttells you how many matches there are in total- paging with
pageandlimit
- only the entries
- no match count, the list is a plain JSON array
limitandpagestill work, for each list separately
If you need the total number of entries of a list, ask the child model directly: POST /hr/employee/query with the filter company.id = … returns meta.totalCount.
Decision table
| Kind | read role of the target model | target present and visible | Content of the field |
|---|---|---|---|
| single | no | – | 403 for the whole request |
| single | yes | no | null |
| single | yes | yes | object with the requested fields |
| list | no | – | 403 for the whole request |
| list | yes | none | empty list [] |
| list | yes | some | only the visible entries |
Instead of the read role of the target model, a role on the relation itself can be enough. See Permissions on relations. The table describes the default, strict mode.
Pitfalls
Where to go next
- If the reference points to an abstract model: Reading through abstract types
- What this means for the database: What happens in the database when reading