infrahub-server 1.2.11__py3-none-any.whl → 1.3.0__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/constants.py +130 -0
- infrahub/actions/gather.py +114 -0
- infrahub/actions/models.py +243 -0
- infrahub/actions/parsers.py +104 -0
- infrahub/actions/schema.py +393 -0
- infrahub/actions/tasks.py +119 -0
- infrahub/actions/triggers.py +21 -0
- infrahub/branch/__init__.py +0 -0
- infrahub/branch/tasks.py +29 -0
- infrahub/branch/triggers.py +22 -0
- infrahub/cli/db.py +3 -4
- infrahub/computed_attribute/gather.py +3 -1
- infrahub/computed_attribute/tasks.py +23 -29
- infrahub/core/account.py +24 -47
- infrahub/core/attribute.py +13 -15
- infrahub/core/constants/__init__.py +10 -0
- infrahub/core/constants/database.py +1 -0
- infrahub/core/constants/infrahubkind.py +9 -0
- infrahub/core/constraint/node/runner.py +3 -1
- infrahub/core/convert_object_type/__init__.py +0 -0
- infrahub/core/convert_object_type/conversion.py +124 -0
- infrahub/core/convert_object_type/schema_mapping.py +56 -0
- infrahub/core/diff/coordinator.py +8 -1
- infrahub/core/diff/query/all_conflicts.py +1 -5
- infrahub/core/diff/query/artifact.py +10 -20
- infrahub/core/diff/query/delete_query.py +8 -4
- infrahub/core/diff/query/diff_get.py +3 -6
- infrahub/core/diff/query/field_specifiers.py +1 -1
- infrahub/core/diff/query/field_summary.py +2 -4
- infrahub/core/diff/query/merge.py +72 -125
- infrahub/core/diff/query/save.py +83 -68
- infrahub/core/diff/query/summary_counts_enricher.py +34 -54
- infrahub/core/diff/query/time_range_query.py +0 -1
- infrahub/core/diff/repository/repository.py +4 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/manager.py +14 -11
- infrahub/core/migrations/graph/__init__.py +6 -0
- infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
- infrahub/core/migrations/graph/m012_convert_account_generic.py +1 -1
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -6
- infrahub/core/migrations/graph/m015_diff_format_update.py +1 -2
- infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -2
- infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
- infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
- infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
- infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +2 -2
- infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
- infrahub/core/migrations/graph/m028_delete_diffs.py +1 -2
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +662 -0
- infrahub/core/migrations/graph/m030_illegal_edges.py +82 -0
- infrahub/core/migrations/query/attribute_add.py +14 -11
- infrahub/core/migrations/query/attribute_rename.py +6 -11
- infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
- infrahub/core/migrations/query/node_duplicate.py +19 -21
- infrahub/core/migrations/query/relationship_duplicate.py +19 -18
- infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
- infrahub/core/migrations/schema/node_remove.py +19 -20
- infrahub/core/models.py +29 -2
- infrahub/core/node/__init__.py +131 -28
- infrahub/core/node/base.py +1 -1
- infrahub/core/node/create.py +211 -0
- infrahub/core/node/resource_manager/number_pool.py +31 -5
- infrahub/core/node/standard.py +6 -1
- infrahub/core/path.py +15 -1
- infrahub/core/protocols.py +57 -0
- infrahub/core/protocols_base.py +3 -0
- infrahub/core/query/__init__.py +2 -2
- infrahub/core/query/delete.py +3 -3
- infrahub/core/query/diff.py +19 -32
- infrahub/core/query/ipam.py +10 -20
- infrahub/core/query/node.py +29 -47
- infrahub/core/query/relationship.py +55 -34
- infrahub/core/query/resource_manager.py +1 -2
- infrahub/core/query/standard_node.py +19 -5
- infrahub/core/query/subquery.py +2 -4
- infrahub/core/relationship/constraints/count.py +10 -9
- infrahub/core/relationship/constraints/interface.py +2 -1
- infrahub/core/relationship/constraints/peer_kind.py +2 -1
- infrahub/core/relationship/constraints/peer_parent.py +56 -0
- infrahub/core/relationship/constraints/peer_relatives.py +72 -0
- infrahub/core/relationship/constraints/profiles_kind.py +1 -1
- infrahub/core/relationship/model.py +4 -1
- infrahub/core/schema/__init__.py +2 -1
- infrahub/core/schema/attribute_parameters.py +160 -0
- infrahub/core/schema/attribute_schema.py +130 -7
- infrahub/core/schema/basenode_schema.py +27 -3
- infrahub/core/schema/definitions/core/__init__.py +29 -1
- infrahub/core/schema/definitions/core/group.py +45 -0
- infrahub/core/schema/definitions/core/resource_pool.py +9 -0
- infrahub/core/schema/definitions/internal.py +43 -5
- infrahub/core/schema/generated/attribute_schema.py +16 -3
- infrahub/core/schema/generated/relationship_schema.py +11 -1
- infrahub/core/schema/manager.py +7 -2
- infrahub/core/schema/schema_branch.py +109 -12
- infrahub/core/validators/__init__.py +15 -2
- infrahub/core/validators/attribute/choices.py +1 -3
- infrahub/core/validators/attribute/enum.py +1 -3
- infrahub/core/validators/attribute/kind.py +1 -3
- infrahub/core/validators/attribute/length.py +13 -7
- infrahub/core/validators/attribute/min_max.py +118 -0
- infrahub/core/validators/attribute/number_pool.py +106 -0
- infrahub/core/validators/attribute/optional.py +1 -4
- infrahub/core/validators/attribute/regex.py +5 -6
- infrahub/core/validators/attribute/unique.py +1 -3
- infrahub/core/validators/determiner.py +18 -2
- infrahub/core/validators/enum.py +12 -0
- infrahub/core/validators/node/hierarchy.py +3 -6
- infrahub/core/validators/query.py +1 -3
- infrahub/core/validators/relationship/count.py +6 -12
- infrahub/core/validators/relationship/optional.py +2 -4
- infrahub/core/validators/relationship/peer.py +177 -12
- infrahub/core/validators/tasks.py +1 -1
- infrahub/core/validators/uniqueness/query.py +5 -9
- infrahub/database/__init__.py +12 -4
- infrahub/database/validation.py +100 -0
- infrahub/dependencies/builder/constraint/grouped/node_runner.py +4 -0
- infrahub/dependencies/builder/constraint/relationship_manager/peer_parent.py +8 -0
- infrahub/dependencies/builder/constraint/relationship_manager/peer_relatives.py +8 -0
- infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
- infrahub/dependencies/builder/constraint/schema/relationship_peer.py +8 -0
- infrahub/dependencies/builder/diff/deserializer.py +1 -1
- infrahub/dependencies/registry.py +4 -0
- infrahub/events/group_action.py +1 -0
- infrahub/events/models.py +1 -1
- infrahub/git/base.py +5 -3
- infrahub/git/integrator.py +96 -5
- infrahub/git/tasks.py +1 -0
- infrahub/graphql/analyzer.py +139 -18
- infrahub/graphql/manager.py +4 -0
- infrahub/graphql/mutations/action.py +164 -0
- infrahub/graphql/mutations/convert_object_type.py +71 -0
- infrahub/graphql/mutations/main.py +25 -176
- infrahub/graphql/mutations/proposed_change.py +20 -17
- infrahub/graphql/mutations/relationship.py +32 -0
- infrahub/graphql/mutations/resource_manager.py +63 -7
- infrahub/graphql/queries/convert_object_type_mapping.py +34 -0
- infrahub/graphql/queries/resource_manager.py +7 -1
- infrahub/graphql/resolvers/many_relationship.py +1 -1
- infrahub/graphql/resolvers/resolver.py +2 -2
- infrahub/graphql/resolvers/single_relationship.py +1 -1
- infrahub/graphql/schema.py +6 -0
- infrahub/menu/menu.py +34 -2
- infrahub/message_bus/messages/__init__.py +0 -10
- infrahub/message_bus/operations/__init__.py +0 -8
- infrahub/message_bus/operations/refresh/registry.py +4 -7
- infrahub/patch/queries/delete_duplicated_edges.py +45 -39
- infrahub/pools/models.py +14 -0
- infrahub/pools/number.py +5 -3
- infrahub/pools/registration.py +22 -0
- infrahub/pools/tasks.py +126 -0
- infrahub/prefect_server/models.py +1 -19
- infrahub/proposed_change/models.py +68 -3
- infrahub/proposed_change/tasks.py +911 -34
- infrahub/schema/__init__.py +0 -0
- infrahub/schema/tasks.py +27 -0
- infrahub/schema/triggers.py +23 -0
- infrahub/task_manager/models.py +10 -6
- infrahub/trigger/catalogue.py +6 -0
- infrahub/trigger/models.py +23 -6
- infrahub/trigger/setup.py +26 -2
- infrahub/trigger/tasks.py +4 -2
- infrahub/types.py +6 -0
- infrahub/webhook/tasks.py +6 -9
- infrahub/workflows/catalogue.py +103 -1
- infrahub_sdk/client.py +43 -10
- infrahub_sdk/ctl/generator.py +4 -4
- infrahub_sdk/ctl/repository.py +1 -1
- infrahub_sdk/node/__init__.py +39 -0
- infrahub_sdk/node/attribute.py +122 -0
- infrahub_sdk/node/constants.py +21 -0
- infrahub_sdk/{node.py → node/node.py} +158 -803
- infrahub_sdk/node/parsers.py +15 -0
- infrahub_sdk/node/property.py +24 -0
- infrahub_sdk/node/related_node.py +266 -0
- infrahub_sdk/node/relationship.py +302 -0
- infrahub_sdk/protocols.py +112 -0
- infrahub_sdk/protocols_base.py +34 -2
- infrahub_sdk/pytest_plugin/items/python_transform.py +2 -1
- infrahub_sdk/query_groups.py +17 -5
- infrahub_sdk/schema/main.py +1 -0
- infrahub_sdk/schema/repository.py +16 -0
- infrahub_sdk/spec/object.py +1 -1
- infrahub_sdk/store.py +1 -1
- infrahub_sdk/testing/schemas/car_person.py +1 -0
- infrahub_sdk/utils.py +7 -20
- infrahub_sdk/yaml.py +6 -5
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/METADATA +5 -5
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/RECORD +197 -168
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/WHEEL +1 -1
- infrahub_testcontainers/container.py +239 -65
- infrahub_testcontainers/docker-compose-cluster.test.yml +321 -0
- infrahub_testcontainers/docker-compose.test.yml +2 -1
- infrahub_testcontainers/helpers.py +23 -3
- infrahub_testcontainers/plugin.py +9 -0
- infrahub/message_bus/messages/check_generator_run.py +0 -26
- infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
- infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
- infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
- infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
- infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
- infrahub/message_bus/operations/check/__init__.py +0 -3
- infrahub/message_bus/operations/check/generator.py +0 -156
- infrahub/message_bus/operations/finalize/__init__.py +0 -3
- infrahub/message_bus/operations/finalize/validator.py +0 -133
- infrahub/message_bus/operations/requests/__init__.py +0 -9
- infrahub/message_bus/operations/requests/generator_definition.py +0 -140
- infrahub/message_bus/operations/requests/proposed_change.py +0 -629
- infrahub/patch/queries/consolidate_duplicated_nodes.py +0 -109
- /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Mapping
|
|
4
|
+
|
|
5
|
+
from infrahub.exceptions import ValidationError
|
|
6
|
+
|
|
7
|
+
from .interface import RelationshipManagerConstraintInterface
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from infrahub.core.branch import Branch
|
|
11
|
+
from infrahub.core.node import Node
|
|
12
|
+
from infrahub.core.schema import MainSchemaTypes
|
|
13
|
+
from infrahub.database import InfrahubDatabase
|
|
14
|
+
|
|
15
|
+
from ..model import RelationshipManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RelationshipPeerParentConstraint(RelationshipManagerConstraintInterface):
|
|
19
|
+
def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
|
|
20
|
+
self.db = db
|
|
21
|
+
self.branch = branch
|
|
22
|
+
|
|
23
|
+
async def _check_relationship_peers_parent(
|
|
24
|
+
self, relm: RelationshipManager, parent_rel_name: str, node: Node, peers: Mapping[str, Node]
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Validate that all peers of a given `relm` have the same parent for the given `relationship_name`."""
|
|
27
|
+
node_parent = await node.get_parent_relationship_peer(db=self.db, name=parent_rel_name)
|
|
28
|
+
if not node_parent:
|
|
29
|
+
# If the schema is properly validated we are not expecting this to happen
|
|
30
|
+
raise ValidationError(f"Node {node.id} ({node.get_kind()}) does not have a parent peer")
|
|
31
|
+
|
|
32
|
+
parents: set[str] = {node_parent.id}
|
|
33
|
+
for peer in peers.values():
|
|
34
|
+
parent = await peer.get_parent_relationship_peer(db=self.db, name=parent_rel_name)
|
|
35
|
+
if not parent:
|
|
36
|
+
# If the schema is properly validated we are not expecting this to happen
|
|
37
|
+
raise ValidationError(f"Peer {peer.id} ({peer.get_kind()}) does not have a parent peer")
|
|
38
|
+
parents.add(parent.id)
|
|
39
|
+
|
|
40
|
+
if len(parents) != 1:
|
|
41
|
+
raise ValidationError(
|
|
42
|
+
f"All the elements of the '{relm.name}' relationship on node {node.id} ({node.get_kind()}) must have the same parent "
|
|
43
|
+
"as the node"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
|
|
47
|
+
if not relm.schema.common_parent:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
peers = await relm.get_peers(db=self.db)
|
|
51
|
+
if not peers:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
await self._check_relationship_peers_parent(
|
|
55
|
+
relm=relm, parent_rel_name=relm.schema.common_parent, node=node, peers=peers
|
|
56
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Mapping
|
|
5
|
+
|
|
6
|
+
from infrahub.core.constants import RelationshipCardinality
|
|
7
|
+
from infrahub.exceptions import ValidationError
|
|
8
|
+
|
|
9
|
+
from .interface import RelationshipManagerConstraintInterface
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from infrahub.core.branch import Branch
|
|
13
|
+
from infrahub.core.node import Node
|
|
14
|
+
from infrahub.core.schema import MainSchemaTypes, NonGenericSchemaTypes
|
|
15
|
+
from infrahub.database import InfrahubDatabase
|
|
16
|
+
|
|
17
|
+
from ..model import RelationshipManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class NodeToValidate:
|
|
22
|
+
uuid: str
|
|
23
|
+
relative_uuids: set[str]
|
|
24
|
+
schema: NonGenericSchemaTypes
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RelationshipPeerRelativesConstraint(RelationshipManagerConstraintInterface):
|
|
28
|
+
def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
|
|
29
|
+
self.db = db
|
|
30
|
+
self.branch = branch
|
|
31
|
+
|
|
32
|
+
async def _check_relationship_peers_relatives(
|
|
33
|
+
self,
|
|
34
|
+
relm: RelationshipManager,
|
|
35
|
+
node_schema: MainSchemaTypes,
|
|
36
|
+
peers: Mapping[str, Node],
|
|
37
|
+
relationship_name: str,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Validate that all peers of a given `relm` have the same set of relatives (aka peers) for the given `relationship_name`."""
|
|
40
|
+
nodes_to_validate: list[NodeToValidate] = []
|
|
41
|
+
|
|
42
|
+
for peer in peers.values():
|
|
43
|
+
peer_schema = peer.get_schema()
|
|
44
|
+
peer_relm: RelationshipManager = getattr(peer, relationship_name)
|
|
45
|
+
peer_relm_peers = await peer_relm.get_peers(db=self.db)
|
|
46
|
+
|
|
47
|
+
nodes_to_validate.append(
|
|
48
|
+
NodeToValidate(
|
|
49
|
+
uuid=peer.id, relative_uuids={n.id for n in peer_relm_peers.values()}, schema=peer_schema
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
relative_uuids = nodes_to_validate[0].relative_uuids
|
|
54
|
+
for node in nodes_to_validate[1:]:
|
|
55
|
+
if node.relative_uuids != relative_uuids:
|
|
56
|
+
raise ValidationError(
|
|
57
|
+
f"All the elements of the '{relm.name}' relationship on node {node.uuid} ({node_schema.kind}) must have the same set of peers "
|
|
58
|
+
f"for their '{node.schema.kind}.{relationship_name}' relationship"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
|
|
62
|
+
if relm.schema.cardinality != RelationshipCardinality.MANY or not relm.schema.common_relatives:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
peers = await relm.get_peers(db=self.db)
|
|
66
|
+
if not peers:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
for rel_name in relm.schema.common_relatives:
|
|
70
|
+
await self._check_relationship_peers_relatives(
|
|
71
|
+
relm=relm, node_schema=node_schema, peers=peers, relationship_name=rel_name
|
|
72
|
+
)
|
|
@@ -23,7 +23,7 @@ class RelationshipProfilesKindConstraint(RelationshipManagerConstraintInterface)
|
|
|
23
23
|
self.branch = branch
|
|
24
24
|
self.schema_branch = registry.schema.get_schema_branch(branch.name if branch else registry.default_branch)
|
|
25
25
|
|
|
26
|
-
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes) -> None:
|
|
26
|
+
async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None: # noqa: ARG002
|
|
27
27
|
if relm.name != "profiles" or not isinstance(node_schema, NodeSchema):
|
|
28
28
|
return
|
|
29
29
|
|
|
@@ -494,7 +494,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
494
494
|
peer_fields = {
|
|
495
495
|
key: value
|
|
496
496
|
for key, value in fields.items()
|
|
497
|
-
if not key.startswith(PREFIX_PROPERTY) or
|
|
497
|
+
if not key.startswith(PREFIX_PROPERTY) or key != "__typename"
|
|
498
498
|
}
|
|
499
499
|
rel_fields = {
|
|
500
500
|
key.replace(PREFIX_PROPERTY, ""): value
|
|
@@ -789,6 +789,9 @@ class RelationshipManager:
|
|
|
789
789
|
|
|
790
790
|
return len(self._relationships)
|
|
791
791
|
|
|
792
|
+
def validate(self) -> None:
|
|
793
|
+
self._relationships.validate()
|
|
794
|
+
|
|
792
795
|
@overload
|
|
793
796
|
async def get_peer(
|
|
794
797
|
self,
|
infrahub/core/schema/__init__.py
CHANGED
|
@@ -21,7 +21,8 @@ from .profile_schema import ProfileSchema
|
|
|
21
21
|
from .relationship_schema import RelationshipSchema
|
|
22
22
|
from .template_schema import TemplateSchema
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
NonGenericSchemaTypes: TypeAlias = NodeSchema | ProfileSchema | TemplateSchema
|
|
25
|
+
MainSchemaTypes: TypeAlias = NonGenericSchemaTypes | GenericSchema
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
# -----------------------------------------------------
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from pydantic import ConfigDict, Field, model_validator
|
|
7
|
+
|
|
8
|
+
from infrahub import config
|
|
9
|
+
from infrahub.core.constants.schema import UpdateSupport
|
|
10
|
+
from infrahub.core.models import HashableModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_attribute_parameters_class_for_kind(kind: str) -> type[AttributeParameters]:
|
|
14
|
+
param_classes: dict[str, type[AttributeParameters]] = {
|
|
15
|
+
"NumberPool": NumberPoolParameters,
|
|
16
|
+
"Text": TextAttributeParameters,
|
|
17
|
+
"TextArea": TextAttributeParameters,
|
|
18
|
+
"Number": NumberAttributeParameters,
|
|
19
|
+
}
|
|
20
|
+
return param_classes.get(kind, AttributeParameters)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AttributeParameters(HashableModel):
|
|
24
|
+
model_config = ConfigDict(extra="forbid")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TextAttributeParameters(AttributeParameters):
|
|
28
|
+
regex: str | None = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
description="Regular expression that attribute value must match if defined",
|
|
31
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
32
|
+
)
|
|
33
|
+
min_length: int | None = Field(
|
|
34
|
+
default=None,
|
|
35
|
+
description="Set a minimum number of characters allowed.",
|
|
36
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
37
|
+
)
|
|
38
|
+
max_length: int | None = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
description="Set a maximum number of characters allowed.",
|
|
41
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@model_validator(mode="after")
|
|
45
|
+
def validate_min_max(self) -> Self:
|
|
46
|
+
if (
|
|
47
|
+
config.SETTINGS.initialized
|
|
48
|
+
and config.SETTINGS.main.schema_strict_mode
|
|
49
|
+
and self.min_length is not None
|
|
50
|
+
and self.max_length is not None
|
|
51
|
+
):
|
|
52
|
+
if self.min_length > self.max_length:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"`max_length` can't be less than `min_length` when the schema is configured with strict mode"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NumberAttributeParameters(AttributeParameters):
|
|
61
|
+
min_value: int | None = Field(
|
|
62
|
+
default=None,
|
|
63
|
+
description="Set a minimum value allowed.",
|
|
64
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
65
|
+
)
|
|
66
|
+
max_value: int | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description="Set a maximum value allowed.",
|
|
69
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
70
|
+
)
|
|
71
|
+
excluded_values: str | None = Field(
|
|
72
|
+
default=None,
|
|
73
|
+
description="List of values or range of values not allowed for the attribute, format is: '100,150-200,280,300-400'",
|
|
74
|
+
pattern=r"^(\d+(?:-\d+)?)(?:,\d+(?:-\d+)?)*$",
|
|
75
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@model_validator(mode="after")
|
|
79
|
+
def validate_ranges(self) -> Self:
|
|
80
|
+
ranges = self.get_excluded_ranges()
|
|
81
|
+
for i, (start_range_1, end_range_1) in enumerate(ranges):
|
|
82
|
+
if start_range_1 > end_range_1:
|
|
83
|
+
raise ValueError("`start_range` can't be less than `end_range`")
|
|
84
|
+
|
|
85
|
+
# Check for overlapping ranges
|
|
86
|
+
for start_range_2, end_range_2 in ranges[i + 1 :]:
|
|
87
|
+
if not (end_range_1 < start_range_2 or start_range_1 > end_range_2):
|
|
88
|
+
raise ValueError("Excluded ranges cannot overlap")
|
|
89
|
+
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
@model_validator(mode="after")
|
|
93
|
+
def validate_min_max(self) -> Self:
|
|
94
|
+
if (
|
|
95
|
+
config.SETTINGS.initialized
|
|
96
|
+
and config.SETTINGS.main.schema_strict_mode
|
|
97
|
+
and self.min_value is not None
|
|
98
|
+
and self.max_value is not None
|
|
99
|
+
):
|
|
100
|
+
if self.min_value > self.max_value:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"`max_value` can't be less than `min_value` when the schema is configured with strict mode"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def get_excluded_single_values(self) -> list[int]:
|
|
108
|
+
if not self.excluded_values:
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
results = [int(value) for value in self.excluded_values.split(",") if "-" not in value]
|
|
112
|
+
return results
|
|
113
|
+
|
|
114
|
+
def get_excluded_ranges(self) -> list[tuple[int, int]]:
|
|
115
|
+
if not self.excluded_values:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
ranges = []
|
|
119
|
+
for value in self.excluded_values.split(","):
|
|
120
|
+
if "-" in value:
|
|
121
|
+
start, end = map(int, value.split("-"))
|
|
122
|
+
ranges.append((start, end))
|
|
123
|
+
|
|
124
|
+
return ranges
|
|
125
|
+
|
|
126
|
+
def is_valid_value(self, value: int) -> bool:
|
|
127
|
+
if self.min_value is not None and value < self.min_value:
|
|
128
|
+
return False
|
|
129
|
+
if self.max_value is not None and value > self.max_value:
|
|
130
|
+
return False
|
|
131
|
+
if value in self.get_excluded_single_values():
|
|
132
|
+
return False
|
|
133
|
+
for start, end in self.get_excluded_ranges():
|
|
134
|
+
if start <= value <= end:
|
|
135
|
+
return False
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class NumberPoolParameters(AttributeParameters):
|
|
140
|
+
end_range: int = Field(
|
|
141
|
+
default=sys.maxsize,
|
|
142
|
+
description="End range for numbers for the associated NumberPool",
|
|
143
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
144
|
+
)
|
|
145
|
+
start_range: int = Field(
|
|
146
|
+
default=1,
|
|
147
|
+
description="Start range for numbers for the associated NumberPool",
|
|
148
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
149
|
+
)
|
|
150
|
+
number_pool_id: str | None = Field(
|
|
151
|
+
default=None,
|
|
152
|
+
description="The ID of the numberpool associated with this attribute",
|
|
153
|
+
json_schema_extra={"update": UpdateSupport.NOT_SUPPORTED.value},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@model_validator(mode="after")
|
|
157
|
+
def validate_ranges(self) -> Self:
|
|
158
|
+
if self.start_range > self.end_range:
|
|
159
|
+
raise ValueError("`start_range` can't be less than `end_range`")
|
|
160
|
+
return self
|
|
@@ -2,15 +2,23 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import enum
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
6
6
|
|
|
7
|
-
from pydantic import field_validator, model_validator
|
|
7
|
+
from pydantic import Field, ValidationInfo, field_validator, model_validator
|
|
8
8
|
|
|
9
9
|
from infrahub import config
|
|
10
|
+
from infrahub.core.constants.schema import UpdateSupport
|
|
10
11
|
from infrahub.core.enums import generate_python_enum
|
|
11
12
|
from infrahub.core.query.attribute import default_attribute_query_filter
|
|
12
13
|
from infrahub.types import ATTRIBUTE_KIND_LABELS, ATTRIBUTE_TYPES
|
|
13
14
|
|
|
15
|
+
from .attribute_parameters import (
|
|
16
|
+
AttributeParameters,
|
|
17
|
+
NumberAttributeParameters,
|
|
18
|
+
NumberPoolParameters,
|
|
19
|
+
TextAttributeParameters,
|
|
20
|
+
get_attribute_parameters_class_for_kind,
|
|
21
|
+
)
|
|
14
22
|
from .generated.attribute_schema import GeneratedAttributeSchema
|
|
15
23
|
|
|
16
24
|
if TYPE_CHECKING:
|
|
@@ -21,10 +29,32 @@ if TYPE_CHECKING:
|
|
|
21
29
|
from infrahub.database import InfrahubDatabase
|
|
22
30
|
|
|
23
31
|
|
|
32
|
+
def get_attribute_schema_class_for_kind(kind: str) -> type[AttributeSchema]:
|
|
33
|
+
return attribute_schema_class_by_kind.get(kind, AttributeSchema)
|
|
34
|
+
|
|
35
|
+
|
|
24
36
|
class AttributeSchema(GeneratedAttributeSchema):
|
|
25
37
|
_sort_by: list[str] = ["name"]
|
|
26
38
|
_enum_class: type[enum.Enum] | None = None
|
|
27
39
|
|
|
40
|
+
@classmethod
|
|
41
|
+
def model_json_schema(cls, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
42
|
+
schema = super().model_json_schema(*args, **kwargs)
|
|
43
|
+
|
|
44
|
+
# Build conditional schema based on attribute_schema_class_by_kind mapping
|
|
45
|
+
# This override allows people using the Yaml language server to get the correct mappings
|
|
46
|
+
# for the parameters when selecting the appropriate kind
|
|
47
|
+
schema["allOf"] = []
|
|
48
|
+
for kind, schema_class in attribute_schema_class_by_kind.items():
|
|
49
|
+
schema["allOf"].append(
|
|
50
|
+
{
|
|
51
|
+
"if": {"properties": {"kind": {"const": kind}}},
|
|
52
|
+
"then": {"properties": {"parameters": {"$ref": f"#/definitions/{schema_class.__name__}"}}},
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return schema
|
|
57
|
+
|
|
28
58
|
@property
|
|
29
59
|
def is_attribute(self) -> bool:
|
|
30
60
|
return True
|
|
@@ -53,16 +83,46 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
53
83
|
|
|
54
84
|
@model_validator(mode="before")
|
|
55
85
|
@classmethod
|
|
56
|
-
def validate_dropdown_choices(cls, values:
|
|
86
|
+
def validate_dropdown_choices(cls, values: Any) -> Any:
|
|
57
87
|
"""Validate that choices are defined for a dropdown but not for other kinds."""
|
|
58
|
-
if values
|
|
59
|
-
|
|
88
|
+
if isinstance(values, dict):
|
|
89
|
+
kind = values.get("kind")
|
|
90
|
+
choices = values.get("choices")
|
|
91
|
+
elif isinstance(values, AttributeSchema):
|
|
92
|
+
kind = values.kind
|
|
93
|
+
choices = values.choices
|
|
94
|
+
else:
|
|
95
|
+
return values
|
|
96
|
+
if kind != "Dropdown" and choices:
|
|
97
|
+
raise ValueError(f"Can only specify 'choices' for kind=Dropdown: {kind}")
|
|
60
98
|
|
|
61
|
-
if
|
|
99
|
+
if kind == "Dropdown" and not choices:
|
|
62
100
|
raise ValueError("The property 'choices' is required for kind=Dropdown")
|
|
63
101
|
|
|
64
102
|
return values
|
|
65
103
|
|
|
104
|
+
@field_validator("parameters", mode="before")
|
|
105
|
+
@classmethod
|
|
106
|
+
def set_parameters_type(cls, value: Any, info: ValidationInfo) -> Any:
|
|
107
|
+
"""Override parameters class if using base AttributeParameters class and should be using a subclass"""
|
|
108
|
+
kind = info.data["kind"]
|
|
109
|
+
expected_parameters_class = get_attribute_parameters_class_for_kind(kind=kind)
|
|
110
|
+
if value is None:
|
|
111
|
+
return expected_parameters_class()
|
|
112
|
+
if not isinstance(value, expected_parameters_class) and isinstance(value, AttributeParameters):
|
|
113
|
+
return expected_parameters_class(**value.model_dump())
|
|
114
|
+
return value
|
|
115
|
+
|
|
116
|
+
@model_validator(mode="after")
|
|
117
|
+
def validate_parameters(self) -> Self:
|
|
118
|
+
if isinstance(self.parameters, NumberPoolParameters) and not self.kind == "NumberPool":
|
|
119
|
+
raise ValueError(f"NumberPoolParameters can't be used as parameters for {self.kind}")
|
|
120
|
+
|
|
121
|
+
if isinstance(self.parameters, TextAttributeParameters) and self.kind not in ["Text", "TextArea"]:
|
|
122
|
+
raise ValueError(f"TextAttributeParameters can't be used as parameters for {self.kind}")
|
|
123
|
+
|
|
124
|
+
return self
|
|
125
|
+
|
|
66
126
|
def get_class(self) -> type[BaseAttribute]:
|
|
67
127
|
return ATTRIBUTE_TYPES[self.kind].get_infrahub_class()
|
|
68
128
|
|
|
@@ -106,7 +166,7 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
106
166
|
|
|
107
167
|
def to_node(self) -> dict[str, Any]:
|
|
108
168
|
fields_to_exclude = {"id", "state", "filters"}
|
|
109
|
-
fields_to_json = {"computed_attribute"}
|
|
169
|
+
fields_to_json = {"computed_attribute", "parameters"}
|
|
110
170
|
data = self.model_dump(exclude=fields_to_exclude | fields_to_json)
|
|
111
171
|
|
|
112
172
|
for field_name in fields_to_json:
|
|
@@ -117,6 +177,15 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
117
177
|
|
|
118
178
|
return data
|
|
119
179
|
|
|
180
|
+
def get_regex(self) -> str | None:
|
|
181
|
+
return self.regex
|
|
182
|
+
|
|
183
|
+
def get_min_length(self) -> int | None:
|
|
184
|
+
return self.min_length
|
|
185
|
+
|
|
186
|
+
def get_max_length(self) -> int | None:
|
|
187
|
+
return self.max_length
|
|
188
|
+
|
|
120
189
|
async def get_query_filter(
|
|
121
190
|
self,
|
|
122
191
|
name: str,
|
|
@@ -144,3 +213,57 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
144
213
|
partial_match=partial_match,
|
|
145
214
|
support_profiles=support_profiles,
|
|
146
215
|
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class NumberPoolSchema(AttributeSchema):
|
|
219
|
+
parameters: NumberPoolParameters = Field(
|
|
220
|
+
default_factory=NumberPoolParameters,
|
|
221
|
+
description="Extra parameters specific to NumberPool attributes",
|
|
222
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TextAttributeSchema(AttributeSchema):
|
|
227
|
+
parameters: TextAttributeParameters = Field(
|
|
228
|
+
default_factory=TextAttributeParameters,
|
|
229
|
+
description="Extra parameters specific to text attributes",
|
|
230
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@model_validator(mode="after")
|
|
234
|
+
def reconcile_parameters(self) -> Self:
|
|
235
|
+
if self.regex != self.parameters.regex:
|
|
236
|
+
final_regex = self.parameters.regex if self.parameters.regex is not None else self.regex
|
|
237
|
+
self.regex = self.parameters.regex = final_regex
|
|
238
|
+
if self.min_length != self.parameters.min_length:
|
|
239
|
+
final_min_length = self.parameters.min_length if self.parameters.min_length is not None else self.min_length
|
|
240
|
+
self.min_length = self.parameters.min_length = final_min_length
|
|
241
|
+
if self.max_length != self.parameters.max_length:
|
|
242
|
+
final_max_length = self.parameters.max_length if self.parameters.max_length is not None else self.max_length
|
|
243
|
+
self.max_length = self.parameters.max_length = final_max_length
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
def get_regex(self) -> str | None:
|
|
247
|
+
return self.parameters.regex
|
|
248
|
+
|
|
249
|
+
def get_min_length(self) -> int | None:
|
|
250
|
+
return self.parameters.min_length
|
|
251
|
+
|
|
252
|
+
def get_max_length(self) -> int | None:
|
|
253
|
+
return self.parameters.max_length
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class NumberAttributeSchema(AttributeSchema):
|
|
257
|
+
parameters: NumberAttributeParameters = Field(
|
|
258
|
+
default_factory=NumberAttributeParameters,
|
|
259
|
+
description="Extra parameters specific to number attributes",
|
|
260
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
attribute_schema_class_by_kind: dict[str, type[AttributeSchema]] = {
|
|
265
|
+
"NumberPool": NumberPoolSchema,
|
|
266
|
+
"Text": TextAttributeSchema,
|
|
267
|
+
"TextArea": TextAttributeSchema,
|
|
268
|
+
"Number": NumberAttributeSchema,
|
|
269
|
+
}
|
|
@@ -13,7 +13,7 @@ from pydantic import field_validator
|
|
|
13
13
|
from infrahub.core.constants import RelationshipCardinality, RelationshipKind
|
|
14
14
|
from infrahub.core.models import HashableModel, HashableModelDiff
|
|
15
15
|
|
|
16
|
-
from .attribute_schema import AttributeSchema
|
|
16
|
+
from .attribute_schema import AttributeSchema, get_attribute_schema_class_for_kind
|
|
17
17
|
from .generated.base_node_schema import GeneratedBaseNodeSchema
|
|
18
18
|
from .relationship_schema import RelationshipSchema
|
|
19
19
|
|
|
@@ -74,6 +74,30 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
74
74
|
Be careful hash generated from hash() have a salt by default and they will not be the same across run"""
|
|
75
75
|
return hash(self.get_hash())
|
|
76
76
|
|
|
77
|
+
@field_validator("attributes", mode="before")
|
|
78
|
+
@classmethod
|
|
79
|
+
def set_attribute_type(cls, raw_attributes: Any) -> Any:
|
|
80
|
+
if not isinstance(raw_attributes, list):
|
|
81
|
+
return raw_attributes
|
|
82
|
+
attribute_schemas_with_types: list[Any] = []
|
|
83
|
+
for raw_attr in raw_attributes:
|
|
84
|
+
if not isinstance(raw_attr, (dict, AttributeSchema)):
|
|
85
|
+
attribute_schemas_with_types.append(raw_attr)
|
|
86
|
+
continue
|
|
87
|
+
if isinstance(raw_attr, dict):
|
|
88
|
+
kind = raw_attr.get("kind")
|
|
89
|
+
attribute_type_class = get_attribute_schema_class_for_kind(kind=kind)
|
|
90
|
+
attribute_schemas_with_types.append(attribute_type_class(**raw_attr))
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
expected_attr_schema_class = get_attribute_schema_class_for_kind(kind=raw_attr.kind)
|
|
94
|
+
if not isinstance(raw_attr, expected_attr_schema_class):
|
|
95
|
+
final_attr = expected_attr_schema_class(**raw_attr.model_dump())
|
|
96
|
+
else:
|
|
97
|
+
final_attr = raw_attr
|
|
98
|
+
attribute_schemas_with_types.append(final_attr)
|
|
99
|
+
return attribute_schemas_with_types
|
|
100
|
+
|
|
77
101
|
def to_dict(self) -> dict:
|
|
78
102
|
data = self.model_dump(
|
|
79
103
|
exclude_unset=True, exclude_none=True, exclude_defaults=True, exclude={"attributes", "relationships"}
|
|
@@ -362,7 +386,7 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
362
386
|
if not self.display_labels:
|
|
363
387
|
return None
|
|
364
388
|
|
|
365
|
-
fields: dict[str, str |
|
|
389
|
+
fields: dict[str, str | dict[str, None] | None] = {}
|
|
366
390
|
for item in self.display_labels:
|
|
367
391
|
fields.update(self.convert_path_to_graphql_fields(path=item))
|
|
368
392
|
return fields
|
|
@@ -377,7 +401,7 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
377
401
|
if not self.human_friendly_id:
|
|
378
402
|
return None
|
|
379
403
|
|
|
380
|
-
fields: dict[str, str |
|
|
404
|
+
fields: dict[str, str | dict[str, None] | None] = {}
|
|
381
405
|
for item in self.human_friendly_id:
|
|
382
406
|
fields.update(self.convert_path_to_graphql_fields(path=item))
|
|
383
407
|
return fields
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
+
from infrahub.actions.schema import (
|
|
4
|
+
core_action,
|
|
5
|
+
core_generator_action,
|
|
6
|
+
core_group_action,
|
|
7
|
+
core_group_trigger_rule,
|
|
8
|
+
core_node_trigger_attribute_match,
|
|
9
|
+
core_node_trigger_match,
|
|
10
|
+
core_node_trigger_relationship_match,
|
|
11
|
+
core_node_trigger_rule,
|
|
12
|
+
core_trigger_rule,
|
|
13
|
+
)
|
|
14
|
+
|
|
3
15
|
from ...generic_schema import GenericSchema
|
|
4
16
|
from ...node_schema import NodeSchema
|
|
5
17
|
from .account import (
|
|
@@ -16,7 +28,13 @@ from .check import core_check_definition
|
|
|
16
28
|
from .core import core_node, core_task_target
|
|
17
29
|
from .generator import core_generator_definition, core_generator_instance
|
|
18
30
|
from .graphql_query import core_graphql_query
|
|
19
|
-
from .group import
|
|
31
|
+
from .group import (
|
|
32
|
+
core_generator_group,
|
|
33
|
+
core_graphql_query_group,
|
|
34
|
+
core_group,
|
|
35
|
+
core_repository_group,
|
|
36
|
+
core_standard_group,
|
|
37
|
+
)
|
|
20
38
|
from .ipam import builtin_ip_address, builtin_ip_prefix, builtin_ipam, core_ipam_namespace
|
|
21
39
|
from .lineage import lineage_owner, lineage_source
|
|
22
40
|
from .menu import generic_menu_item, menu_item
|
|
@@ -69,6 +87,9 @@ from .webhook import core_custom_webhook, core_standard_webhook, core_webhook
|
|
|
69
87
|
|
|
70
88
|
core_models_mixed: dict[str, list] = {
|
|
71
89
|
"generics": [
|
|
90
|
+
core_action,
|
|
91
|
+
core_trigger_rule,
|
|
92
|
+
core_node_trigger_match,
|
|
72
93
|
core_node,
|
|
73
94
|
lineage_owner,
|
|
74
95
|
core_profile_schema_definition,
|
|
@@ -97,12 +118,19 @@ core_models_mixed: dict[str, list] = {
|
|
|
97
118
|
],
|
|
98
119
|
"nodes": [
|
|
99
120
|
menu_item,
|
|
121
|
+
core_group_action,
|
|
100
122
|
core_standard_group,
|
|
101
123
|
core_generator_group,
|
|
102
124
|
core_graphql_query_group,
|
|
125
|
+
core_repository_group,
|
|
103
126
|
builtin_tag,
|
|
104
127
|
core_account,
|
|
105
128
|
core_account_token,
|
|
129
|
+
core_generator_action,
|
|
130
|
+
core_group_trigger_rule,
|
|
131
|
+
core_node_trigger_rule,
|
|
132
|
+
core_node_trigger_attribute_match,
|
|
133
|
+
core_node_trigger_relationship_match,
|
|
106
134
|
core_password_credential,
|
|
107
135
|
core_refresh_token,
|
|
108
136
|
core_proposed_change,
|