@dataverse-kit/runtime
v1.0.0
Published
Framework-agnostic runtime helpers for Dynamics 365 / Dataverse: FetchXML parser+builder+OData converter, ConditionEvaluator (business-rule AND/OR groups with 18 operators), and DirtyTracker (ProForma-style change tracking).
Maintainers
Readme
@dataverse-kit/runtime
Framework-agnostic runtime helpers for Dynamics 365 / Dataverse. Zero React deps.
| Surface | Purpose |
|---|---|
| FetchXML | Parser, fluent builder, OData converter, LayoutXML utilities |
| ConditionEvaluator | RuleConditionGroup evaluation with 18 FetchXML-style operators (nested AND/OR groups, field-to-field comparisons, dirty tracking) |
| DirtyTracker | ProForma-style baseline change tracking with Dynamics-flavored coercion (null ↔ "", "5" ↔ 5, "true" ↔ true) |
Install
npm install --save @dataverse-kit/runtimeDOMParser requirement
The FetchXML parser/builder/updaters call the browser-native DOMParser. That's available in:
- Any modern browser (custom pages, web resources, PCF, dev servers)
- Node 21+
- Test environments configured with
happy-domorjsdom
On Node 18-20 you need a polyfill. The simplest pattern, used in this package's own test setup, is to register happy-dom as the global:
// vitest.config.ts (consumer)
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: { environment: 'happy-dom' },
});The ConditionEvaluator and DirtyTracker surfaces are pure JS — they have no DOM dependency and run in any Node version.
FetchXML
import {
parseFetchXml,
FetchXmlBuilder,
convertFilterTreeToOData,
flattenFilterTree,
buildLayoutXml,
updateFetchXmlFilterTree,
type FilterGroup,
} from '@dataverse-kit/runtime';
// Parse an existing query.
const parsed = parseFetchXml(view.fetchxml);
// { entityName: 'account', attributes: ['name', 'revenue'], filterTree: { type: 'and', conditions: [...], groups: [...] }, orders: [...], top: 50, ... }
// Build a new query.
const xml = new FetchXmlBuilder('account')
.select('name', 'revenue')
.where('statecode', 'eq', '0')
.andWhere('revenue', 'gt', '100000')
.orderBy('revenue', true)
.top(50)
.build();
// Convert FetchXML filters to OData $filter — preserves AND/OR grouping.
const odataFilter = parsed.filterTree && convertFilterTreeToOData(parsed.filterTree);
// "statecode eq 0 and (revenue gt 100000)" — paren-wrap when grouped
// Need the flat condition list (e.g. for a flat-only UI)?
const flat = parsed.filterTree ? flattenFilterTree(parsed.filterTree) : [];
// Update an existing fetchxml string in place with a tree.
const tree: FilterGroup = {
type: 'and',
conditions: [{ attribute: 'statecode', operator: 'eq', value: '0' }],
groups: [],
};
const withFilters = updateFetchXmlFilterTree(view.fetchxml, tree);Operators supported by parseFetchXml / FetchXmlBuilder: eq, ne, gt/ge/lt/le, like, not-like, begins-with, ends-with, null, not-null, in, not-in, between, not-between, date keywords (today, yesterday, last-x-days, this-month, …), user/team keywords (eq-userid, eq-userteams), hierarchy (above, under, not-under).
OData conversion rules:
- Polymorphic lookups (
regardingobjectid,ownerid,customerid) are excluded (unsupported in OData) - Boolean fields (prefixed
is/has/can/do/allow/enable/disable) convert0/1totrue/false likemaps tocontains()/startswith()/endswith()based on%positioninexpands to an OR chain ofeqcomparisons
ConditionEvaluator
Evaluates business-rule condition trees against record data. The operator set is intentionally the same shape as FetchXML so consultants can carry one mental model across the platform.
import { evaluateConditionGroup, type RuleConditionGroup } from '@dataverse-kit/runtime';
const rule: RuleConditionGroup = {
id: 'g1',
logic: 'and',
conditions: [
{ id: 'c1', source: 'field', fieldName: 'status', operator: 'eq', value: 'active' },
{
id: 'g2',
logic: 'or',
conditions: [
{ id: 'c2', source: 'field', fieldName: 'tier', operator: 'in', value: ['gold', 'platinum'] },
{ id: 'c3', source: 'field', fieldName: 'revenue', operator: 'gt', value: 1_000_000 },
],
},
],
};
const shouldFire = evaluateConditionGroup(rule, {
data: { status: 'active', tier: 'silver', revenue: 2_000_000 },
});
// true (status matches AND revenue gt 1M)Supports: nested AND/OR groups, 18 operators (eq, ne, gt/ge/lt/le, like, not-like, begins-with, ends-with, null, not-null, between, in, not-in, contain-values, not-contain-values, changed, not-changed), four condition sources (field, data-source, form-state, calculated — the last is reserved), and field-to-field comparisons via compareToField.
For trace output (useful for debugging):
import { evaluateConditionGroupWithDetails } from '@dataverse-kit/runtime';
const trace = evaluateConditionGroupWithDetails(rule, context);
// { result: true, details: [{ conditionId, leftValue, operator, rightValue, result }, …] }changed / not-changed operators read from context.originalData — pair them with DirtyTracker below.
DirtyTracker
import { DirtyTracker } from '@dataverse-kit/runtime';
const tracker = new DirtyTracker({ name: 'Contoso', revenue: 100_000 });
tracker.trackChange('name', 'Contoso Ltd');
tracker.trackChange('revenue', 100_000); // back to baseline → not dirty
tracker.isFieldDirty('name'); // true
tracker.isFieldDirty('revenue'); // false
tracker.getDirtyFields(); // Set { 'name' }
tracker.getDirtyCount(); // 1
// After a save, re-anchor:
tracker.setBaseline(updatedRecord);
tracker.getDirtyCount(); // 0Constructor accepts either a baseline record directly (the common case) or an options object for advanced cases:
// Restoring persisted state:
new DirtyTracker({ baseline: { name: 'X' }, initialDirtyFields: ['name'] });Coercion semantics (matches the original ProForma pattern):
null/undefined/""are all equivalent- Number ↔ string:
"5"equals5 - Boolean ↔ string:
"true"equalstrue
The same coercion is exposed as a standalone helper:
import { valuesEqual } from '@dataverse-kit/runtime';
valuesEqual(null, undefined); // true
valuesEqual(5, '5'); // true
valuesEqual(true, 'false'); // falseOrigin / sync notes
This package consolidates code that was previously duplicated:
- FetchXML: originally vendored from
msft/04-dynamics/tools/fetchxml/; that upstream was deleted once@dataverse-kit/odataand@dataverse-kit/viewswere re-pointed at this package, andruntimeis now the canonical source. - ConditionEvaluator: ported from
msft/03-frontend/dynamics-ui-kit/packages/dynamics-bpf-core/src/services/ConditionEvaluator.ts. - DirtyTracker: ported from
msft/03-frontend/dynamics-ui-kit/apps/form-builder/src/engine/DirtyTracker.ts, generalized away from form-builder'sRuleExecutionContext. 47 tests ported.
Downstream migration of the original copies is tracked in IMPLEMENTATION_STATUS.md.
Testing
npm test # vitest (uses happy-dom for DOMParser)
npm run typecheck