handlers.relation_sync_handler

class RelationSyncHandler(handlers.eventhandler.EventHandler):

Synchronizes relations from one fact sheet type to another fact sheet type.

Relation triangle:

  • There are 3 fact sheet types: 0, 1 and 2.
  • rel_0_to_1 and rel_1_to_2 are leading.
  • rel_0_to_2 is derived from the two leading relations.

Example: A Server with the ITComponents "Windows Server 2019" and "IBM DB2" is linked to an Application. Then this automation links the ITComponents "Windows Server 2019" and "IBM DB2" to the Application as well. If the Server is removed from the Application or upgraded to another operating system, the ITComponents of the Application are kept consistent automatically (outdated ones removed, new ones added).

You can set the config value "delete_outdated" to False (default: True) if you don't want to automatically clean up outdated relations. Works if you touch rel_0_to_1 or rel_1_to_2.
WARNING: If this is set to True, it really removes all relations between fs0 and fs2 that don't have a linking fs1 between them! In the "Server" example, it really removes all ITComponents from an Application that are not linked to any of the Application's Servers, also the manually-linked ones. If you don't want that, set keep_manual_relations to True.

Managed relations: every rel_0_to_2 relation that has a linking fs1 gets a generated description, ending with description_suffix in square brackets (default: "ARONIS_AUTOLINK" -> "[ARONIS_AUTOLINK]"). With trace_relations (default: False), the description lists the linking fs1 fact sheets as Markdown links; without, it only names the fs1 fact sheet type. description_prefix replaces that leading English sentence with your own text (e.g. in your workspace's language). The description is rewritten whenever it is outdated (a manual relation becomes managed, or - with trace_relations - its fs1 links change). With keep_manual_relations, only relations carrying the suffix (or provided by the fs1 of a just-removed relation) are deleted - this also works when the fs1 was archived, whose relations LeanIX no longer exposes. Run an initial sync after activating this, so all existing relations get their description. Restoring an archived fs1 recovers its relations (RelationRecoveredEvent, handled like RelationCreatedEvent), so the rel_0_to_2 relations its archiving removed come back. Requires RELATION_RECOVERED in the webhook's tag sets (default for new webhooks; existing ones: add it in the Webhooks manager). An initial sync also applies delete_outdated: without keep_manual_relations, it deletes every rel_0_to_2 relation without a linking fs1; with it, only managed ones (none right after migrating unmarked relations).

You can set the config value "watch_auto_relations" to True (default: False) if you want to monitor freshly created/modified/deleted rel_0_to_2 relations and see if the edit was ok, or if it needs to be reverted.

A deleted rel_0_to_2 relation that still has a linking fs1 is restored (as a managed relation). Without keep_manual_relations, a manual rel_0_to_2 relation created / updated without a linking fs1 is deleted as well. Managed relations (description suffix) are never deleted this way: LeanIX shows freshly written relations with a delay, so one the sync just created can look unjustified for a few seconds - if it really is outdated, the regular sync deletes it. The config syncs as usual, too. Note: ANY deleted relation that still has a linking fs1 is restored, also one a user deleted on purpose - to get rid of it, remove the rel_0_to_1 or rel_1_to_2 relation.

The two configs for permanent use (delete_outdated / keep_manual_relations / watch_auto_relations):

  • true / true / true: outdated relations are deleted, wrongly deleted ones restored, manual ones are kept.
  • true / false / true: same, but manual relations are not allowed (deleted right away / on the next sync). Other combinations are meant for rollout only (e.g. delete_outdated false for a careful first activation, watch_auto_relations false during the initial sync).

Whenever you set up a new relation_sync automation, make sure to follow this procedure:

  1. Deactivate the Webhook
  2. Create the feature config using watch_auto_relations=false
  3. Trigger the initial full sync in the web UI
  4. Update the feature config: watch_auto_relations=true
  5. Re-activate the webhook.
name = 'Relation Synchronization Handler'
description = 'Synchronizes relations from one fact sheet type to another fact sheet type.'
conditions = {'event_types': ['RelationCreatedEvent', 'RelationUpdatedEvent', 'RelationSwitchEvent', 'RelationArchivedEvent', 'RelationDeletedEvent', 'RelationRecoveredEvent']}
supports_initial_sync = True
config_template = {'delete_outdated': True, 'keep_manual_relations': True, 'description_suffix': 'ARONIS_AUTOLINK', 'trace_relations': False, 'description_prefix': None, 'watch_auto_relations': False, 'skip_fact_sheet_preload': False, 'fs_types': ['Application', 'Server', 'ITComponent'], 'leading_relations': ['relApplicationToServer', 'relServerToITComponent'], 'following_relation': 'relApplicationToITComponent', 'rel_fields': {'field1': 'value1', 'field2': 'value2'}}
def get_event_types(self):
def data_requirements(self):

Declare the exact fact-sheet data this handler needs, so the webhook event loop (process_event in workspace/views_module.py) can load a single lean FastFactSheet per event instead of the full fact sheet.

Return a dict: { "fields": [], "relations": [], "subscriptions": bool, # True if handle() reads fs["subscriptions"] "tags": bool, # True if handle() reads tags / has_tag() "documents": bool, "acls": bool, "completion": bool, # True if handle() reads fs["completion"] "base_fields": [], # optional; omit/None = full # default base field set. A list restricts the # base scalars to just those (id and type are # always loaded). Advanced opt-in for max speed. } Fields referenced by this handler's trigger are folded in automatically by the event loop (into "fields"), so only declare what handle() itself needs on top of them.

Return None (the default) to signal "requirements cannot be declared statically" - e.g. handlers that exec/eval config-supplied code. The event loop then loads the full fact sheet for that event, so correctness is always preserved. This is a standalone hook (deliberately not tied to the initial_sync_* hooks, which are slated to migrate onto it later).

def handle(self, event, fs, **kwargs):

Called automatically whenever a FactSheetUpdated event comes in via webhook.

def triangle_corners(self, rel_type, from_fs, to_fs):

For a leading relation (rel_0_to_1 / rel_1_to_2, either direction): the fs0 / fs2 end(s) to re-sync, as (corner, fs1, mode) tuples - mode None for an fs0 corner, "reverse" for an fs2 corner. Decided by the relation type, not the fact sheet type, so that configs with identical fact sheet types work, e.g. (Organization, Organization, Application) with relToParent: there, both ends of a relToParent relation are Organizations, but only the "from" end of relToParent (= the "to" end of relToChild) is the fs0.

def load_delete_candidates(self, class1, fs1_id, relevant_rel):

Legacy detection of auto-created relations (before descriptions were managed): the fact sheets the fs1 of a just-removed relation is linked to. Empty if the fs1 is archived (LeanIX hides its relations) or gone - the managed description (see is_managed) covers those cases.

def description_suffix(self):
def is_managed(self, description):

True if a rel_0_to_2 relation was created / taken over by this handler (its description carries the suffix).

def managed_description(self, fs1_list):

Description of a managed rel_0_to_2 relation. With trace_relations: one Markdown link per linking fs1, sorted, so both triangle corners (normal and reverse mode) generate exactly the same text and never flip-flop. Without: a fixed sentence naming the fs1 fact sheet type. description_prefix (if set) replaces that leading sentence verbatim - e.g. in the workspace's language.

def unjustified_manual_relation(self, from_id, from_type, to_id, to_type, rel_type):

The id of the rel_0_to_2 relation from_id -> to_id if it must be deleted: NO fs1 links the two fact sheets AND it is a manual relation (no managed description). None otherwise, or if a request failed.

Relations this handler creates (sync / restore) always carry the managed description. LeanIX returns freshly written relations with a delay, so such a relation, created for a brand-new rel_0_to_1 / rel_1_to_2 relation, may look unjustified for a few seconds - the marker tells it apart from a manual one without waiting. (If it really is outdated, the regular sync deletes it.)

def path_exists(self, from_id, from_type, to_id, to_type):

True / False whether an fs1 links the two fact sheets; None if the request failed.

def linking_fs1s(self, from_id, from_type, to_id, to_type):

The fs1 fact sheets (dicts with id, type, displayName) linking from_id and to_id (fs0 and fs2, in either order); None if the request failed.

def create_and_delete_on_base_fs0(self, fs, delete_outdated, mode=None, delete_candidates=None):
def make_fast_fact_sheet(self, fs, custom_facets):
def initial_sync_event(self, fs):