infrahub-server 1.6.3__py3-none-any.whl → 1.7.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/actions/tasks.py +4 -2
- infrahub/api/schema.py +3 -1
- infrahub/artifacts/tasks.py +1 -0
- infrahub/auth.py +2 -2
- infrahub/cli/db.py +6 -6
- infrahub/computed_attribute/gather.py +3 -4
- infrahub/computed_attribute/tasks.py +23 -6
- infrahub/config.py +8 -0
- infrahub/constants/enums.py +12 -0
- infrahub/core/account.py +5 -8
- infrahub/core/attribute.py +106 -108
- infrahub/core/branch/models.py +44 -71
- infrahub/core/branch/tasks.py +5 -3
- infrahub/core/changelog/diff.py +1 -20
- infrahub/core/changelog/models.py +0 -7
- infrahub/core/constants/__init__.py +17 -0
- infrahub/core/constants/database.py +0 -1
- infrahub/core/constants/schema.py +0 -1
- infrahub/core/convert_object_type/repository_conversion.py +3 -4
- infrahub/core/diff/data_check_synchronizer.py +3 -2
- infrahub/core/diff/enricher/cardinality_one.py +1 -1
- infrahub/core/diff/merger/merger.py +27 -1
- infrahub/core/diff/merger/serializer.py +3 -10
- infrahub/core/diff/model/diff.py +1 -1
- infrahub/core/diff/query/merge.py +376 -135
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/constraints.py +2 -2
- infrahub/core/graph/schema.py +2 -12
- infrahub/core/manager.py +132 -126
- infrahub/core/metadata/__init__.py +0 -0
- infrahub/core/metadata/interface.py +37 -0
- infrahub/core/metadata/model.py +31 -0
- infrahub/core/metadata/query/__init__.py +0 -0
- infrahub/core/metadata/query/node_metadata.py +301 -0
- infrahub/core/migrations/graph/__init__.py +4 -0
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +3 -8
- infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
- infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
- infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
- infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
- infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
- infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
- infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +38 -0
- infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
- infrahub/core/migrations/query/attribute_add.py +17 -6
- infrahub/core/migrations/query/attribute_remove.py +19 -5
- infrahub/core/migrations/query/attribute_rename.py +21 -5
- infrahub/core/migrations/query/node_duplicate.py +19 -4
- infrahub/core/migrations/schema/attribute_kind_update.py +25 -7
- infrahub/core/migrations/schema/attribute_supports_profile.py +3 -1
- infrahub/core/migrations/schema/models.py +3 -0
- infrahub/core/migrations/schema/node_attribute_add.py +4 -1
- infrahub/core/migrations/schema/node_remove.py +24 -2
- infrahub/core/migrations/schema/tasks.py +4 -1
- infrahub/core/migrations/shared.py +13 -6
- infrahub/core/models.py +6 -6
- infrahub/core/node/__init__.py +156 -57
- infrahub/core/node/create.py +7 -3
- infrahub/core/node/standard.py +100 -14
- infrahub/core/property.py +0 -1
- infrahub/core/protocols_base.py +6 -2
- infrahub/core/query/__init__.py +6 -7
- infrahub/core/query/attribute.py +161 -46
- infrahub/core/query/branch.py +57 -69
- infrahub/core/query/diff.py +4 -4
- infrahub/core/query/node.py +618 -180
- infrahub/core/query/relationship.py +449 -300
- infrahub/core/query/standard_node.py +25 -5
- infrahub/core/query/utils.py +2 -4
- infrahub/core/relationship/constraints/profiles_removal.py +168 -0
- infrahub/core/relationship/model.py +293 -139
- infrahub/core/schema/attribute_parameters.py +1 -28
- infrahub/core/schema/attribute_schema.py +17 -11
- infrahub/core/schema/manager.py +63 -43
- infrahub/core/schema/relationship_schema.py +6 -2
- infrahub/core/schema/schema_branch.py +48 -76
- infrahub/core/task/task.py +4 -2
- infrahub/core/utils.py +0 -22
- infrahub/core/validators/attribute/kind.py +2 -5
- infrahub/core/validators/determiner.py +3 -3
- infrahub/database/__init__.py +3 -3
- infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
- infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
- infrahub/dependencies/registry.py +2 -0
- infrahub/display_labels/tasks.py +12 -3
- infrahub/git/integrator.py +18 -18
- infrahub/git/tasks.py +1 -1
- infrahub/graphql/app.py +2 -2
- infrahub/graphql/constants.py +3 -0
- infrahub/graphql/context.py +1 -1
- infrahub/graphql/initialization.py +11 -0
- infrahub/graphql/loaders/account.py +134 -0
- infrahub/graphql/loaders/node.py +5 -12
- infrahub/graphql/loaders/peers.py +5 -7
- infrahub/graphql/manager.py +158 -18
- infrahub/graphql/metadata.py +91 -0
- infrahub/graphql/models.py +33 -3
- infrahub/graphql/mutations/account.py +5 -5
- infrahub/graphql/mutations/attribute.py +0 -2
- infrahub/graphql/mutations/branch.py +9 -5
- infrahub/graphql/mutations/computed_attribute.py +1 -1
- infrahub/graphql/mutations/display_label.py +1 -1
- infrahub/graphql/mutations/hfid.py +1 -1
- infrahub/graphql/mutations/ipam.py +4 -6
- infrahub/graphql/mutations/main.py +9 -4
- infrahub/graphql/mutations/profile.py +16 -22
- infrahub/graphql/mutations/proposed_change.py +4 -4
- infrahub/graphql/mutations/relationship.py +40 -10
- infrahub/graphql/mutations/repository.py +14 -12
- infrahub/graphql/mutations/schema.py +2 -2
- infrahub/graphql/queries/branch.py +62 -6
- infrahub/graphql/queries/diff/tree.py +5 -5
- infrahub/graphql/resolvers/account_metadata.py +84 -0
- infrahub/graphql/resolvers/ipam.py +6 -8
- infrahub/graphql/resolvers/many_relationship.py +77 -35
- infrahub/graphql/resolvers/resolver.py +16 -12
- infrahub/graphql/resolvers/single_relationship.py +87 -23
- infrahub/graphql/subscription/graphql_query.py +2 -0
- infrahub/graphql/types/__init__.py +0 -1
- infrahub/graphql/types/attribute.py +10 -5
- infrahub/graphql/types/branch.py +40 -53
- infrahub/graphql/types/enums.py +3 -0
- infrahub/graphql/types/metadata.py +28 -0
- infrahub/graphql/types/node.py +22 -2
- infrahub/graphql/types/relationship.py +10 -2
- infrahub/graphql/types/standard_node.py +4 -3
- infrahub/hfid/tasks.py +12 -3
- infrahub/profiles/gather.py +56 -0
- infrahub/profiles/mandatory_fields_checker.py +116 -0
- infrahub/profiles/models.py +66 -0
- infrahub/profiles/node_applier.py +153 -12
- infrahub/profiles/queries/get_profile_data.py +143 -31
- infrahub/profiles/tasks.py +79 -27
- infrahub/profiles/triggers.py +22 -0
- infrahub/proposed_change/tasks.py +4 -1
- infrahub/tasks/artifact.py +1 -0
- infrahub/transformations/tasks.py +2 -2
- infrahub/trigger/catalogue.py +2 -0
- infrahub/trigger/models.py +1 -0
- infrahub/trigger/setup.py +3 -3
- infrahub/trigger/tasks.py +3 -0
- infrahub/validators/tasks.py +1 -0
- infrahub/webhook/models.py +1 -1
- infrahub/webhook/tasks.py +1 -1
- infrahub/workers/dependencies.py +9 -3
- infrahub/workers/infrahub_async.py +13 -4
- infrahub/workflows/catalogue.py +19 -0
- infrahub_sdk/node/constants.py +1 -0
- infrahub_sdk/node/related_node.py +13 -4
- infrahub_sdk/node/relationship.py +8 -0
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/METADATA +17 -16
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/RECORD +161 -143
- infrahub_testcontainers/container.py +3 -3
- infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
- infrahub_testcontainers/docker-compose.test.yml +13 -5
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/entry_points.txt +0 -0
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -2,10 +2,13 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
from infrahub.core.attribute import BaseAttribute
|
|
4
4
|
from infrahub.core.branch import Branch
|
|
5
|
+
from infrahub.core.constants import InfrahubKind
|
|
5
6
|
from infrahub.core.node import Node
|
|
7
|
+
from infrahub.core.relationship import RelationshipManager
|
|
8
|
+
from infrahub.core.relationship.model import Relationship
|
|
6
9
|
from infrahub.database import InfrahubDatabase
|
|
7
10
|
|
|
8
|
-
from .queries.get_profile_data import GetProfileDataQuery, ProfileData
|
|
11
|
+
from .queries.get_profile_data import GetProfileDataQuery, ProfileData, RelationshipFilter
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class NodeProfilesApplier:
|
|
@@ -42,13 +45,51 @@ class NodeProfilesApplier:
|
|
|
42
45
|
attr_names_for_profiles.append(attr_name)
|
|
43
46
|
return attr_names_for_profiles
|
|
44
47
|
|
|
48
|
+
async def _get_rel_names_for_profiles(self, node: Node) -> list[str]:
|
|
49
|
+
node_schema = node.get_schema()
|
|
50
|
+
|
|
51
|
+
rel_names_for_profiles: list[str] = []
|
|
52
|
+
for rel_schema in node_schema.relationships:
|
|
53
|
+
if not rel_schema.support_profiles:
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
rel_name = rel_schema.name
|
|
57
|
+
node_rel = node.get_relationship(rel_name)
|
|
58
|
+
|
|
59
|
+
current_rels = await node_rel.get_relationships(db=self.db)
|
|
60
|
+
if node_rel.is_from_profile or len(current_rels) == 0:
|
|
61
|
+
rel_names_for_profiles.append(rel_name)
|
|
62
|
+
|
|
63
|
+
return rel_names_for_profiles
|
|
64
|
+
|
|
65
|
+
async def _get_rel_filters_for_profiles(self, node: Node, rel_names: list[str]) -> list[RelationshipFilter]:
|
|
66
|
+
node_schema = node.get_schema()
|
|
67
|
+
|
|
68
|
+
identifiers: list[RelationshipFilter] = []
|
|
69
|
+
for rel_name in rel_names:
|
|
70
|
+
rel_schema = node_schema.get_relationship(name=rel_name)
|
|
71
|
+
identifiers.append(
|
|
72
|
+
RelationshipFilter(
|
|
73
|
+
relationship_identifier=f"profile_{rel_schema.get_identifier()}", direction=rel_schema.direction
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return identifiers
|
|
78
|
+
|
|
45
79
|
async def _get_sorted_profile_data(
|
|
46
|
-
self,
|
|
80
|
+
self,
|
|
81
|
+
profile_ids: list[str],
|
|
82
|
+
attr_names_for_profiles: list[str],
|
|
83
|
+
relationship_filters: list[RelationshipFilter] | None = None,
|
|
47
84
|
) -> list[ProfileData]:
|
|
48
85
|
if not profile_ids:
|
|
49
86
|
return []
|
|
50
87
|
query = await GetProfileDataQuery.init(
|
|
51
|
-
db=self.db,
|
|
88
|
+
db=self.db,
|
|
89
|
+
branch=self.branch,
|
|
90
|
+
profile_ids=profile_ids,
|
|
91
|
+
attr_names=attr_names_for_profiles,
|
|
92
|
+
relationship_filters=relationship_filters,
|
|
52
93
|
)
|
|
53
94
|
await query.execute(db=self.db)
|
|
54
95
|
profile_data_list = query.get_profile_data()
|
|
@@ -76,35 +117,135 @@ class NodeProfilesApplier:
|
|
|
76
117
|
node_attr.is_default = True
|
|
77
118
|
node_attr.is_from_profile = False
|
|
78
119
|
|
|
120
|
+
async def _apply_profile_to_relationship(
|
|
121
|
+
self, node: Node, node_rel: RelationshipManager, peer_ids: list[str], profile_id: str
|
|
122
|
+
) -> bool:
|
|
123
|
+
"""Apply profile relationship peers to a node relationship.
|
|
124
|
+
|
|
125
|
+
Profile relationships are only applied if the node has no user-set peers for this relationship.
|
|
126
|
+
User-set peers (not from profile) take precedence over profile values.
|
|
127
|
+
"""
|
|
128
|
+
is_changed = False
|
|
129
|
+
|
|
130
|
+
current_rels = await node_rel.get_relationships(db=self.db)
|
|
131
|
+
profile_peer_ids = set(peer_ids)
|
|
132
|
+
|
|
133
|
+
user_set_peer_ids: set[str] = set()
|
|
134
|
+
profile_set_rels: list[Relationship] = []
|
|
135
|
+
for rel in current_rels:
|
|
136
|
+
if rel.peer_id:
|
|
137
|
+
if rel.is_from_profile:
|
|
138
|
+
profile_set_rels.append(rel)
|
|
139
|
+
else:
|
|
140
|
+
user_set_peer_ids.add(rel.peer_id)
|
|
141
|
+
|
|
142
|
+
# If user has set any peers, they override profile values entirely
|
|
143
|
+
if user_set_peer_ids:
|
|
144
|
+
for rel in profile_set_rels:
|
|
145
|
+
if rel.peer_id and rel.peer_id not in user_set_peer_ids:
|
|
146
|
+
await node_rel.remove_locally(peer_id=rel.peer_id, db=self.db)
|
|
147
|
+
is_changed = True
|
|
148
|
+
elif rel.peer_id and rel.peer_id in user_set_peer_ids:
|
|
149
|
+
rel.clear_source()
|
|
150
|
+
is_changed = True
|
|
151
|
+
|
|
152
|
+
if is_changed:
|
|
153
|
+
node_rel.is_from_profile = False
|
|
154
|
+
await node_rel.save(db=self.db)
|
|
155
|
+
return is_changed
|
|
156
|
+
|
|
157
|
+
profile_set_peer_ids = {rel.peer_id for rel in profile_set_rels if rel.peer_id}
|
|
158
|
+
relationships_to_replace: dict[str, list[Relationship]] = {"insert": [], "remove": []}
|
|
159
|
+
|
|
160
|
+
# Remove relationships that are from profile but not in the new profile value
|
|
161
|
+
for rel in profile_set_rels:
|
|
162
|
+
if rel.peer_id and rel.peer_id not in profile_peer_ids:
|
|
163
|
+
relationships_to_replace["remove"].append(rel)
|
|
164
|
+
|
|
165
|
+
# Add relationships that are in profile but not present
|
|
166
|
+
for peer_id in profile_peer_ids:
|
|
167
|
+
if peer_id not in profile_set_peer_ids:
|
|
168
|
+
new_rel = Relationship(
|
|
169
|
+
schema=node_rel.schema,
|
|
170
|
+
branch=self.branch,
|
|
171
|
+
source_kind=InfrahubKind.PROFILE,
|
|
172
|
+
node=node,
|
|
173
|
+
)
|
|
174
|
+
await new_rel.new(db=self.db, data=peer_id)
|
|
175
|
+
new_rel.set_source(value=profile_id)
|
|
176
|
+
relationships_to_replace["insert"].append(new_rel)
|
|
177
|
+
|
|
178
|
+
is_changed |= await node_rel.update_relationships(
|
|
179
|
+
db=self.db,
|
|
180
|
+
relationships_to_remove=relationships_to_replace["remove"],
|
|
181
|
+
relationships_to_insert=relationships_to_replace["insert"],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if profile_peer_ids:
|
|
185
|
+
node_rel.is_from_profile = True
|
|
186
|
+
|
|
187
|
+
if is_changed:
|
|
188
|
+
await node_rel.save(db=self.db)
|
|
189
|
+
|
|
190
|
+
return is_changed
|
|
191
|
+
|
|
192
|
+
async def _remove_profile_from_relationship(self, relationship_manager: RelationshipManager) -> None:
|
|
193
|
+
relationship_manager.is_from_profile = False
|
|
194
|
+
await relationship_manager.delete(db=self.db)
|
|
195
|
+
|
|
79
196
|
async def apply_profiles(self, node: Node) -> list[str]:
|
|
80
197
|
profile_ids = await self._get_profile_ids(node=node)
|
|
81
198
|
attr_names_for_profiles = await self._get_attr_names_for_profiles(node=node)
|
|
82
|
-
|
|
83
|
-
|
|
199
|
+
rel_names_for_profiles = await self._get_rel_names_for_profiles(node=node)
|
|
200
|
+
rel_filters_for_profiles = await self._get_rel_filters_for_profiles(node=node, rel_names=rel_names_for_profiles)
|
|
201
|
+
if not attr_names_for_profiles and not rel_filters_for_profiles:
|
|
84
202
|
return []
|
|
85
203
|
|
|
86
|
-
# get profiles priorities
|
|
204
|
+
# get profiles priorities, attribute values, and relationship peers on branch
|
|
87
205
|
sorted_profile_data = await self._get_sorted_profile_data(
|
|
88
|
-
profile_ids=profile_ids,
|
|
206
|
+
profile_ids=profile_ids,
|
|
207
|
+
attr_names_for_profiles=attr_names_for_profiles,
|
|
208
|
+
relationship_filters=rel_filters_for_profiles,
|
|
89
209
|
)
|
|
90
210
|
|
|
91
|
-
updated_field_names = []
|
|
211
|
+
updated_field_names: list[str] = []
|
|
92
212
|
# set attribute values/is_default/is_from_profile on nodes
|
|
93
213
|
for attr_name in attr_names_for_profiles:
|
|
94
|
-
|
|
214
|
+
has_profile_attr_data = False
|
|
95
215
|
node_attr = node.get_attribute(attr_name)
|
|
96
216
|
for profile_data in sorted_profile_data:
|
|
97
217
|
profile_value = profile_data.attribute_values.get(attr_name)
|
|
98
218
|
if profile_value is not None:
|
|
99
|
-
|
|
100
|
-
is_changed = False
|
|
219
|
+
has_profile_attr_data = True
|
|
101
220
|
is_changed = self._apply_profile_to_attribute(
|
|
102
221
|
node_attr=node_attr, profile_value=profile_value, profile_id=profile_data.uuid
|
|
103
222
|
)
|
|
104
223
|
if is_changed:
|
|
105
224
|
updated_field_names.append(attr_name)
|
|
106
225
|
break
|
|
107
|
-
if not
|
|
226
|
+
if not has_profile_attr_data and node_attr.is_from_profile:
|
|
108
227
|
self._remove_profile_from_attribute(node_attr=node_attr)
|
|
109
228
|
updated_field_names.append(attr_name)
|
|
229
|
+
|
|
230
|
+
for rel_filter in rel_filters_for_profiles:
|
|
231
|
+
has_profile_rel_data = False
|
|
232
|
+
node_rel = node.get_relationship_by_identifier(rel_filter.relationship_identifier.removeprefix("profile_"))
|
|
233
|
+
|
|
234
|
+
for profile_data in sorted_profile_data:
|
|
235
|
+
profile_peers = profile_data.relationship_peers.get(rel_filter)
|
|
236
|
+
if profile_peers:
|
|
237
|
+
has_profile_rel_data = True
|
|
238
|
+
is_changed = await self._apply_profile_to_relationship(
|
|
239
|
+
node=node, node_rel=node_rel, peer_ids=profile_peers, profile_id=profile_data.uuid
|
|
240
|
+
)
|
|
241
|
+
if is_changed:
|
|
242
|
+
updated_field_names.append(node_rel.name)
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
# Refresh the relationship manager to update the is_from_profile property
|
|
246
|
+
await node_rel.fetch_relationship_ids(db=self.db)
|
|
247
|
+
if not has_profile_rel_data and node_rel.is_from_profile:
|
|
248
|
+
await self._remove_profile_from_relationship(relationship_manager=node_rel)
|
|
249
|
+
updated_field_names.append(node_rel.name)
|
|
250
|
+
|
|
110
251
|
return updated_field_names
|
|
@@ -1,33 +1,67 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
-
from infrahub.core.constants import NULL_VALUE
|
|
4
|
+
from infrahub.core.constants import NULL_VALUE, RelationshipDirection
|
|
5
5
|
from infrahub.core.query import Query, QueryType
|
|
6
6
|
from infrahub.database import InfrahubDatabase
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
@dataclass
|
|
10
|
+
class RelationshipFilter:
|
|
11
|
+
relationship_identifier: str
|
|
12
|
+
direction: RelationshipDirection
|
|
13
|
+
|
|
14
|
+
def __hash__(self) -> int:
|
|
15
|
+
return hash((self.relationship_identifier, self.direction))
|
|
16
|
+
|
|
17
|
+
|
|
9
18
|
@dataclass
|
|
10
19
|
class ProfileData:
|
|
11
20
|
uuid: str
|
|
12
21
|
priority: float | int
|
|
13
22
|
attribute_values: dict[str, Any]
|
|
23
|
+
relationship_peers: dict[RelationshipFilter, list[str]]
|
|
14
24
|
|
|
15
25
|
|
|
16
26
|
class GetProfileDataQuery(Query):
|
|
17
27
|
type: QueryType = QueryType.READ
|
|
18
28
|
insert_return: bool = False
|
|
19
29
|
|
|
20
|
-
def __init__(
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*args: Any,
|
|
33
|
+
profile_ids: list[str],
|
|
34
|
+
attr_names: list[str],
|
|
35
|
+
relationship_filters: list[RelationshipFilter] | None = None,
|
|
36
|
+
**kwargs: Any,
|
|
37
|
+
):
|
|
21
38
|
super().__init__(*args, **kwargs)
|
|
22
39
|
self.profile_ids = profile_ids
|
|
23
40
|
self.attr_names = attr_names
|
|
41
|
+
self.relationship_filters = relationship_filters or []
|
|
24
42
|
|
|
25
43
|
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
26
|
-
branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at
|
|
44
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
|
|
27
45
|
self.params.update(branch_params)
|
|
28
46
|
self.params["profile_ids"] = self.profile_ids
|
|
29
47
|
self.params["attr_names"] = self.attr_names + ["profile_priority"]
|
|
30
48
|
|
|
49
|
+
# Prepare relationship filters
|
|
50
|
+
outbound_identifiers = []
|
|
51
|
+
inbound_identifiers = []
|
|
52
|
+
bidirectional_identifiers = []
|
|
53
|
+
for rf in self.relationship_filters:
|
|
54
|
+
if rf.direction == RelationshipDirection.OUTBOUND:
|
|
55
|
+
outbound_identifiers.append(rf.relationship_identifier)
|
|
56
|
+
elif rf.direction == RelationshipDirection.INBOUND:
|
|
57
|
+
inbound_identifiers.append(rf.relationship_identifier)
|
|
58
|
+
elif rf.direction == RelationshipDirection.BIDIR:
|
|
59
|
+
bidirectional_identifiers.append(rf.relationship_identifier)
|
|
60
|
+
|
|
61
|
+
self.params["outbound_identifiers"] = outbound_identifiers
|
|
62
|
+
self.params["inbound_identifiers"] = inbound_identifiers
|
|
63
|
+
self.params["bidirectional_identifiers"] = bidirectional_identifiers
|
|
64
|
+
|
|
31
65
|
query = """
|
|
32
66
|
// --------------
|
|
33
67
|
// get the Profile nodes
|
|
@@ -48,51 +82,129 @@ WHERE is_active = TRUE
|
|
|
48
82
|
// --------------
|
|
49
83
|
// get the attributes that we care about
|
|
50
84
|
// --------------
|
|
51
|
-
MATCH (profile)-[:HAS_ATTRIBUTE]-(attr:Attribute)
|
|
85
|
+
OPTIONAL MATCH (profile)-[:HAS_ATTRIBUTE]-(attr:Attribute)
|
|
52
86
|
WHERE attr.name IN $attr_names
|
|
53
87
|
WITH DISTINCT profile, attr
|
|
54
88
|
CALL (profile, attr) {
|
|
55
|
-
MATCH (profile)-[r:HAS_ATTRIBUTE]->(attr)
|
|
89
|
+
OPTIONAL MATCH (profile)-[r:HAS_ATTRIBUTE]->(attr)
|
|
56
90
|
WHERE %(branch_filter)s
|
|
57
91
|
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
58
|
-
RETURN r.status = "active" AS
|
|
92
|
+
RETURN r.status = "active" AS r1_is_active
|
|
59
93
|
}
|
|
60
|
-
WITH profile, attr
|
|
61
|
-
WHERE is_active = TRUE
|
|
62
94
|
// --------------
|
|
63
95
|
// get the attribute values
|
|
64
96
|
// --------------
|
|
65
97
|
CALL (attr) {
|
|
66
|
-
MATCH (attr)-[r:HAS_VALUE]->(av)
|
|
98
|
+
OPTIONAL MATCH (attr)-[r:HAS_VALUE]->(av)
|
|
67
99
|
WHERE %(branch_filter)s
|
|
68
100
|
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
69
|
-
RETURN av, r.status = "active" AS
|
|
101
|
+
RETURN av, r.status = "active" AS r2_is_active
|
|
70
102
|
LIMIT 1
|
|
71
103
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
104
|
+
// --------------
|
|
105
|
+
// filter out null and inactive attributes
|
|
106
|
+
// --------------
|
|
107
|
+
WITH profile, CASE
|
|
108
|
+
WHEN attr IS NOT NULL AND av IS NOT NULL AND r1_is_active = TRUE AND r2_is_active = TRUE THEN [attr.name, av.value]
|
|
109
|
+
ELSE NULL
|
|
110
|
+
END AS attribute_details
|
|
111
|
+
WITH profile, collect(attribute_details) AS attributes
|
|
112
|
+
// --------------
|
|
113
|
+
// get all possible relationships we might want for this profile
|
|
114
|
+
// --------------
|
|
115
|
+
OPTIONAL MATCH (profile)-[r:IS_RELATED]-(rel:Relationship)
|
|
116
|
+
WHERE rel.name IN $outbound_identifiers + $bidirectional_identifiers + $inbound_identifiers
|
|
117
|
+
AND %(branch_filter)s
|
|
118
|
+
WITH DISTINCT profile, attributes, rel
|
|
119
|
+
// --------------
|
|
120
|
+
// filter to active near-side relationships with names and directions we want
|
|
121
|
+
// --------------
|
|
122
|
+
CALL (profile, rel) {
|
|
123
|
+
OPTIONAL MATCH (profile)-[r:IS_RELATED]-(rel)
|
|
124
|
+
WHERE (
|
|
125
|
+
(rel.name IN $outbound_identifiers AND startNode(r) = profile)
|
|
126
|
+
OR (rel.name IN $bidirectional_identifiers AND startNode(r) = profile)
|
|
127
|
+
OR (rel.name IN $inbound_identifiers AND startNode(r) = rel)
|
|
128
|
+
)
|
|
129
|
+
AND %(branch_filter)s
|
|
130
|
+
RETURN r AS r1
|
|
131
|
+
ORDER BY r1.branch_level DESC, r1.from DESC, r1.status ASC
|
|
132
|
+
LIMIT 1
|
|
133
|
+
}
|
|
134
|
+
WITH profile, attributes, r1, rel
|
|
135
|
+
// --------------
|
|
136
|
+
// filter to active far-side relationships with names and directions we want
|
|
137
|
+
// --------------
|
|
138
|
+
CALL (profile, rel) {
|
|
139
|
+
OPTIONAL MATCH (rel)-[r:IS_RELATED]-(peer)
|
|
140
|
+
WHERE peer <> profile
|
|
141
|
+
AND (
|
|
142
|
+
(rel.name IN $outbound_identifiers AND startNode(r) = rel)
|
|
143
|
+
OR (rel.name IN $bidirectional_identifiers AND startNode(r) = peer)
|
|
144
|
+
OR (rel.name IN $inbound_identifiers AND startNode(r) = peer)
|
|
145
|
+
)
|
|
146
|
+
AND %(branch_filter)s
|
|
147
|
+
RETURN r AS r2, peer
|
|
148
|
+
ORDER BY r2.branch_level DESC, r2.from DESC, r2.status ASC
|
|
149
|
+
LIMIT 1
|
|
150
|
+
}
|
|
151
|
+
WITH profile, attributes, r1, rel, r2, peer
|
|
152
|
+
// --------------
|
|
153
|
+
// save the direction of the relationship
|
|
154
|
+
// --------------
|
|
155
|
+
WITH *, CASE
|
|
156
|
+
WHEN r1 IS NULL OR r2 IS NULL THEN NULL
|
|
157
|
+
WHEN startNode(r1) = profile AND startNode(r2) = rel THEN "outbound"
|
|
158
|
+
WHEN startNode(r1) = rel AND startNode(r2) = peer THEN "inbound"
|
|
159
|
+
ELSE "bidirectional"
|
|
160
|
+
END AS direction
|
|
161
|
+
// --------------
|
|
162
|
+
// filter out null and inactive relationships
|
|
163
|
+
// --------------
|
|
164
|
+
WITH profile, attributes, CASE
|
|
165
|
+
WHEN rel IS NOT NULL AND peer IS NOT NULL AND r1.status = "active" AND r2.status = "active" THEN [rel.name, direction, peer.uuid]
|
|
166
|
+
ELSE NULL
|
|
167
|
+
END AS relationship_details
|
|
168
|
+
WITH profile, attributes, collect(relationship_details) AS relationships
|
|
169
|
+
RETURN profile.uuid AS profile_uuid, attributes, relationships
|
|
75
170
|
""" % {"branch_filter": branch_filter}
|
|
76
171
|
self.add_to_query(query)
|
|
77
|
-
self.return_labels = ["profile_uuid", "
|
|
172
|
+
self.return_labels = ["profile_uuid", "attributes", "relationships"]
|
|
78
173
|
|
|
79
174
|
def get_profile_data(self) -> list[ProfileData]:
|
|
80
|
-
|
|
175
|
+
profile_data_list: list[ProfileData] = []
|
|
81
176
|
for result in self.results:
|
|
82
177
|
profile_uuid = result.get_as_type(label="profile_uuid", return_type=str)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
178
|
+
attributes = result.get(label="attributes")
|
|
179
|
+
relationships = result.get(label="relationships")
|
|
180
|
+
|
|
181
|
+
profile_data = ProfileData(
|
|
182
|
+
uuid=profile_uuid, priority=float("inf"), attribute_values={}, relationship_peers={}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
for attr_pair in attributes:
|
|
186
|
+
if not isinstance(attr_pair, list) or len(attr_pair) != 2:
|
|
187
|
+
continue
|
|
188
|
+
attr_name, attr_value = attr_pair
|
|
189
|
+
if attr_value == NULL_VALUE:
|
|
190
|
+
attr_value = None
|
|
191
|
+
if attr_name == "profile_priority":
|
|
192
|
+
if attr_value is not None and not isinstance(attr_value, int):
|
|
193
|
+
attr_value = int(attr_value)
|
|
194
|
+
profile_data.priority = attr_value
|
|
195
|
+
else:
|
|
196
|
+
profile_data.attribute_values[attr_name] = attr_value
|
|
197
|
+
|
|
198
|
+
# Parse relationships
|
|
199
|
+
for rel_tuple in relationships:
|
|
200
|
+
if not isinstance(rel_tuple, list) or len(rel_tuple) != 3:
|
|
201
|
+
continue
|
|
202
|
+
rel_name, direction_str, peer_uuid = rel_tuple
|
|
203
|
+
direction = RelationshipDirection(direction_str)
|
|
204
|
+
rel_filter = RelationshipFilter(relationship_identifier=rel_name, direction=direction)
|
|
205
|
+
if rel_filter not in profile_data.relationship_peers:
|
|
206
|
+
profile_data.relationship_peers[rel_filter] = []
|
|
207
|
+
profile_data.relationship_peers[rel_filter].append(peer_uuid)
|
|
208
|
+
|
|
209
|
+
profile_data_list.append(profile_data)
|
|
210
|
+
return profile_data_list
|
infrahub/profiles/tasks.py
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
3
5
|
from prefect import flow
|
|
4
6
|
from prefect.logging import get_run_logger
|
|
5
7
|
|
|
6
|
-
from infrahub.
|
|
8
|
+
from infrahub.context import InfrahubContext # noqa: TC001 needed for prefect flow
|
|
9
|
+
from infrahub.trigger.models import TriggerSetupReport, TriggerType
|
|
10
|
+
from infrahub.trigger.setup import setup_triggers_specific
|
|
11
|
+
from infrahub.workers.dependencies import get_client, get_component, get_database, get_workflow
|
|
7
12
|
from infrahub.workflows.catalogue import PROFILE_REFRESH
|
|
8
|
-
from infrahub.workflows.utils import add_tags
|
|
13
|
+
from infrahub.workflows.utils import add_tags, wait_for_schema_to_converge
|
|
14
|
+
|
|
15
|
+
from .gather import gather_trigger_profile_refresh
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from infrahub_sdk.node.relationship import RelationshipManager
|
|
9
19
|
|
|
10
20
|
REFRESH_PROFILES_MUTATION = """
|
|
11
21
|
mutation RefreshProfiles(
|
|
@@ -20,44 +30,86 @@ mutation RefreshProfiles(
|
|
|
20
30
|
"""
|
|
21
31
|
|
|
22
32
|
|
|
23
|
-
@flow(
|
|
24
|
-
|
|
25
|
-
flow_run_name="Refresh profiles for {node_id}",
|
|
26
|
-
)
|
|
27
|
-
async def object_profiles_refresh(
|
|
28
|
-
branch_name: str,
|
|
29
|
-
node_id: str,
|
|
30
|
-
) -> None:
|
|
33
|
+
@flow(name="object-profiles-refresh", flow_run_name="Refresh profiles for {node_id}")
|
|
34
|
+
async def object_profiles_refresh(branch_name: str, node_id: str) -> None:
|
|
31
35
|
log = get_run_logger()
|
|
32
36
|
client = get_client()
|
|
33
37
|
|
|
34
38
|
await add_tags(branches=[branch_name], nodes=[node_id], db_change=True)
|
|
35
|
-
await client.execute_graphql(
|
|
36
|
-
query=REFRESH_PROFILES_MUTATION,
|
|
37
|
-
variables={"id": node_id},
|
|
38
|
-
branch_name=branch_name,
|
|
39
|
-
)
|
|
39
|
+
await client.execute_graphql(query=REFRESH_PROFILES_MUTATION, variables={"id": node_id}, branch_name=branch_name)
|
|
40
40
|
log.info(f"Profiles refreshed for {node_id}")
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
@flow(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
@flow(name="objects-profiles-refresh-multiple", flow_run_name="Refresh profiles for multiple objects")
|
|
44
|
+
async def objects_profiles_refresh_multiple(branch_name: str, node_ids: list[str]) -> None:
|
|
45
|
+
log = get_run_logger()
|
|
46
|
+
|
|
47
|
+
await add_tags(branches=[branch_name])
|
|
48
|
+
|
|
49
|
+
for node_id in node_ids:
|
|
50
|
+
log.info(f"Requesting profile refresh for {node_id}")
|
|
51
|
+
await get_workflow().submit_workflow(
|
|
52
|
+
workflow=PROFILE_REFRESH, parameters={"branch_name": branch_name, "node_id": node_id}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@flow(name="profile-refresh-setup", flow_run_name="Setup profile refresh triggers")
|
|
57
|
+
async def profile_refresh_setup(
|
|
58
|
+
context: InfrahubContext, # noqa: ARG001
|
|
59
|
+
branch_name: str | None = None,
|
|
60
|
+
event_name: str | None = None, # noqa: ARG001
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Setup Prefect automations for profile refresh triggers.
|
|
63
|
+
|
|
64
|
+
This flow is triggered by schema changes and sets up automations that will
|
|
65
|
+
listen for profile updates. When a profile's attributes or relationships
|
|
66
|
+
change, the corresponding automation will trigger profile refresh for all
|
|
67
|
+
related nodes.
|
|
68
|
+
"""
|
|
69
|
+
database = await get_database()
|
|
70
|
+
async with database.start_session() as db:
|
|
71
|
+
log = get_run_logger()
|
|
72
|
+
|
|
73
|
+
if branch_name:
|
|
74
|
+
await add_tags(branches=[branch_name])
|
|
75
|
+
component = await get_component()
|
|
76
|
+
await wait_for_schema_to_converge(branch_name=branch_name, component=component, db=db, log=log)
|
|
77
|
+
|
|
78
|
+
report: TriggerSetupReport = await setup_triggers_specific(
|
|
79
|
+
gatherer=gather_trigger_profile_refresh, trigger_type=TriggerType.PROFILE
|
|
80
|
+
) # type: ignore[misc]
|
|
81
|
+
|
|
82
|
+
log.info(f"{report.in_use_count} Profile refresh automation configuration completed")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@flow(name="profile-refresh-process", flow_run_name="Process profile refresh for {profile_kind}")
|
|
86
|
+
async def profile_refresh_process(
|
|
48
87
|
branch_name: str,
|
|
49
|
-
|
|
88
|
+
profile_kind: str,
|
|
89
|
+
profile_id: str,
|
|
90
|
+
context: InfrahubContext, # noqa: ARG001
|
|
50
91
|
) -> None:
|
|
92
|
+
"""Process profile refresh when a profile's attributes or relationships change.
|
|
93
|
+
|
|
94
|
+
This flow fetches all nodes related to the profile via the `related_nodes`
|
|
95
|
+
relationship and submits profile refresh workflows for each of them.
|
|
96
|
+
"""
|
|
51
97
|
log = get_run_logger()
|
|
98
|
+
client = get_client()
|
|
52
99
|
|
|
53
100
|
await add_tags(branches=[branch_name])
|
|
54
101
|
|
|
55
|
-
|
|
102
|
+
profile = await client.get(kind=profile_kind, id=profile_id, branch=branch_name, include=["related_nodes"])
|
|
103
|
+
related_nodes: RelationshipManager = profile.related_nodes # type: ignore
|
|
104
|
+
|
|
105
|
+
if not related_nodes.peer_ids:
|
|
106
|
+
log.info(f"No related nodes found for profile {profile_id}")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
log.info(f"Found {len(related_nodes.peer_ids)} related nodes for profile {profile_id}")
|
|
110
|
+
|
|
111
|
+
for node_id in related_nodes.peer_ids:
|
|
56
112
|
log.info(f"Requesting profile refresh for {node_id}")
|
|
57
113
|
await get_workflow().submit_workflow(
|
|
58
|
-
workflow=PROFILE_REFRESH,
|
|
59
|
-
parameters={
|
|
60
|
-
"branch_name": branch_name,
|
|
61
|
-
"node_id": node_id,
|
|
62
|
-
},
|
|
114
|
+
workflow=PROFILE_REFRESH, parameters={"branch_name": branch_name, "node_id": node_id}
|
|
63
115
|
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from infrahub.events.branch_action import BranchDeletedEvent
|
|
2
|
+
from infrahub.events.schema_action import SchemaUpdatedEvent
|
|
3
|
+
from infrahub.trigger.models import BuiltinTriggerDefinition, EventTrigger, ExecuteWorkflow
|
|
4
|
+
from infrahub.workflows.catalogue import PROFILE_REFRESH_SETUP
|
|
5
|
+
|
|
6
|
+
TRIGGER_PROFILE_REFRESH_SETUP = BuiltinTriggerDefinition(
|
|
7
|
+
name="profile-refresh-setup-all",
|
|
8
|
+
trigger=EventTrigger(events={SchemaUpdatedEvent.event_name, BranchDeletedEvent.event_name}),
|
|
9
|
+
actions=[
|
|
10
|
+
ExecuteWorkflow(
|
|
11
|
+
workflow=PROFILE_REFRESH_SETUP,
|
|
12
|
+
parameters={
|
|
13
|
+
"branch_name": "{{ event.resource['infrahub.branch.name'] }}",
|
|
14
|
+
"event_name": "{{ event.event }}",
|
|
15
|
+
"context": {
|
|
16
|
+
"__prefect_kind": "json",
|
|
17
|
+
"value": {"__prefect_kind": "jinja", "template": "{{ event.payload['context'] | tojson }}"},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
),
|
|
21
|
+
],
|
|
22
|
+
)
|
|
@@ -622,6 +622,7 @@ async def validate_artifacts_generation(model: RequestArtifactDefinitionCheck, c
|
|
|
622
622
|
|
|
623
623
|
log = get_run_logger()
|
|
624
624
|
client = get_client()
|
|
625
|
+
client.request_context = context.to_request_context()
|
|
625
626
|
|
|
626
627
|
artifact_definition = await client.get(
|
|
627
628
|
kind=CoreArtifactDefinition,
|
|
@@ -781,6 +782,7 @@ async def run_generator_as_check(model: RunGeneratorAsCheckModel, context: Infra
|
|
|
781
782
|
await add_tags(branches=[model.branch_name], nodes=[model.proposed_change], db_change=True)
|
|
782
783
|
|
|
783
784
|
client = get_client()
|
|
785
|
+
client.request_context = context.to_request_context()
|
|
784
786
|
log = get_run_logger()
|
|
785
787
|
|
|
786
788
|
repository = await get_initialized_repo(
|
|
@@ -859,7 +861,7 @@ async def run_generator_as_check(model: RunGeneratorAsCheckModel, context: Infra
|
|
|
859
861
|
if check:
|
|
860
862
|
check.created_at.value = Timestamp().to_string()
|
|
861
863
|
check.conclusion.value = conclusion.value
|
|
862
|
-
await check.save()
|
|
864
|
+
await check.save(request_context=context.to_request_context())
|
|
863
865
|
else:
|
|
864
866
|
check = await client.create(
|
|
865
867
|
kind=InfrahubKind.GENERATORCHECK,
|
|
@@ -925,6 +927,7 @@ async def request_generator_definition_check(model: RequestGeneratorDefinitionCh
|
|
|
925
927
|
|
|
926
928
|
log = get_run_logger()
|
|
927
929
|
client = get_client()
|
|
930
|
+
client.request_context = context.to_request_context()
|
|
928
931
|
|
|
929
932
|
proposed_change = await client.get(kind=InfrahubKind.PROPOSEDCHANGE, id=model.proposed_change)
|
|
930
933
|
|
infrahub/tasks/artifact.py
CHANGED
|
@@ -13,6 +13,7 @@ from infrahub.workers.dependencies import get_client
|
|
|
13
13
|
async def define_artifact(model: CheckArtifactCreate | RequestArtifactGenerate) -> tuple[InfrahubNode, bool]:
|
|
14
14
|
"""Return an artifact together with a flag to indicate if the artifact is created now or already existed."""
|
|
15
15
|
client = get_client()
|
|
16
|
+
client.request_context = model.context.to_request_context()
|
|
16
17
|
created = False
|
|
17
18
|
if model.artifact_id:
|
|
18
19
|
artifact = await client.get(kind=InfrahubKind.ARTIFACT, id=model.artifact_id, branch=model.branch_name)
|
|
@@ -33,7 +33,7 @@ async def transform_python(message: TransformPythonData) -> Any:
|
|
|
33
33
|
location=message.transform_location,
|
|
34
34
|
data=message.data,
|
|
35
35
|
convert_query_response=message.convert_query_response,
|
|
36
|
-
) # type: ignore[
|
|
36
|
+
) # type: ignore[call-overload]
|
|
37
37
|
|
|
38
38
|
return transformed_data
|
|
39
39
|
|
|
@@ -54,6 +54,6 @@ async def transform_render_jinja2_template(message: TransformJinjaTemplateData)
|
|
|
54
54
|
|
|
55
55
|
rendered_template = await repo.render_jinja2_template.with_options(timeout_seconds=message.timeout)(
|
|
56
56
|
commit=message.commit, location=message.template_location, data={"data": message.data}
|
|
57
|
-
) # type: ignore[
|
|
57
|
+
) # type: ignore[call-overload]
|
|
58
58
|
|
|
59
59
|
return rendered_template
|
infrahub/trigger/catalogue.py
CHANGED
|
@@ -6,6 +6,7 @@ from infrahub.computed_attribute.triggers import (
|
|
|
6
6
|
)
|
|
7
7
|
from infrahub.display_labels.triggers import TRIGGER_DISPLAY_LABELS_ALL_SCHEMA
|
|
8
8
|
from infrahub.hfid.triggers import TRIGGER_HFID_ALL_SCHEMA
|
|
9
|
+
from infrahub.profiles.triggers import TRIGGER_PROFILE_REFRESH_SETUP
|
|
9
10
|
from infrahub.schema.triggers import TRIGGER_SCHEMA_UPDATED
|
|
10
11
|
from infrahub.trigger.models import TriggerDefinition
|
|
11
12
|
from infrahub.webhook.triggers import TRIGGER_WEBHOOK_DELETE, TRIGGER_WEBHOOK_SETUP_UPDATE
|
|
@@ -17,6 +18,7 @@ builtin_triggers: list[TriggerDefinition] = [
|
|
|
17
18
|
TRIGGER_COMPUTED_ATTRIBUTE_PYTHON_SETUP_COMMIT,
|
|
18
19
|
TRIGGER_DISPLAY_LABELS_ALL_SCHEMA,
|
|
19
20
|
TRIGGER_HFID_ALL_SCHEMA,
|
|
21
|
+
TRIGGER_PROFILE_REFRESH_SETUP,
|
|
20
22
|
TRIGGER_SCHEMA_UPDATED,
|
|
21
23
|
TRIGGER_WEBHOOK_DELETE,
|
|
22
24
|
TRIGGER_WEBHOOK_SETUP_UPDATE,
|