25 KiB
Vitruvio Development Guidelines
What is Vitruvio
Vitruvio is a Java 21 / Spring / Vaadin 7 multi-tenant BPM platform. Developers extend it by creating content repositories — git repos that contain panels, processes, scripts, queries, reports, endpoints, and more. Vitruvio reads these repos and renders their content at runtime. This file is one such repository.
Every repository must have a vitruvio.json manifest at its root that declares all artifacts. That manifest is the source of truth for what the platform will register.
Repository Structure
Artifacts live under their own top-level directories, matching the paths declared in vitruvio.json:
vitruvio.json
panels/
<panel-key>/
<panel-key>-desktop.xml
<panel-key>-mobile.xml # optional
default-state.json
thumbnail.png
processes/
<process-key>/
<process-key>.bpmn
<process-key>-desktop.xml
<process-key>-mobile.xml # optional
scripts/
<script-key>.js
<script-key>.md # optional documentation
endpoints/
<endpoint-key>.js
queries/
<query-key>.sql
reports/
<report-key>/
template.jrxml
params.xml
libraries/
<library-key>/
patches/
All key values in vitruvio.json must be kebab-case, unique within their artifact type, and stable — changing a key is a breaking change.
vitruvio.json Manifest
version and metadata.key are the only required fields. All other sections are optional arrays.
metadata
{
"name": "Human-readable module name",
"key": "module-key", // kebab-case, unique across tenants
"standardProduct": false // true only for widespread modules
}
Artifact sections
Each section is an array. Omit the section entirely if there are no items — don't leave empty arrays unless needed.
Key rules per artifact:
- panels — UI screens built from XML forms.
forms.desktopis the primary form path; mobile is optional. - processes — BPMN workflows. Can declare
schedules(cron or simple interval triggers). Are composed from a BPMN file for Activiti, a xml file for the web form and one or two xml files for the mobile forms. - scripts — Reusable JS logic.
domainis either"SISTEMA"(system-level) or"USUARIO"(user-level). Will generally be USUARIO. For non-trivial logic, see Unit Testing below. - endpoints — REST endpoints.
authModeis one of"PUBLIC","STATIC_TOKEN","VITRUVIO_WS_USER_AUTH", or"HTTP_BASIC_AUTH". Default to"PUBLIC".active: trueto enable. For non-trivial logic, see Unit Testing below. - queries — Named SQL queries.
connectionreferences a configured datasource. Use"vitruvio_producao"for the default datasource. - reports — Jasper reports. Require a query key, a
.jrxmltemplate, and optionally a parameter form. - libraries — Static file bundles (JS/CSS/images).
type: "LOCAL"for files in this repo. - groups — User groups this module manages or depends on.
- properties — User-configurable key/value settings.
- menu — Navigation tree. See the Menu section below for structure and type values.
- permissions — Process-level access control per group. Mostly unused, generally defined in the bpmn file for the process.
- patches — Path to a directory containing per-database Liquibase changelogs. Has
oracle/andpostgresql/subdirectories, each with its own XML changelog file.
XML Forms (Panels & Processes)
Forms are XML files validated against Vitruvio's XSD schema. Vaadin components are declared in XML and rendered by the platform's Java presenter layer — there is no manual Java UI code in content repos (except if imported in scripts using the Rhino engine).
Form files are named <key>-desktop.xml / <key>-mobile.xml and the BPMN <key>.bpmn (artifact key + suffix), so each form is searchable by its key rather than dozens of identical form-desktop.xml tabs.
Desktop vs mobile forms are different schemas. Desktop (<key>-desktop.xml) uses the <panel-form> / <forms> schemas, the 78-component desktop set, and Rhino ES5 scripts with auto-injected process variables. Mobile (<key>-mobile.xml) uses the <mobile-forms> schema (namespace .../vitruvio/mobile-form), only the 18 mobile components, modern React-Native JS on the client (Promises) with server-side <ServerSide><Bridge> blocks in Rhino ES5 for any lib/DB/service access, and explicitly declared data (<Autoload> variables, <QueryDataSource>, bridges via vCommunicationService.executeOnServer). Use the focused skills — vitruvio-criar-processo-bpmn, vitruvio-criar-form-desktop, vitruvio-criar-form-mobile (driven by the vitruvio-criar-painel / vitruvio-criar-processo orchestrators) — and read docs/components/mobile/ before building a mobile form.
General rules:
- Always save form/XML files as UTF-8 without a BOM. A leading byte-order mark (
EF BB BF) — or any whitespace/content before the<?xmldeclaration — makes the platform's XML parser fail withContent is not allowed in prologwhen the form is opened, even though the file looks fine in an editor. When writing or editing.xml/.bpmnfiles, never prepend a BOM or blank line; the declaration must be the very first bytes. Runvitruvio validate(which checks for this) before committing. - Always validate your XML against the XSD before committing. Malformed forms fail on open.
- Keep forms focused. One screen = one form. Split complex layouts into sub-forms or components if the schema allows.
- Use
<script>blocks with<![CDATA[...]]>for inline JavaScript on components (button actions, lifecycle hooks, field validators). For non-trivial logic, delegate to a named script file rather than writing large CDATA blocks inline or repetitive code and import it usinglibService.loadScript("lib_name"). displayOrderon panels controls the order they appear in listings. Use gaps (10, 20, 30…) to allow future insertions without reshuffling.
JavaScript — ES5 / Rhino Engine
All JavaScript in this platform runs on Mozilla Rhino, an ES5 interpreter embedded in the JVM. Modern JS features are not available.
What you CANNOT use
// NO — ES6+ is not supported
const x = 1;
let y = 2;
const fn = () => {};
`template ${literal}`;
const { a, b } = obj;
const [first, ...rest] = arr;
class Foo {}
async function bar() {}
await something();
import x from 'y';
export default x;
for (const item of iterable) {}
Promise.resolve();
Array.from(x);
Object.assign({}, a, b);
What you MUST use instead
// YES — ES5 style
var x = 1;
var y = 2;
var fn = function() {};
"template " + variable;
var a = obj.a; var b = obj.b;
// loop with index
for (var i = 0; i < arr.length; i++) {}
// prototype-based, not class
function Foo() {}
Foo.prototype.bar = function() {};
// callbacks, not async/await
doSomething(function(result) { ... });
Additional Rhino caveats
JSON.parseandJSON.stringifyare available.typeof,instanceof, standardArray/Object/Stringmethods from ES5 work fine.- No browser globals (
window,document,fetch,XMLHttpRequest). - No Node.js globals (
require,process,Buffer,__dirname). - Rhino exposes Java interop — avoid it in business scripts unless you know what you're doing.
- Always declare variables with
var. Undeclared variables become globals and cause hard-to-trace bugs. - When comparing values that originate from Java (DB query results, form field values, process variables), use
==/!=— Java types do not strict-equal JavaScript primitives. Use===/!==only when you control both sides and know they are pure JS values. Array.prototypeiteration methodsforEach,map,filter,reduce,some,everyare available — they are ES5 and work fine in Rhino.
Unit Testing
Tests run under Jest on Node, not Rhino — this is the one place in the repo where modern JS
(const, arrow functions, destructuring) is fine, because *.test.js files never execute on the
platform. The production code they test still must be ES5/Rhino-safe.
Scope: scripts/*.js and endpoints/*.js only. Inline <script> CDATA in panel/process XML and
.sql queries have no module boundary to hook Jest into and are not unit tested this way.
When to write a test. Only for non-trivial logic — a function with a branch, a loop, or a call
into db/http/a platform service. A script or endpoint that's a straight passthrough doesn't need
one. Any bug fix always gets a regression test, written test-first:
- Write a test asserting the correct behavior.
- Run it and confirm it fails (proves it actually catches the bug).
- Fix the root cause.
- Run it and confirm it passes.
- Run the full suite (
npm test) to check for regressions elsewhere.
Only add the dependency-injection seam below to scripts/endpoints that actually get a test — don't restructure trivial code preemptively for testability it doesn't need.
Making a script testable
Wrap the logic in a constructor accepting an optional deps object, falling back to the real
platform globals. Guard the CommonJS export with typeof module — Rhino's script-loading context
(libService.loadScript) never defines module, Node always does. See scripts/exemplo.js /
scripts/exemplo.test.js for the full example.
function MinhaLib(deps) {
var banco = deps ? deps.banco : new db(db.VITRUVIO_DATASOURCE);
this.metodo = function() { ... };
}
if (typeof module !== 'undefined') module.exports = MinhaLib;
Making an endpoint testable
Endpoints have a fixed production contract: the platform's Rhino endpoint loader expects
module.exports to already be an instance with onGet/onPost/etc, not a constructor — and Rhino
does define module there, so the typeof module guard doesn't distinguish it from Node. Instead,
guard on process, which only exists in Node:
function WebService(deps) {
if (deps) {
this.db = deps.db;
this.webApi = deps.webApi;
} else {
this.db = libService.loadScript('db');
this.webApi = libService.loadScript('minha-lib');
}
this.onPost = function(params, headers, res) { ... };
}
if (typeof process !== "undefined" && process.env.NODE_ENV === "test") {
module.exports = WebService;
} else {
module.exports = new WebService();
}
When there's more than one dependency, swap the whole deps object in one if/else block
rather than per-field fallback (deps.x || real). A test that forgets to mock one dependency then
gets undefined and fails loudly, instead of silently hitting a real service.
Mocking
Prefer the shared helpers over hand-rolled stubs:
@davinti/vitruvio-core-libs/db/mock→MockDb,createQueryResultfor anything usingdb.@davinti/vitruvio-test-utils→createMockServletResponse,createMockLibServicefor endpoint responses andlibService.loadScriptlookups.- The shared Jest preset (
jest.config.js→preset: '@davinti/vitruvio-test-utils') already stubsprintln,java,libService,vLoggeras globals —libService.loadScriptreturnsnullfor anything not explicitly mocked, by design, so an unmocked lib dependency fails loudly rather than silently.
For a platform service with no shared mock yet (most of them — only db has one so far), stub the
global inline in the test file:
beforeEach(() => {
global.vEmailService = { enviarEmail: jest.fn() };
});
If the same stub keeps getting copy-pasted across test files, that's a signal to request it be added
to @davinti/vitruvio-test-utils upstream — don't duplicate it locally across many test files.
Running
npm test must pass before scripts/endpoints work is considered done — same standing as
vitruvio validate for XML forms.
E2E Testing
E2E tests run against a real Vitruvio instance spun up by vitruvio test:start. Use them for
process workflows — happy paths that cross multiple artifacts (BPMN → script → DB write) and are
too integrated to unit test meaningfully. Don't E2E test things that unit tests already cover
(pure script logic, single endpoint calls with mocked deps).
Setup
Nenhuma configuração é necessária — vitruvio test:start funciona direto. Um usuário de
teste é gerado automaticamente a cada execução (veja resolveE2ECredentials em
@davinti/vitruvio-test-utils).
Se quiser usar um usuário fixo em vez do gerado automaticamente:
cp tests/.env.example tests/.env
# preencha TEST_USER e TEST_PASSWORD — nunca use admin
tests/.env é gitignored.
Running
vitruvio test:start # spin up Vitruvio + Postgres + gitea-mock (Docker)
vitruvio test:sync # push current working tree and import into Vitruvio
npm run test:e2e # run tests in tests/*.e2e.js
vitruvio test:stop # tear down containers and volumes
test:sync pushes uncommitted changes — you don't need to commit before testing. Re-run it whenever
you change a process, form, script, or vitruvio.json.
Writing a test
Tests live in tests/*.e2e.js. Use VitruvioClient from @davinti/vitruvio-test-utils:
'use strict';
var { VitruvioClient } = require('@davinti/vitruvio-test-utils');
var client = new VitruvioClient();
beforeAll(async function() {
await client.login(process.env.TEST_USER, process.env.TEST_PASSWORD);
});
test('meu-processo: completa tarefa de aprovação', async function() {
var instance = await client.startProcess('meu-processo-key');
var task = await client.waitForTask('Task_aprovacao', { processInstanceId: instance.processInstanceId });
var data = await client.getTaskData(task.id);
await client.completeTask(task.id, Object.assign({}, data, { aprovado: true }));
});
global-setup.js runs before every test suite — it pings Vitruvio and seeds the test user. Your
test file just needs beforeAll for client.login.
Prerequisites for a process to be testable
Two things must be true or startProcess returns 404:
-
forms.mobiledeclared invitruvio.json— the REST API only exposes processes with a mobile form entry. Point it at the desktop XML if there's no real mobile form:"forms": { "desktop": "processes/meu-processo/meu-processo-desktop.xml", "mobile": "processes/meu-processo/meu-processo-desktop.xml" } -
candidateStarterGroupsmust exist in nauth withtag = 'vi_task_group'— Vitruvio filters starter groups by that tag. Groups imported fromvitruvio.jsonget it automatically. If you add a new group just for a process, pass it inglobal-setup.jssocreateTestUserseeds it:await createTestUser(client, { login: login, password: password, groups: ['meu-grupo'] });If
candidateStarterGroupsis absent from the BPMN, any authenticated user can start — fine for tests but usually wrong for production.
VitruvioClient API
| Method | What it does |
|---|---|
login(user, pass) |
Authenticates and stores the JWT |
startProcess(key, data?) |
Starts a process instance; returns { processInstanceId, ... } |
getTasks() |
Returns all tasks visible to the logged-in user |
waitForTask(taskKey, opts?) |
Polls until a task with that taskDefinitionKey appears; opts: { processInstanceId, timeout, interval } |
waitForProcessEnd(instanceId, opts?) |
Polls until no tasks remain for the given process instance |
getTaskData(id) |
Returns the task's current form data |
completeTask(id, data?) |
Completes the task with the given form data |
callEndpoint(key, opts?) |
Calls a public integration endpoint |
waitForTask and waitForProcessEnd default to a 30 s timeout with 2 s polling.
DB seed utilities
Available from @davinti/vitruvio-test-utils for use in global-setup.js:
deleteTestProcessInstances(pg, processKey) — deletes all instances of a process key from the
test DB (Activiti runtime + history + all Vitruvio child rows). Call it at the top of the setup
function so each run starts clean and waitForTask can't match a task from a previous run:
await deleteTestProcessInstances(client, 'meu-processo-key');
await createTestUser(client, { login: login, password: password, groups: ['vi_user'] });
seedConexao(pg, options) — upserts a conexao row so scripts using new db('my-key') or
queries with connection: 'my-key' can resolve the datasource. Idempotent — safe to call on every
test run. Required fields: siglaId, host, instancia. Optional: nome, porta (default 5432),
usuario, senha, plataforma (default 2 = PostgreSQL), aliases (array of alias strings):
await seedConexao(client, {
siglaId: 'minha-conexao',
host: process.env.TEST_DB_HOST || 'localhost',
porta: parseInt(process.env.TEST_DB_PORT || '15432'),
instancia: process.env.TEST_DB_NAME || 'vitruvio',
usuario: process.env.TEST_DB_USER || 'postgres',
senha: process.env.TEST_DB_PASSWORD || 'postgres',
});
Vitruvio Platform Services
Injected objects available in script and endpoint contexts (e.g. vEmailService, vProcessService, libService, vFileService, vLoginService…).
Before calling any service method, read its doc file:
~/.local/share/vitruvio-platform/docs/services/<ServiceName>.md
Every injected service has its own file there — method signatures, parameters, return types, and examples. Do not guess method names or signatures; always check the file first.
Developer Libraries
These are widely-used libraries already available in the platform environment:
| Library | Purpose |
|---|---|
http |
HTTP request utilities (GET, POST, etc.) |
db |
Database query helpers — prefer named queries from queries/ over raw SQL in scripts |
Also not full list, there are more usable libs.
db usage notes
db.query()always returns a result wrapper object, nevernull— even with zero rows, you get a wrapper whose.each()simply doesn't invoke the callback. Don't null-check it, just call.each()directly:
var rows = banco.query("SELECT ...", {});
rows.each(function(row) { ... });
db.queryRow() is the one that returns null when there are no rows — that one you do need to null-check.
- Values from
db.queryRow()anddb.query()row properties are Java objects. Use==/!=when comparing them to JavaScript primitives (follows from the Java interop rule above). - The default datasource connection key is
"vitruvio_producao". Use it forconnection-keyinsqlBuilderDataSource/freeQuery, forconnectioninvitruvio.jsonqueries, and when constructingnew db('vitruvio_producao')in scripts. The string"default"does not resolve to anything and will cause errors.
Menu
The menu section of vitruvio.json defines the navigation tree shown to users. It is a flat-ish array — nesting is done via children on MENU items.
"menu": [
{
"key": "my-panel-item",
"name": "My Panel",
"order": 1,
"type": "PAINEL",
"panelKey": "my-panel-key",
"children": []
},
{
"key": "my-group",
"name": "My Group",
"icon": 61946,
"order": 2,
"type": "MENU",
"children": [
{
"key": "my-group/child-panel",
"name": "Child Panel",
"icon": 62030,
"order": 0,
"type": "PAINEL",
"panelKey": "child-panel-key",
"children": []
}
]
}
]
Field reference
| Field | Required | Notes |
|---|---|---|
key |
yes | Unique identifier. For children, use Parent/Child path convention |
name |
yes | Label shown in the UI |
type |
yes | MENU (group/submenu), PAINEL (panel), RELATORIO (report), PROCESSO (process) |
order |
yes | Display order within its level |
icon |
no | Numeric codepoint for the item icon |
panelKey |
for PAINEL | Must match the key of a registered panel |
children |
yes | Array of child items. Always include, even as [] on leaf items |
Rules
MENUitems are containers only — they do not link to anything themselves, only theirchildrendo.- Keys must be unique across the whole menu tree. Use the
Parent/Childpath convention for nested items to avoid collisions. ordercontrols sorting within a level; gaps (0, 10, 20…) allow future insertions.- Never generate menu entries unless the user explicitly asks for it. Creating a panel, process, or other artifact does not imply adding it to the menu.
Global Platform Reference
A shared directory exists outside this repo with platform-wide resources. Always check it before writing new code.
| OS | Path |
|---|---|
| Linux | ~/.local/share/vitruvio-platform/ |
| Windows | %LOCALAPPDATA%\vitruvio-platform\ |
What's where and when to use it
| Folder | Contents | When to look here |
|---|---|---|
libs/ |
Core JS library files (db, http, messages, vaadinComponents, etc.) |
Before implementing something that a platform lib likely already handles |
docs/services/ |
One .md per injected Java service — method signatures, parameters, return types. Named by injection variable (e.g. vProcessInstanceService.md, vEmailService.md). |
When you need to call a platform service and don't know what methods it exposes |
docs/java/ |
Full exported JavaDocs (HTML). Package structure: br/, com/, org/. |
When you need to importClass(Packages....) a Java class and want to know its API |
docs/components/desktop/ |
78 Vaadin component reference markdowns — attributes, events, usage examples. See docs/components/INDEX.md for the full list. |
When working with any component and unsure of its attributes or behaviour. Read docs/components/desktop/<ComponentName>.md before using a component you're not certain about. |
docs/components/mobile/ |
18 mobile component reference markdowns. | When building mobile forms. |
examples/panels/ |
Example panel XML forms | Before writing a panel from scratch |
examples/processes/ |
Example BPMN + process XML forms | Before writing a process from scratch |
examples/scripts/ |
Example library and task scripts | Before writing a script from scratch |
examples/endpoints/ |
Example REST endpoints | Before writing an endpoint from scratch |
examples/reports/ |
Example Jasper report configs | Before writing a report from scratch |
Examples show how things generally work in the platform — treat them as functional reference, not necessarily best-practice code.
Services vs. libs — key distinction
- Services (
docs/services/) are Java objects injected directly into every script context. Call them by their variable name — no loading needed. Names follow thevClassNameconvention (e.g.vProcessInstanceService,vEmailService,vConfigService). Exceptions:libService,engine,execution. - Libs (
libs/) are JS files loaded on demand withlibService.loadScript('key'). Thedblib is the primary example — it wrapsConexaoServiceinto a friendlier JS API.
General Conventions
- Named queries versus inline SQL. Put SQL in
queries/*.sqland reference by key if they will be reused only. Keeps SQL auditable and reusable across scripts/reports. - Group keys are shared contracts. If a
group-keyis referenced across permission rules and menu items, treat it as a public API — dont change the key. - Patches are append-only.
patches/hasoracle/andpostgresql/subdirectories, each with its own Liquibase XML changelog. Never modify existing changesets — only append new ones. Keep both files in sync. - No BOM, ever. All text artifacts (XML forms, BPMN, scripts, queries, reports) must be plain UTF-8 with no byte-order mark. A BOM breaks XML parsing at runtime and is invisible in most editors —
vitruvio validatenow flags it, but don't introduce it in the first place.
Git Commits
Applies to every commit in this repo. Follow Conventional Commits
(type(scope): summary, e.g. feat, fix, refactor, docs, chore), in Brazilian Portuguese or
English. The body must give context on what changed, why, and how — not just restate the diff —
and reference the ticket number when one exists.
feat(compras): adiciona comprador específico na solicitação de compra
Permite abrir a solicitação para um comprador específico e filtra os
produtos por comprador no painel de precificação.
Ref: ticket 23917
README
Check README.md whenever the repo looks near-complete or complete (most artifacts registered,
feature work wrapping up). If it's missing or still the projeto-base placeholder, warn the dev and
ask them to describe what the module does — then write/update the README covering: what it manages,
how it works, the most important files and how they interact, and any caveats worth flagging to a
future reader. Only write it once the dev has confirmed the description; don't invent one.