Skip to content

76 Event Handler Guide

Преподобный Ален edited this page Feb 18, 2026 · 1 revision

Event Handler Guide

Events are the mechanism by which the Platform executes custom business logic when workflow actions occur. Every time a method is executed on an object, the engine finds all events registered for that action on the object's class (and parent classes) and executes them in sequence.

Event Architecture

How Events Fire

When ExecuteMethod(pObject, pMethod) is called:

  1. The engine looks up the action for the method
  2. It finds all events registered for the object's class + that action
  3. Events are executed in sequence order
  4. parent event types cause the engine to look up events on parent classes
  5. event event types execute a PL/pgSQL function call

Event Types

Code Purpose
parent Inherit and execute events from the parent class
event Execute a PL/pgSQL function
plpgsql Execute inline PL/pgSQL code (rarely used)

Event Cascade

Events cascade from child to parent (or parent to child, depending on registration order). The order you register events in AddEvent determines execution order:

For most actions (create, open, edit, save, enable, disable, restore):

PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');     -- parent events FIRST
PERFORM AddEvent(pClass, uEvent, r.id, 'Entity created', 'EventSensorCreate();');  -- entity event SECOND

Parent class events fire first, then the entity's own event. This means the parent (e.g., Document) initializes its data before the child (e.g., Sensor) does its work.

For destructive actions (delete, drop):

PERFORM AddEvent(pClass, uEvent, r.id, 'Entity will be deleted', 'EventSensorDelete();');  -- entity event FIRST
PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');     -- parent events SECOND

The entity's own event fires first (to clean up specialized data), then the parent events fire (to clean up parent data). This prevents FK violations.

Function Naming Convention

Event handler functions follow the naming pattern:

Event{EntityName}{ActionName}

Examples:

  • EventClientCreate -- fires when a Client is created
  • EventStationEnable -- fires when a Station is enabled
  • EventRegionDrop -- fires when a Region is permanently destroyed

Function Signature

Every event handler has the same basic signature:

CREATE OR REPLACE FUNCTION EventSensorCreate (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  -- handler logic here
END;
$$ LANGUAGE plpgsql;

Key points:

  • pObject defaults to context_object() -- the object the method is being executed on
  • Returns void -- events are side-effects
  • No SECURITY DEFINER or SET search_path (executes in the caller's context)

For events that need the method parameters (e.g., old/new values on edit):

CREATE OR REPLACE FUNCTION EventClientEdit (
  pObject       uuid default context_object(),
  pParams       jsonb default context_params()
) RETURNS       void
AS $$
DECLARE
  old_email     text;
  new_email     text;
BEGIN
  old_email = pParams#>'{old, email}';
  new_email = pParams#>'{new, email}';

  IF old_email <> new_email THEN
    PERFORM EventMessageConfirmEmail(pObject);
  END IF;

  PERFORM WriteToEventLog('M', 1000, 'edit', 'Client updated.', pObject);
END;
$$ LANGUAGE plpgsql;

The pParams parameter receives the JSON passed to ExecuteMethod. For edit actions, this typically contains {old: {...}, new: {...}} with the before/after state.

The 9 Standard Events

Every entity should implement these 9 event handlers:

1. EventCreate -- Object created

Fires after the object is inserted into the database. Use for:

  • Logging
  • Sending notifications
  • Creating dependent objects
CREATE OR REPLACE FUNCTION EventSensorCreate (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'create', 'Sensor created.', pObject);
END;
$$ LANGUAGE plpgsql;

2. EventOpen -- Object opened for viewing

Fires when the object is accessed/viewed.

CREATE OR REPLACE FUNCTION EventSensorOpen (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'open', 'Sensor opened.', pObject);
END;
$$ LANGUAGE plpgsql;

3. EventEdit -- Object modified

Fires after the object is updated. Can receive old/new values via pParams.

CREATE OR REPLACE FUNCTION EventSensorEdit (
  pObject   uuid default context_object(),
  pParams   jsonb default context_params()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'edit', 'Sensor updated.', pObject);
END;
$$ LANGUAGE plpgsql;

4. EventSave -- Object saved

Fires when the object is explicitly saved (distinct from edit in some workflows).

CREATE OR REPLACE FUNCTION EventSensorSave (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'save', 'Sensor saved.', pObject);
END;
$$ LANGUAGE plpgsql;

5. EventEnable -- Object activated

Fires when the object transitions to an enabled state. Common uses:

  • Unlocking user accounts
  • Activating dependent resources
  • Sending welcome notifications
CREATE OR REPLACE FUNCTION EventSensorEnable (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'enable', 'Sensor enabled.', pObject);
END;
$$ LANGUAGE plpgsql;

Real-world example from Client:

CREATE OR REPLACE FUNCTION EventClientEnable (
  pObject       uuid default context_object()
) RETURNS       void
AS $$
DECLARE
  uUserId       uuid;
  uArea         uuid;
  uInterface    uuid;
BEGIN
  SELECT userid INTO uUserId FROM db.client WHERE id = pObject;

  IF uUserId IS NOT NULL THEN
    PERFORM UserUnLock(uUserId);
    PERFORM DeleteGroupForMember(uUserId, GetGroup('guest'));
    PERFORM AddMemberToGroup(uUserId, GetGroup('user'));

    SELECT area INTO uArea FROM db.document WHERE id = pObject;
    PERFORM AddMemberToArea(uUserId, uArea);
    PERFORM SetDefaultArea(uArea, uUserId);

    uInterface := GetInterface('user');
    PERFORM AddMemberToInterface(uUserId, uInterface);
    PERFORM SetDefaultInterface(uInterface, uUserId);
  END IF;

  PERFORM WriteToEventLog('M', 1000, 'enable', 'Client enabled.', pObject);
END;
$$ LANGUAGE plpgsql;

6. EventDisable -- Object deactivated

Fires when the object transitions to a disabled state. Common uses:

  • Locking user accounts
  • Suspending dependent resources
CREATE OR REPLACE FUNCTION EventSensorDisable (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'disable', 'Sensor disabled.', pObject);
END;
$$ LANGUAGE plpgsql;

7. EventDelete -- Object soft-deleted

Fires when the object is soft-deleted. The object still exists but is marked as deleted. Use for:

  • Cleaning up active resources
  • Anonymizing personal data
  • Cascading soft-delete to children
CREATE OR REPLACE FUNCTION EventSensorDelete (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'delete', 'Sensor deleted.', pObject);
END;
$$ LANGUAGE plpgsql;

8. EventRestore -- Object restored from deleted state

Fires when a deleted object is restored back to the created state.

CREATE OR REPLACE FUNCTION EventSensorRestore (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'restore', 'Sensor restored.', pObject);
END;
$$ LANGUAGE plpgsql;

9. EventDrop -- Object permanently destroyed

Fires immediately before the object is permanently removed from the database. This is the most critical event handler because it must:

  1. Delete all child/dependent objects first (to avoid FK violations)
  2. Delete the specialized row from the entity table
  3. Log the destruction
CREATE OR REPLACE FUNCTION EventSensorDrop (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
DECLARE
  r         record;
BEGIN
  -- 1. Get the label for logging before deletion
  SELECT label INTO r FROM db.object_text WHERE object = pObject AND locale = current_locale();

  -- 2. Delete the specialized row
  DELETE FROM db.sensor WHERE id = pObject;

  -- 3. Log the destruction (note: higher severity 2000 and 'W' category)
  PERFORM WriteToEventLog('W', 2000, 'drop', '[' || pObject || '] [' || coalesce(r.label, '') || '] Sensor destroyed.');
END;
$$ LANGUAGE plpgsql;

For entities with child objects, cascade the drop:

CREATE OR REPLACE FUNCTION EventClientDrop (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
DECLARE
  r         record;
BEGIN
  -- Drop child accounts
  FOR r IN SELECT id FROM db.account WHERE client = pObject
  LOOP
    IF IsActive(r.id) THEN
      PERFORM DoDisable(r.id);
    END IF;
    IF IsDisabled(r.id) THEN
      PERFORM DoDelete(r.id);
    END IF;
    PERFORM DoDrop(r.id);
  END LOOP;

  -- Drop child identities
  FOR r IN SELECT id FROM db.identity WHERE client = pObject
  LOOP
    IF IsActive(r.id) THEN
      PERFORM DoDisable(r.id);
    END IF;
    IF IsDisabled(r.id) THEN
      PERFORM DoDelete(r.id);
    END IF;
    PERFORM DoDrop(r.id);
  END LOOP;

  -- Clean up links and files
  DELETE FROM db.object_link WHERE linked = pObject;
  DELETE FROM db.object_file WHERE object = pObject;

  -- Delete specialized row
  DELETE FROM db.client_name WHERE client = pObject;
  DELETE FROM db.client WHERE id = pObject;

  SELECT label INTO r FROM db.object_text WHERE object = pObject AND locale = current_locale();
  PERFORM WriteToEventLog('W', 1000, 'drop', '[' || pObject || '] [' || coalesce(r.label, '') || '] Client destroyed.');
END;
$$ LANGUAGE plpgsql;

Key pattern: to properly drop a child object, you must transition it through its lifecycle states first (DoDisable -> DoDelete -> DoDrop), because each state transition may have its own cleanup logic.

Custom Events

For entities with custom workflow actions, add corresponding event handlers:

-- Custom: Station heartbeat
CREATE OR REPLACE FUNCTION EventStationHeartbeat (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'heartbeat', 'Station heartbeat.', pObject);
END;
$$ LANGUAGE plpgsql;

-- Custom: Station becomes available
CREATE OR REPLACE FUNCTION EventStationAvailable (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'available', 'Station is available.', pObject);
END;
$$ LANGUAGE plpgsql;

-- Custom: Station becomes unavailable (with side-effects)
CREATE OR REPLACE FUNCTION EventStationUnavailable (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'unavailable', 'Station is unavailable.', pObject);
  PERFORM StopStationTransactions(pObject);  -- stop active data transactions
END;
$$ LANGUAGE plpgsql;

Registering Events in init.sql

Events are registered in the Add<Entity>Events function. The complete pattern:

CREATE OR REPLACE FUNCTION AddSensorEvents (
  pClass        uuid
)
RETURNS         void
AS $$
DECLARE
  r             record;

  uParent       uuid;
  uEvent        uuid;
BEGIN
  uParent := GetEventType('parent');
  uEvent := GetEventType('event');

  FOR r IN SELECT * FROM Action
  LOOP

    IF r.code = 'create' THEN
      PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');
      PERFORM AddEvent(pClass, uEvent, r.id, 'Sensor created', 'EventSensorCreate();');
    END IF;

    IF r.code = 'open' THEN
      PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');
      PERFORM AddEvent(pClass, uEvent, r.id, 'Sensor opened', 'EventSensorOpen();');
    END IF;

    IF r.code = 'edit' THEN
      PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');
      PERFORM AddEvent(pClass, uEvent, r.id, 'Sensor updated', 'EventSensorEdit();');
    END IF;

    IF r.code = 'save' THEN
      PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');
      PERFORM AddEvent(pClass, uEvent, r.id, 'Sensor saved', 'EventSensorSave();');
    END IF;

    IF r.code = 'enable' THEN
      PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');
      PERFORM AddEvent(pClass, uEvent, r.id, 'Sensor enabled', 'EventSensorEnable();');
    END IF;

    IF r.code = 'disable' THEN
      PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');
      PERFORM AddEvent(pClass, uEvent, r.id, 'Sensor disabled', 'EventSensorDisable();');
    END IF;

    IF r.code = 'delete' THEN
      PERFORM AddEvent(pClass, uEvent, r.id, 'Sensor will be deleted', 'EventSensorDelete();');
      PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');
    END IF;

    IF r.code = 'restore' THEN
      PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');
      PERFORM AddEvent(pClass, uEvent, r.id, 'Sensor restored', 'EventSensorRestore();');
    END IF;

    IF r.code = 'drop' THEN
      PERFORM AddEvent(pClass, uEvent, r.id, 'Sensor will be destroyed', 'EventSensorDrop();');
      PERFORM AddEvent(pClass, uParent, r.id, 'Parent class events');
    END IF;

  END LOOP;
END
$$ LANGUAGE plpgsql
   SECURITY DEFINER
   SET search_path = kernel, pg_temp;

WriteToEventLog Parameters

PERFORM WriteToEventLog(pCategory, pPriority, pAction, pMessage, pObject);
Parameter Type Description
pCategory char 'M' = message, 'W' = warning, 'E' = error
pPriority integer Severity: 1000 = normal, 2000 = elevated
pAction text Action code (matches the action that triggered the event)
pMessage text Human-readable log message
pObject uuid The object UUID (optional, defaults to NULL)

Sending Notifications from Events

Events can trigger notifications (push, email, message):

-- Send push notification
PERFORM SendPush(pObject, 'Title', 'Body', uUserId);

-- Send email confirmation
PERFORM EventMessageConfirmEmail(pObject);

Summary of Event Order Rules

Action Entity Event Parent Event Reason
create second first Parent initializes base data first
open second first Parent logging first
edit second first Parent updates first
save second first Parent saves first
enable second first Parent activates first
disable second first Parent deactivates first
restore second first Parent restores first
delete first second Entity cleans up before parent
drop first second Entity deletes specialized row before parent cascade

Clone this wiki locally