Events
Three different "events"
Distinguish them before reading further:
| Kind | Where |
|---|---|
Domain events |
Business facts emitted by backend services and stored in the event logger. Queryable,
visible in |
Event rules |
Stored rules that match a domain event, filter it, and run actions. Two generations. This page. |
In-app events |
|
The domain event stream
Events live in the event logger, of which two generations are configured:
| Base URL | Role |
|---|---|
|
v2. |
|
v1. |
Event taxonomy
manage/event-rules/event-rule.models.ts declares the vocabulary a rule can match:
export const eventTypes = [
'analitics', // sic — the analytics stream MonitorService writes to
'api-event',
'cli-event',
'event',
'messaging-log',
'messaging-result',
'task-change-event',
'underwriting-event',
];
export const eventTargets = ['compliance', 'crm-task', 'email', 'marketplace', 'supplier'];
An event therefore has a type (which subsystem produced it) and a target type (what
kind of thing it is about), plus a targetType-scoped identifier — for a supplier, the
supplier key. That identifier is carried on requests as
X-EventLogger-TargetUUID (or X-Actions-TargetUUID for CRM). See
CRM.
|
|
Browser-side Elasticsearch
EventsService instantiates the elasticsearch npm client in the browser:
this.es = new Client({ host: environment.eventRulesOldUrl + 'es', log: 'warning' });
private esRequest(params) {
return this.store.select('auth').pipe(take(1), switchMap((authState) =>
observableFrom(this.es.search(Object.assign({
index: 'em_events', type: 'em_events',
headers: { 'X-Session-Token': authState.user.jwtToken },
}, params)))));
}
query(query = { match_all: {} }, limit = 30, page = 1, sort = { created_at: 'desc' })
This is the only direct Elasticsearch client in the application, and the reason
elasticsearch appears in allowedCommonJsDependencies.
|
Elasticsearch querying is currently disabled in the rule editor.
So the "example matching events" preview in the rule editor always shows zero. The
|
/manage/events (manage/events/ with event-badge/, event-filters/,
event-preview/) is the raw stream browser. The CRM timeline’s events stream comes from
eventLoggerUrl es-changelog instead — and requests limit=2000000, because there is no
paging on that endpoint. See CRM.
Event rules
A rule is: when an event of this type about this kind of target matches these filters, do these actions.
export interface EventRule extends Entity { // Entity = { uuid, created_at }
eventType: string; // one of eventTypes
eventTypeCode: string;
targetType: string; // one of eventTargets
message: string;
user: string;
filters: Array<Filter>;
actions: Array<Action>;
}
Filters
Filter is the one piece of real parsing logic in the front end. Rules are authored as
expressions and stored as structured objects.
export class Filter {
constructor(public name: string, public op: string, public val: string | number) {}
static regex = /([a-zA-Z.]+ ) *(!=|=|<|>|>=|<=|=<|=>) *([^ ]+)/;
static fromExpression(text) { … } // "amount >= 100" → Filter('amount', 'gte', '100')
static fromJson({ name, op, val }) { … }
toExpression() { … } // Filter('amount','gte','100') → "amount >= 100"
}
Operator mapping:
| Expression | Stored op |
Rendered back as |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The round trip is not lossless. The regex also requires a trailing space in the field-name group
( Field names may contain dots ( |
Actions
export class Action {
static CREATE_TASK = 'create-task';
static SEND_EMAIL = 'send-email';
static SLACK_NOTIF = 'notify-on-slack'; // ← not in actionTypes
static EDIT_SUPPLIER = 'edit-supplier';
static SEND_TO_EVENT_BRIDGE = 'push-to-event-bridge';
type: string;
template: string;
to?: string;
contextMapping: object;
dataMapping: object;
data: object;
delay: number;
delayTime: number;
delayPeriod: number;
delayEnabled: boolean;
}
export const actionTypes = [
Action.CREATE_TASK,
Action.SEND_EMAIL,
// Action.SLACK_NOTIF, ← commented out
Action.EDIT_SUPPLIER,
Action.SEND_TO_EVENT_BRIDGE,
];
Four active action types; Slack notification is defined but commented out of the selectable list. The constant remains, so existing rules of that type would still load.
| Action | Editor | Behaviour |
|---|---|---|
|
|
Creates a CRM task. See CRM. |
|
|
Sends a templated email — the origin of most |
|
|
Mutates supplier fields. |
|
|
Publishes to AWS EventBridge, extending automation beyond this system. |
Mappings
Three fields shape an action’s payload:
| Field | Meaning |
|---|---|
|
|
|
|
|
Static data attached to the action. |
RulesUtilsService builds these as form groups with an all-or-nothing validator: within
a mapping pair, if any field is filled, all must be:
static addAllOrNothingValidator(group: FormGroup) {
// for each control: if any sibling is non-empty and this one is empty → { required: true }
// and when this one becomes non-empty, revalidate the empty siblings
}
tryAddNext appends a fresh blank row when the last row is filled — the "grow as you
type" list pattern used for filters, mappings and recipients. Email recipients use
emailOrEmpty: validate as an email unless blank.
Delays
An action can be delayed. The wire format is a single delay in seconds; the editor
splits it into a magnitude and a unit:
export const MINUTES = 60;
export const DAYS = 24 * 60 * 60;
export const WEEKS = 7 * 24 * 60 * 60;
On load (RulesAddEditComponent), the seconds value is decomposed largest-unit-first:
delay > 0 → delayEnabled = true
delay % WEEKS === 0 → delayPeriod = WEEKS, delayTime = delay / WEEKS
else delay % DAYS === 0 → delayPeriod = DAYS, delayTime = delay / DAYS
else → delayPeriod = MINUTES, delayTime = delay / MINUTES
|
|
EventBridge action naming
export const PREFIX_EVNET_RULE = 'em_event_rule_'; // sic
export function eventBridgeActionNameValidator(control: AbstractControl) {
return control.value.substring(0, PREFIX_EVNET_RULE.length) !== PREFIX_EVNET_RULE
? { eventRuleNameError: true } : null;
}
EventBridge action names must begin with em_event_rule_. The constant name is
misspelled (EVNET); the value is correct.
The rule editor
/manage/event-rules with:
| Route | Component |
|---|---|
|
|
|
|
|
|
RulesAddEditComponent composes two child forms:
selectorForm = new FormGroup({}) ← SelectorFormComponent registers itself into it
actionsForm = new FormArray([]) ← ActionsFormComponent contributes actions
ngAfterViewInit:
combineLatest(selector.selectorValue$, actions.value$, sendButton$)
→ { ...selector, actions }
→ isEditing ? eventsService.save(uuid, rule) : eventsService.create(rule)
→ navigate to manage/event-rules
|
The save is wired as a
|
SelectorFormComponent uses ViewEncapsulation.None, so its styles are global.
Loading a rule for edit converts stored filters back to expressions
(Filter.fromJson(f).toExpression()) and decomposes action delays, as above.
Rule history
EventsService.historyList(uuid, limit = 10, last?):
GET <eventRulesOldUrl>event_rules/history/
?limit=10
&order_by={"field":"created_at","direction":"desc"}
&q={"filters":[{"name":"event_rule_uuid","op":"eq","val":"<uuid>"}]}
[&last=…]
Note the query language is the same { name, op, val } filter shape as rule filters —
so Filter describes both what a rule matches and how the API is queried.
Rule CRUD
All on eventRulesOldUrl under event_rules/, each with an X-InstanceId: 8001 header:
| Method | Endpoint |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Responses are enveloped as Response<T> / ListResponse<T>
({ data, meta }). The X-InstanceId value is hardcoded '8001' in this service rather
than read from env.instanceId — a divergence from CrmService and
GlobalSettingsService, which use the config value (also 8001).
Event rules v2
/manage/event-rules-2 (EventRules2Component) is the beta rewrite. The component in
this repository does almost nothing:
this.userService.checkPermission(USER_PERMISSION.CRM_ADMIN)
.subscribe((permitted) => { this.hasCRMPermission = permitted; this.cd.detectChanges(); });
It resolves CRM_ADMIN and renders accordingly — the editor itself is elsewhere. The
sidebar labels it "Event Rules v2.0 (beta)".
Both generations are live and reachable. Determine which one a change belongs to before starting.
Permissions
| Permission | Governs |
|---|---|
|
Editing event rules. |
|
The v2 editor, and CRM administration generally. |
/manage denies REST_UW_ACCESS and ADVANCE_AUDITORS at the parent route.
Gotchas summary
| Issue | Detail |
|---|---|
Elasticsearch preview is disabled |
Always zero example events, with an info toast. |
|
On the wire too. |
Filter round-trip is lossy |
|
Filter regex needs a trailing space |
|
Slack action is defined but unselectable |
Commented out of |
Delay units are minutes/days/weeks only |
No hours; fractional minutes are possible. |
|
The constant, not the value. |
Two rule generations |
v1 in this repo, v2 largely elsewhere. |
|
Can re-fire on later form changes. |
Hardcoded instance id |
|