infrahub-server 1.1.6__py3-none-any.whl → 1.2.0rc0__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.
Files changed (346) hide show
  1. infrahub/api/artifact.py +16 -4
  2. infrahub/api/dependencies.py +8 -0
  3. infrahub/api/oauth2.py +0 -1
  4. infrahub/api/oidc.py +0 -1
  5. infrahub/api/query.py +18 -7
  6. infrahub/api/schema.py +32 -6
  7. infrahub/api/transformation.py +12 -5
  8. infrahub/{message_bus/messages/check_artifact_create.py → artifacts/models.py} +2 -4
  9. infrahub/{message_bus/operations/check/artifact.py → artifacts/tasks.py} +26 -25
  10. infrahub/cli/__init__.py +0 -2
  11. infrahub/cli/db.py +6 -7
  12. infrahub/cli/events.py +8 -3
  13. infrahub/cli/git_agent.py +9 -7
  14. infrahub/cli/tasks.py +4 -6
  15. infrahub/computed_attribute/tasks.py +63 -17
  16. infrahub/computed_attribute/triggers.py +90 -0
  17. infrahub/config.py +1 -1
  18. infrahub/context.py +39 -0
  19. infrahub/core/account.py +5 -8
  20. infrahub/core/attribute.py +53 -21
  21. infrahub/core/branch/models.py +4 -4
  22. infrahub/core/branch/tasks.py +89 -130
  23. infrahub/core/changelog/__init__.py +0 -0
  24. infrahub/core/changelog/diff.py +232 -0
  25. infrahub/core/changelog/models.py +488 -0
  26. infrahub/core/constants/__init__.py +19 -2
  27. infrahub/core/constants/infrahubkind.py +1 -0
  28. infrahub/core/diff/combiner.py +12 -8
  29. infrahub/core/diff/coordinator.py +49 -70
  30. infrahub/core/diff/data_check_synchronizer.py +86 -7
  31. infrahub/core/diff/enricher/aggregated.py +3 -3
  32. infrahub/core/diff/enricher/cardinality_one.py +2 -7
  33. infrahub/core/diff/enricher/hierarchy.py +5 -3
  34. infrahub/core/diff/enricher/labels.py +14 -4
  35. infrahub/core/diff/enricher/path_identifier.py +3 -9
  36. infrahub/core/diff/enricher/summary_counts.py +3 -1
  37. infrahub/core/diff/merger/merger.py +8 -4
  38. infrahub/core/diff/model/path.py +47 -29
  39. infrahub/core/diff/query/all_conflicts.py +6 -3
  40. infrahub/core/diff/query/artifact.py +1 -1
  41. infrahub/core/diff/query/delete_query.py +1 -1
  42. infrahub/core/diff/query/diff_get.py +3 -2
  43. infrahub/core/diff/query/diff_summary.py +1 -1
  44. infrahub/core/diff/query/field_specifiers.py +3 -1
  45. infrahub/core/diff/query/field_summary.py +3 -2
  46. infrahub/core/diff/query/filters.py +12 -1
  47. infrahub/core/diff/query/get_conflict_query.py +1 -1
  48. infrahub/core/diff/query/has_conflicts_query.py +6 -3
  49. infrahub/core/diff/query/merge.py +3 -3
  50. infrahub/core/diff/query/{drop_tracking_id.py → merge_tracking_id.py} +4 -4
  51. infrahub/core/diff/query/roots_metadata.py +9 -2
  52. infrahub/core/diff/query/save.py +151 -66
  53. infrahub/core/diff/query/summary_counts_enricher.py +220 -0
  54. infrahub/core/diff/query/time_range_query.py +3 -2
  55. infrahub/core/diff/query/update_conflict_query.py +1 -1
  56. infrahub/core/diff/query_parser.py +49 -24
  57. infrahub/core/diff/repository/deserializer.py +24 -25
  58. infrahub/core/diff/repository/repository.py +76 -20
  59. infrahub/core/diff/tasks.py +9 -8
  60. infrahub/core/enums.py +1 -1
  61. infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
  62. infrahub/core/ipam/reconciler.py +1 -1
  63. infrahub/core/ipam/tasks.py +2 -3
  64. infrahub/core/manager.py +18 -13
  65. infrahub/core/merge.py +5 -2
  66. infrahub/core/migrations/graph/m001_add_version_to_graph.py +1 -1
  67. infrahub/core/migrations/graph/m002_attribute_is_default.py +2 -2
  68. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +2 -2
  69. infrahub/core/migrations/graph/m004_add_attr_documentation.py +1 -1
  70. infrahub/core/migrations/graph/m005_add_rel_read_only.py +1 -1
  71. infrahub/core/migrations/graph/m006_add_rel_on_delete.py +1 -1
  72. infrahub/core/migrations/graph/m007_add_rel_allow_override.py +1 -1
  73. infrahub/core/migrations/graph/m008_add_human_friendly_id.py +1 -1
  74. infrahub/core/migrations/graph/m009_add_generate_profile_attr.py +1 -1
  75. infrahub/core/migrations/graph/m010_add_generate_profile_attr_generic.py +1 -1
  76. infrahub/core/migrations/graph/m011_remove_profile_relationship_schema.py +2 -2
  77. infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -23
  78. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -11
  79. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +2 -2
  80. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -1
  81. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -1
  82. infrahub/core/migrations/graph/m017_add_core_profile.py +1 -1
  83. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -2
  84. infrahub/core/migrations/query/attribute_add.py +1 -1
  85. infrahub/core/migrations/query/attribute_rename.py +1 -1
  86. infrahub/core/migrations/query/delete_element_in_schema.py +1 -1
  87. infrahub/core/migrations/query/node_duplicate.py +1 -1
  88. infrahub/core/migrations/query/relationship_duplicate.py +1 -1
  89. infrahub/core/migrations/query/schema_attribute_update.py +1 -1
  90. infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
  91. infrahub/core/migrations/schema/node_remove.py +1 -1
  92. infrahub/core/migrations/schema/tasks.py +5 -5
  93. infrahub/core/migrations/shared.py +4 -4
  94. infrahub/core/models.py +7 -8
  95. infrahub/core/node/__init__.py +161 -40
  96. infrahub/core/node/base.py +1 -1
  97. infrahub/core/node/constraints/grouped_uniqueness.py +9 -2
  98. infrahub/core/node/delete_validator.py +4 -4
  99. infrahub/core/node/ipam.py +13 -8
  100. infrahub/core/node/permissions.py +4 -0
  101. infrahub/core/node/resource_manager/ip_prefix_pool.py +8 -5
  102. infrahub/core/node/standard.py +3 -5
  103. infrahub/core/property.py +1 -1
  104. infrahub/core/protocols.py +4 -0
  105. infrahub/core/protocols_base.py +4 -2
  106. infrahub/core/query/__init__.py +2 -5
  107. infrahub/core/query/attribute.py +9 -9
  108. infrahub/core/query/branch.py +5 -5
  109. infrahub/core/query/delete.py +1 -1
  110. infrahub/core/query/diff.py +45 -7
  111. infrahub/core/query/ipam.py +4 -4
  112. infrahub/core/query/node.py +19 -14
  113. infrahub/core/query/relationship.py +10 -11
  114. infrahub/core/query/resource_manager.py +13 -11
  115. infrahub/core/query/standard_node.py +6 -6
  116. infrahub/core/query/task.py +3 -3
  117. infrahub/core/query/task_log.py +1 -1
  118. infrahub/core/query/utils.py +5 -5
  119. infrahub/core/registry.py +0 -2
  120. infrahub/core/relationship/constraints/count.py +1 -1
  121. infrahub/core/relationship/constraints/peer_kind.py +1 -1
  122. infrahub/core/relationship/model.py +66 -26
  123. infrahub/core/schema/__init__.py +6 -4
  124. infrahub/core/schema/basenode_schema.py +1 -3
  125. infrahub/core/schema/definitions/core.py +14 -2
  126. infrahub/core/schema/definitions/internal.py +16 -0
  127. infrahub/core/schema/generated/genericnode_schema.py +5 -0
  128. infrahub/core/schema/generated/node_schema.py +5 -0
  129. infrahub/core/schema/generic_schema.py +5 -1
  130. infrahub/core/schema/manager.py +45 -42
  131. infrahub/core/schema/node_schema.py +4 -0
  132. infrahub/core/schema/profile_schema.py +4 -0
  133. infrahub/core/schema/relationship_schema.py +2 -2
  134. infrahub/core/schema/schema_branch.py +248 -14
  135. infrahub/core/schema/template_schema.py +36 -0
  136. infrahub/core/task/user_task.py +7 -5
  137. infrahub/core/timestamp.py +1 -1
  138. infrahub/core/utils.py +3 -2
  139. infrahub/core/validators/attribute/choices.py +1 -1
  140. infrahub/core/validators/attribute/enum.py +1 -1
  141. infrahub/core/validators/attribute/kind.py +1 -1
  142. infrahub/core/validators/attribute/length.py +1 -1
  143. infrahub/core/validators/attribute/optional.py +1 -1
  144. infrahub/core/validators/attribute/regex.py +1 -1
  145. infrahub/core/validators/attribute/unique.py +1 -1
  146. infrahub/core/validators/checks_runner.py +37 -0
  147. infrahub/core/validators/node/generate_profile.py +1 -1
  148. infrahub/core/validators/node/hierarchy.py +1 -1
  149. infrahub/core/validators/query.py +1 -1
  150. infrahub/core/validators/relationship/count.py +1 -1
  151. infrahub/core/validators/relationship/optional.py +1 -1
  152. infrahub/core/validators/relationship/peer.py +1 -1
  153. infrahub/core/validators/tasks.py +8 -6
  154. infrahub/core/validators/uniqueness/query.py +20 -17
  155. infrahub/database/__init__.py +15 -2
  156. infrahub/database/memgraph.py +1 -1
  157. infrahub/dependencies/builder/constraint/grouped/node_runner.py +0 -2
  158. infrahub/dependencies/builder/diff/combiner.py +1 -1
  159. infrahub/dependencies/builder/diff/conflicts_enricher.py +1 -1
  160. infrahub/dependencies/builder/diff/coordinator.py +0 -2
  161. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  162. infrahub/dependencies/builder/diff/enricher/summary_counts.py +1 -1
  163. infrahub/events/branch_action.py +47 -21
  164. infrahub/events/group_action.py +73 -0
  165. infrahub/events/models.py +159 -51
  166. infrahub/events/node_action.py +74 -8
  167. infrahub/events/repository_action.py +8 -8
  168. infrahub/events/schema_action.py +21 -8
  169. infrahub/generators/tasks.py +12 -13
  170. infrahub/git/base.py +3 -5
  171. infrahub/git/constants.py +0 -1
  172. infrahub/git/integrator.py +36 -35
  173. infrahub/git/repository.py +7 -8
  174. infrahub/git/tasks.py +43 -107
  175. infrahub/git_credential/helper.py +2 -3
  176. infrahub/graphql/analyzer.py +572 -11
  177. infrahub/graphql/app.py +34 -26
  178. infrahub/graphql/auth/query_permission_checker/anonymous_checker.py +5 -5
  179. infrahub/graphql/auth/query_permission_checker/default_branch_checker.py +4 -4
  180. infrahub/graphql/auth/query_permission_checker/merge_operation_checker.py +4 -4
  181. infrahub/graphql/auth/query_permission_checker/object_permission_checker.py +28 -35
  182. infrahub/graphql/auth/query_permission_checker/super_admin_checker.py +5 -5
  183. infrahub/graphql/enums.py +1 -1
  184. infrahub/graphql/initialization.py +5 -1
  185. infrahub/graphql/loaders/node.py +2 -2
  186. infrahub/graphql/manager.py +59 -54
  187. infrahub/graphql/mutations/account.py +20 -13
  188. infrahub/graphql/mutations/artifact_definition.py +16 -12
  189. infrahub/graphql/mutations/branch.py +61 -40
  190. infrahub/graphql/mutations/computed_attribute.py +19 -13
  191. infrahub/graphql/mutations/diff.py +37 -9
  192. infrahub/graphql/mutations/diff_conflict.py +9 -8
  193. infrahub/graphql/mutations/graphql_query.py +19 -11
  194. infrahub/graphql/mutations/ipam.py +21 -19
  195. infrahub/graphql/mutations/main.py +197 -44
  196. infrahub/graphql/mutations/menu.py +8 -8
  197. infrahub/graphql/mutations/proposed_change.py +36 -28
  198. infrahub/graphql/mutations/relationship.py +302 -105
  199. infrahub/graphql/mutations/repository.py +41 -35
  200. infrahub/graphql/mutations/resource_manager.py +26 -26
  201. infrahub/graphql/mutations/schema.py +51 -33
  202. infrahub/graphql/mutations/tasks.py +16 -10
  203. infrahub/graphql/parser.py +1 -1
  204. infrahub/graphql/permissions.py +6 -4
  205. infrahub/graphql/queries/account.py +22 -18
  206. infrahub/graphql/queries/branch.py +6 -4
  207. infrahub/graphql/queries/diff/tree.py +48 -42
  208. infrahub/graphql/queries/event.py +112 -0
  209. infrahub/graphql/queries/internal.py +3 -3
  210. infrahub/graphql/queries/ipam.py +23 -18
  211. infrahub/graphql/queries/relationship.py +11 -10
  212. infrahub/graphql/queries/resource_manager.py +43 -27
  213. infrahub/graphql/queries/search.py +9 -8
  214. infrahub/graphql/queries/status.py +12 -9
  215. infrahub/graphql/queries/task.py +11 -9
  216. infrahub/graphql/resolvers/resolver.py +69 -43
  217. infrahub/graphql/resolvers/single_relationship.py +16 -10
  218. infrahub/graphql/schema.py +2 -0
  219. infrahub/graphql/subscription/__init__.py +1 -1
  220. infrahub/graphql/subscription/events.py +1 -1
  221. infrahub/graphql/subscription/graphql_query.py +8 -8
  222. infrahub/graphql/types/branch.py +2 -2
  223. infrahub/graphql/types/common.py +6 -1
  224. infrahub/graphql/types/enums.py +2 -0
  225. infrahub/graphql/types/event.py +100 -0
  226. infrahub/graphql/types/interface.py +2 -2
  227. infrahub/graphql/types/node.py +3 -3
  228. infrahub/graphql/types/permission.py +2 -2
  229. infrahub/graphql/types/relationship.py +3 -3
  230. infrahub/graphql/types/standard_node.py +9 -11
  231. infrahub/graphql/utils.py +28 -182
  232. infrahub/groups/tasks.py +2 -3
  233. infrahub/lock.py +1 -1
  234. infrahub/menu/constants.py +1 -0
  235. infrahub/menu/generator.py +14 -3
  236. infrahub/menu/menu.py +116 -127
  237. infrahub/menu/models.py +4 -4
  238. infrahub/message_bus/messages/__init__.py +0 -4
  239. infrahub/message_bus/messages/event_branch_merge.py +3 -0
  240. infrahub/message_bus/messages/request_proposedchange_pipeline.py +2 -0
  241. infrahub/message_bus/operations/__init__.py +3 -5
  242. infrahub/message_bus/operations/check/__init__.py +2 -2
  243. infrahub/message_bus/operations/check/generator.py +1 -3
  244. infrahub/message_bus/operations/check/repository.py +1 -1
  245. infrahub/message_bus/operations/event/branch.py +7 -3
  246. infrahub/message_bus/operations/event/schema.py +1 -1
  247. infrahub/message_bus/operations/finalize/validator.py +1 -1
  248. infrahub/message_bus/operations/git/file.py +2 -2
  249. infrahub/message_bus/operations/git/repository.py +1 -1
  250. infrahub/message_bus/operations/requests/__init__.py +0 -2
  251. infrahub/message_bus/operations/requests/generator_definition.py +1 -1
  252. infrahub/message_bus/operations/requests/proposed_change.py +26 -11
  253. infrahub/message_bus/operations/requests/repository.py +2 -2
  254. infrahub/message_bus/operations/send/echo.py +1 -1
  255. infrahub/message_bus/types.py +1 -1
  256. infrahub/permissions/__init__.py +2 -1
  257. infrahub/permissions/types.py +26 -0
  258. infrahub/pools/prefix.py +29 -165
  259. infrahub/prefect_server/__init__.py +0 -0
  260. infrahub/prefect_server/app.py +18 -0
  261. infrahub/prefect_server/database.py +20 -0
  262. infrahub/prefect_server/events.py +28 -0
  263. infrahub/prefect_server/models.py +46 -0
  264. infrahub/proposed_change/models.py +15 -1
  265. infrahub/proposed_change/tasks.py +173 -35
  266. infrahub/pytest_plugin.py +4 -4
  267. infrahub/server.py +12 -11
  268. infrahub/services/__init__.py +147 -62
  269. infrahub/services/adapters/cache/__init__.py +7 -5
  270. infrahub/services/adapters/cache/nats.py +40 -22
  271. infrahub/services/adapters/cache/redis.py +0 -4
  272. infrahub/services/adapters/event/__init__.py +10 -18
  273. infrahub/services/adapters/http/__init__.py +0 -5
  274. infrahub/services/adapters/http/httpx.py +22 -15
  275. infrahub/services/adapters/message_bus/__init__.py +23 -6
  276. infrahub/services/adapters/message_bus/local.py +8 -6
  277. infrahub/services/adapters/message_bus/nats.py +12 -6
  278. infrahub/services/adapters/message_bus/rabbitmq.py +22 -9
  279. infrahub/services/adapters/workflow/__init__.py +11 -8
  280. infrahub/services/adapters/workflow/local.py +28 -7
  281. infrahub/services/adapters/workflow/worker.py +23 -7
  282. infrahub/services/component.py +38 -35
  283. infrahub/services/scheduler.py +32 -29
  284. infrahub/storage.py +2 -4
  285. infrahub/task_manager/constants.py +1 -1
  286. infrahub/task_manager/event.py +182 -0
  287. infrahub/task_manager/models.py +125 -1
  288. infrahub/task_manager/task.py +1 -1
  289. infrahub/tasks/artifact.py +14 -16
  290. infrahub/tasks/registry.py +1 -1
  291. infrahub/tasks/telemetry.py +13 -14
  292. infrahub/transformations/tasks.py +3 -5
  293. infrahub/trigger/__init__.py +0 -0
  294. infrahub/trigger/catalogue.py +15 -0
  295. infrahub/trigger/constants.py +9 -0
  296. infrahub/trigger/models.py +69 -0
  297. infrahub/trigger/tasks.py +85 -0
  298. infrahub/types.py +1 -1
  299. infrahub/utils.py +1 -1
  300. infrahub/webhook/constants.py +0 -2
  301. infrahub/webhook/models.py +8 -2
  302. infrahub/webhook/tasks.py +20 -73
  303. infrahub/webhook/triggers.py +20 -0
  304. infrahub/workers/infrahub_async.py +36 -25
  305. infrahub/workers/utils.py +63 -0
  306. infrahub/workflows/catalogue.py +13 -37
  307. infrahub/workflows/initialization.py +6 -8
  308. infrahub/workflows/models.py +3 -5
  309. infrahub/workflows/utils.py +1 -1
  310. infrahub_sdk/ctl/check.py +3 -3
  311. infrahub_sdk/ctl/cli_commands.py +11 -10
  312. infrahub_sdk/ctl/exceptions.py +0 -6
  313. infrahub_sdk/ctl/exporter.py +1 -1
  314. infrahub_sdk/ctl/generator.py +5 -5
  315. infrahub_sdk/ctl/importer.py +3 -2
  316. infrahub_sdk/ctl/menu.py +1 -1
  317. infrahub_sdk/ctl/object.py +1 -1
  318. infrahub_sdk/ctl/repository.py +23 -15
  319. infrahub_sdk/ctl/schema.py +2 -2
  320. infrahub_sdk/ctl/utils.py +4 -3
  321. infrahub_sdk/ctl/validate.py +2 -1
  322. infrahub_sdk/exceptions.py +6 -0
  323. infrahub_sdk/generator.py +3 -0
  324. infrahub_sdk/node.py +2 -2
  325. infrahub_sdk/schema/__init__.py +14 -2
  326. infrahub_sdk/schema/main.py +7 -0
  327. infrahub_sdk/utils.py +11 -1
  328. infrahub_sdk/yaml.py +2 -3
  329. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/METADATA +46 -12
  330. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/RECORD +338 -321
  331. infrahub_testcontainers/container.py +14 -6
  332. infrahub_testcontainers/docker-compose.test.yml +24 -5
  333. infrahub_testcontainers/haproxy.cfg +43 -0
  334. infrahub_testcontainers/helpers.py +85 -1
  335. infrahub/core/branch/constants.py +0 -2
  336. infrahub/graphql/query.py +0 -52
  337. infrahub/message_bus/messages/request_artifactdefinition_check.py +0 -17
  338. infrahub/message_bus/operations/requests/artifact_definition.py +0 -148
  339. infrahub/schema/constants.py +0 -1
  340. infrahub/schema/tasks.py +0 -76
  341. infrahub/services/adapters/database/__init__.py +0 -9
  342. infrahub_sdk/ctl/_file.py +0 -13
  343. /infrahub/{schema → artifacts}/__init__.py +0 -0
  344. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/LICENSE.txt +0 -0
  345. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/WHEEL +0 -0
  346. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/entry_points.txt +0 -0
@@ -1,38 +1,344 @@
1
- from typing import Any, Optional
1
+ from __future__ import annotations
2
2
 
3
- from graphql import GraphQLSchema, OperationType
3
+ from collections import deque
4
+ from copy import deepcopy
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from functools import cached_property
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from graphql import (
11
+ FieldNode,
12
+ FragmentDefinitionNode,
13
+ FragmentSpreadNode,
14
+ GraphQLSchema,
15
+ InlineFragmentNode,
16
+ NamedTypeNode,
17
+ NonNullTypeNode,
18
+ OperationDefinitionNode,
19
+ OperationType,
20
+ SelectionSetNode,
21
+ )
4
22
  from infrahub_sdk.analyzer import GraphQLQueryAnalyzer
5
23
  from infrahub_sdk.utils import extract_fields
6
24
 
7
- from infrahub.core.branch import Branch
25
+ from infrahub.core.constants import RelationshipCardinality
26
+ from infrahub.core.schema import GenericSchema
27
+ from infrahub.exceptions import SchemaNotFoundError
8
28
  from infrahub.graphql.utils import extract_schema_models
9
29
 
30
+ if TYPE_CHECKING:
31
+ from infrahub.core.branch import Branch
32
+ from infrahub.core.schema import MainSchemaTypes
33
+ from infrahub.core.schema.schema_branch import SchemaBranch
34
+
35
+
36
+ class MutateAction(str, Enum):
37
+ CREATE = "create"
38
+ DELETE = "delete"
39
+ UPDATE = "update"
40
+
41
+
42
+ class ContextType(str, Enum):
43
+ EDGE = "edge"
44
+ NODE = "node"
45
+ DIRECT = "direct"
46
+ OBJECT = "object"
47
+
48
+ @classmethod
49
+ def from_operation(cls, operation: OperationType) -> ContextType:
50
+ match operation:
51
+ case OperationType.QUERY:
52
+ return cls.EDGE
53
+ case OperationType.MUTATION:
54
+ return cls.OBJECT
55
+ case OperationType.SUBSCRIPTION:
56
+ return cls.EDGE
57
+
58
+ @classmethod
59
+ def from_relationship_cardinality(cls, cardinality: RelationshipCardinality) -> ContextType:
60
+ match cardinality:
61
+ case RelationshipCardinality.MANY:
62
+ return cls.EDGE
63
+ case RelationshipCardinality.ONE:
64
+ return cls.NODE
65
+
66
+
67
+ class GraphQLOperation(str, Enum):
68
+ QUERY = "query"
69
+ MUTATION = "mutation"
70
+ SUBSCRIPTION = "subscription"
71
+ UNDEFINED = "undefined"
72
+
73
+ @classmethod
74
+ def from_operation(cls, operation: OperationType) -> GraphQLOperation:
75
+ match operation:
76
+ case OperationType.QUERY:
77
+ return cls.QUERY
78
+ case OperationType.MUTATION:
79
+ return cls.MUTATION
80
+ case OperationType.SUBSCRIPTION:
81
+ return cls.SUBSCRIPTION
82
+
83
+
84
+ @dataclass
85
+ class GraphQLSelectionSet:
86
+ field_nodes: list[FieldNode]
87
+ fragment_spread_nodes: list[FragmentSpreadNode]
88
+ inline_fragment_nodes: list[InlineFragmentNode]
89
+
90
+
91
+ @dataclass
92
+ class GraphQLArgument:
93
+ name: str
94
+ value: str
95
+ kind: str
96
+
97
+
98
+ @dataclass
99
+ class ObjectAccess:
100
+ attributes: set[str] = field(default_factory=set)
101
+ relationships: set[str] = field(default_factory=set)
102
+
103
+
104
+ @dataclass
105
+ class GraphQLVariable:
106
+ name: str
107
+ type: str
108
+ required: bool
109
+
110
+
111
+ @dataclass
112
+ class GraphQLQueryModel:
113
+ model: MainSchemaTypes
114
+ root: bool
115
+ arguments: list[GraphQLArgument]
116
+ attributes: set[str]
117
+ relationships: set[str]
118
+ mutate_actions: list[MutateAction] = field(default_factory=list)
119
+
120
+
121
+ @dataclass
122
+ class GraphQLQueryNode:
123
+ path: str
124
+ operation: GraphQLOperation = field(default=GraphQLOperation.UNDEFINED)
125
+ arguments: list[GraphQLArgument] = field(default_factory=list)
126
+ variables: list[GraphQLVariable] = field(default_factory=list)
127
+ context_type: ContextType = field(default=ContextType.EDGE)
128
+ parent: GraphQLQueryNode | None = field(default=None)
129
+ children: list[GraphQLQueryNode] = field(default_factory=list)
130
+ infrahub_model: MainSchemaTypes | None = field(default=None)
131
+ infrahub_node_models: list[MainSchemaTypes] = field(default_factory=list)
132
+ infrahub_attributes: set[str] = field(default_factory=set)
133
+ infrahub_relationships: set[str] = field(default_factory=set)
134
+ field_node: FieldNode | None = field(default=None)
135
+ mutate_actions: list[MutateAction] = field(default_factory=list)
136
+
137
+ def context_model(self) -> MainSchemaTypes | None:
138
+ """Return the closest Infrahub object by going up in the tree"""
139
+ if self.infrahub_model:
140
+ return self.infrahub_model
141
+ if self.parent:
142
+ return self.parent.context_model()
143
+
144
+ return None
145
+
146
+ def context_path(self) -> str:
147
+ """Return the relative path for the current context with the closest Infrahub object as the root"""
148
+ if self.infrahub_model:
149
+ return f"/{self.path}"
150
+ if self.parent:
151
+ return f"{self.parent.context_path()}/{self.path}"
152
+ return self.path
153
+
154
+ def properties_path(self) -> str:
155
+ """Indicate the expected path to where Infrahub attributes and relationships would be defined."""
156
+ if self.infrahub_model:
157
+ match self.context_type:
158
+ case ContextType.DIRECT:
159
+ return f"/{self.path}"
160
+ case ContextType.EDGE:
161
+ return f"/{self.path}/edges/node"
162
+ case ContextType.NODE:
163
+ return f"/{self.path}/node"
164
+ case ContextType.OBJECT:
165
+ return f"/{self.path}/object"
166
+ if self.parent:
167
+ return self.parent.properties_path()
168
+
169
+ return self.path
170
+
171
+ def full_path(self) -> str:
172
+ """Return the full path within the tree for the current context."""
173
+ if self.parent:
174
+ return f"{self.parent.full_path()}/{self.path}"
175
+ return self.path
176
+
177
+ @property
178
+ def at_root(self) -> bool:
179
+ if self.parent:
180
+ return False
181
+ return True
182
+
183
+ @property
184
+ def in_property_level(self) -> bool:
185
+ """Indicate if properties, i.e., attributes and relationships could exist at this level."""
186
+ return self.context_path() == self.properties_path()
187
+
188
+ def append_attribute(self, attribute: str) -> None:
189
+ """Add attributes to the closes parent Infrahub object."""
190
+ if self.infrahub_model:
191
+ self.infrahub_attributes.add(attribute)
192
+ elif self.parent:
193
+ self.parent.append_attribute(attribute=attribute)
194
+
195
+ def append_relationship(self, relationship: str) -> None:
196
+ """Add relationships to the closes parent Infrahub object."""
197
+ if self.infrahub_model:
198
+ self.infrahub_relationships.add(relationship)
199
+ elif self.parent:
200
+ self.parent.append_relationship(relationship=relationship)
201
+
202
+ def get_models(self) -> list[GraphQLQueryModel]:
203
+ """Return all models defined on this node along with child nodes"""
204
+ models: list[GraphQLQueryModel] = []
205
+ if self.infrahub_model:
206
+ models.append(
207
+ GraphQLQueryModel(
208
+ model=self.infrahub_model,
209
+ root=self.at_root,
210
+ arguments=self.arguments,
211
+ attributes=self.infrahub_attributes,
212
+ relationships=self.infrahub_relationships,
213
+ mutate_actions=self.mutate_actions,
214
+ )
215
+ )
216
+ for used_by in self.infrahub_node_models:
217
+ models.append(
218
+ GraphQLQueryModel(
219
+ model=used_by,
220
+ root=self.at_root,
221
+ arguments=self.arguments,
222
+ attributes=self.infrahub_attributes,
223
+ relationships=self.infrahub_relationships,
224
+ mutate_actions=self.mutate_actions,
225
+ )
226
+ )
227
+
228
+ for child in self.children:
229
+ models.extend(child.get_models())
230
+ return models
231
+
232
+
233
+ @dataclass
234
+ class GraphQLQueryReport:
235
+ queries: list[GraphQLQueryNode]
236
+
237
+ @property
238
+ def impacted_models(self) -> list[str]:
239
+ """Return a list of all Infrahub objects that are impacted by queries within the request"""
240
+ models: set[str] = set()
241
+ for query in self.queries:
242
+ query_models = query.get_models()
243
+ models.update([query_model.model.kind for query_model in query_models])
244
+
245
+ return sorted(models)
246
+
247
+ @cached_property
248
+ def requested_read(self) -> dict[str, ObjectAccess]:
249
+ """Return Infrahub objects and the fields (attributes and relationships) that this query would attempt to read"""
250
+ access: dict[str, ObjectAccess] = {}
251
+ for query in self.queries:
252
+ query_models = query.get_models()
253
+ for query_model in query_models:
254
+ if query_model.model.kind not in access:
255
+ access[query_model.model.kind] = ObjectAccess()
256
+ access[query_model.model.kind].attributes.update(query_model.attributes)
257
+ access[query_model.model.kind].relationships.update(query_model.relationships)
258
+
259
+ return access
260
+
261
+ @cached_property
262
+ def kind_action_map(self) -> dict[str, set[MutateAction]]:
263
+ access: dict[str, set[MutateAction]] = {}
264
+ root_models: set[str] = set()
265
+ includes_mutations: bool = False
266
+ for query in self.queries:
267
+ query_models = query.get_models()
268
+ for query_model in query_models:
269
+ if query_model.mutate_actions:
270
+ includes_mutations = True
271
+ if includes_mutations:
272
+ if query_model.model.kind not in access:
273
+ access[query_model.model.kind] = set()
274
+ if query_model.root:
275
+ root_models.add(query_model.model.kind)
276
+ access[query_model.model.kind].update(query_model.mutate_actions)
277
+
278
+ # Until we properly analyze the data payload it is assumed that the required permissions on non-root objects is update
279
+ # the idea around this is that at this point even if we only return data from a relationship without actually updating
280
+ # that relationship we'd still expect to have UPDATE permissions on that related object. However, this is still a step
281
+ # in the right direction as we'd previously require the same permissions as that of the base object so this is still
282
+ # more correct.
283
+ for node_kind, node_actions in access.items():
284
+ if node_kind not in root_models:
285
+ node_actions.add(MutateAction.UPDATE)
286
+
287
+ return access
288
+
10
289
 
11
290
  class InfrahubGraphQLQueryAnalyzer(GraphQLQueryAnalyzer):
12
291
  def __init__(
13
292
  self,
14
293
  query: str,
15
- query_variables: Optional[dict[str, Any]] = None,
16
- schema: Optional[GraphQLSchema] = None,
17
- operation_name: Optional[str] = None,
18
- branch: Optional[Branch] = None,
294
+ branch: Branch,
295
+ schema_branch: SchemaBranch,
296
+ schema: GraphQLSchema | None = None,
297
+ query_variables: dict[str, Any] | None = None,
298
+ operation_name: str | None = None,
19
299
  ) -> None:
20
- self.branch: Optional[Branch] = branch
21
- self.operation_name: Optional[str] = operation_name
300
+ self.branch = branch
301
+ self.schema_branch = schema_branch
302
+ self.operation_name = operation_name
22
303
  self.query_variables: dict[str, Any] = query_variables or {}
304
+ self._named_fragments: dict[str, GraphQLQueryNode] = {}
305
+ self._fragment_dependencies: dict[str, set[str]] = {}
23
306
  super().__init__(query=query, schema=schema)
24
307
 
25
308
  @property
26
309
  def operation_names(self) -> list[str]:
27
310
  return [operation.name for operation in self.operations if operation.name is not None]
28
311
 
312
+ @cached_property
313
+ def _fragment_definitions(self) -> list[FragmentDefinitionNode]:
314
+ return [
315
+ definition for definition in self.document.definitions if isinstance(definition, FragmentDefinitionNode)
316
+ ]
317
+
318
+ @cached_property
319
+ def _operation_definitions(self) -> list[OperationDefinitionNode]:
320
+ return [
321
+ definition for definition in self.document.definitions if isinstance(definition, OperationDefinitionNode)
322
+ ]
323
+
324
+ def get_named_fragment_with_parent(self, name: str, parent: GraphQLQueryNode) -> GraphQLQueryNode:
325
+ """Return a copy of the named fragment and attach it to a parent.
326
+
327
+ We return a copy of the object as a named fragment could be used by multiple queries and as we're
328
+ generally working with references to objects we wouldn't want to override the parent of a previously
329
+ assigned object
330
+ """
331
+ named_fragment = deepcopy(self._named_fragments[name])
332
+ named_fragment.parent = parent
333
+ return named_fragment
334
+
29
335
  async def get_models_in_use(self, types: dict[str, Any]) -> set[str]:
30
336
  """List of Infrahub models that are referenced in the query."""
31
337
  graphql_types = set()
32
338
  models = set()
33
339
 
34
- if not (self.schema and self.branch):
35
- raise ValueError("Schema and Branch must be provided to extract the models in use.")
340
+ if not self.schema:
341
+ raise ValueError("Schema must be provided to extract the models in use.")
36
342
 
37
343
  for definition in self.document.definitions:
38
344
  fields = await extract_fields(definition.selection_set)
@@ -58,3 +364,258 @@ class InfrahubGraphQLQueryAnalyzer(GraphQLQueryAnalyzer):
58
364
  continue
59
365
 
60
366
  return models
367
+
368
+ @cached_property
369
+ def query_report(self) -> GraphQLQueryReport:
370
+ self._populate_named_fragments()
371
+ operations = self._get_operations()
372
+
373
+ return GraphQLQueryReport(queries=operations)
374
+
375
+ def _get_operations(self) -> list[GraphQLQueryNode]:
376
+ operations: list[GraphQLQueryNode] = []
377
+ for operation_definition in self._operation_definitions:
378
+ selections = self._get_selections(selection_set=operation_definition.selection_set)
379
+
380
+ for field_node in selections.field_nodes:
381
+ schema_model: MainSchemaTypes
382
+ infrahub_node_models: list[MainSchemaTypes] = []
383
+ model_name = self._get_model_name(node=field_node, operation_definition=operation_definition)
384
+
385
+ if model_name in self.schema_branch.node_names:
386
+ schema_model = self.schema_branch.get_node(name=model_name, duplicate=False)
387
+ elif model_name in self.schema_branch.generic_names:
388
+ schema_model = self.schema_branch.get_generic(name=model_name, duplicate=False)
389
+ infrahub_node_models = [
390
+ self.schema_branch.get(name=used_by, duplicate=False) for used_by in schema_model.used_by
391
+ ]
392
+ elif model_name in self.schema_branch.profile_names:
393
+ schema_model = self.schema_branch.get_profile(name=model_name, duplicate=False)
394
+ else:
395
+ continue
396
+
397
+ operational_node = GraphQLQueryNode(
398
+ operation=GraphQLOperation.from_operation(operation=operation_definition.operation),
399
+ path=schema_model.kind,
400
+ infrahub_model=schema_model,
401
+ infrahub_node_models=infrahub_node_models,
402
+ mutate_actions=self._get_model_mutations(
403
+ node=field_node, operation_definition=operation_definition
404
+ ),
405
+ context_type=ContextType.from_operation(operation=operation_definition.operation),
406
+ arguments=self._parse_arguments(field_node=field_node),
407
+ variables=self._get_variables(operation=operation_definition),
408
+ )
409
+
410
+ if field_node.selection_set:
411
+ selections = self._get_selections(selection_set=field_node.selection_set)
412
+ for selection_field_node in selections.field_nodes:
413
+ operational_node.children.append(
414
+ self._populate_field_node(node=selection_field_node, query_node=operational_node)
415
+ )
416
+ operations.append(operational_node)
417
+ return operations
418
+
419
+ @staticmethod
420
+ def _get_model_name(node: FieldNode, operation_definition: OperationDefinitionNode) -> str:
421
+ if operation_definition.operation == OperationType.MUTATION and node.name.value.endswith(
422
+ ("Create", "Delete", "Update", "Upsert")
423
+ ):
424
+ return node.name.value[:-6]
425
+ return node.name.value
426
+
427
+ @staticmethod
428
+ def _get_model_mutations(node: FieldNode, operation_definition: OperationDefinitionNode) -> list[MutateAction]:
429
+ if operation_definition.operation == OperationType.MUTATION:
430
+ if node.name.value.endswith("Create"):
431
+ return [MutateAction.CREATE]
432
+ if node.name.value.endswith("Delete"):
433
+ return [MutateAction.DELETE]
434
+ if node.name.value.endswith("Update"):
435
+ return [MutateAction.UPDATE]
436
+ if node.name.value.endswith("Upsert"):
437
+ return [MutateAction.CREATE, MutateAction.UPDATE]
438
+ return []
439
+
440
+ @property
441
+ def _sorted_fragment_definitions(self) -> list[FragmentDefinitionNode]:
442
+ """Sort fragments so that we start processing fragments that don't depend on other fragments"""
443
+ dependencies = deepcopy(self._fragment_dependencies)
444
+
445
+ independent_fragments = deque([frag for frag, deps in dependencies.items() if not deps])
446
+
447
+ sorted_fragments = []
448
+
449
+ while independent_fragments:
450
+ fragment_name = independent_fragments.popleft()
451
+ sorted_fragments.append(fragment_name)
452
+
453
+ for dependent, deps in dependencies.items():
454
+ if fragment_name in deps:
455
+ deps.remove(fragment_name)
456
+ if not deps:
457
+ independent_fragments.append(dependent)
458
+
459
+ if len(sorted_fragments) != len(self._fragment_dependencies):
460
+ raise ValueError("Circular fragment dependency detected.")
461
+
462
+ fragment_name_to_definition = {frag.name.value: frag for frag in self._fragment_definitions}
463
+ return [fragment_name_to_definition[name] for name in sorted_fragments]
464
+
465
+ def _populate_fragment_dependency(self, name: str, selection_set: SelectionSetNode | None) -> None:
466
+ if selection_set:
467
+ for selection in selection_set.selections:
468
+ if isinstance(selection, FragmentSpreadNode):
469
+ self._fragment_dependencies[name].add(selection.name.value)
470
+ elif isinstance(selection, FieldNode):
471
+ self._populate_fragment_dependency(name=name, selection_set=selection.selection_set)
472
+ elif isinstance(selection, InlineFragmentNode):
473
+ self._populate_fragment_dependency(name=name, selection_set=selection.selection_set)
474
+
475
+ def _populate_fragment_dependencies(self) -> None:
476
+ for fragment in self._fragment_definitions:
477
+ fragment_name = fragment.name.value
478
+ self._fragment_dependencies[fragment_name] = set()
479
+ self._populate_fragment_dependency(name=fragment_name, selection_set=fragment.selection_set)
480
+
481
+ def _populate_named_fragments(self) -> None:
482
+ self._populate_fragment_dependencies()
483
+ self._named_fragments = {}
484
+
485
+ for fragment_definition in self._sorted_fragment_definitions:
486
+ fragment_name = fragment_definition.name.value
487
+ condition_name = fragment_definition.type_condition.name.value
488
+ selections = self._get_selections(selection_set=fragment_definition.selection_set)
489
+
490
+ try:
491
+ infrahub_model = self.schema_branch.get(name=condition_name, duplicate=False)
492
+ except SchemaNotFoundError:
493
+ infrahub_model = None
494
+
495
+ named_fragment = GraphQLQueryNode(
496
+ path=fragment_definition.type_condition.name.value,
497
+ context_type=ContextType.DIRECT,
498
+ infrahub_model=infrahub_model,
499
+ )
500
+ for field_node in selections.field_nodes:
501
+ named_fragment.children.append(self._populate_field_node(node=field_node, query_node=named_fragment))
502
+ for inline_fragment_node in selections.inline_fragment_nodes:
503
+ named_fragment.children.append(
504
+ self._populate_inline_fragment_node(node=inline_fragment_node, query_node=named_fragment)
505
+ )
506
+
507
+ self._named_fragments[fragment_name] = named_fragment
508
+
509
+ def _populate_field_node(self, node: FieldNode, query_node: GraphQLQueryNode) -> GraphQLQueryNode:
510
+ context_type = query_node.context_type
511
+ infrahub_model = None
512
+ infrahub_node_models: list[MainSchemaTypes] = []
513
+ if query_node.in_property_level:
514
+ if model := query_node.context_model():
515
+ if node.name.value in model.attribute_names or node.name.value == "display_label":
516
+ query_node.append_attribute(attribute=node.name.value)
517
+ elif node.name.value in model.relationship_names:
518
+ rel = model.get_relationship_or_none(name=node.name.value)
519
+ if rel:
520
+ infrahub_model = self.schema_branch.get(name=rel.peer, duplicate=False)
521
+ if isinstance(infrahub_model, GenericSchema):
522
+ infrahub_node_models = [
523
+ self.schema_branch.get(name=used_by, duplicate=False)
524
+ for used_by in infrahub_model.used_by
525
+ ]
526
+
527
+ context_type = ContextType.from_relationship_cardinality(cardinality=rel.cardinality)
528
+ query_node.append_relationship(relationship=node.name.value)
529
+
530
+ current_node = GraphQLQueryNode(
531
+ parent=query_node,
532
+ path=node.name.value,
533
+ context_type=context_type,
534
+ infrahub_model=infrahub_model,
535
+ infrahub_node_models=infrahub_node_models,
536
+ arguments=self._parse_arguments(field_node=node),
537
+ )
538
+
539
+ if node.selection_set:
540
+ selections = self._get_selections(selection_set=node.selection_set)
541
+ for field_node in selections.field_nodes:
542
+ current_node.children.append(self._populate_field_node(node=field_node, query_node=current_node))
543
+ for inline_fragment_node in selections.inline_fragment_nodes:
544
+ current_node.children.append(
545
+ self._populate_inline_fragment_node(node=inline_fragment_node, query_node=current_node)
546
+ )
547
+ for fragment_spread_node in selections.fragment_spread_nodes:
548
+ current_node.children.append(
549
+ self._populate_fragment_spread_node(node=fragment_spread_node, query_node=current_node)
550
+ )
551
+
552
+ return current_node
553
+
554
+ def _populate_inline_fragment_node(
555
+ self, node: InlineFragmentNode, query_node: GraphQLQueryNode
556
+ ) -> GraphQLQueryNode:
557
+ context_type = query_node.context_type
558
+ infrahub_model = self.schema_branch.get(name=node.type_condition.name.value)
559
+ context_type = ContextType.DIRECT
560
+ current_node = GraphQLQueryNode(
561
+ parent=query_node,
562
+ path=node.type_condition.name.value,
563
+ context_type=context_type,
564
+ infrahub_model=infrahub_model,
565
+ )
566
+ if node.selection_set:
567
+ selections = self._get_selections(selection_set=node.selection_set)
568
+ for field_node in selections.field_nodes:
569
+ current_node.children.append(self._populate_field_node(node=field_node, query_node=current_node))
570
+ for inline_fragment_node in selections.inline_fragment_nodes:
571
+ current_node.children.append(
572
+ self._populate_inline_fragment_node(node=inline_fragment_node, query_node=current_node)
573
+ )
574
+
575
+ return current_node
576
+
577
+ def _populate_fragment_spread_node(
578
+ self, node: FragmentSpreadNode, query_node: GraphQLQueryNode
579
+ ) -> GraphQLQueryNode:
580
+ return self.get_named_fragment_with_parent(name=node.name.value, parent=query_node)
581
+
582
+ @staticmethod
583
+ def _get_selections(selection_set: SelectionSetNode) -> GraphQLSelectionSet:
584
+ return GraphQLSelectionSet(
585
+ field_nodes=[selection for selection in selection_set.selections if isinstance(selection, FieldNode)],
586
+ fragment_spread_nodes=[
587
+ selection for selection in selection_set.selections if isinstance(selection, FragmentSpreadNode)
588
+ ],
589
+ inline_fragment_nodes=[
590
+ selection for selection in selection_set.selections if isinstance(selection, InlineFragmentNode)
591
+ ],
592
+ )
593
+
594
+ @staticmethod
595
+ def _get_variables(operation: OperationDefinitionNode) -> list[GraphQLVariable]:
596
+ variables = []
597
+ for variable in operation.variable_definitions:
598
+ if isinstance(variable.type, NamedTypeNode):
599
+ variables.append(
600
+ GraphQLVariable(name=variable.variable.name.value, type=variable.type.name.value, required=False)
601
+ )
602
+ elif isinstance(variable.type, NonNullTypeNode):
603
+ if isinstance(variable.type.type, NamedTypeNode):
604
+ variables.append(
605
+ GraphQLVariable(
606
+ name=variable.variable.name.value, type=variable.type.type.name.value, required=True
607
+ )
608
+ )
609
+
610
+ return variables
611
+
612
+ @staticmethod
613
+ def _parse_arguments(field_node: FieldNode) -> list[GraphQLArgument]:
614
+ return [
615
+ GraphQLArgument(
616
+ name=argument.name.value,
617
+ value=getattr(argument.value, "value", ""),
618
+ kind=argument.value.kind,
619
+ )
620
+ for argument in field_node.arguments
621
+ ]