Read services basically work with two structures: type <ClassName>
(single object) and type <ClassName>Page
(subset of objects). By combining the two structures, it is possible to request entire object graphs for output.
Reference engine model #
All examples on this page reference the engine model in the figure:
The Employee class has a role for each supported relation type, as well as native, query, math, and platform attributes declared on it.
get
service #
The Query.<ClassName>___get
service allows you to get a graph of objects from an object of a certain class.
type Query {
Employee___get(_id: ID!): Employee
}
The service requests the ID of the object to be retrieved as input and returns the corresponding structure <ClassName>
. More information about the output of the service is available at the bottom.
For example, the service to retrieve an Employee object is as follows:
type Query {
Employee___get(_id: ID!): Employee
}
An example query with this service is available here.
getBy<UniqueConstraints>
services #
For each uniqueness constraint defined on <ClassName>
a Query.<ClassName>___getBy<UniqueConstraints>
service is generated that works with that constraint. Each service allows to retrieve an object graph from one object of a certain class, identified by a unique constraint (or key) on it defined. Clearly, services of this type are not generated if no unique constraints are defined on the class in question.
A key may include one or more attributes; in the case of multiple attributes, they appear separated by underscores in the service name (getBy<Constraint1>_<Constraint2>_<Constraint3>
).
type Query {
ClassName___getByConstraint1_Constraint2(
constraint1: Constraint1Type!
constraint2: Constraint2Type!
): ClassName
}
Each service requests as input the values of the attributes that form the key and returns the structure <ClassName>
.
For example, for the Employee class, the services are available:
type Query {
Employee___getByLast_name_First_name_Date_of_birth(
last_name: String!
first_name: String!
date_of_birth: Date!
): Employee
Employee___getByUsername(username: String!): Employee
}
which can be used as follows:
{
Employee___getByLast_name_First_name_Date_of_birth(
date_of_birth: "06/23/1968"
first_name: "Dolorita"
last_name: "Wanell"
) {
username
is_active
email_address
}
Employee___getByUsername(username: "vanna.bamforth") {
full_name
date_of_birth
is_active
email_address
}
}
getPage
service #
The Query.<ClassName>___getPage
service allows you to get a graph of objects from a subset of objects of a certain class.
type Query {
ClassName___getPage(options: ClassNamePageOptions): ClassNamePage
}
The service is generated for all managed and main (non-part) classes, so it makes sense to retrieve a list of persistent instances; it is therefore not generated for Form, Enum and Singleton classes.
The service returns the structure <ClassName>Page
as output. You can control the subset of objects returned by the page by enhancing the optional options
argument. More information is available here: Customize-page request.
For example, the service to retrieve a subset of Employee objects is as follows:
type Query {
Employee___getPage(options: EmployeePageOptions): EmployeePage
}
An example query with this service is available here.
type <ClassName>
#
Let’s recap what we said on the page The Graphql Schema: Model-Schema Mapping:
- each class
<ClassName>
in the engine model is mapped to thetype <ClassName>
structure; type <ClassName>
is generated for all managed (enabled) main classes and for all enabled part classes that are reachable by at least one enabled main class;type <ClassName>
contains all attributes and enabled roles of the class;- attribute types are mapped as shown in Primitive data types.
- The platform attribute
__id
is always present named_id
, even when not explicitly enabled (in GraphQL, the double underscore__
is a prefix reserved for metadata);
For example, for the Employee class, you have the following Employee
structure:
type Employee {
# platform attributes
_id: ID!
# native attributes
first_name: String
last_name: String
date_of_birth(format: String = "default"): Date
phone_number: String
email_address: String
date_joined(format: String = "default"): Date
hourly_cost(format: String = "default"): Real
username: String
# derived attributes
full_name: String
age: Int
junior: Boolean
team_name: String
qualifications: String
is_active: Boolean
# outgoing roles
# ...
# assignable
# ...
}
All outgoing roles (associations and compositions) from <ClassName>
to a target class are mapped, with respect to the class pointed to by the relation, as follows:
- to-one roles :
<TargetClassName>
(analogous to<ClassName>
); - to-many roles:
<TargetClassName>Page
(analogous to<ClassNamePage>
).
The name of the outgoing role on <ClassName>
is the same as the name of the role on the engine model class (TargetRoleName>
).
For Employee, the outgoing roles are mapped as follows:
type Employee {
# attributes
# ...
# outgoing roles
team: Team # association to 1
supervisor: Employee # reflexive association to 1
address: Address # composition to 1
qualification_(options: QualificationPageOptions): QualificationPage # association to N
assignments(options: Project_assignmentPageOptions): Project_assignmentPage # composition to N
# assignments
# ...
}
Finally, on <ClassName>
you can find information about the assignable objects for the outgoing roles of that class. The name of these fields is <TargetRoleName>___associables
, while the type is <TargetClassName>Page
for both many and one associations; also in GraphQL, records that do not satisfy any Selection filter (or Selection path) do not appear in the list of associables.
For Employee, associables are mapped as follows:
type Employee {
# attributes
# ...
# outgoing roles
# ...
# associates
team___associables(options: TeamPageOptions) # association to 1
supervisor___associables(options: EmployeePageOptions) # reflexive 1-way association
qualification___associables(options: QualificationPageOptions) # association to N
}
As is true for the getPage
service, for <ClassName>
fields that return a page, you can control the subset of objects returned by properly setting the optional options
argument, of type <TargetClassName>PageOptions
. More information is available here: Customize-page request.
type <ClassName>Page
. #
Both the <ClassName>___getPage
service and type <ClassName>
fields related to roles and associates return the type <ClassName>Page
structure, containing a subset of objects of class <ClassName>
.
type ClassNamePage {
items: [ClassName!]!
totalCount: Int
hasNext: Boolean
hasPrev: Boolean
nextCursor: Cursor
prevCursor: Cursor
}
The actual objects are included in the non-null field items
, which returns a list of type <ClassName>
. When you request this field in output, you must specify which fields you want to retrieve for each record in the list; in other words, the structure of each <ClassName>
element in the list of items
must be described in the query.
{
items {
scalarField1
scalarField2
# ...
}
}
{
// all objects have the same structure
"items": [
{
"scalarField1": "abcdef",
"scalarField2": 12
},
{
"scalarField1": "xxyyzz",
"scalarField2": 314
}
// ...
]
}
Example: using the items
field.
In addition to items
, the structure contains the following metadata:
totalCount
: total number of records retrievable for that class from that path, net of any filters modeled on the class or role; the count also depends on any filters applied to the page. It should be noted thattotalCount
does not correspond to the number ofitems
of which the page consists, except when the page is large enough to retrieve all possible records;hasPrev
andhasNext
: boolean flags indicating whether there are previous or next records to the current page;prevCursor
andnextCursor
:Cursor
type structures that point to the previous page and current page, respectively, based on the current sort and offset.
This information can be reused used to navigate between pages.
Customize the paged request #
As stated earlier, it is possible to control the subset of objects returned by the page; both the <ClassName>___getPage
service, and type <ClassName>
fields related to roles and associates, in fact accept the optional options
argument, of type <ClassName>PageOptions
.
input ClassNamePageOptions {
orderBy: [ClassNameSort!].
next: Int
prev: Int
offset: Int
cursor: Cursor
fromCursor: ClassNameCursor
filter: ClassNameFilter
filter_exp: String
}
Valorizing the options of this input type allows you to:
- order the data to be returned (with the
orderBy
field); - limit the size of the current page, i.e. the number of values to return (with the
next
andprev
fields); - navigate between pages by scrolling through the set of retrievable records (with the fields
offset
,cursor
andfromCursor
); - filter the data to be returned (with the
filter
andfilter_exp
fields).
Default options #
When the options
argument is not specified, the server sorts the records by increasing ID and selects the first 10 results.
type Query {
ClassName___getPage(
options: ClassNamePageOptions = {next: 10, offset: 0, orderBy: [_id___ASC]}
): ClassNamePage
}
type ClassName {
# ...
targetRoleN: TargetClassNamePage(
options: TargetClassNamePageOptions = {next: 10, offset: 0, orderBy: [_id___ASC]}
)
targetRoleN___associables: TargetClassNamePage(
options: TargetClassNamePageOptions = {next: 10, offset: 0, orderBy: [_id___ASC]}
)
}
The server uses these default values for the next
, offset
and orderBy
fields even when the request includes options
but omits these fields. In practice, the server performs the merge of the default options with the custom options.
Example: equivalent queries
Sort results #
The set of objects returned by the page can be sorted by populating the orderBy
field, which contains a list of constant values of type enum <ClassName>Sort
.
input ClassNamePageOptions {
orderBy: [ClassNameSort!]
next: Int
prev: Int
offset: Int
cursor: Cursor
fromCursor: ClassNameCursor
filter: ClassNameFilter
filter_exp: String
}
Each constant represents a sorting criterion, based on an attribute of the class and a verse (ascending or descending); the criterion name respects the format <AttributeName>___ASC
or <AttributeName>___DESC
. These constants are generated for all managed attributes of the class.
enum ClassNameSort {
attribute1Name___ASC
attribute1Name___DESC
attribute2Name___ASC
attribute2Name___DESC
# ...
}
For example, the following criteria are available for Employee:
enum EmployeeSort {
_id___ASC
_id___DESC
age___ASC
age___DESC
date_of_birth___ASC
date_of_birth___DESC
email_address___ASC
email_address___DESC
first_name___ASC
first_name___DESC
# ...
}
The page is sorted according to the chosen criteria:
- if no criteria are selected, the server automatically sorts by
_id___ASC
; - if only non-unique attributes are selected, the server automatically includes
_id___ASC
as a sub-criterion (if the order is equal, records are sorted by ID).
Example: sorting
Limit the number of results #
As mentioned earlier, the default pagination returns a maximum of 10 elements. You can reduce or increase the size of the page by populating a field between next
and prev
.
input ClassNamePageOptions {
orderBy: [ClassNameSort!]
next: Int
prev: Int
offset: Int
cursor: Cursor
fromCursor: ClassNameCursor
filter: ClassNameFilter
filter_exp: String
}
next
allows you to select the next N records forward from the current position. Conversely, prev
lets you select N records backward, again relative to the current position. The current position is dictated by the offset
(or cursor
), so it is useful to combine these fields to navigate between pages.
It may also be useful to use the hasNext
and hasPrev
flags returned by <ClassName>Page
to dynamically correct the next
or prev
values.
type ClassNamePage {
hasNext: Boolean
hasPrev: Boolean
nextCursor: Cursor
prevCursor: Cursor
totalCount: Int
}
Navigate between pages #
The GraphQL server supports two pagination modes: offset-based and cursor-based. The former makes use of the offset
field (in addition to next
and prev
), while the latter makes use of the cursor
or fromCursor
fields. In both, the goal is to move the current position of the pointer to the list of records retrievable by the paged request. In fact, as stated earlier, by default, the server sorts the records by ascending ID, “positions” itself on the first one (offset=0
), and selects the next 10 records (next=10
) from there.
input ClassNamePageOptions {
orderBy: [ClassNameSort!]
next: Int
prev: Int
offset: Int
cursor: Cursor
fromCursor: ClassNameCursor
filter: ClassNameFilter
filter_exp: String
}
In offset-based pagination, we specify a shift (offset) from “zero”, which is the first element of the list sorted according to the chosen criteria (by default, _id___ASC
).
Examples: offset-based pagination.
In cursor-based pagination, we make use of cursors to scroll through results. A cursor is a structure that points to the current record uniquely in the context of pagination; it, therefore, depends on the sorting chosen (orderBy
), the direction of the scroll, and the size of the page (both set by prev
or next
).
We take the nextCursor
and prevCursor
fields offered by the <ClassName>Page
structure:
type ClassNamePage {
hasNext: Boolean
hasPrev: Boolean
nextCursor: Cursor
prevCursor: Cursor
totalCount: Int
}
The Cursor
type is a scalar, serialized as a string. When we make a paged request, the server creates temporary values and offers them in the nextCursor
and prevCursor
fields; in subsequent calls, we can reuse these strings by inserting them into the cursor
field of <ClassName>PageOptions
. This way, we can scroll the results forward or backward, depending on which value we used between nextCursor
and prevCursor
.
Example: using the cursor
field.
You can make use of cursors pre-calculated by the server with prevCursor
and nextCursor
, as seen above, or you can specify your own. In fact, the fromCursor
field allows you to specify a value of type <ClassName>Cursor
. This structure describes a record from which you want to scroll through the list; starting with the record described by the cursor, you can request previous or subsequent records.
input ClassNameCursor {
_id: ID
# attributes of ClassName
}
The available fields are all the managed attributes of <ClassName>
, all of which are optional. We can reference an existing record from which to start scrolling by populating these fields. The check on the record actual existence is intentionally loose: if the record exists, the result is a page obtained starting from the included record; vice versa, if it doesn’t exist, the page will start from the first “next” record, according to the chosen sorting criteria and respecting the pagination direction.
Filter results #
The set of objects returned by the page can be filtered by populating the filter
or filter_exp
fields; the former consists of a way of defining filters based on the GraphQL schema, the latter allows you to use Livebase expressions, in the same way as math expressions or filters are defined in the engine model.
input ClassNamePageOptions {
orderBy: [ClassNameSort!]
next: Int
prev: Int
offset: Int
cursor: Cursor
fromCursor: ClassNameCursor
filter: ClassNameFilter
filter_exp: String
}
The filter
field is of type <ClassNameFilter>
; this structure provides filters on a class based on its managed attributes. Each filter thus generated follows the format <AttributeName>___<FilterOperation>
, while the data type depends on the operation. Logical operators (AND
, OR
and NOT
) are also available to apply to a set of filters.
input ClassNameFilter {
AND: [ClassNameFilter!]
OR: [ClassNameFilter!]
NOT: ClassNameFilter
Attribute1Name___FilterOperation1: Attribute1NameFilter1Type
Attribute1Name___FilterOperation2: Attribute1NameFilter2Type
# ...
}
The supported operations are:
___eq
: the value exactly matches with the given value of the same type as the attribute;___ne
: the value is different from the given value of the same type as the attribute;___gt
: the value is greater than the given value of the same type as the attribute;___gte
: the value is greater than or equal to the given value of the same type as the attribute;___lt
: the value is less than the given value of the same type as the attribute;___lte
: the value is less than or equal to the given value of the same type as the attribute;___in
: the value appears in the given list of values of the same type as the attribute;___null
: checks if the value is null (true) or not null (false);___not___in
: the value does not appear in the given list of values of the same type as the attribute;___not___null
: checks if the value is non-null (true) or null (false).
Additional operations are available for attributes of type string:
___starts_with
: the string starts with the given prefix;___ends_with
: the string ends with the given suffix;___contains
: the string contains the given sub-string;___not___starts_with
: the string does not begin with the given prefix;___not___ends_with
: the string does not end with the given suffix;___not___contains
: the string does not contain the given sub-string.
For example, the following filters are available for Employee:
input EmployeeFilter {
AND: [EmployeeFilter!]
NOT: EmployeeFilter
OR: [EmployeeFilter!]
full_name___eq: String
full_name___ne: String
full_name___gt: String
full_name___gte: String
full_name___lt: String
full_name___lte: String
full_name___in: [String!]
full_name___null: Boolean
full_name___not___in: [String!]
full_name___not___null: Boolean
full_name___starts_with: String
full_name___ends_with: String
full_name___contains: String
full_name___not___starts_with: String
full_name___not___ends_with: String
full_name___not___contains: String
# ...
}
If multiple different filters are valued, they are implicitly evaluated in AND
with each other.
Example: implicit AND
{
Employee___getPage(
options: {
filter: {
AND: {
first_name___starts_with: "A"
last_name___ends_with: "E"
}
}
}
) {
totalCount
hasNext
items {
_id
first_name
last_name
date_of_birth
}
}
}
{
Employee___getPage(
options: {
filter_exp: "startsWith(first_name,\"A\") && endsWith(last_name,\"E\")"
}
) {
totalCount
hasNext
items {
_id
first_name
last_name
date_of_birth
}
}
}