GraphQL Features /businessnxtapi/features section Comprehensive GraphQL features - filtering, sorting, pagination, fragments, aliases, directives, error handling, unoptimized queries, batch requests. 2024-09-24T15:57:29+02:00 # GraphQL Features Comprehensive GraphQL features - filtering, sorting, pagination, fragments, aliases, directives, error handling, unoptimized queries, batch requests. Filtering /businessnxtapi/features/filtering page API filtering enables precise data retrieval using logical operators like _gt, _lt, _like, _in, and _between. It supports complex queries and comparisons. 2025-04-15T09:48:42+02:00 # Filtering API filtering enables precise data retrieval using logical operators like _gt, _lt, _like, _in, and _between. It supports complex queries and comparisons. ## Overview Filtering is a key feature of the API. You can specify the filter as a parameter to a connection. Let's look at some examples: In this first example, we fetch all the general ledger accounts that have the account number greater or equal than 6000 and lower than 7000. ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { generalLedgerAccount( filter: {_and :[ {accountNo :{_gte : 6000}}, {accountNo :{_lt : 7000}} ]} ) { totalCount items { accountNo name } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "generalLedgerAccount": { "totalCount": 46, "items": [ { "accountNo": 6000, "name": "Avskr. bygn. og annen eiendom" }, { "accountNo": 6010, "name": "Avskr. maskiner, inventar mv." }, ... ] } } } } ``` We can complicate the query a bit, and ask only for those general ledger accounts in the range 6000 - 7000 that start with "Leie". In this case, the filter would look as follows: ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { generalLedgerAccount( filter: {_and :[ {accountNo :{_gte : 6000}}, {accountNo :{_lt : 7000}}, {name :{_like:"Leie%"}} ]} ) { totalCount items { accountNo name } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "generalLedgerAccount": { "totalCount": 6, "items": [ { "accountNo": 6300, "name": "Leie lokaler" }, { "accountNo": 6400, "name": "Leie maskiner" }, { "accountNo": 6410, "name": "Leie inventar" }, { "accountNo": 6420, "name": "Leie datasystemer" }, { "accountNo": 6430, "name": "Leie andre kontormaskiner" }, { "accountNo": 6440, "name": "Leie transportmidler" } ] } } } } ``` In the next example, we fetch all the associates that are either customers with the number in the range 10000 - 10010, or suppliers, with the number in the range 50000 - 50010. ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { associate ( filter:{ _or: [ { _and : [ {customerNo :{_gt:10000}}, {customerNo :{_lt:10010}} ] }, { _and : [ {supplierNo : {_gt:50000}}, {supplierNo : {_lt:50010}}, ] } ] } ) { totalCount items { name customerNo supplierNo } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "associate": { "totalCount": 18, "items": [ { "name": "YoYo Solutions AS", "customerNo": 10001, "supplierNo": 0 }, { "name": "Spire Business", "customerNo": 10002, "supplierNo": 0 }, ... { "name": "Scorpio Services Limited", "customerNo": 0, "supplierNo": 50001 }, { "name": "Atlas Systems", "customerNo": 0, "supplierNo": 50002 }, ... ] } } } } ``` Filtering is very similar to an open-source GraphQL implementation for Postgres databases. You can read more about that [here](https://hasura.io/docs/1.0/graphql/core/queries/query-filters.html#using-multiple-filters-in-the-same-query-and-or). This document can be used for finding more examples. The filter for each table type (connection) has its own type. For instance, the filter type for the `Associate` table is called `FilterExpression_Associate`. You can see this if you explore the schema with GraphiQL, for instance: ![Filter expression](../filter1.png) `FilterExpression_Associate` type, in turn, has field for each column in the table. This fields are of a type called *_FilterClause*, such as `Int32_FilterClause`, `LimitedString_FilterClause`, etc. Two additional fields, `_and` and `_or`, of the same `FilterExpression_Associate` allow composing complex filter with the AND and OR logical operators. ![Filter expression type](../filter2.png) The filter clause type, in turn, has fields representing operators, such as `_lt` or "lower than" or `_like` for the text comparison *like* operator. ![Filter clause type](../filter3.png) The following table lists the available operators: | Operator | Description | | -------- | ----------- | | `_eq` | equal to `value` | | `_gt` | greater than `value` | | `_gte` | greater than or equal to `value` | | `_in` | in list of values | | `_is_not_null` | is not `null` | | `_is_null` | is `null` | | `_is_off` | is `off` - available for bit field columns only | | `_is_on` | is `on` - available for bit field columns only | | `_lt` | less than `value` | | `_lte` | less than or equal to `value` | | `_like` | like `value` | | `_not_eq` | not equal `value` | | `_not_in` | not in list of values | | `_not_like` | not like `value` | | `_between` | between `from` and `to` (inclusive bounds) | | `_not_between` | not between `from` and `to` (exclusive bounds) | The filter can also be provided as an argument to the query. The filter value is an object of the filter expression type. This is shown in the following example: ```graphql { title = "Query" } query GetGLAs( $companyNo: Int, $filter : FilterExpression_GeneralLedgerAccount) { useCompany(no: $companyNo) { generalLedgerAccount(filter: $filter) { totalCount items { accountNo name } } } } ``` ```graphql { title = "Variables" } { "companyNo": 9112233, "filter": {"_and" :[ {"accountNo" :{"_gte" : 6000}}, {"accountNo" :{"_lt" : 7000}}]} } ``` ## `_in` and `_not_in` operators Most of the operators take a single value. The `_in` and `_not_in` operators, however, take an array of values, as show in the following example: ```graphql query read($cid : Int!) { useCompany(no: $cid) { associate (filter : {customerNo : {_in: [10001,10002,10003]}}) { totalCount items { name associateNo customerNo supplierNo } } } } ``` These two operators (`_in` and `_not_in`) are helpful for simplifying more complex filters. The following two tables show a filter using these two operators, and the equivalent expression without them. ```graphql { title = "With_in" } query read($cid : Int!) { useCompany(no: $cid) { associate (filter : { customerNo : {_in: [10001,10002,10003]}}) { totalCount items { name associateNo customerNo supplierNo } } } } ``` ```graphql { title = "Without_in" } query read($cid : Int!) { useCompany(no: $cid) { associate (filter : {_or: [ customerNo : {_eq: 10001}, customerNo : {_eq: 10002}, customerNo : {_eq: 10003} ]}) { totalCount items { name associateNo customerNo supplierNo } } } } ``` ```graphql { title = "With_in" } query read($cid : Int!) { useCompany(no: $cid) { associate (filter : { customerNo : {_not_in: [10001,10002,10003]}}) { totalCount items { name associateNo customerNo supplierNo } } } } ``` ```graphql { title = "Without_in" } query read($cid : Int!) { useCompany(no: $cid) { associate (filter : {_and: [ customerNo : {_not_eq: 10001}, customerNo : {_not_eq: 10002}, customerNo : {_not_eq: 10003} ]}) { totalCount items { name associateNo customerNo supplierNo } } } } ``` If the argument for the `_in` or `_not_in` operators is an empty array, the result will be an empty set. An example is shown below, where we first query the `Order` table, exporting the value of the `sellerOrBuyer` column to the variable `sellers`. We then query the `Associate` table, filtering for the employees with the numbers in the `sellers` array. Because no order matches the filter, the result is an empty `sellers` array, which results in no data being returned. ```graphql { title = "Query" } query find_selers($cid: Int!, $sellers: [Int!] = []) { useCompany(no: $cid) { order( filter: {sellerOrBuyer: {_gt: 999}}, first: 5) { items { sellerOrBuyer @export(as: "sellers") } } associate( filter: {employeeNo: {_in: $sellers}}) { items { employeeNo name } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "order": { "items": null }, "associate": { "items": null } } } } ``` If the `_in` or `_not_in` operators are used in an `AND` expression, and the argument is an empty array, the resulting filter will be an empty expression, and no data will be returned. ```graphql order( filter: {_and: [ {changeDateTime : {_gte: {"2024-01-24T12:00:00"}}} {orderNo: {_in: []}} ]}) { items { orderNo dueDate } } ``` This results in an empty filter. No data is returned when a filter is provided but it is empty. On the other hand, when no filter is provided, all the data is returned. If the `_in` or `_not_in` operators are used in an `OR` expression and the argument is an empty array, the subexpression is removed from the filter expression, which may still produce a valid filter that can return data. ```graphql { title = "Filter" } filter: {_or: [ {changeDateTime : {_gte: {"2024-01-24T12:00:00"}}} {orderNo: {_in: []}} ]} ``` ```graphql { title = "Equivalent" } filter: {_or: [ {changeDateTime : {_gte: {"2024-01-24T12:00:00"}}} ]} ``` ## `_between` / `_not_between` operators These two operators require two values that define the lower and upper bounds of the range. The range is inclusive, meaning that the values that are equal to the lower or upper bound are included in the result. ```graphql query read_glas($cid : Int!, $pagesize : Int!) { useCompany(no: $cid) { generalLedgerAccount( first: $pagesize, filter : {accountNo : {_between : {from: 1000, to: 2000}}}) { totalCount items { accountNo name } } } } ``` These operators are support for fields of the following types: - numeric (`Int`, `Long`, `Decimal`, etc.) - Date - Time - DateTime The following example shows an expression using both the `_between` operator (for an int field) and the `_not_between` operator (for a datetime field): ```graphql query read_glas($cid : Int!) { useCompany(no: $cid) { generalLedgerAccount(filter: { _and : [ {accountNo : { _not_between : { from : 3000, to : 5000 } }}, {changedDateTime : { _between : { from : "2024-05-01T12:00:00", to : "2024-05-30T23:59:59" } }} ]}) { totalCount items { accountNo name } } } } ``` ## Comparing with current date/time The model define various fields for date and time. All the nodes that have the type `Date_FilterClause`, `Time_FilterClause`, or `DateTime_FilterClause` can be compared with the current date/time. These GraphQL types, have, in addition for fields defining operators, an additional field called `_cmp` whose type is `ReferenceDateTime_FilterClause`, which looks as follows: ![ReferenceDateTime_FilterClause type](../filter4.png) This type features nodes for all the comparison operators available for date and time fields. However, their type is `ReferenceDateTime` that has the following fields: | Name | Type | Description | | ---- | ---- | ----------- | | `day` | `ReferenceDays` | The day this moment refers to. One of `YESTERDAY`, `TODAY`, or `TOMORROW`. | | `dayOffset` | `Int` | The number of days to offset from the current date. For instance `TODAY` with an offset of one is `TOMORROW`, and `TODAY` with an offset of -1 is `YESTERDAY`. | | `time` | `ReferenceTimePoints` | Can only have the value `NOW`. | | `minuteOffset` | `Int` | The number of minutes to offset from the current time. | The following example shows a query that fetches all orders that where changed in the last hour: ```graphql query read_orders ($cid: Int) { useCompany(no: $cid) { order(filter : { _and : [ { changedDateAsDate : { _cmp : { _eq : {day : TODAY, dayOffset : 0} } } }, { changedTimeAsTime : { _cmp : { _gte : {time : NOW, minuteOffset: -60} } } } ] }) { totalCount items { orderNo } } } } ``` This particular query can be simplified by using the `createdDateTime` field, as shown bellow: ```graphql query read_orders ($cid: Int) { useCompany(no: $cid) { order(filter : { createdDateTime : { _cmp : { _gte : { day : TODAY, time : NOW, minuteOffset : -60} } } }) { totalCount items { orderNo } } } } ``` ## Having expressions The same operators described here for filters can be used for `having` clauses in queries that include aggregations. To learn more about this, see [Grouping: The `having` argument](../schema/queries/grouping.md#the-having-argument). Sorting /businessnxtapi/features/sorting page API documentation explaining how to specify custom sorting of query results using the orderBy parameter in GraphQL queries. 2025-04-15T09:48:42+02:00 # Sorting API documentation explaining how to specify custom sorting of query results using the orderBy parameter in GraphQL queries. For every query, you can specify a sorting expression through the `orderBy` parameter on each connection. Every connection has the `orderBy` parameter, although its type differs for each connection. The name format is `OrderBy_`. Therefore, the sort order types have names such as `OrderBy_Associate` or `OrderBy_GeneralLedgerAccount`. This sort order type contains fields for all the columns of the table, except for the in-memory ones which cannot be searched or sorted. The type of all these fields is called `SortOrder` and is an enumeration with two values: `ASC` for asceding order and `DESC` for descending order. Therefore, this makes it possible to sort the results ascending or desceding by any column in the table. The following image shows a partial view of the `OrderBy_Associate` type of the `orderBy` parameter of the `UseCompany_Associate_Connection` connection. ![Sort order type](../sort1.png) Here is an example of a query for the first 10 customers from the associates table sorted descending by their name. ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { associate(first: 10, filter:{customerNo:{_not_eq:0}}, orderBy:[{name:DESC}]) { totalCount items { associateNo customerNo name } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "associate": { "totalCount": 319, "items": [ { "associateNo": 286, "customerNo": 10285, "name": "Yggdrasil Ltd." }, { "associateNo": 12, "customerNo": 10011, "name": "Wilbur Andersen" }, { "associateNo": 285, "customerNo": 10284, "name": "Werner Systems AS" }, ... ] } } } } ``` As you can see, the `orderBy` is an array of values which makes it possible to define multiple sorting columns. They are considered in the give order. ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { postalAddress( first :5, orderBy: [ {postalArea:ASC}, {postCode:ASC} ] ) { totalCount items { postCode postalArea } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "postalAddress": { "totalCount": 53, "items": [ { "postCode": "0001", "postalArea": "Bergen" }, { "postCode": "0010", "postalArea": "Bergen" }, { "postCode": "0015", "postalArea": "Bergen" }, { "postCode": "0018", "postalArea": "Oslo" }, { "postCode": "0021", "postalArea": "Oslo" } ] } } } } ``` The sort order can also be passed as a variable. Here is an example: ```graphql { title = "Query" } query read($cid : Int!, $size : Int!, $order : [OrderBy_PostalAddress]!) { useCompany(no: $cid) { postalAddress( first :$size, orderBy: $order) { totalCount items { postCode postalArea } } } } ``` ```graphql { title = "Variables" } { "cid": 9112233, "size": 10, "order": [ {"postalArea" : "ASC"}, {"postCode" :"ASC"} ] } ``` ## Deprecated sorting The sorting order can be specified with the `sortOrder` argument, is similar to `orderBy`, except that it is a single object instead of an array. > [!WARNING] > > The `sortOrder` argument is deprecated and will be removed in the future. Therefore, it is recommended to use the `orderBy` argument instead. When you need to specify more than one column for sorting, the only way to do it when using `sortOrder` is by using the composition field `_thenBy`. This is exemplified in the next query, where we fetch the first 10 postal addresses, sorted first by postal area, asceding, and then by postal code, also ascending. ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { postalAddress( first :5, sortOrder:{ postalArea:ASC _thenBy : { postCode:ASC}}) { totalCount items { postCode postalArea } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "postalAddress": { "totalCount": 53, "items": [ { "postCode": "0001", "postalArea": "Bergen" }, { "postCode": "0010", "postalArea": "Bergen" }, { "postCode": "0015", "postalArea": "Bergen" }, { "postCode": "0018", "postalArea": "Oslo" }, { "postCode": "0021", "postalArea": "Oslo" } ] } } } } ``` Any number or fields can be chained to define the sort order in this manner. However, only one table column per level can be specified without the `_thenBy` field. Otherwise, an error message is returned and the result wil be sorted in the alphabetical order of the specified columns. ```graphql { title = "Query" } query read($cid: Int!) { useCompany(no :$cid) { postalAddress( first : 3, sortOrder: { postalArea:ASC postCode:ASC }) { totalCount items { postCode postalArea } } } } ``` ```graphql { title = "Result" } { "errors": [ { "message": "More than one field is specified in one sort clause (postCode,postalArea). The result may be incorrect. Use the _thenBy field to chain multiple table fields in the sort order." } ], "data": { "useCompany": { "postalAddress": { "totalCount": 5093, "items": [ { "postCode": "0001", "postalArea": "OSLO" }, { "postCode": "0010", "postalArea": "OSLO" }, { "postCode": "0015", "postalArea": "OSLO" } ] } } } } ``` Pagination /businessnxtapi/features/pagination page Pagination uses relay style, supporting forward and backward navigation. 2025-04-15T09:48:42+02:00 # Pagination Pagination uses relay style, supporting forward and backward navigation. ## Overview Pagination is implemented in the relay style. Reference documentation can be found [here](https://relay.dev/graphql/connections.htm). Both forward and backward pagination are supported: - the arguments for forward pagination are `first` (specifing the maximum number of items to return) and `after` (which is an opaque cursor, typically pointing to the last item in the previous page) - the arguments for backward pagination are `last` (specifying the maximum number of items to return) and `before` (which is an opaque cursor, typically pointing to the first item in the next page) - a `skip` argument is available for both forward and backward pagination and specifies a number of records to be skipped (from the given cursor) before retrieving the requested number of records. > [!NOTE] > > The values for `first`, `last`, and `skip` must be positive integers. > [!WARNING] > > Pagination does not work with grouping (`groupBy` and `having` arguments). Information about a page is returned in the `pageInfo` node. This contains the following fields: | Field | Description | | ----- | ----------- | | `hasNextPage` | Indicates whether there are more records to fetch after the current page. | | `hasPreviousPage` | Indicates whether there are more records to fetch before the current page. | | `startCursor` | An opaque cursor pointing to the first record in the current page. | | `endCursor` | An opaque cursor pointing to the last record in the current page. | When forward pagination is performed, the `hasNextPage` field is `true` if there are more records to fetch after the current page. When backward pagination is performed, the `hasNextPage` field is `true` if there are more records to fetch before the current page. Similarly, when forward pagination is performed, the `hasPreviousPage` field is `true` if there are more records to fetch before the current page. When backward pagination is performed, the `hasPreviousPage` field is always `false`. ## Understanding pagination In the following example, we fetch the first 5 associtates. This being the first page, there is no argument for the `after` parameter. Passing a `null` value for the `after` parameter is equivalent to not passing it at all. ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { associate(first: 5) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { associateNo name } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "associate": { "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "startCursor": "MQ==", "endCursor": "NQ==" }, "items": [ { "associateNo": 1, "name": "Et Cetera Solutions" }, { "associateNo": 2, "name": "Rock And Random" }, { "associateNo": 3, "name": "Handy Help AS" }, { "associateNo": 4, "name": "EasyWay Crafting" }, { "associateNo": 5, "name": "Zen Services AS" } ] } } } } ``` For a following page, we need to take the value of the `endCursor` return from a query and supply it as the argument to `after`. This is shown in the following example, where `"NQ=="` from the query above was supplied for the `after` parameter: ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { associate(first: 5, after: "NQ==") { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { associateNo name } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "associate": { "pageInfo": { "hasNextPage": true, "hasPreviousPage": true, "startCursor": "Ng==", "endCursor": "MTA=" }, "items": [ { "associateNo": 6, "name": "Smart Vita AS" }, { "associateNo": 7, "name": "Full Force Services" }, { "associateNo": 8, "name": "HomeRun Auto AS" }, { "associateNo": 9, "name": "Trade Kraft AS" }, { "associateNo": 10, "name": "Nodic Cool Sports AS" } ] } } } } ``` Requests with backwards pagination are performed similarly, exept that `last` and `before` are used instead of `first` and `after`. In the following example, the value of `before` is taken from the value of `startCursor` returned by the previous query that returned the second page of associates (with five records per page). As a result, the data returned from this new query is actually the first page of associates. ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { associate(last: 5, before: "Ng==") { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { associateNo name } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "associate": { "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "startCursor": "MQ==", "endCursor": "NQ==" }, "items": [ { "associateNo": 1, "name": "Et Cetera Solutions" }, { "associateNo": 2, "name": "Rock And Random" }, { "associateNo": 3, "name": "Handy Help AS" }, { "associateNo": 4, "name": "EasyWay Crafting" }, { "associateNo": 5, "name": "Zen Services AS" } ] } } } } ``` Providing both forward and backward pagination arguments (`first`/`after` and `last`/`before`) is illegal and the query will fail with an error. If pagination arguments are not supplied a default page size of 5000 records is used. When you fetch data in pages (which is recommended for most scenarious) you can fetch the total number of objects in the table with disregard to the page that is fetched or the size of the page. This is possible with the `totalCount`. This will return the number of records that match the filter (if any) but ignoring paginagion (if any). This feature is exemplified below. We, again, fetch the first 5 associates that have a customer number greater or equal than 11000 but also request the total number of records that match this filter. ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { associate( first: 5, filter: {customerNo :{_gte: 11000}}) { totalCount pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { associateNo customerNo name } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "associate": { "totalCount": 29, "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "startCursor": "MQ==", "endCursor": "NQ==" }, "items": [ { "associateNo": 308, "customerNo": 11000, "name": "Auto Glory AS" }, { "associateNo": 309, "customerNo": 11001, "name": "Home Team Business" }, { "associateNo": 310, "customerNo": 11003, "name": "Corpus Systems" }, { "associateNo": 311, "customerNo": 11004, "name": "Dash Credit AS" }, { "associateNo": 312, "customerNo": 11007, "name": "Smart Help Services AS" } ] } } } } ``` You can skip a number of records before feetching a page (with the size indicated by either the `first` or the `last` arguments) using the argument `skip`. This is an optional argument. When present, it indicates the number of records to be skipped. Here is an example: ```graphql { title = "Forward" } query read($cid : Int!) { useCompany(no: $cid) { generalLedgerAccount(skip : 5, first: 10) { totalCount items { accountNo } } } } ``` ```graphql { title = "Backward" } query read($cid : Int!) { useCompany(no: $cid) { generalLedgerAccount(skip : 5, last : 10, before: "MTE=") { totalCount items { accountNo } } } } ``` > [!WARNING] > > Because the execution of queries for joined tables is optimized for performance, pagination does not work as described in this document. To understand the problem and the workaround see [Unoptimized queries](./unoptimized.md). ## Pagination in depth To understand how pagination works, we will consider the following example, where the general ledger account table contains 25 records. It could look like this: ``` page 1 (10 records) page 2 (10 records) page 3 (5 records) |---------------------------------|---------------------------------|---------------------------------| | 1000 | 1020 | ... | 1100 | 1120 | 1130 | 1140 | ... | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 | ``` ### Forward pagination To fetch the records from the beginning, we perform a query like this: ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { generalLedgerAccount(first : 10, after: null) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { accountNo } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "generalLedgerAccount": { "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "startCursor": "MQ==", "endCursor": "MTA=" }, "items": [ { "accountNo": 1000 }, { "accountNo": 1020 }, ... { "accountNo": 1100 }, { "accountNo": 1120 } ] } } } } ``` This returns the first ten records, from index 1 to 10, and the `startCursor` and `endCursor` are pointing to the first and last elements in the set. ``` page 1 (10 records) |---------------------------------| | 1000 | 1020 | ... | 1100 | 1120 | 1130 | 1140 | ... | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 | ^ ^ | | startCursor endCursor ``` To fetch the next page of 10 records, we provide the value of `endCursor` as the argument for `after`: ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { generalLedgerAccount(first : 10, after: "MTA=") { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { accountNo } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "generalLedgerAccount": { "pageInfo": { "hasNextPage": true, "hasPreviousPage": true, "startCursor": "MTE=", "endCursor": "MjA=" }, "items": [ { "accountNo": 1130 }, { "accountNo": 1140 }, ... { "accountNo": 1240 }, { "accountNo": 1250 } ] } } } } ``` This returns the second page of ten records, from index 11 to 20, and the `startCursor` and `endCursor` are pointing to the first and last elements in the set. ``` after page 2 (10 records) | |---------------------------------| v | 1000 | 1020 | ... | 1100 | 1120 | 1130 | 1140 | ... | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 | ^ ^ | | startCursor endCursor ``` To fetch the next page of 10 records, we repeat the operation, again, using the `endCursor` for the value of the `after` argument: ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { generalLedgerAccount(first : 10, after: "MjA=") { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { accountNo } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "generalLedgerAccount": { "pageInfo": { "hasNextPage": false, "hasPreviousPage": true, "startCursor": "MjE=", "endCursor": "MjU=" }, "items": [ { "accountNo": 1260 }, { "accountNo": 1270 }, { "accountNo": 1280 }, { "accountNo": 1300 }, { "accountNo": 1310 }, ] } } } } ``` This returns a page of only 5 records, from index 21 to 25, and the `startCursor` and `endCursor` are pointing to the first and last elements in the set. ``` after page 3 (5 records) | |----------------------------------| v | 1000 | 1020 | ... | 1100 | 1120 | 1130 | 1140 | ... | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 | ^ ^ | | startCursor endCursor ``` The value of the `hasNextPage` field is `false` because there are no more records to fetch. > [!NOTE] > > The value used for the `after` argument should be the value of the `endCursor` from the previous page (unless it's `null`, in which case it means the fetching should start from the beginning). The `after` cursor identifies the position of the last record in a (previous) page. The record at the position represented by the cursor passed to the `after` argument is not included in the result set. A page starts with the record following the one identified by `after`. It is also possible to skip records before fetching a page. This is done by providing the `skip` argument. The following example skips the first 3 records before fetching a page of 10 records. We don't start from the beginning, but at the end of the first page of 10 records, as exemplify below: ``` after skip 3 records page of 10 records | >-------------------<|-----------------------------------------------| v | 1000 | 1020 | ... | 1100 | 1120 | 1130 | 1140 | 1150 | 1160 | ... | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 | ^ ^ | | startCursor endCursor ``` ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { generalLedgerAccount(first : 10, skip : 3, after: "MTA=") { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { accountNo } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "generalLedgerAccount": { "pageInfo": { "hasNextPage": true, "hasPreviousPage": true, "startCursor": "MTQ=", "endCursor": "MjM=" }, "items": [ { "accountNo": 1160 }, { "accountNo": 1200 }, ... { "accountNo": 1270 }, { "accountNo": 1280 } ] } } } } ``` ### Backward pagination Backward pagination works similarly, except that the `last` and `before` arguments are used instead of `first` and `after`. The argument `last` indicates how many records the page should contain. The argument `before` represents the position of the record before which the page is located. The record at the `before` position is not included in the result set. Therefore, if in the preceding example we fetched all the records in the table, and now want to move backwards, but use the value of the `endCursor` as the arguement for `before`, then this last record will not be included in the returned page. ``` page of 10 records before |---------------------------------------------------------------------| | v | 1000 | 1020 | ... | 1160 | 1200 | 1210 | 1220 | 1230 | 1240 | 1250 | 1260 | 1270 | 1280 | 1300 | 1310 | ^ ^ | | startCursor endCursor ``` ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { generalLedgerAccount(last : 10, before: "MjU=") { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { accountNo } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "generalLedgerAccount": { "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "startCursor": "MTU=", "endCursor": "MjQ=" }, "items": [ { "accountNo": 1200 }, { "accountNo": 1210 }, ... { "accountNo": 1280 }, { "accountNo": 1300 } ] } } } } ``` Fragments /businessnxtapi/features/fragments page Documentation on using and nesting fragments in GraphQL queries to efficiently construct reusable sets of fields. 2025-04-15T09:48:42+02:00 # Fragments Documentation on using and nesting fragments in GraphQL queries to efficiently construct reusable sets of fields. You can build fragments to construct sets of fields and then include them in queries where you need to. Nested fragments are supported. Here is an example of a query constructed using a fragment called `associateBasic`, which in turn uses two other fragments, `associateContact` and `associateAddress`. ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no: $cid) { associate { items { ...associateBasic customerNo supplierNo } } } } fragment associateBasic on Associate { name userName ... associateContact ... associateAddress } fragment associateContact on Associate { emailAddress phone } fragment associateAddress on Associate { addressLine1 postCode postalArea countryNo } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "associate": { "items": [ { "name": "ABB Installasjon AS", "userName": "", "emailAddress": "", "phone": "12345678", "addressLine1": "Ole Deviks Vei 10", "postCode": "1375", "postalArea": "Billingstad", "countryNo": 0, "customerNo": 10000, "supplierNo": 0 }, { "name": "ABB Kraft AS", "userName": "", "emailAddress": "", "phone": "12345678", "addressLine1": "Jacob Borchs Gate 6", "postCode": "3002", "postalArea": "Drammen", "countryNo": 0, "customerNo": 10001, "supplierNo": 0 } ] } } } } ``` Named queries and parameters /businessnxtapi/features/parameters page GraphQL supports named queries and variables, enhancing query clarity. Example includes defining and utilizing variables for efficient data retrieval. 2024-09-24T15:57:29+02:00 # Named queries and parameters GraphQL supports named queries and variables, enhancing query clarity. Example includes defining and utilizing variables for efficient data retrieval. GraphQL supports variables and naming queries, which should always be used since they make the query more clear while reading. Let's look at an example. In the following snippet, `GetCustomers` is a named query that returns a page associates. This query has three parameters: `$companyNo` specifies the Visma.net company number that uniquely identifies the company within the system, `$pageSize` the maximum number of records to be returned, and `$after` the cursor that indicates the position after which the records are to be fetched. If nothing is specified, this means the first `$pageSize` records are to be fetched. ```graphql query GetCustomers($companyNo: Int, $pageSize: Int, $after :String) { useCompany(no: $companyNo) { associate(first: $pageSize, after: $after) { totalCount pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { associateNo customerNo name } } } } ``` If you use GraphiQL, you can define the variables in the query variable pane in the lower left of the IDE, as shown in the following image: ![GraphiQL Query Variables](../graphiqlvars.png) When you perform the request to GraphQL programmatically, or from a tool that does not directly support GraphQL variables, you must put the request containing both the query and variables in the body, as `application/json`. ```json { "query" : "query GetCustomers($companyNo: Int, $pageSize: Int, $after :String)\n{\n useCompany(no: $companyNo)\n {\n associate(first: $pageSize, after: $after)\n {\n totalCount\n pageInfo\n {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n items\n {\n associateNo\n customerNo\n name\n }\n }\n }\n}", "variables":"{\"companyNo\": 9112233,\"pageSize\": 5}" } ``` Details about the raw form of a GraphQL query were presented in an earlier section of this tutorial. Aliases /businessnxtapi/features/aliases page GraphQL API documentation on using aliases to rename fields and query the same field multiple times with different arguments. 2025-04-15T09:48:42+02:00 # Aliases GraphQL API documentation on using aliases to rename fields and query the same field multiple times with different arguments. GraphQL allows you to rename the result of a field to anything you want. This is possible with the help of [aliases](https://graphql.org/learn/queries/#aliases). You can use this feature to: - rename long fields and use name that you prefer over fields from the schema - query for the same field multiple times but with different arguments Here is an example: ```graphql query GetPaymentLines($cid: Int!) { useCompany(no: $cid) { paymentLine(first: 1) { items { paymentNo lineNo currency : joinup_Currency { isoCode } supplier : joinup_Associate_via_Supplier { bankAccount country : joinup_Country { isoCode } } } } } } ``` Aliases can be used in: - queries (example shown above) - inserts and updates - aggregates Here is another example in an aggregate function: ```graphql query GetAgregates($cid: Int!) { useCompany(no: $cid) { order_aggregate { totalvat : sum { vatAmountDomestic } } } } ``` As previously mentioned, an important use case is to query for the same field multiple times but using different arguments. An example is shown below: ```graphql { title = "Query" } query ($cid : Int!) { useCompany(no : $cid) { some : generalLedgerAccount(first : 2) { items { accountNo name } } more : generalLedgerAccount(first : 2, filter : {accountNo : {_gt : 6000}}) { items { accountNo name } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "some": { "items": [ { "accountNo": 1000, "name": "Forskning og utvikling" }, { "accountNo": 1020, "name": "Konsesjoner" } ] }, "more": { "items": [ { "accountNo": 6010, "name": "Avskr. maskiner, inventar mv." }, { "accountNo": 6020, "name": "Avskr. immaterielle eiendeler" } ] } } }, "extensions": { "vbnxt-trace-id": "02c029a3a07c7a..." } } ``` Directives /businessnxtapi/features/directives page GraphQL directives modify query execution, including core directives like @include and @skip, and custom ones like @export. 2025-04-15T09:48:42+02:00 # Directives GraphQL directives modify query execution, including core directives like @include and @skip, and custom ones like @export. Directives are a GraphQL feature that affect the execution of a query in any way the server desires. Directives can be attached to different parts of the schema (field, fragment inclusion, etc.). There are several core GraphQL directives: | Directive | Attached to | Description | | --------- | ----------- | ----------- | | `@include` | field, fragment inclusion | Only include this field in the result if the argument is true. | | `@skip` | field, fragment inclusion | Skip this field if the argument is true. | In addition, we provide custom directives: | Directive | Attached to | Description | | --------- | ----------- | ----------- | | `@export` | field, fragment inclusion | Export the value of a field into a variable that can be used somewhere else in the query. | | `@dependsOn` | field, fragment inclusion | Specify that a field depends on another field. | ## The `@include` directive Includes a field or fragment in the result only if the Boolean argument is `true`. **Syntax**: ```graphql @include(if: Boolean!) ``` **Example**: ```graphql query($cid : Int!, $pagesize : Int, $withdetails : Boolean!) { useCompany(no : $cid) { order(first : $pagesize) { totalCount items { orderNo orderDate lines : joindown_OrderLine_via_Order(first: 2) @include(if: $withdetails) { totalCount items { lineNo transactionDate } } } } } } ``` **Result**: ```graphql { title = "$withdetails is false" } { "data": { "useCompany": { "order": { "totalCount": 451, "items": [ { "orderNo": 1, "orderDate": 20210212 }, { "orderNo": 2, "orderDate": 20130203 } ] } } } } ``` ```graphql { title = "$withdetails is true" } { "data": { "useCompany": { "order": { "totalCount": 451, "items": [ { "orderNo": 1, "orderDate": 20210212, "lines": { "totalCount": 6, "items": [ { "lineNo": 1, "transactionDate": 0 }, { "lineNo": 2, "transactionDate": 0 } ] } }, { "orderNo": 2, "orderDate": 20130203, "lines": { "totalCount": 5, "items": [ { "lineNo": 1, "transactionDate": 20140904 }, { "lineNo": 2, "transactionDate": 20140904 } ] } } ] } } } } ``` ## The `@skip` directive Skips a field if the Boolean argument is `true`. **Syntax**: ```graphql @skip(if: Boolean!) ``` **Example**: ```graphql query($cid : Int!, $pagesize : Int, $nodetails : Boolean!) { useCompany(no : $cid) { order(first : $pagesize) { totalCount items { orderNo orderDate lines : joindown_OrderLine_via_Order(first: 2) @skip(if: $nodetails) { totalCount items { lineNo transactionDate } } } } } } ``` **Result**: ```graphql { title = "$nodetails is true" } { "data": { "useCompany": { "order": { "totalCount": 451, "items": [ { "orderNo": 1, "orderDate": 20210212 }, { "orderNo": 2, "orderDate": 20130203 } ] } } } } ``` ```graphql { title = "$nodetails is false" } { "data": { "useCompany": { "order": { "totalCount": 451, "items": [ { "orderNo": 1, "orderDate": 20210212, "lines": { "totalCount": 6, "items": [ { "lineNo": 1, "transactionDate": 0 }, { "lineNo": 2, "transactionDate": 0 } ] } }, { "orderNo": 2, "orderDate": 20130203, "lines": { "totalCount": 5, "items": [ { "lineNo": 1, "transactionDate": 20140904 }, { "lineNo": 2, "transactionDate": 20140904 } ] } } ] } } } } ``` ## The `@export` directive The `@export` directive in GraphQL exports the value of a field into a variable that is used somewhere else in the query. This can be either a single value or an array. **Syntax**: ```graphql @export(as: "variablename", distinct : true) ``` **Example**: Fetch the cutomer number of the associate whose indentifier is specified and then use the customer number to fetch orders. ```graphql { title = "Query" } query($cid : Int!, $ano : Int!, $pagesize: Int, $customerId : Int = 0) { useCompany(no: $cid) { associate(filter : {associateNo : {_eq: $ano}}) { items { customerNo @export(as: "customerId") } } order(first : $pagesize, filter : {customerNo : {_eq : $customerId}}) { totalCount items { orderNo orderDate customerNo } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "associate": { "items": [ { "customerNo": 10000 } ] }, "order": { "totalCount": 14, "items": [ { "orderNo": 81, "orderDate": 20150115, "customerNo": 10000 } ] } } } } ``` **Example**: Add one order and two order lines for the order with a single request. ```graphql { title = "Query" } mutation ($cid: Int, $cno : Int, $pid1 : String, $pid2 : String, $orderId: Int = 0) { useCompany(no: $cid) { order_create( values: [ { orderDate: 20221104, customerNo: $cno, orderType: 1, transactionType: 1 } ] ) { affectedRows items { orderNo @export(as: "orderId") } } orderLine_create( values: [ { orderNo: $orderId, productNo: $pid1, quantity: 1 }, { orderNo: $orderId, productNo: $pid2, quantity: 2 } ] ) { affectedRows items { lineNo orderNo productNo } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "order_create": { "affectedRows": 1, "items": [ { "orderNo": 632 } ] }, "orderLine_create": { "affectedRows": 2, "items": [ { "lineNo": 1, "orderNo": 632, "productNo": "1001" }, { "lineNo": 2, "orderNo": 632, "productNo": "1002" } ] } } } } ``` There are several things to keep in mind when using this directive: - The variable into which the field value is exported must be defined in the query parameter list. Otherwise, when you use the variable later on in another part of the query, the server will complain that it is not defined. - You can only export the value of one field into one variable. If you attempt to write values from multiple fields into the same variable they will be overwritten based of the order of evaluation. - If you export multiple values into the same variable, the last field that is evaluated will define the value of the variable. - If the variable is defined as an array, you can store multiple values. In the previous examples, we have used a variable that could store a single value. Therefore, if a query returned multiple elements, a field would get evaluated multiple times and each time the variable would be overritten. The value from the last evaluation is the one that is finally stored in the variable. However, all the values can be preserved in an array. The only change is that you need to define the variable of an array type. Moreover, you can use the Boolean optional argument `distinct` to retain only the distict values and discard duplicates. An array variable can be used for instance with the `_in` and `_not_in` filters. The following example shows a query that fetches information about all the orders that have lines that were updated after a given moment in time: ```graphql query read_modified_orders($cid : Int!, $dt : DateTime, $ono : [Int] = []) { useCompany(no : $cid) { orderLine(filter : {changedDateTime : {_gt : $dt}}) { items { orderNo @export(as : "ono", distict : true) } } order(filter : {orderNo : {_in : $ono}) { items { orderNo orderDate customerNo } } } } ``` ## The `@dependsOn` directive The `@dependsOn` directive in GraphQL specifies that a field depends on another field. This directive can be used in the rare situations when fields are executed out of order due. Typically, fields are executed in the order they are defined in the query. However, in order to optimize the query execution, some requests are packed together before being sent to the back-end. This changes the order of execution as some fiels defined later in the query are executed before fields defined earlier. Moreoever, some fields depend on others because they use a variable set from an earlier field with the use of the `@export` directive. Use the `@dependsOn` directive to ensure that a field is executed only after a previous field was executed. **Syntax**: ```graphql @dependsOn(field: "name") ``` **Example**: ```graphql mutation CreateBatchWithAttachment ($cid: Int, $batchId: Int = 0, $fina: String, $fiby: String, $tok: String) { useCompany(no: $cid) { # create the batch batch_create( values: { voucherSeriesNo: 1, valueDate: 20250121 description: "Demo batch" } ) { affectedRows items { batchNo @export(as: "batchId") } } # create the voucher voucher_create( values: { batchNo: $batchId voucherDate: null voucherType: 21 voucherNo: null debitAccountNo: 5000 creditAccountNo: 1920 amountDomestic: 500.00 text: "first voucherline" } ) { affectedRows items { batchNo voucherNo } # add a document to the batch voucher_processings { addNewDocument( filter: {batchNo: {_eq: $batchId}}, args: { fileName: $fina, fileBytes: $fiby } ) { succeeded } } # upload the document to the file service incomingAccountingDocumentAttachment_processings { uploadToFileService( filter: {fileName: {_eq: $fina}}, args: {connectToken: $tok} ) @dependsOn(field: "addNewDocument") { succeeded } } } } ``` ## References You can learn more about the core GraphQL directives from these articles: - [Directives](https://graphql-dotnet.github.io/docs/getting-started/directives/) - [GraphQL Directives](https://spec.graphql.org/October2021/#sec-Type-System.Directives) Error handling /businessnxtapi/features/errors page GraphQL API error handling - status codes, error messages, syntax errors, multiple request outcomes, execution tracing for troubleshooting, and detailed error property explanations. 2025-04-15T09:48:42+02:00 # Error handling GraphQL API error handling - status codes, error messages, syntax errors, multiple request outcomes, execution tracing for troubleshooting, and detailed error property explanations. A GraphQL query is an HTTP POST request with the content type `application/json` and the body having the form: ```json { "query" : "...", "variables" : "..." } ``` If the authorization for a request fails (no authorization header, expired token, etc.) the return status code is `401` (`Unauthorized)` and the response body is the following: ```html 401 Authorization Required

401 Authorization Required


nginx
``` However, for most requests, whether they are successful or they failed, the return status code is `200`. When a successful query is executed, the JSON object that is returned contains a property called `data` whose value is an object representing the returned data. Here is an example: ```json { "data": { "useCompany": { "generalLedgerAccount": { "items": [ { "accountNo": 9001, "name": "Special account", } ] } } } } ``` When an error occurs during the execution of the request, the return JSON object contains both the `data` property, as well as a property called `errors`, which is an array of objects containing information about the error(s) that occurred. The following is an example: ```json { "errors": [ { "message": "Error: Could not get authorize user using VismaNetCompanyId: 1234567. Description: External integration error. Status: 18." } ], "data": { "useCompany": { "generalLedgerAccount": null } } } ``` If the GraphQL query has a syntax error, the returned information contains not just a message but also detains about the location of the error within the query. This is exemplified below: ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no : $cid) { generalLedgerAccount { totalRows } } } ``` ```graphql { title = "Result" } { "errors": [ { "message": "GraphQL.Validation.Errors.FieldsOnCorrectTypeError: Cannot query field 'totalRows' on type 'Query_UseCompany_GeneralLedgerAccount_Connection'. Did you mean 'totalCount'?", "locations": [ { "line": 4, "column": 7 } ], "extensions": { "code": "FIELDS_ON_CORRECT_TYPE", "codes": [ "FIELDS_ON_CORRECT_TYPE" ], "number": "5.3.1" } } ] } ``` A GraphQL query may contain multiple requests (such as reading from different tables). It is possible that some may be successful, while other will fail. The result will contain data that was fetched successfully but also information about the errors that occurred for the other requests. Here is an example: ```graphql { title = "Query" } query read($cid : Int!) { useCompany(no : $cid) { generalLedgerAccount(first: 2) { totalCount items { accountNo name } } generalLedgerBalance(last: 10) { totalCount items { accountNo sumDebitObDomestic sumCreditObDomestic } } } } ``` ```graphql { title = "Result" } { "errors": [ { "message": "The cursor 'before' must have a value.", "path": [ "useCompany", "generalLedgerBalance" ] } ], "data": { "useCompany": { "generalLedgerAccount": { "totalCount": 340, "items": [ { "accountNo": 1000, "name": "Forskning og utvikling" }, { "accountNo": 1020, "name": "Konsesjoner" } ] }, "generalLedgerBalance": null } } } ``` On the other hand, an operation may be successful (such as inserting a new row) although sub-operations (such as assigning a value to a column) may fail. GraphQL returns the result as well as all the errors messages from executing the request. ```graphql { title = "Query" } mutation create_batch($cid : Int!) { useCompany(no: $cid) { batch_create(values: [ { valueDate: 20211122 voucherSeriesNo: 3 orgUnit1 : 0 orgUnit2 : 0 orgUnit3 : 0 orgUnit4 : 0 orgUnit5 : 0 period :11 year :2021 } ]) { affectedRows items { batchNo valueDate voucherSeriesNo } } } } ``` ```graphql { title = "Result" } { "errors": [ { "message": "Error: Illegal value date 11/22/2021. Check suspension date and the accounting periods and VAT periods tables..", "path": [ "useCompany", "batch_create", "values/0" ], "extensions": { "data": { "status": 0 } } }, { "message": "Error: Org unit class not named. Description: Not read access to destination column. Column: OrgUnit3.", "path": [ "useCompany", "batch_create", "values/0" ], "extensions": { "data": { "status": 3, "status_name" : "NotReadAccessToDestinationColumn" } } }, { "message": "Error: Org unit class not named. Description: Not read access to destination column. Column: OrgUnit4.", "path": [ "useCompany", "batch_create", "values/0" ], "extensions": { "data": { "status": 3, "status_name" : "NotReadAccessToDestinationColumn" } } }, { "message": "Error: Org unit class not named. Description: Not read access to destination column. Column: OrgUnit5.", "path": [ "useCompany", "batch_create", "values/0" ], "extensions": { "data": { "status": 3, "status_name" : "NotReadAccessToDestinationColumn" } } } ], "data": { "useCompany": { "batch_create": { "affectedRows": 1, "items": [ { "batchNo": 26, "valueDate": 20211122, "voucherSeriesNo": 3 } ] } } } } ``` ## Understanding error information When an error occurs during the execution of the query, you may see the following information for each returned error: - `message`: always present, contains a description of the error - `path`: contains the path in the query (schema) of the field that produced the error - `extensions`: additional information about the error. An example is `data:status` that contains an internal error code that could be useful for troubleshouting a failed execution. In addition, `data:status_name` provides a symbolic name of the status code, such as `NotReadAccessToDestinationColumn`. Another example of an error message from attempting to create an order with a duplicate ID is shown below. You can see that `status` is 3 but `status_name` is set to `PrimaryKeyAssignmentFailed` to give you a better understanding of what the status code 3 means in this context. ```json { "errors": [ { "message": "A record with the same primary key already exists.", "path": [ "useCompany", "order_create", "values/0" ], "extensions": { "data": { "status": 3, "status_name": "PrimaryKeyAssignmentFailed" } } } ], "data": { "useCompany": { "order_create": { "items": null } } } } ``` However, it is important to note that these status codes (and their names) are not unique. A request is composed of multiple operations, such as selecting a table, assigning a value to a field, etc. There are various status codes for each such contextual operation. Therefore, an operation may return status code 3 that means `NotReadAccessToDestinationColumn`, or status code 3 that means `NotInsertAccessToTable`. That is why the `status_name` field is useful to help you better understand the problem. > [!TIP] > > For more information about the GraphQL engine errors (such as schema errors, input errors and processing errors) see [GraphQL.NET Error Handling](https://graphql-dotnet.github.io/docs/getting-started/errors/). ## Tracing query execution Each GraphQL query that executes is assigned a unique identifier. This is used to trace the execution of the query and can be used for identifying problems with the execution. If you need to contact the Business NXT support for help to investigate a problem, you need to provide this unique identifier. You can find this unique ID in the response of a GraphQL request. Although this was skipped in the examples shown in this tutorial (for simplicity), each response has a field called `extensions` that contains an object named `vbnxt-trace-id`. ```graphql { title = "Query" } query read($cid : Int, $pagesize : Int) { useCompany(no: $cid) { generalLedgerAccount(first: $pagesize) { totalCount items { accountNo name } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "generalLedgerAccount": { "totalCount": 340, "items": [ { "accountNo": 1000, "name": "Forskning og utvikling" }, { "accountNo": 1020, "name": "Konsesjoner" }, ... ] } } }, "extensions": { "vbnxt-trace-id": "00000000000000000196b4ea383242fa" } } ``` Use the value of the `vbnxt-trace-id` when contacting the Business NXT support. > [!TIP] > > The same trace identifier can be retrieved from the response headers. GraphQL responses have a custom `x-vbc-traceid` header containing the value of the trace identifier.
Unoptomized queries /businessnxtapi/features/unoptimized page Learn about the limitations and handling of optimized queries for joined tables, including performance trade-offs and the usage of the unoptimized Boolean argument for accurate pagination and counting. 2025-04-15T09:48:42+02:00 # Unoptomized queries Learn about the limitations and handling of optimized queries for joined tables, including performance trade-offs and the usage of the unoptimized Boolean argument for accurate pagination and counting. Because of the way fetching data from the backend is optimized, several features do not work for joined tables (the fields that start with the `joindown_` or `joinup_` prefix): - pagination (requesting data in pages and diplaying paging information - the `pageInfo` field) - counting the total number of rows that match the filter (the `totalCount` field) In order to have these features working properly, you must explicitly request that the execution of the query is not optimized by the system. This is done with a Boolean field argument called `unoptimized`. The way this works is described below. First, let's understand the problem. ## Query optimization When you request data from joined tables we are faced with a problem called the N+1 problem. Let's say you want to fetch the last 10 orders of a customer, but along with the order heads also the order lines info. Typically, this means we will do one request to fetch the order heads first, and then, for each order, we perform one request to fetch the lines. For 10 orders that is 10 more request for the lines, amounting to a total of 11 requests. For N orders, that amounts to N+1 requests. This incurs a performace loss and this is aggravated if more tables are joined together. For instance, fetching the orders, and for each order the order lines, and for each order line the order notes, just to give an example. To avoid the performance penalty, our system is optimizing the queries. As a result, we are performing only 2 requests instead of N+1. For the orders and orders line example, we perform a first request to fetch the heads and then a second request to fetch the lines. The result is a potential 10x or more speed-up for executing a query. The downside is the features mentioned above no longer work. ## The problem with the optimization The understand the problem with optimization let us discuss the following scenario. Consider a query that fetches the first 10 orders and for each order the first 3 order lines. That means that for each order, the query should return a maximum of 3 order lines, therefore, potentially, a total of 30 order lines in total. However, some orders may have less than 3 order lines others may have more. The current implementation takes the first 30 order lines (that match the given filter, if any) regardless to which order they belong. Let's explain this with an example: | Order no | No. of order lines | No. of expected returned lines | No. of actual returned lines | | -------- | ----------- | ----------------------- | --------------------- | | 1 | 2 lines | 2 lines | 2 lines | | 2 | 5 lines | 3 lines | 5 lines | | 3 | 2 line | 2 lines | 2 lines | | 4 | 1 lines | 1 lines | 1 lines | | 5 | 7 lines | 3 lines | 7 lines | | 6 | 3 lines | 3 lines | 3 lines | | 7 | 5 lines | 3 lines | 5 lines | | 8 | 8 lines | 3 lines | 5 lines | | 9 | 5 lines | 3 lines | 0 lines | | 10 | 2 lines | 2 lines | 0 lines | The third column in this table shows the expected result when you ask for the first 3 order lines for each order. What is returned instead is the data of the fourth column. This is shown with the following query: ```graphql { title = "Query" } query ($cid :Int!, $pagesize : Int) { useCompany(no: $cid) { order(first: $pagesize) { totalCount items { orderNo joindown_OrderLine_via_Order(first: 3) { items { lineNo } } } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "order": { "totalCount": 348, "items": [ { "orderNo": 1, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 } ] } }, { "orderNo": 2, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 }, { "lineNo": 4 }, { "lineNo": 5 } ] } }, { "orderNo": 3, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 } ] } }, { "orderNo": 4, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 } ] } }, { "orderNo": 5, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 }, { "lineNo": 4 }, { "lineNo": 5 }, { "lineNo": 6 }, { "lineNo": 7 } ] } }, { "orderNo": 6, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 } ] } }, { "orderNo": 7, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 }, { "lineNo": 4 }, { "lineNo": 5 } ] } }, { "orderNo": 8, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 }, { "lineNo": 4 }, { "lineNo": 5 } ] } }, { "orderNo": 9, "joindown_OrderLine_via_Order": { "items": null } }, { "orderNo": 10, "joindown_OrderLine_via_Order": { "items": null } } ] } } } } ``` The reason for this is that we fetch a total of 10\*3 order lines from the order lines table, in a single request. This means the result is not what was actually requested from with the query. Notice this is not a problem when no pagination is requested. The following example, that fetches some orders and all their order lines works without any problem. ```graphql { title = "Query" } query ($cid :Int!, $pagesize : Int) { useCompany(no: $cid) { order(first: $pagesize) { totalCount items { orderNo joindown_OrderLine_via_Order { items { lineNo } } } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "order": { "totalCount": 348, "items": [ { "orderNo": 1, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 } ] } }, { "orderNo": 2, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 }, { "lineNo": 4 }, { "lineNo": 5 } ] } }, { "orderNo": 3, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 } ] } }, { "orderNo": 4, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 } ] } }, { "orderNo": 5, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 }, { "lineNo": 4 }, { "lineNo": 5 }, { "lineNo": 6 }, { "lineNo": 7 } ] } }, { "orderNo": 6, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 } ] } }, { "orderNo": 7, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 }, { "lineNo": 4 }, { "lineNo": 5 } ] } }, { "orderNo": 8, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 }, { "lineNo": 4 }, { "lineNo": 5 }, { "lineNo": 6 }, { "lineNo": 7 }, { "lineNo": 8 } ] } }, { "orderNo": 9, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 }, { "lineNo": 4 }, { "lineNo": 5 } ] } }, { "orderNo": 10, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 } ] } } ] } } } } ``` However, fetching the total count of order lines per each order and displaying paging information for each chunk of order lines does not work in this case either. ## Unoptimized queries To solve the problem of the optimized queries for joined tables, you can explicitly request to run the query without optimizations. This means there will be N+1 requests to the backend and the execution time will increase significantly. However, counting records from the joined table and fetching data in pages works as expected. The unoptimized execution is requested with a Boolean argument for the field called `unoptimized` that must be set to `true`. This is shown in the following example: ```graphql { title = "Query" } query ($cid :Int!, $pagesize : Int) { useCompany(no: $cid) { order(first: $pagesize) { totalCount items { orderNo joindown_OrderLine_via_Order(first: 3, unoptimized: true) { items { lineNo } } } } } } ``` ```graphql { title = "Result" } { "data": { "useCompany": { "order": { "totalCount": 348, "items": [ { "orderNo": 1, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 } ] } }, { "orderNo": 2, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 } ] } }, { "orderNo": 3, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 } ] } }, { "orderNo": 4, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 } ] } }, { "orderNo": 5, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 } ] } }, { "orderNo": 6, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 } ] } }, { "orderNo": 7, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 } ] } }, { "orderNo": 8, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 } ] } }, { "orderNo": 9, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 }, { "lineNo": 3 } ] } }, { "orderNo": 10, "joindown_OrderLine_via_Order": { "items": [ { "lineNo": 1 }, { "lineNo": 2 } ] } } ] } } } } ``` The `unoptimized` flag is an argument to a connection (see [Queries](../schema/queries/query.md)). However, a connection may contain other inner connections. These are the joindown tables discussed here and seen in the previous examples. When you are requesting the fetching of data in an unoptimized manner, this applies not only to the connection on which the argument was specified, but also on all its descendants. This behavior can be overridden by using the `unoptimized` argument again on a descendant connection. Here are several examples: - the order lines are fetched in an optimized manner but not the order line notes ```graphql query ($cid :Int!, $pagesize : Int) { useCompany(no: $cid) { order(first: $pagesize) { totalCount items { orderNo joindown_OrderLine_via_Order(first: 3) { items { lineNo joindown_OrderLineNote_via_OrderLine(unoptimized : true) { items { note } } } } } } } } ``` - both the order lines and the order line notes are fetched unoptimized ```graphql query ($cid :Int!, $pagesize : Int) { useCompany(no: $cid) { order(first: $pagesize) { totalCount items { orderNo joindown_OrderLine_via_Order(first: 3, unoptimized : true) { items { lineNo joindown_OrderLineNote_via_OrderLine { items { note } } } } } } } } ``` - the order lines are fetched unoptimized but the fetching of order line notes is optimized ```graphql query ($cid :Int!, $pagesize : Int) { useCompany(no: $cid) { order(first: $pagesize) { totalCount items { orderNo joindown_OrderLine_via_Order(first: 3, unoptimized : true) { items { lineNo joindown_OrderLineNote_via_OrderLine(unoptimized : false) { items { note } } } } } } } } ``` Using the `unoptimized` flag on a top-level table (such as the orders table in these examples) has no effect on fetching data from that table. However, it will affect the manner of fetching data from all its joined tables (the fields prefixed with either `joindown_` or `joinup_`). ## Asynchronous execution Executing the queries with joined down tables fast and having pagination or counting the total items in the joined down table are mutually exclusive. However, if you do need these features but do not want to wait for the execution of the query you can execute the query asynchronously. To learn more about this, see [Async queries](../schema/async.md). Batch requests /businessnxtapi/features/batches page Batch multiple GraphQL queries or mutations in a single API request, with results returned after complete execution. 2025-04-15T09:48:42+02:00 # Batch requests Batch multiple GraphQL queries or mutations in a single API request, with results returned after complete execution. A GraphQL request is basically a JSON object that has the following form: ```json { "query" : "..." "variables" : {} "operationname" : "..." } ``` The `OperationName` property is optional, but if present, it must match the operation name from the query. To exemplify, let's consider the following request: ```graphql { title = "Query" } query read_accounts($cid : Int, $pagesize : Int){ useCompany(no: $cid) { generalLedgerAccount(first: $pagesize) { totalCount pageInfo { hasNextPage hasPreviousPage startCursor endCursor } items { accountNo name } } } } ``` ```graphql { title = "Result" } { "cid" : 987654321, "pagesize" : 10 } ``` This query is formatted as the following JSON content when dispatched to the GraphQL API: ```json { "query" : "query read_accounts($cid : Int,\n $pagesize : Int){\n useCompany(no: $cid) {\n generalLedgerAccount(first: $pagesize) {\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n items {\n accountNo\n name\n }\n }\n }\n}", "variables": {"cid": 987654321, "pagesize" : 10}, } ``` > [!NOTE] > > Notice that JSON does not support multiple line strings. Therefore, new lines must be escaped, and the entire query provided as a single line string. The result you get back is also a JSON object, such as the following: ```json { "data": { "useCompany": { "generalLedgerAccount": { "totalCount": 412, "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "startCursor": "MA==", "endCursor": "MTA=" }, "items": [ { "accountNo": 1200, "name": "Plant and machinery" }, { "accountNo": 1209, "name": "Depreciation plant and machinery" }, ... ] } } }, "extensions": { "vbnxt-trace-id": "a2f3f1ad045d7bd2f62f833e136ff0b0" } } ``` It is possible that a single request contains multiple parts. For instance, you can query orders, products, and customers in a single request. You can also create an order and its order lines in a single mutation. However, it's not possible to mix queries and mutations inside the same request. VBNXT GraphQL allows you to batch multiple queries inside a single API requests. That is possible by sending an array of JSON objects, as follows: ```json [ { "query" : "..." "variables" : {} "operationname" : "..." }, { "query" : "..." "variables" : {} "operationname" : "..." } ] ``` The result, is also an array of objects, one for each requested operation. ```json [ , , ... ] ``` An example with two requests (a query and a mutation) batched together is shown below: ```json [ { "query" : "query read($cid : Int, $pagesize : Int){\n useCompany(no: $cid) {\n generalLedgerAccount(first: $pagesize) {\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n items {\n accountNo\n name\n }\n }\n }\n}", "variables": {"cid": 987654321, "pagesize" : 10}, "operationname" : "read" }, { "query" : "mutation update_gla($cid : Int!, $no : Int!, $name : String!)\n{\n useCompany(no: $cid) \n {\n generalLedgerAccount_update(\n filter : {accountNo : {_eq: $no}},\n value : {shortName : $name})\n {\n affectedRows\n items {\n accountNo\n name\n shortName\n }\n }\n }\n}", "variables": {"cid": 987654321, "no" : 9999, "name": "test"}, "operationname" : "update_gla" } ] ``` ```json [ { "data": { "useCompany": { "generalLedgerAccount": { "totalCount": 117, "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "startCursor": "MA==", "endCursor": "MTA=" }, "items": [ { "accountNo": 1200, "name": "Plant and machinery" }, { "accountNo": 1209, "name": "Depreciation plant and machinery" }, { "accountNo": 1210, "name": "Buildings" }, { "accountNo": 1211, "name": "Land" }, { "accountNo": 1219, "name": "Depreciation, Buildings" }, { "accountNo": 1220, "name": "Office Equipment" }, { "accountNo": 1229, "name": "Depreciation, Office Equipment" }, { "accountNo": 1240, "name": "Motor vehicles" }, { "accountNo": 1249, "name": "Depreciation motor vehicles" }, { "accountNo": 1300, "name": "Goodwill" } ] } } } }, { "data": { "useCompany": { "generalLedgerAccount_update": { "affectedRows": 1, "items": [ { "accountNo": 9999, "name": "SUSPENSE ACCOUNT", "shortName": "test" } ] } } } } ] ``` It is possible to batch both queries and mutations together in any order. They are executed individually, one by one, in the order they where provided. When batching multiple request, the results will only be returned after all the requests have been executed. A large number of requests could make the overall operation exceed the HTTP timeout interval. In this case, the operation will timeout and the result will be lost. > [!NOTE] > > There is currently a limit of the number of requests that can be batched together. This limit is set to 32 requests. > > This limit may be subject to future changes. > [!TIP] > > Use batching requests judiciously, making sure their overall execution time will not trigger an operation timeout!