Built-in models
Goerp has some built-in service variables, which can be used to pass some processing settings or to get some service variables, such as session key, client code, etc.
Goerp has some built-in service variables, which can be used to pass some processing settings or to get some service variables, such as session key, client code, etc.
Dynamic api gives an alternative way to handle the data. As opposed to the older model based handling. For benefits this functionality relies on the api’s business logic entirely and does not need adjustments to models once the api’s change.
It also allows to create as many request as you want against the same api and implements various data manipulation helper functions.
Requests are defined by either input parameters from the browser (query or post) or by static preset values. Preset values are values that are saved to the template during its save time and this means that changing values cannot be used with them.
The syntax of the requests is the following
Request names are case-sensitive. Prefer giving self-explanatory names to the requests, so they will not
confuse while developing huge template with many requests. For example, if request made for v1/products
and used method Get
, then name it getProducts
, but not something like request1
, records
, etc.
Also, GoErp have reserved names that cannot be used while defining the name for request.
Reserved names:
Session
(with Capital S, session
is ok to use)Parameters
(with Capital P)Storage
(with Capital S)The definition of the name is given to the form input name and the endpoint to call from that api is given as the value.
Note that the value should not start with the character /.
<form method="get">
<input name="ReportsApi.Api.Get.getProductsCall" value="v1/POSDay">
</form>
The call is made when the page is accessed only when the parameter is actually passed. In the sample above the parameter is registered in the form but is never actually sent as a parameter to the server. In order to load the data we would need to submit the form.
The method of the form would define how the parameter is passed to the server either via query parameter or a post parameter.
We can also load parameter into links to make the api to be called right away (before the page is enriched with data):
?ReportsApi.Api.Get.someReportsCall=v1/POSDay
This link would load the data when it is opened.
When opening a page via url that has no query parameters then even if we have defined some api call into form they would not be triggered. Regular form values are not read from the template content, rather it is just written there to make the actual calls by the browser.
There is a way however to set defaults to parameters, these values are saved during template save time and are static values. This means that dynamic values that change in the template view process cannot be passed onto them, the values that are added to it are the exact values that will be used for the request.
This allows the request to be made with some default values and to make it load something when a page is opened without any parameters.
For this we can use the data-preset-val attribute on the inputs
<form method="get">
<input name="ReportsApi.Api.Get.myReportsCall"
value="v1/POSDay"
data-preset-val="v1/POSDay">
</form>
Note that these do not need to be in the form as we might never send the parameter if we never intend to send the form. In this case a single hidden input that is not part of a form is enough.
<input type="hidden" name="ReportsApi.Api.Get.myStaticReportsCall"
data-preset-val="v1/POSDay">
If the browser passes a parameter to the same value it will overwrite the preset value.
From version 1.179.1 we can also set static preset values in the editor under the “URL configuration” menu. These name:value pairs work the same way as regular preset value definitions, but when defined here the values do not need to be in the template body at all.
Useful for pages that generate content that might not want to view the input but automatic loading of content is required.
To pass parameters to the api we will use similar syntax like the one we use to define the requests.
The custom request name here should match exactly the one we used to define the request.
In the following example we add the query parameter currencyId for the request we defined as myReportsCall
<form method="get">
<input type="hidden" name="ReportsApi.Api.Get.myReportsCall"
value="v1/POSDay"
data-preset-val="v1/POSDay">
<label for="currencyId">Currency ID</label>
<input id="currencyId" type="number"
name="ReportsApi.Api.Query.myReportsCall.currencyId">
<button type="submit">Fetch</button>
</form>
Similar to the way we can give default values to requests we can also give default values to parameters.
Note that the values here are static meaning that we cannot add placeholders to the data-preset-val attribute. The value is being saved on template save time and will not change during the page view process.
<form method="get">
<input type="hidden" name="ReportsApi.Api.Get.myReportsCall"
value="v1/POSDay"
data-preset-val="v1/POSDay">
<!-- We make the page load with currency ID 1, however we still allow it to be overwritten by the parameter -->
<label for="currencyId">Currency ID</label>
<input id="currencyId" type="number"
name="ReportsApi.Api.Query.myReportsCall.currencyId"
data-preset-val="1">
<button type="submit">Fetch</button>
</form>
If the browser passes a parameter to the same value it will overwrite the preset value.
Parameters are passed either via query or as post parameters. Samples here are generated into form inputs, but they can as well be used in url query parameters (name=value).
We define the query type with the type name Query
<input type="number"
name="ReportsApi.Api.Query.myReportsCall.currencyId">
If we do not want to lose the value from the form field we can set the value to read from the request.
<input type="number"
name="ReportsApi.Api.Query.myReportsCall.currencyId"
value="{{ .Data.ReportsApi.Api.Requests.myReportsCall.Queries.currencyId }}">
This means the input will keep its value after the possible form submit and page reload
Allows to pass as many query parameters as needed using one input. Parameter name here doesn’t matter
actually as it would be ignored anyway, but we can always name it url
just to make it clear to read
<input type="hidden"
name="CaFaApi.Api.QueryBulk.getConf.url"
value="application=my-app&level=Company&name=dynamic-sample">
And to access those parameters just use the same Queries
field like with regular Query
parameters
<h1>Query parameters from bulk</h1>
<p>App: {{ .Data.CaFaApi.Api.Requests.getConf.Queries.application }}</p>
<p>Level: {{ .Data.CaFaApi.Api.Requests.getConf.Queries.level }}</p>
<p>Name: {{ .Data.CaFaApi.Api.Requests.getConf.Queries.name }}</p>
Mostly for api’s that use urlencoded parameters or multipart/form-data (example: ErplyApi). Supports file uploading as well.
PostParam
also used with FTP related calls to pass additional parameters like username
, password
and path
.
We define the query type with the type name PostParam
<input type="number"
name="ErplyApi.Api.PostParam.getProducts.productID">
If we do not want to lose the value from the form field we can set the value to read from the request.
<input type="number"
name="ErplyApi.Api.PostParam.getProducts.productID"
value="{{ .Data.ErplyApi.Api.Requests.getProducts.PostParams.productID }}">
And to uploading the file:
<input type="hidden" name="CDNApi.Api.Post.r1" value="images">
<input type="file" name="CDNApi.Api.PostParam.uploadImage.image">
We define the query type with the type name Header
<input id="currencyId" type="number"
name="ErplyApi.Api.Header.getProducts.myHeader">
If we do not want to lose the value from the form field we can set the value to read from the request.
<input type="number"
name="ErplyApi.Api.Header.getProducts.myHeader"
value="{{ .Data.ErplyApi.Api.Requests.getProducts.Headers.myHeader }}">
If the api expects one of the parameters to be a path parameter we would define the request with a placeholder and follow it up with a value for this placeholder.
<input type="hidden" name="PricingApi.Api.Get.myCall"
value="v1/price-lists/summary/{id}">
<input type="number" name="PricingApi.Api.Path.myCall.id" value="1">
Note here that the placeholder in the request definition needs to match that of the parameter name.
If we do not want to lose the value from the form field we can set the value to read from the request.
<input type="number"
name="PricingApi.Api.Path.myCall.id"
value="{{ .Data.PricingApi.Api.Requests.myCall.PathParams.id }}">
Json inputs can be composed in a couple of different ways.
With this method we will construct json with multiple input fields.
We also need to define the type of the field for the json constructor. Type is given just
before the parameter name. In many cases parameter name may represent the xpath in json, like
string.inner.name
will result in {"inner":{"name":""}}
Omitting key definition after the dot will result in setting input value as a root object to the json body.
Supported values
string
- result: ({"name":"foo"}
)number
- result: ({"name":123}
)boolean
- result: ({"name":true}
)json
- result: ({"name":{"foo":"bar"}}
). Allows to set any json structure.delete
- (special case to delete entire path from the json, value of the input would be ignored because only path is needed)<input type="number"
name="PricingApi.Api.Json.createPriceList.string.name"
value="myTestVal">
This sample would generate a json object like this
{
"name": "myTestVal"
}
Adding multiple fields
<input type="text"
name="PricingApi.Api.Json.createPriceList.string.name"
value="myTestVal">
<input type="number"
name="PricingApi.Api.Json.createPriceList.number.start"
value="1000">
This sample would generate a json object like this
{
"name": "myTestVal",
"start": 1000
}
We can also define nested field
<input type="text"
name="PricingApi.Api.Json.createPriceList.string.name"
value="myTestVal">
<input type="number"
name="PricingApi.Api.Json.createPriceList.number.test.start"
value="1000">
This sample would generate a json object like this
{
"name": "myTestVal",
"test": {
"start": 1000
}
}
When one of the fields is of type array
<input type="text"
name="PricingApi.Api.Json.myCall.string.name"
value="myTestVal">
<input type="number"
name="PricingApi.Api.Json.myCall.number.start.0"
value="1000">
This sample would generate a json object like this
{
"name": "myTestVal",
"start": [
1000
]
}
The number indicates the index of the array, knowing this we can construct the input with specific indexes
<input type="text"
name="PricingApi.Api.Json.myCall.string.name"
value="myTestVal">
<input type="number"
name="PricingApi.Api.Json.myCall.number.start.0"
value="1000">
<input type="number"
name="PricingApi.Api.Json.myCall.number.start.1"
value="1001">
This sample would generate a json object like this
{
"name": "myTestVal",
"start": [
1000, 1001
]
}
Creating an array of objects to input.
<input type="text"
name="PricingApi.Api.Json.myRequest1.string.code"
value="myTestVal">
<input type="number"
name="PricingApi.Api.Json.myRequest1.string.pairs.0.name"
value="1000">
<input type="number"
name="PricingApi.Api.Json.myRequest1.number.pairs.0.value"
value="1001">
This sample would generate a json object like this
{
"code": "myTestVal",
"pairs": [
{
"name": "1000",
"value": 1001
}
]
}
Set entire json as a root object:
<input type="hidden" name="CaFaApi.Api.Json.putConf.json."
value='{"code":"test","name":"Foo","numbers":[1,2,4]}'>
Will generate payload like this:
{
"code": "test",
"name": "Foo",
"numbers": [1, 2, 4]
}
Starting from version 1.201.1, GoErp allows to make json modifications more flexible, like predefined json, connecting json parts from another request and many more. Check how it works with CAFA sample.
To send some custom data to the api endpoint that does not use a specific format (json, post params etc). Note that the key name here is not important (myCustomData) and it will not be used in the request.
<input type="hidden" name="AutomatApi.Api.Post.myRequest1"
value="v5/some-endpoint">
<input type="hidden" name="AutomatApi.Api.Raw.myRequest1.myCustomData"
value="custom-data">
Usable for franchise accounts. Where the HQ account can make calls against certain stores. Note that the user on the HQ account needs to have access to the specified store through the multi account users feature.
<input type="hidden" name="AutomatApi.Api.Get.myRequest1"
value="v2/local-application" data-preset-val="v2/local-application">
<input type="hidden" name="AutomatApi.Api.Franchise.myRequest1.clientCode"
value="104373" data-preset-val="104373">
Using the value here will run the api call against that specific franchise account store.
Can be used to speed up page loads by caching certain api calls that do not use any parameters and are used mostly to fill selections. Items will be stored against the session and will refresh automatically within 1 hour (this can be altered with the duration parameter).
Define the cache key with a value of 1 to use the cache features. Adjusting the value to 0 or removing the key will disable the feature.
<input type="hidden" name="AccountAdminApi.Api.Get.timezones"
data-preset-val="v1/timezone">
<input type="hidden" name="AccountAdminApi.Api.Cache.timezones.session" data-preset-val="1">
Send the following parameter once to refresh the cached content for that request
<form method="post">
<input type="hidden" name="AccountAdminApi.Api.Cache.timezones.refresh" value="1">
<button type="submit">Refresh timezone cache</button>
</form>
Optionally you can set a different duration for the cached elements (in seconds). This works together with the cache parameter and needs to exist when the data element is created.
If this value is not provided then the data will be cached by default for 3600 seconds.
<input type="hidden" name="AccountAdminApi.Api.Cache.timezones.duration" data-preset-val="7200">
The dynamic functionality returns the entire api response json content back as special json object that we can manipulate with special commands to get back data.
If we had for example previously defined a request like this
<input type="hidden" name="PricingApi.Api.Get.myCall"
value="v1/price-lists"
data-preset-val="v1/price-lists">
Then we can print out the json content with the following syntax.
{{ .Data.PricingApi.Api.Requests.myCall.Response }}
The response object holds all the requests in it and can be used to reference the results back for any of them.
The response objects will contain
{{ .Data.PricingApi.Api.Requests.myRequest1.Response }}
{{ .Data.PricingApi.Api.Requests.myRequest1.ResponseHttpCode }}
Check if the response had a specific http code
{{ if .Data.PricingApi.Api.Requests.myRequest1.ResponseHttpCode 204 }}
204 was indeed returned!
{{ end }}
{{ .Data.PricingApi.Api.Requests.myRequest1.ResponseHeaders }}
Read specific response header content
{{ .Data.PricingApi.Api.Requests.myRequest1.ResponseHeaders.X-Some-Header }}
To get a specific field back from the response json content we can use the Get name.
Lets assume our api response data looks like this
[
{
"id": 1,
"name": "Test",
"start": "0000-00-00",
"end": "0000-00-00",
"isActive": 1
},
{
"id": 3,
"name": "Testing Update",
"start": "0000-00-00",
"end": "0000-00-00",
"isActive": 1
}
]
Our data is an array of elements, we can use a couple of different methods to read the keys.
To iterate the through all items we would append the Response with Array, this tells the system that we are expecting an array of elements.
Each item in the array would correspond to the actual object, where we can use the Get function to fetch a specific field.
{{
<ul>
{{ range .Data.PricingApi.Api.Requests.myRequest1.Response.Array }}
<li>{{ .Get "id" }}</li>
{{ end }}
</ul>
}}
The Get function can be used to return any single value from the json data.
Get a value from a specific index in the array. This would return the first results id value. The number indicates the array index.
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "0.id" }}
This function can return multiple items based on the provided syntax
{
"name": {"first": "Tom", "last": "Anderson"},
"age":37,
"children": ["Sara","Alex","Jack"],
"fav.movie": "Deer Hunter",
"friends": [
{"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]},
{"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]},
{"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]}
]
}
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "name.last" }} => Anderson
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "age" }} => 37
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "children" }} => ["Sara","Alex","Jack"]
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "children.#" }} => 3
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "children.1" }} => Alex
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "child*.2" }} => jack
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "c?ildren.0" }} => Sara
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "fav\.movie" }} => Deer Hunter
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "friends.#.first" }} => ["Dale","Roger","Jane"]
{{ .Data.PricingApi.Api.Requests.myCall.Response.Get "friends.1.last" }} => Craig
The json values by default have special type that is always converted to a string when used in the template.
This means that if the intention is to use these values in some inbuilt helper function that expects a certain type and the expected types are not what is expected then the function can break the template entirely.
To prevent this we can cast the values to the correct type in the functions.
{{ if eq (.Data.PricingApi.Api.Requests.myCall.Response.Get "age").Int }} 37 }}
<!-- Do something -->
{{ end }}
The following type based functions can be called
{{ (.Data.PricingApi.Api.Requests.myCall.Response.Get "val").Exists }} => bool
{{ (.Data.PricingApi.Api.Requests.myCall.Response.Get "val").Int }} => int64
{{ (.Data.PricingApi.Api.Requests.myCall.Response.Get "val").Float }} => float64
{{ (.Data.PricingApi.Api.Requests.myCall.Response.Get "val").String }} => string
{{ (.Data.PricingApi.Api.Requests.myCall.Response.Get "val").Bool }} => bool
On array elements we can also use extra filtering options
If we used the sample below
[
{
"id": 1,
"name": "Test",
"start": "0000-00-00",
"end": "0000-00-00",
"isActive": 1
},
{
"id": 3,
"name": "Testing Update",
"start": "0000-00-00",
"end": "0000-00-00",
"isActive": 1
}
]
The following options are available
`#(name=="%s").id` "Test"` => 1
`#(name=="%s")#.id` "Test"` => [1]
`#(id>2)#.id` "Test"` => 1
Then in order to print only the name of the item that has id 3
<ul>
{{ $cs := .Data.CRMApi.Api.Requests.customer.Response.Array }}
<li>{{ $cs.Get (printf `#(id==%d)#.name` 3) }}</li>
{{ end }}
</ul>
These are special functions that can be used against the dynamic api json response content, to do special conversions or other special functions with them.
Note that the following functions can also be used on the same result, for example we want to flatten and get unique values at the same time.
{{ .Data.ErplyApi.Api.Requests.getProducts.Response.Get "records.#.productId|@flatten|@unique" }}
If we had a json result like this
{
"records": [
{
"id": 1,
"name": "Test 1"
},
{
"id": 2,
"name": "Test 2"
}
]
}
Get the arrays specific values as comma separated string
{{ $commaSeparatedString := .Data.ErplyApi.Api.Requests.getProducts.Response.Get "records.#.id|@commaSepStr" }}
If we had a json result like this
{
"records": [
{
"id": 1,
"name": "Test 1"
},
{
"id": 2,
"name": "Test 2"
}
]
}
We can use the function here to get the records array in reverse order
{{ $reversedRecords := .Data.ErplyApi.Api.Requests.getProducts.Response.Get "records@reverse" }}
We can get the last element in the array using this
{{ $lastRecord := .Data.ErplyApi.Api.Requests.getProducts.Response.Get "records@reverse.0" }}
Using the same json sample above
Get the keys of the first record
{{ $keys := .Data.ErplyApi.Api.Requests.getProducts.Response.Get "records.0|@keys" }}
Using the same json sample above
Get the values of the first record
{{ $values := .Data.ErplyApi.Api.Requests.getProducts.Response.Get "records.0|@values" }}
{
"records": [
{
"id": 1,
"rows": [
{"id": 3},
{"id": 4}
]
},
{
"id": 2,
"rows": [
{"id": 5},
{"id": 6}
]
}
]
}
Get a flat array of values
{{ $values := .Data.ErplyApi.Api.Requests.getProducts.Response.Get "records.#.rows.#.id|@flatten" }}
[3,4,5,6]
{
"records": [
{
"productId": 1
},
{
"productId": 2
},
{
"productId": 1
}
]
}
Get the unique values
{{ $values := .Data.ErplyApi.Api.Requests.getProducts.Response.Get "records.#.productId|@unique" }}
[1,2]
It’s possible to chain different api fetches together. This is useful when a second api call would depend on the results of the first.
By default, all subsequent requests will be skipped if parent one fails. Although, it is possible to configure this behavior by making link optional. More details
NB! Chaining options supported through value AND through data-preset-val parameters
Every request input parameters may be chained with other request and with data from GoErp in-build data sets.
{{ toJson .Session }}
)
<input type="hidden" name="ErplyApi.Api.PostParam.getSD.<-clientID" value="Session.customer.ID" data-preset-val="Session.customer.ID">
<input type="hidden" name="ErplyApi.Api.PostParam.getSD.<-clientID" value="Parameters.customerId" data-preset-val="Parameters.customerId">
<input type="hidden" name="ErplyApi.Api.PostParam.getSD.<-clientID" value="Storage.customerId" data-preset-val="Storage.customerId">
<input type="hidden" name="ErplyApi.Api.PostParam.getSD.<-clientID" value="Variables.customerId" data-preset-val="Variables.customerId">
There is also option to chain request input parameters with parameters taken from other requests:
<input type="hidden" name="ErplyApi.Api.PostParam.getSD.<-clientID" value="otherRequest.PathParams.customerId" data-preset-val="otherRequest.PathParams.customerId">
<input type="hidden" name="ErplyApi.Api.PostParam.getSD.<-clientID" value="otherRequest.Queries.customerId" data-preset-val="otherRequest.Queries.customerId">
<input type="hidden" name="ErplyApi.Api.PostParam.getSD.<-clientID" value="otherRequest.Headers.customerId" data-preset-val="otherRequest.Headers.customerId">
<input type="hidden" name="ErplyApi.Api.PostParam.getSD.<-clientID" value="otherRequest.PostParams.customerId" data-preset-val="otherRequest.PostParams.customerId">
<input type="hidden" name="ErplyApi.Api.PostParam.getSD.<-clientID" value="otherRequest.Response.customerId" data-preset-val="otherRequest.Response.customerId">
<input type="hidden" name="ErplyApi.Api.PostParam.getSD.<-clientID" value="otherRequest.ResponseHeaders.customerId" data-preset-val="otherRequest.ResponseHeaders.customerId">
We can use the grouping definition to set the order of calls.
The pipe | at the end indicates grouping and the number says the order. Multiple requests can have the same group number, this means they would be done at the same time, and they do not depend on each other.
<input type="hidden" name="PIMApi.Api.Get.getProducts|1" data-preset-val="v1/product">
<input type="hidden" name="CDNApi.Api.Get.getImages|2" data-preset-val="v1/images">
The following case is also ok, as the non numbered calls will be called first
<input type="hidden" name="PIMApi.Api.Get.getProducts" data-preset-val="v1/product">
<input type="hidden" name="PricingApi.Api.Get.getPrices|1" data-preset-val="v1/products/price-tax-rate">
It is recommended to make as few requests as possible. This means that creating requests in first api response loops is not recommended as it would take a large performance hit.
Instead, we can use special helper functions to collect the id’s and make a separate calls. |@commaSepStr converts all the id’s of the get products response to comma separated string input for the pricing api. Then we assign this value to productId parameter using the <- sign.
<input type="hidden" name="PricingApi.Api.Query.getPrices.<-productIDs" value="getProducts.Response.#.id|@commaSepStr">
The value syntax here is similar to how the Get function works, so we can also assign single values when needed.
<!-- Add the id value of the first result -->
<input type="hidden" name="PricingApi.Api.Query.getPrices.<-productIDs" value="getProducts.Response.0.id">
Dynamics provides option to make link optional, which means, if parent request fails or there is no
data in chained location then request would be still executed but with empty parameter (from version 1.262.1: query, path and headers parameters will be skipped instead of empty values).To make link
optional just put ?
after the link operator <-
, like this <-?
:
<input type="hidden" name="PricingApi.Api.Query.getPrices.<-?productIDs" value="getProducts.Response.0.id">
Dynamics provides option to skip the chain when parent has a value. That option could be useful
when we need to create a new record only if the parent request has no data. To skip the chain,
put !
after the link operator <-
, like this <-!
:
<!-- getPrices would be not executed if getProducts.Response.0.id has id in there -->
<input type="hidden" name="PricingApi.Api.Query.getPrices.<-!productIDs" value="getProducts.Response.0.id">
Sometimes we need to get first non-empty value from multiple sources. This can be done by using the
||
operator between sources. This will take the first non-empty value from the provided set of
sources. If all sources are empty and chain not optional, then request will be not executed. Works
with presets as well.
<input type="hidden"
name="CRMApi.Api.Query.customers.<-ids"
value="Parameters.cid||Storage.cid||Variables.defaultValue">
We can use the json query mechanism to get items from other api responses, or we can use the old-fashioned id matching.
Using the inbuilt query features to return exact values. This can be useful when dealing with more complex pages that try to compose multiple requests.
<ul>
{{ range .Data.PIMApi.Api.Requests.getProducts.Response.Array }}
<!-- As values returned by the _Get_ function do not have a specific type we need to cast it into one -->
{{ $productId := (.Get "id").Int }}
<li>
Product code: {{ .Get "code" }}<br>
<!-- Get the price_with_tax value from the prices array by matching the productID there with current iteration id -->
Price: {{ .Data.PricingApi.Api.Requests.getPrices.Response.Get (printf `#(productID==%d).price_with_tax` $productId) }}
</li>
{{ end }}
</ul>
Iterating through the second set is also an option. For something smaller this will probably be enough.
<ul>
{{ range $product := .Data.PIMApi.Api.Requests.getProducts.Response.Array }}
<li>
Product code: {{ $product.Get "code" }}<br>
{{ range $price := $.Data.PricingApi.Api.Requests.getPrices.Response.Array }}
<!-- Still need to cast to the appropriate types -->
{{ if eq ($price.Get "productID").Int ($product.Get "id").Int }}
<!-- Get the price_with_tax value if the id matches -->
Price: {{ $price.Get "price_with_tax" }}
{{ end }}
{{ end }}
</li>
{{ end }}
</ul>
In here we fetch a list of products and generate a list of images for them.
<div class="my-error-container">
{{ range .Data.Errors }}
<span class="my-error-message">{{ . }}</span>
{{ end }}
</div>
<input type="hidden" name="PIMApi.Api.Get.getProducts" data-preset-val="v1/product">
<input type="hidden" name="PricingApi.Api.Get.getPrices|1" data-preset-val="v1/products/price-tax-rate">
<input type="hidden" name="PricingApi.Api.Query.getPrices.warehouseID" data-preset-val="1">
<input type="hidden" name="PricingApi.Api.Query.getPrices.<-productIDs"
data-preset-val="getProducts.Response.#.id|@commaSepStr">
<ul>
{{ range .Data.PIMApi.Api.Requests.getProducts.Response.Array }}
<!-- As values returned by the _Get_ function do not have a specific type we need to cast it into one -->
{{ $productId := (.Get "id").Int }}
<li>
Product code: {{ .Get "code" }}<br>
<!-- Get the price_with_tax value from the prices array by matching the productID there with current iteration id -->
Price: {{ $.Data.PricingApi.Api.Requests.getPrices.Response.Get (printf `#(productID==%d).price_with_tax` $productId) }}
</li>
{{ end }}
</ul>
This is a more complex sample that uses multiple api calls to save a new customer and various other elements related to the customer.
Errors: {{ .Data.Errors }}<br/>
<h1>Customer input with address and fetching customer with all data in place</h1>
<form method="post">
<!-- requests initialization -->
<input type="hidden" name="CRMApi.Api.Get.customerGroups" value="v1/customers/groups"
data-preset-val="v1/customers/groups">
<input type="hidden" name="CRMApi.Api.Post.createCustomer" value="v1/customers/individuals">
<input type="hidden" name="CRMApi.Api.Post.createAddress|1" value="v1/addresses">
<input type="hidden" name="CRMApi.Api.Get.customer|1" value="v1/customers">
<input type="hidden" name="CRMApi.Api.Get.getAddress|2" value="v1/addresses">
<input type="hidden" name="CRMApi.Api.Put.createAttribute|2" value="v1/attributes">
<input type="hidden" name="CRMApi.Api.Delete.delAttribute|3" value="v1/attributes/{id}">
<input type="hidden" name="CRMApi.Api.Get.getAddressTypes|3" value="v1/addresses/types">
<input type="hidden" name="CRMApi.Api.Get.getBusinessAreas|3" value="v1/business/areas">
<input type="hidden" name="CRMApi.Api.Get.getAttributes|4" value="v1/attributes">
<!-- presets data -->
<input type="hidden" name="CRMApi.Api.Query.customerGroups.take" value="" data-preset-val="50">
<!-- customer post, depth = 1 -->
<br/>First name:
<input type="text" name="CRMApi.Api.Json.createCustomer.string.firstName"
value="{{ .Data.CRMApi.Api.Requests.customer.Json.Get "firstName" }}">
<br/>Last name:
<input type="text" name="CRMApi.Api.Json.createCustomer.string.lastName"
value="{{ .Data.CRMApi.Api.Requests.customer.Json.Get "lastName" }}">
{{ $gId := (.Data.CRMApi.Api.Requests.customer.Json.Get "customerGroupId").Int }}
<br/>Group: <select name="CRMApi.Api.Json.createCustomer.number.customerGroupId">
{{ range $cg := .Data.CRMApi.Api.Requests.customerGroups.Response.Array }}
<option value="{{ $cg.Get "id" }}" {{ if eq ($cg.Get "id").Int $gId }}selected{{ end }}>
{{ $cg.Get "name.en" }}
</option>
{{ end }}
</select>
<!-- address post, depth = 2 -->
<br/>Address street:
<input type="text" name="CRMApi.Api.Json.createAddress.string.street"
value="{{ .Data.CRMApi.Api.Requests.createAddress.Json.Get "street" }}">
<input type="hidden" name="CRMApi.Api.Json.createAddress.number.<-customerId"
value="createCustomer.Response.id">
<input type="hidden" name="CRMApi.Api.Json.createAddress.number.typeId" value="1">
<!-- get created customer, depth = 2 -->
<input type="hidden" name="CRMApi.Api.Query.customer.<-ids" value="createCustomer.Response.id">
<!-- get created address, depth = 3 -->
<input type="hidden" name="CRMApi.Api.Query.getAddress.<-customerIds"
value="customer.Response.#.id|@commaSepStr">
<br/>Attribute int:
<input type="number" name="CRMApi.Api.Json.createAttribute.number.value" value="int">
<input type="hidden" name="CRMApi.Api.Json.createAttribute.string.entity" value="customer">
<input type="hidden" name="CRMApi.Api.Json.createAttribute.string.name" value="test-dynamic-4">
<input type="hidden" name="CRMApi.Api.Json.createAttribute.string.type" value="int">
<input type="hidden" name="CRMApi.Api.Json.createAttribute.number.<-record_id"
value="customer.Response.#.id|@commaSepStr">
<!-- get created address, depth = 4 -->
<input type="hidden" name="CRMApi.Api.Path.delAttribute.<-id" value="createAttribute.Response.id">
<input type="hidden" name="CRMApi.Api.Query.getAttributes.entityName" value="customer">
<input type="hidden" name="CRMApi.Api.Query.getAttributes.<-recordIds"
value="customer.Response.#.id|@commaSepStr">
<br/>
<button type="submit">Submit</button>
</form>
<h1>Created customer</h1>
{{ $cs := .Data.CRMApi.Api.Requests.customer.Response.Array }}
{{ if $cs }}
<br/>Response count: {{ len $cs }}
{{ $c := index $cs 0 }}
{{ $cId := ($c.Get "id").Int }}
<br/>ID: {{ $cId }}
<br/>First name: {{ $c.Get "firstName"}}
{{ $cgId := ($c.Get "customerGroupId").Int }}
<br/>Customer group: {{ .Data.CRMApi.Api.Requests.customerGroups.Response.Get (printf `#(id==%d).name.en` $cgId) }}
<br/>Addresses: {{ $.Data.CRMApi.Api.Requests.getAddress.Response.Get (printf `#(customerId==%d)#.street` $cId) }}
{{ end }}
<h1>Attributes</h1>
{{ $attrs := .Data.CRMApi.Api.Requests.getAttributes.Response.Array }}
{{ if $attrs }}
{{ range $attrs }}
<br/>{{ .Get "name" }} | {{ .Get "type" }} | {{ .Get "value" }}
{{end}}
{{ end }}
A sample where we connect CDN images to product groups. In here we can see that we can connect items that naturally do not have a connection in the database.
Before this sample 2 images have been created using the CDN api create image endpoint.
context: erply-product-group productId: 0 for default and 3 for the existing one
<!-- Define request 1 for product groups -->
<input type="hidden" name="PIMApi.Api.Get.myProdGroups" value="v1/product/group" data-preset-val="v1/product/group">
<!-- Chain images from CDN api, use results from first as input -->
<input type="hidden" name="CDNApi.Api.Get.myImages|1" value="images" data-preset-val="images">
<input type="hidden" name="CDNApi.Api.Query.myImages.context" data-preset-val="erply-product-group">
<input type="hidden" name="CDNApi.Api.Query.myImages.<-productId" data-preset-val="myProdGroups.Response.#.id|@commaSepStr">
<p>Groups</p>
<ul>
{{ range .Data.PIMApi.Api.Requests.myProdGroups.Response.Array }}
{{ $groupId := (.Get "id").Int }}
<li>
{{ .Get "name.en" }} (ID: {{ $groupId }})
<!-- Fetch images for this group id using the dynamic query method -->
{{ $images := $.Data.CDNApi.Api.Requests.myImages.Response.Get (printf `images.#(productId==%d)#.key` $groupId) }}
<ul>
{{ if $images.Array }}
<!-- print matching items -->
{{ range $images.Array }}
<li>
<img src="{{ $.Session.Services.CDNApi.URL }}/images/{{ $.Session.ClientCode }}/{{ . }}?width=200&height=200">
</li>
{{ end }}
{{ else }}
<!-- Print default -->
<li>
<img src="{{ $.Session.Services.CDNApi.URL }}/images/{{ $.Session.ClientCode }}/jJumxcXTSamiGhJgDGJ1kGFyQx4iqtksv4R3MnEsIc4APVqt2v.png?width=200&height=200">
</li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
Result should look like this
Dynamic api uses a different kind of redirect from the models.
This redirect is only triggered if all the calls in the request succeed. If even one of them fails (errors model in the response contains something) then by default redirection is not triggered. However, there is a way to define optional requests that allowed to fail.
We define the redirection with Form.Redirect parameter The syntax here is exactly the same as with responses, only difference is that instead of the usual {{ and }} escapes we use [[ and ]] instead.
Use Form.AllowedToFailRequests
input name to pass request names that allowed to fail and make redirect
to the desired page even if they failed. Request names should be separated by comma (,
)
<!-- Assign the first id from the response to the redirect link -->
<input type="hidden" name="Form.Redirect" value="da-dynamic-redirect?someParameter=[[ .Data.PIMApi.Api.Requests.request1.Response.Get `0.id` ]]">
<form method="get">
<!-- For 'request1' use your own custom identifier, use it to access the results -->
<input type="hidden" name="PIMApi.Api.Get.request1" value="/v1/product">
<!-- Only redirects if all form containing requests succeed -->
<input type="hidden" name="Form.Redirect" value="da-dynamic-redirect?wat=[[ .Data.PIMApi.Api.Requests.request1.Response.Get `0.id` ]]">
<input type="hidden" name="Form.AllowedToFailRequests" value="request1">
<button type="submit">Redirect on success</button>
</form>
These are functions that help in generating input in a certain format that is difficult to create using regular html inputs.
The regular pimFilter does not support ‘or’ operator between conditions and the operator always set to ‘and’. It also does not support nested conditions. If the operator between conditions is needed or nesting of conditions then look for the pimFilterV2 helper instead
PIM api filter structure is in json format and simple input by default cannot use it in a clean way. The following functions help in generating the filters for it.
This is used at the end of the input name definition
<input type="text" name="PIMApi.Api.Query.getProductsReq.filter@pimFilter(id,in,number)">
[["id","in",[1]]]
Syntax for it is as follows:
Note that in and not in operators expect the value to be a comma separated list of values.
<input type="text" name="PIMApi.Api.Query.getProductsReq.filter@pimFilter(id,in,number)"
value='1,2,3'>
[["id","in",[1,2,3]]]
Due to json filter structure and special characters in the instructions it’s not possible to read the value back into the input fields using regular methods. For this we can use another helper function
<input type="text" name="PIMApi.Api.Query.getProductsReq.filter@pimFilter(id,in,number)"
value='{{ readPimFilter .Data.Parameters "getProductsReq" "id,in,number" }}'>
[["id","in",[1]]]
Required parameters:
We can add multiple filters to the same page, they will all be appended with the “and” operator automatically
<input type="text" name="PIMApi.Api.Query.getProductsReq.filter@pimFilter(id,in,number)"
value='{{ readPimFilter .Data.Parameters "getProductsReq" "id,in,number" }}'>
<input type="text" name="PIMApi.Api.Query.getProductsReq.filter@pimFilter(code,contains,string)"
value='{{ readPimFilter .Data.Parameters "getProductsReq" "code,contains,string" }}'>
[["id","in",[1]],"and",["code","contains","value"]]
Since . is reserved for template operators we need to define nested fields with -> instead
<input type="text" name="PIMApi.Api.Query.getProductsReq.filter@pimFilter(name->en,contains,string)"
value='{{ readPimFilter .Data.Parameters "getProductsReq" "name->en,contains,string" }}'>
[["name.en","contains","value"]]
Used to help generate pim filter json type structure from html inputs. Similar to the version 1 of the pimFilter only that this one supports different operators between conditions and optionally also nesting of conditions.
Also since the ordering of the elements is important then the order helper should also be used when using the V2 variant.
Syntax for it is as follows:
Note that in and not in operators expect the value to be a comma separated list of values.
<input type="text" name="PIMApi.Api.Query.getProductsReq.filter@{order(1);pimFilterV2(id,in,number)}">
[["id","in",[1]]]
Due to json filter structure and special characters in the instructions it’s not possible to read the value back into the input fields using regular methods. For this we can use another helper function readPimFilterV2
<input type="text" name="PIMApi.Api.Query.getProductsReq.filter@{order(1);pimFilterV2(id,in,number)}"
value='{{ readPimFilterV2 .Data.Parameters "getProductsReq" "id,in,number" }}'>
[["id","in",[1]]]
We can add multiple filters to the same page, but we also need add an operator between the conditions using the pimFilerOperatorV2 helper.
<input type="text" name="PIMApi.Api.Query.getProductsReq.filter@{order(1);pimFilterV2(id,in,number)}"
value='{{ readPimFilter .Data.Parameters "getProductsReq" "id,in,number" }}'>
<input type="hidden" name="PIMApi.Api.Query.getProductsReq.filter@{order(2);pimFilterOperatorV2()}" value="and">
<input type="text" name="PIMApi.Api.Query.getProductsReq.filter@{order(3);pimFilter(code,contains,string)}"
value='{{ readPimFilter .Data.Parameters "getProductsReq" "code,contains,string" }}'>
[["id","in",[1]],"and",["code","contains","value"]]
Optionally we can also create nested filters with the group identifier parameter. Do not forget to update the order of the functions and also the operator between possible groups.
In the sample we provide it a value of ‘a’, but the value is user defined. Everything with the value will be nested in the same array.
<input type="text" name="PIMApi.Api.Query.myRequest1.filter@{order(1);pimFilterV2(id,in,number,a)}"
value='{{ readPimFilterV2 .Data.Parameters "myRequest1" "id,in,number,a" }}'>
<input type="hidden" name="PIMApi.Api.Query.myRequest1.filter@{order(2);pimFilterOperatorV2(a)}" value="or">
<input type="text" name="PIMApi.Api.Query.myRequest1.filter@{order(3);pimFilterV2(code,contains,string,a)}"
value='{{ readPimFilterV2 .Data.Parameters "myRequest1" "code,contains,string,a" }}'>
<input type="hidden" name="PIMApi.Api.Query.myRequest1.filter@{order(4);pimFilterOperatorV2()}" value="or">
<input type="text" name="PIMApi.Api.Query.myRequest1.filter@{order(5);pimFilterV2(code2,contains,string)}"
value='{{ readPimFilterV2 .Data.Parameters "myRequest1" "code2,contains,string" }}'>
<input type="hidden" name="PIMApi.Api.Query.myRequest1.filter@{order(6);pimFilterOperatorV2()}" value="or">
<input type="text" name="PIMApi.Api.Query.myRequest1.filter@{order(7);pimFilterV2(code3,contains,string)}"
value='{{ readPimFilterV2 .Data.Parameters "myRequest1" "code3,contains,string" }}'>
[["id","in",[1]],"or",["code","contains","test"],"or",["code2","contains","test2"],"or",["code3","contains","test3"]]
Functions that related to the Json
parameter type.
JSON field names may contain @
symbol, so function section definition starts with @{
and ends with }
to avoid collisions. E.g. @{order(1);foo(bar,rab)}
. Functions separated by semicolon ;
. Function
parameters separated by comma ,
.
Defines the order of inputs related to the json generation process. In next sample initial structure of the json input would always be executed first and only then all other inputs. This allows us to predefine the payload of json body and then change only necessary fields.
<input type="hidden"
name="CaFaApi.Api.Json.putConf.json.@{order(1)}"
value='{"application":"my-app","level":"Company","name":"dynamic-sample","value":{}}'>
also may be used with chaining:
<input type="hidden"
name="CaFaApi.Api.Json.putConf.json.<-@{order(1)}"
value="getConfState.Response.0">
Used to set default preset date values that have a value based on the current date. For example some api should get a default start time of today.
Parameters:
Note it’s important to give the preset value of “default” for the function to insert the date value. This is so that possible manual values will never be overwritten.
<input type="text" name="ReportsApi.Api.Query.myRequest1.date@{setDefaultDate(UTC,02-01-06 15:04:05 MST)}"
value="" data-preset-val="default">
With the optional date adjustment parameters.
<input type="text" name="ReportsApi.Api.Query.myRequest1.dateFrom@{setDefaultDate(UTC,02-01-06 15:04:05 MST)}"
value="" data-preset-val="default">
<input type="text" name="ReportsApi.Api.Query.myRequest1.dateTo@{setDefaultDate(UTC,02-01-06 15:04:05 MST,month,1)}"
value="" data-preset-val="default">
Using the optional suffix and prefix.
<!-- As suffix, without the 6th optional parameter -->
<input type="hidden" name="PIMApi.Api.Query.myRequest1.date@{setDefaultDate(UTC,2006-01-02,2006-01,day,0, 00:00:00)}"
value="" data-preset-val="default">
<!-- Would generate: 2024-08 00:00:00 -->
<!-- As prefix with the 6th parameter -->
<input type="hidden" name="PIMApi.Api.Query.myRequest1.date@{setDefaultDate(UTC,2006-01-02,2006-01,day,0,00:00:00 ,true)}"
value="" data-preset-val="default">
<!-- Would generate: 00:00:00 2024-08-->
It’s also possible to set the function to read the current accounts timezone configuration value instead. To do this set the timezone value to from-conf.
<input type="text" name="ReportsApi.Api.Query.myRequest1.date@{setDefaultDate(from-conf,02-01-06 15:04:05 MST)}"
value="" data-preset-val="default">
Doing this the function will look for the value from accounts configuration:timezone value, if the value is not set or has an invalid value then it defaults to UTC.
Used to alter the date format to formats that the possible api requires. Mostly useful when the visual representation of the value in the UI is not the same as the api requires.
For example of the api needs unix but the UI should display regular year, month and day.
Parameters:
<!-- In this sample we take the value of 2024-04-25 and set it as unix timestamp when the api request is made -->
<input type="text" name="PIMApi.Api.Query.myRequest1.date@{formatDate(UTC,2006-01-02,unix)}"
value="2024-04-25">
With the optional date adjustment parameters.
<!-- In this sample we take the value of 2024-04-25, append 5 days and set it as unix timestamp when the api request is made -->
<input type="text" name="PIMApi.Api.Query.myRequest1.date@{formatDate(UTC,2006-01-02,unix,day,5)}"
value="2024-04-25">
Using the optional suffix and prefix.
<!-- As suffix, without the 7th optional parameter -->
<input type="hidden" name="PIMApi.Api.Query.myRequest1.date@{formatDate(UTC,2006-01-02,2006-01,day,0, 00:00:00)}"
data-preset-val="2024-04-25">
<!-- Would generate: 2024-08 00:00:00 -->
<!-- As prefix with the 7th parameter -->
<input type="hidden" name="PIMApi.Api.Query.myRequest1.date@{formatDate(UTC,2006-01-02,2006-01,day,0,00:00:00 ,true)}"
data-preset-val="2024-04-25">
<!-- Would generate: 00:00:00 2024-08-->
Version 1.222+
Can be used to encode the value. Supports base32, base64 and hex. Default is base64 if parameter is omitted, provide the parameter to use a different encoding.
<!-- Defaults to base64 -->
<input type="text" name="ReportsApi.Api.Query.myRequest1.someData@{encode}"
value="some value to be encoded">
<!-- Request specific -->
<input type="text" name="ReportsApi.Api.Query.myRequest1.someData@{encode(base64)}"
value="some value to be encoded">
<!-- Hex example -->
<input type="text" name="ReportsApi.Api.Query.myRequest1.someData@{encode(hex)}"
value="some value to be encoded">
Version 1.222+
Can be used to decode an encoded api response value to a string (supports base32, base64 and hex). Default is base64 if parameter is omitted, provide the parameter to use a different encoding.
<!-- Defaults to base64 -->
<input type="text" name="ReportsApi.Api.Query.myRequest1.someData@{decode}"
value="some value to be encoded">
<!-- Request specific -->
<input type="text" name="ReportsApi.Api.Query.myRequest1.someData@{decode(base64)}"
value="some value to be encoded">
<!-- Hex example -->
<input type="text" name="ReportsApi.Api.Query.myRequest1.someData@{decode(hex)}"
value="dGVzdA==">
Version 1.222+
Can be used to hash an api value according to an algorithm. Supports md5, sha1, sha256 and sha512. Defaults to sha1 if the parameter is not provided.
<!-- Defaults to sha1 -->
<input type="text" name="ReportsApi.Api.Query.myRequest1.someData@{hash}"
value="some value to be encoded">
<!-- Request specific -->
<input type="text" name="ReportsApi.Api.Query.myRequest1.someData@{hash(sha1)}"
value="some value to be encoded">
<!-- md5 example -->
<input type="text" name="ReportsApi.Api.Query.myRequest1.someData@{hash(md5)}"
value="value to be hashed">
Version 1.254+
This can be used to encrypt the data that is being saved in the api (kvs for example). This means that the data will only be readable when the correct key is provided.
The feature uses AES cipher, and the protection strength is determined by the given key length. Note that the keys need to use the correct length (16, 24 or 32)
Set the key for the request so the encryption knows what it needs to use.
<!--
Remember that ordering is important here as we need the system to always have the key before any encryption.
In this sample we use the chaining to take the key from the variables file, but the value can be taken from
any other api call or from a hardcoded value.
-->
<input type="hidden" name="KvsApi.Api.Encrypt.myRequest1.<-key@{order(1)}"
value="Variables.someKey" data-preset-val="Variables.someKey">
Also note that if the save and read is done on the same page then the key should be added for both calls.
Once we have set the key then we can use the encrypt helper on the values we want to protect.
<!--
Use the order here to make sure that this is done after the key set.
Any api string value can be protected like this (limited to the max length of them).
-->
<input type="text" name="KvsApi.Api.Json.myRequest1.string.value@{order(2);encrypt}"
value="some value to be encoded">
Remember to set the key for reading, otherwise the api will return an unreadable string.
<!-- When reading we also need to set the key -->
<input type="hidden" name="KvsApi.Api.Encrypt.myRequest2.<-key"
value="Variables.someKey" data-preset-val="Variables.someKey">
Use the decrypt tools helper to read the data.
The method will not inform you of an invalid or missing keys, instead it will generate random garbage values every single load.
<ul>
{{ range .Data.KvsApi.Api.Requests.myRequest2.Response.Array }}
<li>{{ $.Tools.Decrypt (.Get "value").String }}</li>
{{ end }}
<ul>
A simple save and read sample using kvs api.
<form method="post">
<input type="hidden" name="KvsApi.Api.Post.myRequest1"
value="api/v1/entry">
<input type="hidden" name="KvsApi.Api.Encrypt.myRequest1.<-key@{order(1)}"
value="Variables.someKey" data-preset-val="Variables.someKey">
<input type="hidden" name="KvsApi.Api.Json.myRequest1.string.topicId"
value="5afb02fc-04a5-4524-a816-4381f97b1few">
<input type="hidden" name="KvsApi.Api.Json.myRequest1.string.key"
value="my-value">
<input type="text" name="KvsApi.Api.Json.myRequest1.string.value@{order(2);encrypt}"
value="some value to be encoded">
<button type="submit">send</button>
</form>
<input type="hidden" name="KvsApi.Api.Get.myRequest2|2"
value="api/v1/entry" data-preset-val="api/v1/entry">
<input type="hidden" name="KvsApi.Api.Encrypt.myRequest2.<-key"
value="Variables.someKey" data-preset-val="Variables.someKey">
<input type="hidden" name="KvsApi.Api.Query.myRequest2.topicId"
value="1" data-preset-val="5afb02fc-04a5-4524-a816-4381f97b1few">
<br>
<ul>
{{ range .Data.KvsApi.Api.Requests.myRequest2.Response.Array }}
<li>{{ $.Tools.Decrypt (.Get "value").String }}</li>
{{ end }}
<ul>
Can be used to generate multiple request parameters based on input array from another dynamic result.
For use cases where the api expects indexed requests.
For example
someValue0, someValue1 etc
someParam[0], someParam[1]
Use %v in the field name to indicate where the index is to be printed.
<input name="PIMApi.Api.Query.myRequest2.<-myField[%v]|@{toArrIndexParameters}" data-preset-val="myRequest1.Response.#.id">
In this example lets assume the result would be [1,2,3], this would be generated as 3 separate parameters for the request:
PIMApi.Api.Query.myRequest2.myField[0] = 1 PIMApi.Api.Query.myRequest2.myField[1] = 2 PIMApi.Api.Query.myRequest2.myField[2] = 3
Basically wraps entire content into provided suffix and prefix. This function always accepts two parameters, if there is more or less than two, then initial value is returned.
<input type="hidden" name="KvsApi.Api.Json.test.string.field1@{wrap([,])}" value='"one","two","three"'>
<!-- Result: ["one","two","three"] -->
Prefixes the value with given string.
<input type="hidden" name="KvsApi.Api.Json.test.string.field1@{prefix(hello )}" value='world'>
<!-- Result: hello world -->
Adds suffix to the value.
<input type="hidden" name="KvsApi.Api.Json.test.string.field1@{prefix( world)}" value='hello'>
<!-- Result: hello world -->
Starting from version 1.201.1, GoErp allows to make json modifications more flexible, like predefined json, connecting json parts from another request and many more.
Let’s take CAFA as an example, as it will use most of the modification options.
<h1>Dynamic CAFA</h1>
<form method="post">
<!-- Defining variables to get rid of very long value definitions in the inputs -->
{{ $putConfBody := .Data.CaFaApi.Api.Requests.putConf.Json }}
{{ $getConfStateResp := .Data.CaFaApi.Api.Requests.getConfState.Response.Get "0" }}
<!-- 1. Load initial state of the configuration -->
<input type="hidden" name="CaFaApi.Api.Get.getConfState" value="configuration" data-preset-val="configuration">
<!-- 2. provide query parameters for the configuration entry -->
<input type="hidden" name="CaFaApi.Api.QueryBulk.getConfState.url" value="application=my-app&level=Company&name=dynamic-sample"
data-preset-val="application=my-app&level=Company&name=dynamic-sample">
<!-- 3. Create save call with initial json payload -->
<input type="hidden" name="CaFaApi.Api.Put.putConf|1" value="v3/configuration">
{{ if $getConfStateResp.Exists }}
<input type="hidden" name="CaFaApi.Api.Json.putConf.json.<-@{order(1)}"
value="getConfState.Response.0">
{{ else }}
<input type="hidden" name="CaFaApi.Api.Json.putConf.json.@{order(1)}"
value='{"application":"my-app","level":"Company","name":"dynamic-sample","value":{}}'>
{{ end }}
<!-- 4. Optionally, check updated state of the entry. -->
<input type="hidden" name="CaFaApi.Api.Get.getConfFinal|2" value="v3/configuration/{id}">
<input type="hidden" name="CaFaApi.Api.Path.getConfFinal.<-id" value="putConf.Response.id">
<!-- 5. Modify section, use same approach to as many fields as needed -->
<br/>Cat Murka age:
<input type="text"
name="CaFaApi.Api.Json.putConf.number.value.pets.murka.age"
value='{{ if $putConfBody.Exists }}{{ $putConfBody.Get "value.pets.murka.age" }}{{ else}}{{ $getConfStateResp.Get "value.pets.murka.age" }}{{ end }}'>
<button type="submit">Submit</button>
</form>
<h2>Before update</h2>
Murka age: {{ .Data.CaFaApi.Api.Requests.getConfState.Response.Get "0.value.pets.murka.age" }}
<h2>After update</h2>
Murka age: {{ .Data.CaFaApi.Api.Requests.getConfFinal.Response.Get "0.value.pets.murka.age" }}
getConfState
request. Later we can use it for the “first page loading” event to display
current state.QueryBulk
parameter type we are preparing a bunch of static query parameters to get
specific configuration entry, based on application
, level
and name
.getConfState
if it is available, or generating totally new one otherwise. Also,
we are using input functions
feature to define the order for json input parameters. By default, GoErp receives all parameters
in a chaotic order. But we need to be sure that initial structure assignment goes always first,
so we are defining order by setting it to 1. Additionally, we need to assign the payload to body
as a root object. To do that, just omit key definition after the dot (with key: .json.key
, and
without: .json.
).getConfFinal
to retrieve configuration updated state,
just to see that configuration was changed. In most cases it is not needed in real applications.The final working example with a couple more fields after removing all unnecessary stuff:
<!-- Vars section -->
{{ $putConfBody := .Data.CaFaApi.Api.Requests.putConf.Json }}
{{ $getConfStateResp := .Data.CaFaApi.Api.Requests.getConfState.Response.Get "0" }}
<h1>Dynamic CAFA</h1>
<form method="post">
<input type="hidden" name="CaFaApi.Api.Get.getConfState" value="configuration" data-preset-val="configuration">
<input type="hidden" name="CaFaApi.Api.QueryBulk.getConfState.url" value="application=my-app&level=Company&name=dynamic-sample" data-preset-val="application=my-app&level=Company&name=dynamic-sample">
<input type="hidden" name="CaFaApi.Api.Put.putConf|1" value="v3/configuration">
{{ if $getConfStateResp.Exists }}
<input type="hidden" name="CaFaApi.Api.Json.putConf.json.<-@{order(1)}" value="getConfState.Response.0">
{{ else }}
<input type="hidden" name="CaFaApi.Api.Json.putConf.json.@{order(1)}" value='{"application":"my-app","level":"Company","name":"dynamic-sample","value":{}}'>
{{ end }}
<input type="hidden" name="CaFaApi.Api.Get.getConfFinal|2" value="v3/configuration/{id}">
<input type="hidden" name="CaFaApi.Api.Path.getConfFinal.<-id" value="putConf.Response.id">
<!-- Modify section -->
<br/>Cat Murka age: <input type="text" name="CaFaApi.Api.Json.putConf.number.value.pets.murka.age"
value='{{ if $putConfBody.Exists }}{{ ($putConfBody.Get "value.pets.murka.age").String }}{{ else}}{{ ($getConfStateResp.Get "value.pets.murka.age").String }}{{ end }}'>
<br/>Cat Murka breed: <input type="text" name="CaFaApi.Api.Json.putConf.string.value.pets.murka.breed"
value='{{ if $putConfBody.Exists }}{{ ($putConfBody.Get "value.pets.murka.breed").String }}{{ else}}{{ ($getConfStateResp.Get "value.pets.murka.breed").String }}{{ end }}'>
<br/>Cat Murka kind: <input type="text" name="CaFaApi.Api.Json.putConf.string.value.pets.murka.kind"
value='{{ if $putConfBody.Exists }}{{ ($putConfBody.Get "value.pets.murka.kind").String }}{{ else}}{{ ($getConfStateResp.Get "value.pets.murka.kind").String }}{{ end }}'>
<br/><button type="submit">Submit </button>
</form>
It’s possible to instruct the form to keep certain dynamic api parameters for api requests but not trigger the corresponding api request with the post until instructed to do so.
This can be used to make ‘drafts’ from inputs that will not trigger the api calls.
The following form entry will prevent all dynamic api calls from triggering, but keeps all the parameters intact.
Note that this also affects api calls that are done by presets.
Server will not run any dynamic api calls when the value of the Form.HoldDraft is true. Inputs against these calls can be changed and posted without the actual calls being initiated.
<form method="post">
<input type="hidden" id="req" name="ErplyApi.Api.Post.myRequest1"
value="getProducts">
<label for="id">Records on page</label>
<input type="text" id="id" name="ErplyApi.Api.PostParam.myRequest1.recordsOnPage"
value="{{ index .Data.Parameters "ErplyApi.Api.PostParam.myRequest1.recordsOnPage" }}">
<label for="hold-state">Hold draft</label>
<input type="checkbox" id="hold-state" name="Form.HoldDraft" value="true">
<button type="submit">send</button>
</form>
In order the enrich data with additional possible api calls but hold others as drafts we can use a different input.
All dynamic requests that have the names will not be executed.
<input type="text" id="hold-state" name="Form.HoldDraftFor" value="myRequest2,myRequest3">
Can possibly make stepper type templates using this method. In this sample the same ‘Send’ will run request1 and request2 in sequence.
<form method="post">
<input type="hidden" id="req" name="ErplyApi.Api.Post.myRequest1"
value="getProducts">
<input type="hidden" id="req" name="ErplyApi.Api.Post.myRequest2"
value="getProducts">
<label for="id">Records on page</label>
<input type="text" id="id" name="ErplyApi.Api.PostParam.myRequest1.recordsOnPage"
value="{{ index .Data.Parameters "ErplyApi.Api.PostParam.myRequest1.recordsOnPage" }}">
<label for="id">Records 2 on page</label>
<input type="text" id="id" name="ErplyApi.Api.PostParam.myRequest2.recordsOnPage"
value="{{ index .Data.Parameters "ErplyApi.Api.PostParam.myRequest2.recordsOnPage" }}">
{{ if ne .Data.ErplyApi.Api.Requests.myRequest1.Response.Exists true }}
<input type="text" id="hold-state" name="Form.HoldDraftFor" value="myRequest2">
{{ end }}
<button type="submit">send</button>
</form>
Since v1.277.0 GoErp supports FTP requests in dynamics. This feature allows to upload or download files to/from servers through FTP protocol.
Initialization of the request is made through FTPGet
, FTPPut
types. Like so:
<input type="hidden" name="CustomApi.Api.FTPPut.file" value="localhost:2121">
. Value in this case
contains the FTP server host with port. Port is mandatory parameter. If provider not specified the
port, then the default ports for ftp 21,2121
, and for ftps 990
.
All request parameters should be provided through PostParam
type. The following parameters are
available:
file
- content of the file to upload.username
- FTP server username.password
- FTP server password.path
- Path to the file on the server. Example: /path/to/file.txt
.In case of FTPGet
requests, the response body will contain the file content.
{
"status": "success",
"base64File": "base64 encoded file content"
}
In case of FTPPut
requests, just a status message will be returned.
{
"status": "success"
}
Both methods have the same error response structure.
{
"error": "550 Could not access file: open /tmp/my-data: no such file or directory",
"status": "error"
}
<h2>Upload file</h2>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="CustomApi.Api.FTPPut.file" value="localhost:2121">
<input type="hidden" name="CustomApi.Api.PostParam.file.username" value="test">
<input type="hidden" name="CustomApi.Api.PostParam.file.password" value="test">
<br>Path: <input type="text" name="CustomApi.Api.PostParam.file.path" value="">
<br>File: <input type="file" name="CustomApi.Api.PostParam.file.file" value="">
<button type="submit">Go</button>
</form>
<h2>Download file</h2>
<form method="post">
<input type="hidden" name="CustomApi.Api.FTPGet.gFile" value="localhost:2121">
<input type="hidden" name="CustomApi.Api.PostParam.gFile.username" value="test">
<input type="hidden" name="CustomApi.Api.PostParam.gFile.password" value="test">
<br>Path: <input type="text" name="CustomApi.Api.PostParam.gFile.path" value="">
<button type="submit">Go</button>
</form>
<br>Response:<pre>{{ .Data.CustomApi.Api.Requests.gFile.Response.Raw | jsonPretty }}</pre>
Always used with forms. Goerp application handles all pages with one dynamic handler and pages may have multiple forms. When the page is processed by the handler, then all forms and related input models are involved. We need to know the exact model to be sure that after submitting the form POST request, the right model gets processed. This model name should be passed with the postActionEntity input name.
All form control fields:
postAction
- what action should be applied to the entity, currently there is only one option -
delete, if passed then form control should also have postActionIds input included. If postAction
is not specified, then by default applied save/update action.postActionIds
- entity IDās separated by comma (,). Those are input parameters for performing
changes under specific entities (in most cases deleting them)postActionEntity
- defines the model name which is related to the form. Needed only for the
save, update and delete options (not needed in query type forms). Supports multiple entities, each
of them should be defined in separate input with the same name, check Multi-entity form topic in
this section.postActionRedirect
- defines the link where to redirect after successful post action (not
applied to delete action and to forms with GET
method)Do not use any form control inputs while submitting the form using GET method (at least for now there is no practical use of form control options in GET requests).
Use GET
requests to fetch multiple data for tables using Query
model, or to fetch
single Input
entity (usually just passing its ID) while defining link to the edit form.
Query
models always connected with the List
models. When defining the List
model in your
template,
GOERP will make request to the related API and fetch data based on the Query
parameters.
<form method="get" id="query-form">
<label for="query-parameter-1">Query parameter 1:</label>
<input type="text" id="query-parameter-1" name="Api.ModelQuery.Field"
value="{{ .Data.Api.ModelQuery.Field }}">
<label for="query-parameter-2">Query parameter 2:</label>
<input type="text" id="query-parameter-2" name="Api.ModelQuery.AnotherField"
value="{{ .Data.Api.ModelQuery.AnotherField }}">
<button type="submit">Find</button>
</form>
<!-- Use List entities to actually apply the query, otherwise Query parameters will be ignored -->
<ul>
{{ range .Data.Api.ModelList }}
<li>{{ .Name }}</li>
{{ end }}
</ul>
Let’s take a look into simple create/update example form:
<form method="post" id="post-form">
<!-- For saving action need to pass only postActionEntity, which will ensure that you are working with correct model -->
<input type="hidden" name="postActionEntity" value="ModelInput">
<!-- Optionally, use redirect option to redirect to the specified page after successful saving -->
<input type="hidden" name="postActionRedirect" value="https://erply.com/">
<!-- Next 2 fields are related to actual model parameters -->
<label for="input-1">Input 1:</label>
<input type="text" id="input-1" name="Api.ModelInput.Field"
value="{{ .Data.Api.ModelInput.Field }}">
<label for="input-2">Input 2:</label>
<input type="text" id="input-2" name="Api.ModelInput.AnotherField"
value="{{ .Data.Api.ModelInput.AnotherField }}">
<button type="submit" id="save">Save</button>
</form>
Often we need to make reference from the list of rows to the specific entity edit form. In this case
pass entity ID from the selected row to the edit link by attaching it to the Input
ID (some input
models doesn’t have Input
suffix, but you can check in data source definitions in editor if model
have “write” option):
<!-- Use built-in .Session object to access account related configuration -->
<!-- Use prefix '$' if linking made inside the range loop, otherwise omit it -->
<a href="/{{ $.Session.ClientCode }}/{{ $.Session.Language.Code }}/example-page?Api.ModelInput.Id={{ .Id }}"
target="_self" class="table-action-button">
<i class="button nowrap button--transparent icon-Edit-Line"></i>
</a>
Delete actions should be made by following next steps:
POST
postAction
input to delete
postActionIds
input to entity IDs (separated by comma) that should be
deleted (using custom JS or predefined goerp components)postActionEntity
input to the entity name that should be deleted. If
it is row in the table, then that entity should have suffix List
(e.g. WarehouseDtoList
or WarehouseDtoList
). If deleting from the single entity edit form, then suffix may be Input
or just model name, check data sources definition in the editor for exact model names. Please
note, postActionEntity
value shouldn’t have API definition, just model name.<form method="get" id="entity-table-id">
<input type="hidden" name="postAction" value="delete">
<input type="hidden" name="postActionIds" value="">
<input type="hidden" name="postActionEntity" value="ModelList">
<!-- Add submit button if delete modal not used -->
<!-- <button type="submit">DELETE</button>-->
</form>
<!-- and then somewhere in the list range -->
<ul>
{{ range .Data.Api.ModelList }}
<li>{{ .Name }}</li>
<!-- If used delete-confirm-partial component, then define next parameters inside the button -->
<button data-form-id="entity-table-id"
data-delete-identifier="{{ .Id }}"
data-post-action-entity="ModelList"
class="button button--transparent nowrap table-action-button action-row-red form-delete-button">
<i class="icon-Trash-Empty-NotFilled"></i>
</button>
{{ end }}
</ul>
<!-- As an option, somewhere in the end of the page import delete-confirm-modal from goerp components and use it -->
<!-- to delete one or many entities. Or write your own deletion logic -->
{{ template "delete-confirm-partial" .}}
GOERP now supports multiple entities definitions in one form, which allows to save data of many different, unrelated entities at once.
Multi-entity form supports only one postAction type for all scoped entities. For example, it cannot be used to delete one and create another entity at once. Only delete for all related entities OR create/update for all related entities.
Let’s say we need to create warehouse and point of sale records by submitting them in one form, then code would look like this:
<form method="POST">
{{/* Define entity names that should be processed */}}
<input type="hidden" name="postActionEntity" value="WarehouseInput">
<input type="hidden" name="postActionEntity" value="PointOfSale">
{{/* Warehouse input data */}}
<fieldset>
<legend>Warehouse:</legend>
<label for="name-eng">Name eng:</label>
<input type="text" id="name-eng" name="AccountAdminApi.WarehouseInput.Name.en"
value="{{ .Data.AccountAdminApi.WarehouseInput.Name.en }}">
<label for="name-est">Name est:</label>
<input type="text" id="name-est" name="AccountAdminApi.WarehouseInput.Name.et"
value="{{ .Data.AccountAdminApi.WarehouseInput.Name.et }}">
</fieldset>
{{/* Point of sale input data */}}
<fieldset>
<legend>Point of sale:</legend>
<label for="name-pos">Name:</label>
<input type="text" id="name-pos" name="AccountAdminApi.PointOfSale.Name"
value="{{ .Data.AccountAdminApi.PointOfSale.Name }}">
<label for="name-shop">Shop name:</label>
<input type="text" id="name-shop" name="AccountAdminApi.PointOfSale.ShopName"
value="{{ .Data.AccountAdminApi.PointOfSale.ShopName }}">
</fieldset>
</form>
Some input entities in GOERP may be submitted as a bulk (having multiple records in scope of one
form submit), allowing to create or update multiple records at once. Such inputs have array types in
data source definition - []string
(e.g. ErplyApi.ProductInSupplierPriceList
). Also, bulk
input fields may be a part of some complex input entities (e.g. ErplyApi.SalesDocumentInput
)
accepts many Row
’s as part of the input.
Name
and Code
and second one have ID
, Name
and Code
. If ID
undefined, then pass empty value with an input.<form method="POST">
{{/* Define entity name that should be processed */}}
<input type="hidden" name="postActionEntity" value="ProductInSupplierPriceList">
{{/* Empty row (to add new) */}}
<fieldset>
<legend>Create row:</legend>
<label for="spl-id">Supplier price list ID:</label>
<input type="text" id="spl-id"
name="ErplyApi.ProductInSupplierPriceList.SupplierPriceListID">
<label for="product-id">Product ID:</label>
<input type="text" id="product-id"
name="ErplyApi.ProductInSupplierPriceList.ProductID">
</fieldset>
<h2>Existing rows</h2>
{{/* Populate existing rows for update */}}
{{ range $i, $el := .Data.ErplyApi.ProductInSupplierPriceListList }}
<fieldset>
<legend>Row {{$i}}:</legend>
<label for="spl-id-{{$i}}">Supplier price list ID:</label>
<input type="text" id="spl-id-{{$i}}"
name="ErplyApi.ProductInSupplierPriceList.SupplierPriceListID"
value="{{$el.SupplierPriceListID}}">
<label for="product-id-{{$i}}">Product ID:</label>
<input type="text" id="product-id-{{$i}}"
name="ErplyApi.ProductInSupplierPriceList.ProductID"
value="{{$el.ProductID}}">
</fieldset>
{{ end }}
</form>
<!-- Display possible api errors -->
<div>
{{ range .Data.Errors }}
<span>{{ . }}</span>
{{ end }}
</div>
<form method="post">
<!-- Field used by goErp to state what we want to save -->
<input type="hidden" name="postActionEntity" value="SalesDocumentInput"/>
<!-- Need to set the ID of the existing document -->
<input type="hidden" name="ErplyApi.SalesDocumentInput.ID"
value="{{ .Data.ErplyApi.SalesDocumentInput.ID }}">
<table>
<thead>
<tr>
<th>Product ID</th>
<th>Amount</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{{ range $row := .Data.ErplyApi.SalesDocument.Rows }}
<tr>
<td>
<!-- Stable row id is used by erply api to determine existing rows
the api always replaces all rows so adding a single row means all rows need to be resaved -->
<input type="hidden" name="ErplyApi.SalesDocumentInput.RowStableRowID"
value="{{ $row.StableRowID }}">
<input name="ErplyApi.SalesDocumentInput.RowProductID" value="{{ $row.ProductID }}">
</td>
<td><input name="ErplyApi.SalesDocumentInput.RowAmount" value="{{ $row.Amount }}"></td>
<td><input name="ErplyApi.SalesDocumentInput.RowPrice" value="{{ $row.Price }}"></td>
</tr>
{{ end }}
<tr>
<td>
<input type="hidden" name="ErplyApi.SalesDocumentInput.RowStableRowID" value="0">
<input name="ErplyApi.SalesDocumentInput.RowProductID" value="" placeholder="Product ID">
</td>
<td><input name="ErplyApi.SalesDocumentInput.RowAmount" value="0"></td>
<!-- Manual pricing allowed? -->
<td><input name="ErplyApi.SalesDocumentInput.RowPrice" value="0"></td>
</tr>
</tbody>
</table>
<!-- Erply api returns this boolean as a string 0 or 1 for some reason -->
{{ if eq .Data.ErplyApi.SalesDocument.Confirmed "1" }}
<h2>Confirmed documents cannot be edited</h2>
{{ else }}
<button type="submit">Save</button>
{{ end }}
</form>
Contains service information, like session key, client code, etcā¦
Syntax: {{ .Session.ClientCode }}
Full list of parameters:
.Session.ClientCode
.Session.SessionKey
.Session.User.ID
.Session.User.FullName
.Session.User.EmployeeID
.Session.User.EmployeeName
.Session.User.GroupID
.Session.User.BOLoginUrl // Back office url for current user
<!-- Services May be extended in feature -->
.Session.Services.ErplyApi.URL
.Session.Services.Auth.URL
.Session.Services.AccountAdmin.URL
.Session.Services.Assignments.URL
.Session.Services.CaFa.URL
.Session.Services.PointOfSaleApi.URL
.Session.Services.UserApi.URL
.Session.Services.WMSApi.URL
.Session.Services.CrmApi.URL
.Session.Services.PricingApi.URL
.Session.Services.PimApi.URL
.Session.Services.VinApi.URL
.Session.Services.InventoryTransactionApi.URL
.Session.Services.InventoryDocumentApi.URL
.Session.Services.IntLog.URL
.Session.Services.ReportsApi.URL
.Session.Services.WebhookManager.URL
.Session.Services.EMSApi.URL
.Session.Services.EInvoiceApi.URL
.Session.Services.KvsApi.URL
.Session.Language.Code
.Session.DefaultLanguage.Code
.Session.AccountConfiguration.configurations
Some notes about specific parameters:
.Session.AccountConfiguration.configurations
contains all erply configuration parameters.Session.Language.Code
two letter code of the currently set account language.Session.DefaultLanguage.Code
two letter code of the default account languageNot controlled by goerp, can be used to mirror some data after submitting the form. Basically, goerp will return all assigned parameters data with the response.
Let’s say we want to pass some data to the next page, using regular query parameter:
<a href="/{{ $.Session.ClientCode }}/{{ $.Session.Language.Code }}/example-page?my-custom-param=hello_there"></a>
And on the referenced page we can call this parameter using .Data.Parameters
<h1>Say Hi!</h1>
<p>{{ .Data.Parameters.my-custom-param }}</p>
It’s possible to instruct the server to validate incoming parameters to certain rules.
Register the rules on page “Form validation” configuration section.
You can use the toggle button to disable or enable the entered rules.
The parameter name is the full parameter name to be validated.
Type can be any of the following:
The validation is completed for each rule and the errors are returned in the regular .Data.Errors array.
{{ range .Data.Errors }}
{{ . }}
{{ end }}
Holds current version of the goerp server, usage: {{ .Version }}
Goerp have an option to define Preset queries, which will be saved into the template data and executed every time when page loaded without passing any form data. Usually preset option is used while querying the table of records. However, inputs also may have preset option in very specific cases. Check data source definitions in editor to ensure if model supports presets or not.
To use presets, just add input tags somewhere in the template, but outside any form, and set desired values:
<body>
<input type="hidden" name="Preset.Api.ModelQuery.FieldOne" value="1">
<input type="hidden" name="Preset.Api.ModelQuery.FieldTwo" value="two">
<!-- When template loaded from the store, goerp already knows the filter criteria based on presets -->
<!-- and this list will contain records that are corresponds to preset query inputs -->
<ul>
{{ range .Data.Api.ModelList }}
<li>{{ .Name }}</li>
{{ end }}
</ul>
</body>
Define all Preset inputs outside the form, so they will not mess up with form data (In most cases goerp editor will throw an error during saving). That make sense, because they are not related to the form at all, consider them as āimportsā in javascript, so write them somewhere in the beginning of the document (page or partial, in case of partial inside ācontent-blockā)
Despite dynamics, models have very restricted options for data chaining. Switching to the dynamic data model is strongly recommended, except cases when using model approach is more convenient or dynamic model not usable at all. But keep in mind, models are significantly less flexible in comparison with dynamics.
Currently, models supports chaining with in-built data sources only. Those are: Session
, Storage
and Parameters
.
If source doesn’t contain data, then chaining would be skipped and target parameter would be set to the empty value.
There is also option to save data from data models to the Storage. Check storage guides for more information.
<!-- in presets -->
<input name="Preset.ErplyApi.SalesDocumentQuery.ClientID<-" value="Session.customer.ID">
<!-- inside the form -->
<form method="post">
<input name="ErplyApi.SalesDocumentQuery.ClientID<-" value="Session.customer.ID">
</form>
<!-- in presets, expects that storage have the value, otherwise chaining would be skipped -->
<input name="Preset.ErplyApi.SalesDocumentQuery.ClientID<-" value="Storage.customerID">
<!-- inside the form, expects that storage have the value, otherwise chaining would be skipped -->
<form method="post">
<input name="ErplyApi.SalesDocumentQuery.ClientID<-" value="Storage.customerID">
</form>
<!-- in presets, expects that storage have the value, otherwise chaining would be skipped -->
<input name="Preset.ErplyApi.SalesDocumentQuery.ClientID<-" value="Parameters.customerID">
<!-- inside the form, expects that storage have the value, otherwise chaining would be skipped -->
<form method="post">
<input name="ErplyApi.SalesDocumentQuery.ClientID<-" value="Parameters.customerID">
</form>
Response data now contains some statistics of performed request. Current structure of the
.Data.RequestStats
parameter is:
RequestStats.TotalTime (Total time spent by performing entire request, including parsing and api calls)
RequestStats.TemplateProcessingTime (Time spent on template preparation and processing)
RequestStats.APICallsTotalTime (Total time of API calls, for DTO may exceed Total request time because some requests performed in parallel)
RequestStats.APICallsCount (Number of API calls)
RequestStats.APICalls[] (Array containing APICall elements)
RequestStats.CacheTotalTime (Total time spent on caching, not available at the moment)
APICall.Took (Time spent on this particular call, without parsing)
APICall.Endpoint (API endpoint)
APICall.ResponseCode (API http response code)
Contains basic connected application information
{{ .App.UUID }}
{{ .App.Name }}
{{ .App.Version }}
{{ .App.CreatedAt }} <!-- unix timestamp -->
{{ .App.UpdatedAt }} <!-- unix timestamp -->