DMN Implementation¶
Component structure¶
src/
âââ components/tabs/
â âââ DMNTab.jsx # Main UI (900+ lines)
â âĸ File upload + card display
â âĸ Syntactic validation panel
â âĸ API configuration
â âĸ Deployment controls
â âĸ Single evaluate (Postman-style)
â âĸ Intermediate decision tests
â âĸ Test cases upload + runner
â âĸ Import preservation notice
â
âââ utils/
â âââ dmnHelpers.js # DMN TTL generation utilities (370 lines)
â âââ parseTTL.enhanced.js # DMN block capture on import (523 lines)
â
âââ config/
âââ vocabularies_config.js # DMN entity type detection
DMN state shape¶
dmnData: {
// File
fileName: string,
content: string, // Raw DMN XML
// Deployment
decisionKey: string, // Primary key extracted from DMN
deployed: boolean,
deploymentId: string | null,
deployedAt: string | null,
// API
apiEndpoint: string,
// Testing
lastTestResult: object | null,
lastTestTimestamp: string | null,
testBody: string | null,
// Import preservation (v1.5.1+)
importedDmnBlocks: string | null, // Raw Turtle lines preserved verbatim
isImported: boolean,
}
Syntactic validation¶
Overview¶
When a DMN file is uploaded or an example is loaded, DMNTab.jsx calls the shared backend validation endpoint and stores the result in local component state.
// State added in v1.9.3
const [validationResult, setValidationResult] = useState(null);
const [isValidating, setIsValidating] = useState(false);
const [validationExpanded, setValidationExpanded] = useState(false);
Backend call¶
const runBackendValidation = async (content) => {
setIsValidating(true);
try {
const response = await fetch(
`${process.env.REACT_APP_BACKEND_URL}/v1/dmns/validate`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content }),
}
);
const data = await response.json();
if (data.success) {
setValidationResult(data.data);
// Auto-expand the panel when errors are present
setValidationExpanded(data.data.summary.errors > 0);
}
} catch (err) {
// Backend unreachable â surface a neutral "unavailable" state rather than
// failing silently. DMN workflow remains fully functional; only the
// syntactic pre-check is skipped.
setValidationResult({
valid: false,
unavailable: true,
parseError:
'Syntax validation result not available â the validation backend ' +
`could not be reached (${err.message}). DMN deployment and testing ` +
'still work; only syntax pre-checks are skipped.',
layers: { base: { issues: [] }, business: { issues: [] }, execution: { issues: [] }, interaction: { issues: [] }, content: { issues: [] } },
summary: { errors: 0, warnings: 0, infos: 0 },
});
} finally {
setIsValidating(false);
}
};
The call is made:
- After
handleFileUploadâreader.onloadcompletes - After
loadExampleDMNâ on successful fetch - Cleared in
handleClearFileâsetValidationResult(null); setIsValidating(false);
Backend unreachability handling¶
A network failure or unavailable backend does not block the DMN workflow â deployment, single evaluate, intermediate tests, and the test-cases runner all continue to function. From v1.9.5 onward the validation panel renders a distinct amber "Syntax validation result not available" state to make the situation visible, kept visually separate from the red "Syntax issues found" state that indicates an actual DMN problem. The amber state is signalled by validationResult.unavailable === true; the per-layer issue arrays and the error/warning/info counts are empty in that case.
Response shape¶
interface ValidationResult {
valid: boolean;
unavailable?: boolean; // v1.9.5+ â true when the backend was unreachable; layers and summary are empty
parseError: string | null;
layers: {
base: { label: string; issues: Issue[] };
business: { label: string; issues: Issue[] };
execution: { label: string; issues: Issue[] };
interaction: { label: string; issues: Issue[] };
content: { label: string; issues: Issue[] };
};
summary: { errors: number; warnings: number; infos: number };
}
interface Issue {
severity: 'error' | 'warning' | 'info';
code: string; // e.g. 'EXEC-001', 'INT-005'
message: string;
location?: string; // element description e.g. '<decision id="d1">'
line?: number;
column?: number;
}
UI rendering¶
The validation panel is rendered inside the file card, between the file info row and the Deploy button. It is hidden when validationResult is null or isValidating is true (a spinner is shown instead).
The collapsible layer rows follow the same pattern as the Linked Data Explorer's DmnValidator component â each layer shows an icon and badge counts at a glance and expands on click to show individual issue rows.
Import preservation¶
Detection¶
vocabularies_config.js detects DMN entities before regular entities to avoid misclassification:
export const detectEntityType = (line) => {
// DMN detection FIRST
if (line.includes('a cprmv:DecisionModel')) return 'dmnModel';
if (line.includes('a cpsv:Input')) return 'dmnInput';
if (line.includes('a cprmv:DecisionRule')) return 'dmnRule';
// Regular entity detection below...
};
Capture¶
parseTTL.enhanced.js captures DMN lines verbatim when detected:
let inDmnSection = false;
let dmnLines = [];
if (['dmnModel', 'dmnInput', 'dmnRule'].includes(detectedType)) {
if (!inDmnSection) {
inDmnSection = true;
parsed.hasDmnData = true;
}
dmnLines.push(rawLine); // exact line, no transformation
continue;
}
if (parsed.hasDmnData && dmnLines.length > 0) {
parsed.importedDmnBlocks = dmnLines.join('\n');
}
Export¶
ttlGenerator.js appends preserved blocks at the end of the generated output â after all form-based sections. Before appending, generateDmnSection runs normalizeImportedDmnBlocks (v1.10.2): a CPSV-AP conformance pass over the preserved cpsv:Rule (Decision Rule) blocks that injects missing dct:title/dct:description and repoints a cpsv:implements that targets the cpsv:PublicService to the eli:LegalResource (or drops it when no legal resource exists). The edits are additive/repointing only â no preserved triples are removed â and idempotent, so re-importing an already-conformant export is a no-op.
Primary decision key extraction¶
The extractPrimaryDecisionKey() helper skips constant parameters automatically:
// Filters decisions with p_* prefix (constants)
// Prefers a *root* decision â one no other decision requires via
// informationRequirement/requiredDecision (v1.10.3)
// Document order breaks ties between multiple independent roots
// Falls back to the first decision if all are constants
As of v1.9.6 this helper lives in src/utils/dmnHelpers.js and is exported, so both
DMNTab.jsx and the DSO import hook share one implementation instead of duplicating the
parser.
Root-decision preference (v1.10.3). The helper previously returned the last
non-constant decision, which broke models whose intended output decision is authored
earlier or in a non-obvious position. It now prefers a decision that no other decision
requires (a DRD root). When several independent roots exist (e.g. a combined
Recht-Ên-Hoogte model) document order still decides, and a console warning points to the
Decision Key dropdown. That dropdown renders on the DMN File card whenever a file
has more than one testable decision, listing each as Name (id); selecting one updates
the decision key and the evaluation URL everywhere.
Console log on extraction:
Request body generation¶
When a DMN file is loaded, generateRequestBodyFromDMN walks the top-level <inputData> elements and builds a starter request body. For each input it picks a starter value using three sources, in priority order:
<inputValues>constraint (v1.9.5+). If the inputData name matches the<inputExpression>text of a decisionTable input column that carries an<inputValues>FEEL allowed-values list, the first allowed value is used. Quoted strings are unwrapped; booleans and numbers are coerced from their FEEL literal forms.typeReffrom the inputData<variable>child. When no<inputValues>constraint applies, the type drives a switch:booleanâfalse,integer/longâ0,number/double/decimalâ0,dateâ today's date inYYYY-MM-DD,stringâ empty.- Name-based heuristics.
string-typed inputs whose name containsdatum/date/dagdefault to today's date;geboortedefaults to a random adult birth date;aantal/bedrag/inkomendefault to0. Heuristics run only when steps 1 and 2 leave the value empty.
The resulting body is editable in the DMN tab's request-body panel before each evaluate call. Authors who want a richer starter body without writing custom code can simply add <inputValues> constraints to the relevant decisionTable input columns â this also documents intent in the DMN itself, which downstream tooling (validators, decision-table editors) can consume.
Test-case verification¶
evaluateTestCaseExpectation (v1.10.3) compares each uploaded test case's expected
outputs against the engine's actual outputs and returns a verdict, so a case only passes
when it is functionally correct â not merely because the HTTP call returned 200. It reads
the expectation from the readable key=value, reden="âĻ" string, a structured expected
object, or the special empty-result case, and yields one of:
| Verdict | Condition |
|---|---|
PASS |
expected outputs match actual |
FAIL |
mismatch â an Expected-vs-Actual table is rendered per output |
ERROR |
the evaluate call itself failed |
OK-unchecked |
no expectation could be parsed (amber; never a silent pass) |
Summary and header counts are verdict-based. Run All Test Cases routes each case to its
own decision â a case's optional decision field is the evaluation key, falling back to
the selected Decision Key (v1.10.4).
Empty-result expectations (v1.10.6). The empty-result branch originally matched only
the descriptive strings empty result / no matching rule, so a literal [] (or {})
expectation fell through to the key=value parser, found no pairs, and was judged
OK-unchecked even though an empty engine response was exactly correct. The check now
also matches a literal empty array or object, so [] is treated as expect an empty
result set (e.g. Thuisbatterij jaarGebondenBudget years 2025/2028 outside the modelled
range now pass). A non-empty response against an [] expectation still fails.
DSO â DMN deep-link import¶
The useDsoImport hook (src/hooks/useDsoImport.js, v1.9.6) lets the Linked Data Explorer
hand a toepasbare-regel DMN straight into the editor via a deep-link:
/?dsoImport=dmn&dmnId=<id>&env=<pre|prod>&activityName=<âĻ>
&authority=<âĻ>&activityUrn=<âĻ>&fsRef=<âĻ>
On mount (guarded by a consumedRef against StrictMode double-invoke) the hook:
- fetches the standalone DMN XML â
GET REACT_APP_BACKEND_URL/v1/dso/toepasbare-regels/{dmnId}/dmn(?env=prodonly whenenv=prod); - prefills
dmnDatawithfileName: decision-{dmnId}.dmn, the fetched XML, and the primary decision key â keeping the tab interactive (isImported: false, not the preserved mode); - prefills the Service tab (title/identifier/description from the DSO activity) and Organization tab (from the resolved authority);
- strips the import params via
history.replaceStateso a refresh can't re-import.
Deploy + test + publish then run through the existing DMNTab / PublishDialog flow.
DMNTab hydrates its internal uploaded-file/decision-key/test-body/validation state from
dmnData.content whenever content arrives from outside the tab and no local file was
uploaded â without this, deploy/test stayed gated on the internal uploadedFile state.
Operaton REST API¶
Deploy endpoint:
POST /engine-rest/deployment/create
Content-Type: multipart/form-data
Body: deployment-name={serviceId}, upload={dmnFile}
Evaluate endpoint:
POST /engine-rest/decision-definition/key/{decisionKey}/evaluate
Content-Type: application/json
Body: { "variables": { ... } }
URI generation¶
DMN URIs are derived from the service identifier. Given service.identifier = "aow-leeftijd":
<https://regels.overheid.nl/services/aow-leeftijd/dmn>
a cprmv:DecisionModel .
<https://regels.overheid.nl/services/aow-leeftijd/dmn/input/1>
a cpsv:Input .
<https://regels.overheid.nl/services/aow-leeftijd/rules/DecisionRule_2020>
a cpsv:Rule, cprmv:DecisionRule .
Spaces in the service identifier are replaced with hyphens. Full URIs in the organisation field are used as-is; short IDs are expanded.
Shell script parity¶
The intermediate tests and test cases features in the UI mirror the shell scripts in examples/organizations/*/:
| Shell script | UI equivalent |
|---|---|
test-dmn-zorgtoeslag.sh |
DMN Tab â Intermediate Decision Tests |
test-cases-zorgtoeslag.sh |
DMN Tab â Test Cases |
The shell scripts can still be used for CI/CD automation independently of the UI.