Files
2026-09-23 12:29:08 -03:00

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.desktop is 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. domain is either "SISTEMA" (system-level) or "USUARIO" (user-level). Will generally be USUARIO. For non-trivial logic, see Unit Testing below.
  • endpoints — REST endpoints. authMode is one of "PUBLIC", "STATIC_TOKEN", "VITRUVIO_WS_USER_AUTH", or "HTTP_BASIC_AUTH". Default to "PUBLIC". active: true to enable. For non-trivial logic, see Unit Testing below.
  • queries — Named SQL queries. connection references a configured datasource. Use "vitruvio_producao" for the default datasource.
  • reports — Jasper reports. Require a query key, a .jrxml template, 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/ and postgresql/ 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 <?xml declaration — makes the platform's XML parser fail with Content is not allowed in prolog when the form is opened, even though the file looks fine in an editor. When writing or editing .xml/.bpmn files, never prepend a BOM or blank line; the declaration must be the very first bytes. Run vitruvio 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 using libService.loadScript("lib_name").
  • displayOrder on 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.parse and JSON.stringify are available.
  • typeof, instanceof, standard Array/Object/String methods 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.prototype iteration methods forEach, map, filter, reduce, some, every are 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:

  1. Write a test asserting the correct behavior.
  2. Run it and confirm it fails (proves it actually catches the bug).
  3. Fix the root cause.
  4. Run it and confirm it passes.
  5. 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, createQueryResult for anything using db.
  • @davinti/vitruvio-test-utils → createMockServletResponse, createMockLibService for endpoint responses and libService.loadScript lookups.
  • The shared Jest preset (jest.config.js → preset: '@davinti/vitruvio-test-utils') already stubs println, java, libService, vLogger as globals — libService.loadScript returns null for 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:

  1. forms.mobile declared in vitruvio.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" }
    
  2. candidateStarterGroups must exist in nauth with tag = 'vi_task_group' — Vitruvio filters starter groups by that tag. Groups imported from vitruvio.json get it automatically. If you add a new group just for a process, pass it in global-setup.js so createTestUser seeds it:

    await createTestUser(client, { login: login, password: password, groups: ['meu-grupo'] });
    

    If candidateStarterGroups is 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, never null — 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() and db.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 for connection-key in sqlBuilderDataSource/freeQuery, for connection in vitruvio.json queries, and when constructing new 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

  • MENU items are containers only — they do not link to anything themselves, only their children do.
  • Keys must be unique across the whole menu tree. Use the Parent/Child path convention for nested items to avoid collisions.
  • order controls 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 the vClassName convention (e.g. vProcessInstanceService, vEmailService, vConfigService). Exceptions: libService, engine, execution.
  • Libs (libs/) are JS files loaded on demand with libService.loadScript('key'). The db lib is the primary example — it wraps ConexaoService into a friendlier JS API.

General Conventions

  • Named queries versus inline SQL. Put SQL in queries/*.sql and 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-key is referenced across permission rules and menu items, treat it as a public API — dont change the key.
  • Patches are append-only. patches/ has oracle/ and postgresql/ 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 validate now 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.