Building apps & reports
Create report types, joins and reports through the API — for provisioning tenants on the fly, or migrating reports in from another BI tool.
What this is for
Most integrations only read from Yurbi. This guide covers writing: creating the semantic layer and the reports on top of it, entirely through the API.
Two situations need it:
- Dynamic data sources. A tenant's schema isn't known ahead of time, so the report type has to be built at provisioning time rather than by hand in App Builder.
- Migration. Bringing reports in from another BI tool. Those tools usually store joins per report; Yurbi stores them in the report type, so the report type has to exist before the report can be created.
The two layers
| Layer | Holds | Written with |
|---|---|---|
| Report type | Tables, joins, the field tree | SaveAnyDbModule |
| Report | Selected fields, filters, formulas, sort | SaveReport |
A report names its report type by name, not by ID. Two report types sharing a name inside one app makes every report on that name ambiguous, so keep names unique.
1. Find the app
GetAnyDbModulesList returns every
AnyDB app with the two identifiers everything else needs:
{ "ID": "17779219821", "ReportModule": { "ID": "1005", "Name": "AssetWorks Source ORACLE" } }
ID is the RegServerID, ReportModule.ID is the ModuleID.
2. Read before you write
GetAnyDbModule returns the app's report
types. Read it first, always — for two reasons:
- to compute the next free
RepTypeID(max + 1) - to snapshot the current state, because the write is destructive
Look at ReportTypes[].SQLTables to see what tables an app already has.
DBSQLTableList.AllDbtables is live introspection of the database server and
comes back empty when the app has no live connection.
These responses get large. An app with a couple of hundred tables and their columns runs to tens of megabytes.
3. Build the report type
A report type is SQLTables plus FieldTree.
Tables and joins
Each table carries a single join describing how it attaches to the model:
{
"tablename": "UNIT_MAIN",
"tableowner": "",
"tablealias": "UNIT_MAIN",
"join": {
"table1": "VIEW_ALL_WORK_ORDERS", "field1": "UnitNo",
"table2": "UNIT_MAIN", "field2": "UNIT_NO",
"jointype": "Inner Join", "joinop": "=", "filter": "", "index": 1
}
}
table1 is the parent — the table already in the model. table2 is the
one being attached. One table (the root) has an empty join.
Because each table has exactly one join, the join model is a tree, not a graph. Every table has one parent. That rules out:
- the same pair of tables joined on two column pairs (composite keys)
- a table reachable from two different parents
- cycles
jointype is "Inner Join", "Left Join" and so on — mixed case, no
OUTER. joinop is a bare "=" with no padding. filter is appended to the
join clause verbatim if set.
tableowner should match whatever the app already uses. Many AnyDB apps
register tables with an empty owner even when the underlying database has a
schema — check SQLTables on an existing report type rather than assuming.
The field tree
FieldTree is the field picker: folder nodes (nodetype: "G") with field
nodes (nodetype: "F") beneath them.
flevel is a colon-delimited materialised path made of the parent's path
plus the node's own id:
id 0 flevel "0" G VIEW_ALL_WORK_ORDERS (root folder)
id 1 flevel "0:1" F WorkOrderNo
id 2 flevel "2" G UNIT_MAIN
id 3 flevel "2:3" F Make
Two things that are easy to get wrong:
tablealiason a field node is its own table's alias.parenttablealiasis the root table of the whole report type, the same value on every field node — not the field's immediate join parent.
idx_order starts at 1.
Common datatype codes: cha text, num numeric, dat timestamp,
doz date-only. The date codes are not interchangeable — see
Dates below.
Writing it
SaveAnyDbModule deletes and re-inserts
every report type in the payload. Report types you leave out are untouched,
so send only the ones you are creating:
{
"sessionToken": "…",
"anydbmodule": {
"ReportTypes": [ { /* just the new one */ } ],
"DBSQLTableList": { "AllDbtables": [] },
"isExcelCSV": false,
"ApiEndpointList": null
}
}
Posting a whole 16 MB module back means deleting and re-inserting thousands of rows that were fine.
It returns 1 on success. Errors are swallowed and returned as 0, so
verify by reading the module back rather than trusting the return value.
4. Build the report
Start from NewReport so you inherit the current
defaults, then add fields.
The reliable way to add a field is to read the report type's tree with
GetReportTree and pass nodes to
NewField, which fills in derived values like
RenamedField (the SELECT alias) rather than making you reproduce the rules.
Filters
Put structured criteria on the field and leave the compiled strings empty —
SaveReport compiles them:
{
"DisplayFieldName": "Status",
"Criteria": [
{ "Index": 0, "Field": "Status", "Op": "inlist",
"Value1": "D,O", "Value2": "", "Logical": "And", "cond": "is" }
],
"SearchCriteria": ""
}
Criteria.Field must be the field's display name. It is the key binding a
criteria back to its field on load; if it doesn't match, the criteria is
orphaned and rebuilt from a compiled string, which changes the operator's
casing and breaks the criteria editor.
Operators are lowercase: =, <, >, <>, between, inlist, like,
not like, null, empty. cond is is or isnot. Multi-value lists are
comma-separated in Value1 — values containing a comma will be split.
Formulas
Formula holds raw SQL in the target dialect. ? is replaced with the
field's own column, alias-qualified:
Formula: "ROUND((SYSDATE - ?), 0)"
becomes: ROUND((SYSDATE - "VIEW_ALL_WORK_ORDERS"."OpenDate"), 0)
Reference a different column by qualifying it explicitly with its table
alias — "UNIT_MAIN"."MAKE". Quoting follows the platform ("x" on Oracle
and PostgreSQL, [x] on SQL Server).
Set Fieldtype to what the formula returns, not what it reads.
ROUND(SYSDATE - "OpenDate", 0) reads a date and returns a number, so it is
num.
Aggregates
There is no aggregate flag. Put an aggregate function in Formula and the
engine detects it and groups every non-aggregated column automatically:
{ "DisplayFieldName": "Ticket Count", "Formula": "count(?)", "Fieldtype": "num" }
Recognised: sum(, avg(, min(, max(, count(, stddev(. A formula
that is a subquery will not be treated as an aggregate.
Saving
Build the object you save from NewReport rather than
assembling it from scratch. The template initialises collections such as
linkreports that the save expects to be present; a hand-built object that omits
them returns ErrorCode 9000.
SaveReport branches on ReportID:
- empty string → create; the server mints an ID and returns it
- existing ID → update
Sending an ID that doesn't exist fails with 9001 Permission Denied — the
permission check runs against a report that isn't there. Never invent an ID.
folderid must be a library folder the user can modify, or the save fails
with Folder Permission Denied. Get candidates from
GetAllLibraryTree; its real tree is
nested under a rootnode key on each entry, and isUserAdmin/isUserModify
tell you what's writable.
5. Validate before you commit
Two endpoints run the pipeline without saving anything:
GetSQLtakes a whole report object and returns the SQL it would generate.ReportProcUIcompiles criteria and totals and hands the object back.
GetSQL does not compile criteria, so a freshly built object produces SQL
with no WHERE clause. Chain them — ReportProcUI first, then GetSQL — to
preview the whole statement.
This is the cheapest possible feedback loop: build the object, look at the SQL, fix it, and only then write.
Dates: pick the right code
dat and doz are both dates, and they behave differently:
| Code | Renders as | Note |
|---|---|---|
dat, dtz, tsp |
'2024-12-01 00:00:00' |
timestamp; the client applies a browser timezone offset |
doz, idt, dpo |
'2024-12-01' |
date only, no offset |
For values that are literal dates with no timezone, doz reproduces them
exactly. dat is right when time of day genuinely matters.
Either way, check period boundaries: with doz, Between '2024-12-01' and '2024-12-31' excludes times later on the 31st.
Putting it together
GetAnyDbModulesList find the app
GetAnyDbModule read current state, snapshot, next RepTypeID
↓
SaveAnyDbModule create the report type (send only the new one)
GetAnyDbModule verify it was written
↓
GetReportTree read the new field tree
NewReport + NewField assemble the report
ReportProcUI → GetSQL preview the SQL, no writes
↓
GetAllLibraryTree pick a writable folder
SaveReport ReportID: "" → returns the new ID
Things worth knowing
- Report types are referenced by name. Re-creating one with the same name but a different join graph breaks existing reports on it. Check for an existing match before creating.
SaveAnyDbModulereturns0on error, with no message. Verify by reading back.- Only the tables a report uses are joined. Selecting fields from two tables of a ten-table report type generates a two-table query, and intermediate tables needed to connect them are pulled in automatically. A broad report type therefore costs a narrow report nothing — one report type can serve many reports.
- Snapshot before writing.
GetAnyDbModuleoutput is your rollback. - Reports can be removed.
DelReportdeletes a report by ID and returns0on success, which makes it straightforward to roll back an automated build that produced the wrong result. - Grant your integration user access to the app. Only the built-in
adminaccount bypasses application permissions. Any other account needs an Architect role on the application before these calls will see it.