From da19bede7b57b378aef8cc8faf55b7fb05ce2c24 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Fri, 15 Sep 2023 17:27:12 +0200 Subject: [PATCH 01/30] initial issue aggregation --- .../model/architecture/RelationPartner.kt | 7 + .../gropius/model/issue/AggregatedIssue.kt | 66 +++ .../model/issue/AggregatedIssueRelation.kt | 26 ++ .../main/kotlin/gropius/model/issue/Issue.kt | 5 + .../gropius/service/NodeBatchUpdateContext.kt | 18 + .../gropius/service/NodeBatchUpdater.kt | 44 ++ .../architecture/ComponentGraphUpdater.kt | 31 +- .../architecture/InterfacePartService.kt | 14 +- .../InterfaceSpecificationVersionService.kt | 23 +- .../service/issue/IssueAggregationUpdater.kt | 417 ++++++++++++++++++ .../gropius/service/issue/IssueService.kt | 24 +- 11 files changed, 641 insertions(+), 34 deletions(-) create mode 100644 core/src/main/kotlin/gropius/model/issue/AggregatedIssue.kt create mode 100644 core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt create mode 100644 core/src/main/kotlin/gropius/service/NodeBatchUpdateContext.kt create mode 100644 core/src/main/kotlin/gropius/service/NodeBatchUpdater.kt create mode 100644 core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt diff --git a/core/src/main/kotlin/gropius/model/architecture/RelationPartner.kt b/core/src/main/kotlin/gropius/model/architecture/RelationPartner.kt index db31a8be..ba967f89 100644 --- a/core/src/main/kotlin/gropius/model/architecture/RelationPartner.kt +++ b/core/src/main/kotlin/gropius/model/architecture/RelationPartner.kt @@ -2,6 +2,7 @@ package gropius.model.architecture import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import gropius.model.issue.AggregatedIssue import gropius.model.template.RelationPartnerTemplate import gropius.model.template.TemplatedNode import io.github.graphglue.model.Direction @@ -16,6 +17,7 @@ abstract class RelationPartner(name: String, description: String) : AffectedByIs companion object { const val INCOMING_RELATION = "INCOMING_RELATION" const val OUTGOING_RELATION = "OUTGOING_RELATION" + const val AGGREGATED_ISSUE = "AGGREGATED_ISSUE" } @NodeRelationship(INCOMING_RELATION, Direction.OUTGOING) @@ -28,6 +30,11 @@ abstract class RelationPartner(name: String, description: String) : AffectedByIs @FilterProperty val outgoingRelations by NodeSetProperty() + @NodeRelationship(AGGREGATED_ISSUE, Direction.OUTGOING) + @GraphQLDescription("AggregatedIssues on this RelationPartner.") + @FilterProperty + val aggregatedIssues by NodeSetProperty() + /** * Helper function to get the associated [RelationPartnerTemplate] * diff --git a/core/src/main/kotlin/gropius/model/issue/AggregatedIssue.kt b/core/src/main/kotlin/gropius/model/issue/AggregatedIssue.kt new file mode 100644 index 00000000..a93b3b6a --- /dev/null +++ b/core/src/main/kotlin/gropius/model/issue/AggregatedIssue.kt @@ -0,0 +1,66 @@ +package gropius.model.issue + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import gropius.model.architecture.RelationPartner +import gropius.model.template.IssueType +import gropius.model.user.permission.NodePermission +import io.github.graphglue.model.* + +@DomainNode +@GraphQLDescription( + """An aggregated Issue on a RelationPartner. + READ is granted if READ is granted on `relationPartner`. + An Issue is aggregated on a ComponentVersion if + - it affects the ComponentVersion + - it affects the associated Component + - it is on the Component, and does not affect anything + An Issue is aggregated on a Interface if + - it affects the associated InterfaceSpecificationVersion + - it affects the associated InterfaceSpecification + - it affects any InterfacePart of the associated InterfaceSpecificationVersion + """ +) +@Authorization(NodePermission.READ, allowFromRelated = ["relationPartner"]) +class AggregatedIssue( + @FilterProperty + @OrderProperty + @property:GraphQLDescription("The amount of Issues of this type on this location.") + var count: Int, + @FilterProperty + @property:GraphQLDescription("If aggregated issues are open or closed.") + val isOpen: Boolean +) : Node() { + + companion object { + const val TYPE = "TYPE" + const val INCOMING_RELATION = "INCOMING_RELATION" + const val OUTGOING_RELATION = "OUTGOING_RELATION" + const val ISSUE = "ISSUE" + } + + @NodeRelationship(TYPE, Direction.OUTGOING) + @GraphQLDescription("The IssueType of this AggregatedIssue.") + @FilterProperty + val type by NodeProperty() + + @NodeRelationship(RelationPartner.AGGREGATED_ISSUE, Direction.INCOMING) + @GraphQLDescription("The RelationPartner this AggregatedIssue is on.") + @FilterProperty + val relationPartner by NodeProperty() + + @NodeRelationship(INCOMING_RELATION, Direction.OUTGOING) + @GraphQLDescription("IssueRelations from this aggregated issue to other aggregated issues.") + @FilterProperty + val incomingRelations by NodeSetProperty() + + @NodeRelationship(OUTGOING_RELATION, Direction.OUTGOING) + @GraphQLDescription("IssueRelations from other aggregated issues to this aggregated issue.") + @FilterProperty + val outgoingRelations by NodeSetProperty() + + @NodeRelationship(ISSUE, Direction.OUTGOING) + @GraphQLDescription("The Issues aggregated by this AggregatedIssue.") + @FilterProperty + val issues by NodeSetProperty() + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt b/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt new file mode 100644 index 00000000..e13fd051 --- /dev/null +++ b/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt @@ -0,0 +1,26 @@ +package gropius.model.issue + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import gropius.model.user.permission.NodePermission +import io.github.graphglue.model.* + +@DomainNode +@GraphQLDescription( + """An aggregated IssueRelation. + IssueRelations are aggregated by both start and end Issue. + """ +) +@Authorization(NodePermission.READ, allowFromRelated = ["start"]) +class AggregatedIssueRelation(var count: Int) : Node() { + + @NodeRelationship(AggregatedIssue.OUTGOING_RELATION, Direction.INCOMING) + @GraphQLDescription("The start of this AggregatedIssueRelation.") + @FilterProperty + val start by NodeProperty() + + @NodeRelationship(AggregatedIssue.INCOMING_RELATION, Direction.INCOMING) + @GraphQLDescription("The end of this AggregatedIssueRelation.") + @FilterProperty + val end by NodeProperty() + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/issue/Issue.kt b/core/src/main/kotlin/gropius/model/issue/Issue.kt index eb53a148..e7a3fa26 100644 --- a/core/src/main/kotlin/gropius/model/issue/Issue.kt +++ b/core/src/main/kotlin/gropius/model/issue/Issue.kt @@ -174,4 +174,9 @@ class Issue( ) @FilterProperty val imsIssues by NodeSetProperty() + + @NodeRelationship(AggregatedIssue.ISSUE, Direction.INCOMING) + @GraphQLDescription("AggregatedIssues which aggregate this Issue.") + @FilterProperty + val aggregatedBy by NodeSetProperty() } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/NodeBatchUpdateContext.kt b/core/src/main/kotlin/gropius/service/NodeBatchUpdateContext.kt new file mode 100644 index 00000000..ed3b63c5 --- /dev/null +++ b/core/src/main/kotlin/gropius/service/NodeBatchUpdateContext.kt @@ -0,0 +1,18 @@ +package gropius.service + +import io.github.graphglue.model.Node +import io.github.graphglue.model.property.NodeCache + +/** + * Default implementation for [NodeBatchUpdater] + */ +class NodeBatchUpdateContext : NodeBatchUpdater { + + override val cache = NodeCache() + + override val deletedNodes = mutableSetOf() + + override val internalUpdatedNodes: MutableSet = mutableSetOf() + + override val updatedNodes: Set get() = internalUpdatedNodes - deletedNodes +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/NodeBatchUpdater.kt b/core/src/main/kotlin/gropius/service/NodeBatchUpdater.kt new file mode 100644 index 00000000..fedba378 --- /dev/null +++ b/core/src/main/kotlin/gropius/service/NodeBatchUpdater.kt @@ -0,0 +1,44 @@ +package gropius.service + +import gropius.repository.common.NodeRepository +import io.github.graphglue.model.Node +import io.github.graphglue.model.property.NodeCache +import kotlinx.coroutines.reactor.awaitSingle +import kotlinx.coroutines.reactor.awaitSingleOrNull + +/** + * Used when updating / deleting multiple nodes in a graph. + * Provides a [cache] to ensure that each node is only loaded once. + * Keeps track of [deletedNodes] and [updatedNodes] which can be saved to the database. + */ +interface NodeBatchUpdater { + /** + * Cache used to ensure that only one instance of each Node is loaded by the algorithm + */ + val cache: NodeCache + + /** + * Nodes which should be deleted + */ + val deletedNodes : MutableSet + + /** + * Nodes which are updated and need to be saved + * This may contain nodes also present in [deletedNodes] + */ + val internalUpdatedNodes: MutableSet + + /** + * Nodes which are updated and need to be saved + * Does not include any deleted nodes + */ + val updatedNodes: Set get() = internalUpdatedNodes - deletedNodes + + /** + * Deletes all [deletedNodes] and saves all [updatedNodes] to the database + */ + suspend fun save(nodeRepository: NodeRepository) { + nodeRepository.deleteAll(deletedNodes).awaitSingleOrNull() + nodeRepository.saveAll(updatedNodes).collectList().awaitSingle() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt b/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt index 5ec0c06c..d489ab72 100644 --- a/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt +++ b/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt @@ -5,8 +5,9 @@ import gropius.model.template.ComponentTemplate import gropius.model.template.InterfaceSpecificationDerivationCondition import gropius.model.template.InterfaceSpecificationTemplate import gropius.model.template.RelationPartnerTemplate -import io.github.graphglue.model.Node -import io.github.graphglue.model.property.NodeCache +import gropius.service.NodeBatchUpdateContext +import gropius.service.NodeBatchUpdater +import gropius.service.issue.IssueAggregationUpdater /** * Helper class to handle anything InterfaceSpecification derivation related @@ -23,29 +24,14 @@ import io.github.graphglue.model.property.NodeCache * - [IntraComponentDependencySpecification] * After using, nodes from [deletedNodes] must be deleted, and nodes from [updatedNodes] must be saved * + * @param updateContext the [NodeBatchUpdater] to use for updating/deleting nodes */ -class ComponentGraphUpdater { - /** - * Cache used to ensure that only one instance of each Node is loaded by the algorithm - */ - private val cache = NodeCache() - - /** - * Nodes which should be deleted - */ - val deletedNodes = mutableSetOf() - - /** - * Nodes which are updated and need to be saved - * This may contain nodes also present in [deletedNodes] - */ - private val internalUpdatedNodes: MutableSet = mutableSetOf() +class ComponentGraphUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateContext()) : NodeBatchUpdater by updateContext { /** - * Nodes which are updated and need to be saved - * Does not include any deleted nodes + * Helper for updating aggregated issues */ - val updatedNodes: Set get() = internalUpdatedNodes - deletedNodes + val issueAggregationUpdater = IssueAggregationUpdater(updateContext) /** * Deletes a [Component] @@ -77,6 +63,7 @@ class ComponentGraphUpdater { componentVersion.incomingRelations(cache).forEach { deleteRelation(it) } + issueAggregationUpdater.deletedComponentVersion(componentVersion) } /** @@ -570,6 +557,7 @@ class ComponentGraphUpdater { if (visibleInterface != null) { deleteInterface(visibleInterface) definition.visibleInterface(cache).value = null + issueAggregationUpdater.deletedInterface(visibleInterface) } if (!definition.invisibleSelfDefined && definition.invisibleDerivedBy(cache).isEmpty()) { deletedNodes += definition @@ -596,6 +584,7 @@ class ComponentGraphUpdater { newInterface.template(cache).value = interfaceTemplate internalUpdatedNodes += definition definition.visibleInterface(cache).value = newInterface + issueAggregationUpdater.createdInterface(newInterface) } /** diff --git a/core/src/main/kotlin/gropius/service/architecture/InterfacePartService.kt b/core/src/main/kotlin/gropius/service/architecture/InterfacePartService.kt index 0796088e..823be995 100644 --- a/core/src/main/kotlin/gropius/service/architecture/InterfacePartService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/InterfacePartService.kt @@ -11,8 +11,10 @@ import gropius.model.architecture.InterfaceSpecification import gropius.model.user.permission.NodePermission import gropius.repository.architecture.InterfacePartRepository import gropius.repository.architecture.InterfaceSpecificationRepository +import gropius.repository.common.NodeRepository import gropius.repository.findAllById import gropius.repository.findById +import gropius.service.issue.IssueAggregationUpdater import gropius.service.template.TemplatedNodeService import io.github.graphglue.authorization.Permission import kotlinx.coroutines.reactor.awaitSingle @@ -24,12 +26,14 @@ import org.springframework.stereotype.Service * @param repository the associated repository used for CRUD functionality * @param interfaceSpecificationRepository used get [InterfaceSpecification] by id * @param templatedNodeService used to update templatedFields + * @param nodeRepository used to update/delete nodes */ @Service class InterfacePartService( repository: InterfacePartRepository, private val interfaceSpecificationRepository: InterfaceSpecificationRepository, - private val templatedNodeService: TemplatedNodeService + private val templatedNodeService: TemplatedNodeService, + private val nodeRepository: NodeRepository ) : AffectedByIssueService(repository) { /** @@ -112,13 +116,16 @@ class InterfacePartService( checkPermission( interfacePart, Permission(NodePermission.ADMIN, authorizationContext), "delete the InterfacePart" ) + val issueAggregationUpdater = IssueAggregationUpdater() + issueAggregationUpdater.deletedInterfacePart(interfacePart) + issueAggregationUpdater.save(nodeRepository) repository.delete(interfacePart).awaitSingle() } /** * Gets [InterfacePart]s by id and validates that all are part of [interfaceSpecification] * - * @param ids the ids of the [InterfacePart]s to get + * @param ids the ids of the [InterfacePart]s to get, can be empty * @param interfaceSpecification all returned [InterfacePart]s must be part of this * @return the found[InterfacePart]s * @throws IllegalArgumentException if any [InterfacePart] was not defined by [interfaceSpecification] @@ -126,6 +133,9 @@ class InterfacePartService( suspend fun findPartsByIdAndValidatePartOfInterfaceSpecification( ids: Collection, interfaceSpecification: InterfaceSpecification ): Set { + if (ids.isEmpty()) { + return emptySet() + } val parts = repository.findAllById(ids) parts.forEach { if (it.definedOn().value != interfaceSpecification) { diff --git a/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationVersionService.kt b/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationVersionService.kt index 4f99f6bd..3d3c0bd4 100644 --- a/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationVersionService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationVersionService.kt @@ -6,6 +6,7 @@ import gropius.dto.input.architecture.InterfaceSpecificationVersionInput import gropius.dto.input.architecture.UpdateInterfaceSpecificationVersionInput import gropius.dto.input.common.DeleteNodeInput import gropius.dto.input.ifPresent +import gropius.dto.input.orElse import gropius.model.architecture.Component import gropius.model.architecture.InterfacePart import gropius.model.architecture.InterfaceSpecification @@ -16,6 +17,7 @@ import gropius.repository.architecture.InterfaceSpecificationRepository import gropius.repository.architecture.InterfaceSpecificationVersionRepository import gropius.repository.common.NodeRepository import gropius.repository.findById +import gropius.service.issue.IssueAggregationUpdater import gropius.service.template.TemplatedNodeService import io.github.graphglue.authorization.Permission import kotlinx.coroutines.reactor.awaitSingle @@ -113,19 +115,20 @@ class InterfaceSpecificationVersionService( "update the InterfaceSpecificationVersion" ) val interfaceSpecification = interfaceSpecificationVersion.interfaceSpecification().value - input.addedActiveParts.ifPresent { - interfaceSpecificationVersion.activeParts().addAll( - interfacePartService.findPartsByIdAndValidatePartOfInterfaceSpecification(it, interfaceSpecification) - ) - } - input.removedActiveParts.ifPresent { - interfaceSpecificationVersion.activeParts().removeAll( - interfacePartService.findPartsByIdAndValidatePartOfInterfaceSpecification(it, interfaceSpecification) - ) - } + val addedParts = interfacePartService.findPartsByIdAndValidatePartOfInterfaceSpecification( + input.addedActiveParts.orElse(emptySet()), interfaceSpecification + ) + interfaceSpecificationVersion.activeParts().addAll(addedParts) + val removedParts = interfacePartService.findPartsByIdAndValidatePartOfInterfaceSpecification( + input.removedActiveParts.orElse(emptySet()), interfaceSpecification + ) + interfaceSpecificationVersion.activeParts().removeAll(removedParts) input.version.ifPresent { interfaceSpecificationVersion.version = it } templatedNodeService.updateTemplatedFields(interfaceSpecificationVersion, input, false) updateNamedNode(interfaceSpecificationVersion, input) + val issueAggregationUpdater = IssueAggregationUpdater() + issueAggregationUpdater.updatedActiveParts(interfaceSpecificationVersion, addedParts, removedParts) + issueAggregationUpdater.save(nodeRepository) return repository.save(interfaceSpecificationVersion).awaitSingle() } diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt new file mode 100644 index 00000000..1bc19ff7 --- /dev/null +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -0,0 +1,417 @@ +package gropius.service.issue + +import gropius.model.architecture.* +import gropius.model.issue.AggregatedIssue +import gropius.model.issue.Issue +import gropius.model.template.IssueState +import gropius.model.template.IssueType +import gropius.service.NodeBatchUpdateContext +import gropius.service.NodeBatchUpdater + +class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateContext()) : + NodeBatchUpdater by updateContext { + + suspend fun changedIssueStateOrType(issue: Issue, oldState: IssueState, oldType: IssueType) { + if (issue.type(cache).value == oldType && issue.state(cache).value.isOpen == oldState.isOpen) { + return + } + val relationPartners = issue.aggregatedBy(cache).map { aggregatedBy -> + removeIssueFromAggregatedIssue(issue, aggregatedBy) + aggregatedBy.relationPartner(cache).value + } + issue.aggregatedBy(cache).clear() + relationPartners.forEach { + createOrUpdateAggregatedIssues(it, setOf(issue)) + } + } + + suspend fun deletedIssue(issue: Issue) { + issue.aggregatedBy(cache).forEach { aggregatedBy -> + removeIssueFromAggregatedIssue(issue, aggregatedBy) + } + } + + suspend fun deletedComponentVersion(componentVersion: ComponentVersion) { + relationPartnerDeleted(componentVersion) + for (issue in componentVersion.affectingIssues(cache)) { + aggregateIssueOnComponentIfNecessary(issue, componentVersion.component(cache).value) + } + } + + suspend fun removedIssueFromTrackable(issue: Issue, trackable: Trackable) { + if (trackable !is Component) { + return + } + val aggregatedBy = issue.aggregatedBy(cache) + val removed = mutableSetOf() + aggregatedBy.forEach { + val target = it.relationPartner(cache).value + if (target is ComponentVersion && target.component(cache).value == trackable) { + if (!isIssueStillAggregatedByComponentVersion(issue, target)) { + removeIssueFromAggregatedIssue(issue, it) + removed += it + } + } + } + aggregatedBy.removeAll(removed) + } + + suspend fun createdInterface(createdInterface: Interface) { + val definition = createdInterface.interfaceDefinition(cache).value + val specificationVersion = definition.interfaceSpecificationVersion(cache).value + val affectableEntities = listOf( + createdInterface, + specificationVersion, + specificationVersion.interfaceSpecification(cache).value + ) + specificationVersion.activeParts(cache) + val affectedIssues = affectableEntities.flatMap { it.affectingIssues(cache) }.toSet() + createOrUpdateAggregatedIssues(createdInterface, affectedIssues) + val component = definition.componentVersion(cache).value.component(cache).value + for (issue in affectedIssues) { + unaggregateIssueOnComponentIfNecessary(issue, component) + } + } + + suspend fun deletedInterface(deletedInterface: Interface) { + relationPartnerDeleted(deletedInterface) + val component = + deletedInterface.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value + for (issue in deletedInterface.affectingIssues(cache)) { + aggregateIssueOnComponentIfNecessary(issue, component) + } + } + + suspend fun deletedInterfacePart(interfacePart: InterfacePart) { + val interfaces = interfacePart.activeOn(cache).flatMap { version -> + version.interfaceDefinitions(cache).mapNotNull { + it.visibleInterface(cache).value + } + } + val components = interfaces.map { + it.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value + } + interfacePart.affectingIssues(cache).forEach { issue -> + issue.affects(cache).remove(interfacePart) + interfaces.forEach { + if (!isIssueStillAggregatedByInterface(issue, it)) { + removeIssueFromAggregatedIssueOnRelationPartner(issue, it) + } + } + for (component in components) { + aggregateIssueOnComponentIfNecessary(issue, component) + } + } + } + + suspend fun updatedActiveParts( + interfaceSpecificationVersion: InterfaceSpecificationVersion, + addedParts: Set, + removedParts: Set + ) { + val interfaces = interfaceSpecificationVersion.interfaceDefinitions(cache).mapNotNull { + it.visibleInterface(cache).value + } + val newAffectingIssues = addedParts.flatMap { it.affectingIssues(cache) }.toSet() + val potentialIssuesToRemove = removedParts.flatMap { it.affectingIssues(cache) }.toSet() + for (inter in interfaces) { + createOrUpdateAggregatedIssues(inter, newAffectingIssues) + for (issue in potentialIssuesToRemove) { + if (!isIssueStillAggregatedByInterface(issue, inter)) { + removeIssueFromAggregatedIssueOnRelationPartner(issue, inter) + } + } + } + val components = interfaces.map { + it.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value + }.toSet() + for (component in components) { + for (issue in newAffectingIssues) { + unaggregateIssueOnComponentIfNecessary(issue, component) + } + for (issue in potentialIssuesToRemove) { + aggregateIssueOnComponentIfNecessary(issue, component) + } + } + } + + suspend fun addedAffectedEntity(issue: Issue, affectedEntity: AffectedByIssue) { + when (affectedEntity) { + is Component -> { + affectedEntity.versions(cache).forEach { + createOrUpdateAggregatedIssues(it, setOf(issue)) + } + } + + is ComponentVersion -> { + createOrUpdateAggregatedIssues(affectedEntity, setOf(issue)) + unaggregateIssueOnComponentIfNecessary(issue, affectedEntity.component(cache).value) + } + + is Interface -> { + addedAffectedInterfaceRelatedEntity(issue, setOf(affectedEntity)) + } + + is InterfacePart -> { + addedAffectedInterfaceRelatedEntity(issue, affectedEntity.activeOn(cache)) + } + + is InterfaceSpecificationVersion -> { + addedAffectedInterfaceRelatedEntity(issue, setOf(affectedEntity)) + } + + is InterfaceSpecification -> { + addedAffectedInterfaceRelatedEntity(issue, affectedEntity.versions(cache)) + } + + is Project -> { + // ignore, does not affect components / interfaces + } + + else -> { + error("Unknown affected entity") + } + } + } + + suspend fun removedAffectedEntity(issue: Issue, affectedEntity: AffectedByIssue) { + when (affectedEntity) { + is Component -> { + if (affectedEntity !in issue.trackables(cache) || doesIssueAffectComponentRelatedEntity(issue, affectedEntity)) { + affectedEntity.versions(cache).forEach { + if (it !in issue.affects(cache)) { + removeIssueFromAggregatedIssueOnRelationPartner(issue, it) + } + } + } + } + + is ComponentVersion -> { + val component = affectedEntity.component(cache).value + if (doesIssueAffectComponentRelatedEntity(issue, component)) { + if (component !in issue.affects(cache)) { + removeIssueFromAggregatedIssueOnRelationPartner(issue, affectedEntity) + } + } else { + if (component in issue.trackables(cache)) { + for (componentVersion in component.versions(cache)) { + createOrUpdateAggregatedIssues(componentVersion, setOf(issue)) + } + } else { + removeIssueFromAggregatedIssueOnRelationPartner(issue, affectedEntity) + } + } + } + + is Interface -> { + removedAffectedInterfaceRelatedEntity(issue, setOf(affectedEntity)) + } + + is InterfacePart -> { + removedAffectedInterfaceRelatedEntity(issue, affectedEntity.activeOn(cache)) + } + + is InterfaceSpecificationVersion -> { + removedAffectedInterfaceRelatedEntity(issue, setOf(affectedEntity)) + } + + is InterfaceSpecification -> { + removedAffectedInterfaceRelatedEntity(issue, affectedEntity.versions(cache)) + } + + is Project -> { + // ignore, does not affect components / interfaces + } + + else -> { + error("Unknown affected entity") + } + } + } + + private suspend fun addedAffectedInterfaceRelatedEntity( + issue: Issue, + interfaceSpecificationVersions: Set + ) { + val interfaces = interfaceSpecificationVersions.flatMap { version -> + version.interfaceDefinitions(cache).mapNotNull { it.visibleInterface(cache).value } + } + addedAffectedInterfaceRelatedEntity(issue, interfaces) + } + + private suspend fun addedAffectedInterfaceRelatedEntity( + issue: Issue, + interfaces: Collection + ) { + for (inter in interfaces) { + createOrUpdateAggregatedIssues(inter, setOf(issue)) + } + val components = interfaces.map { + it.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value + }.toSet() + for (component in components) { + unaggregateIssueOnComponentIfNecessary(issue, component) + } + } + + private suspend fun removedAffectedInterfaceRelatedEntity( + issue: Issue, + interfaceSpecificationVersions: Set + ) { + val interfaces = interfaceSpecificationVersions.flatMap { version -> + version.interfaceDefinitions(cache).mapNotNull { it.visibleInterface(cache).value } + } + removedAffectedInterfaceRelatedEntity(issue, interfaces) + } + + private suspend fun removedAffectedInterfaceRelatedEntity( + issue: Issue, + interfaces: Collection + ) { + val addToComponentPotentially = mutableSetOf() + for (inter in interfaces) { + if (!isIssueStillAggregatedByInterface(issue, inter)) { + removeIssueFromAggregatedIssueOnRelationPartner(issue, inter) + addToComponentPotentially += inter.interfaceDefinition(cache).value.componentVersion(cache).value.component( + cache + ).value + } + } + for (component in addToComponentPotentially) { + aggregateIssueOnComponentIfNecessary(issue, component) + } + } + + private suspend fun removeIssueFromAggregatedIssueOnRelationPartner( + issue: Issue, + relationPartner: RelationPartner + ) { + val type = issue.type(cache).value + val isOpen = issue.state(cache).value.isOpen + val aggregatedIssue = relationPartner.aggregatedIssues(cache).find { + it.type(cache).value == type && it.isOpen == isOpen + } ?: return + removeIssueFromAggregatedIssue(issue, aggregatedIssue) + } + + private suspend fun removeIssueFromAggregatedIssue( + issue: Issue, + aggregatedIssue: AggregatedIssue + ) { + aggregatedIssue.issues(cache).remove(issue) + aggregatedIssue.count-- + internalUpdatedNodes += aggregatedIssue + if (aggregatedIssue.issues(cache).isEmpty()) { + deleteAggregatedIssue(aggregatedIssue) + } + } + + private suspend fun isIssueStillAggregatedByComponentVersion( + issue: Issue, + componentVersion: ComponentVersion + ): Boolean { + val affected = issue.affects(cache) + if (componentVersion in affected || componentVersion.component(cache).value in affected) { + return true + } + val component = componentVersion.component(cache).value + if (component in issue.trackables(cache)) { + return !doesIssueAffectComponentRelatedEntity(issue, component) + } + return false + } + + private suspend fun isIssueStillAggregatedByInterface( + issue: Issue, + inter: Interface + ): Boolean { + val specificationVersion = inter.interfaceDefinition(cache).value.interfaceSpecificationVersion(cache).value + val affectableEntities = listOf( + inter, + specificationVersion, + specificationVersion.interfaceSpecification(cache).value + ) + specificationVersion.activeParts(cache) + return issue.affects(cache).any { it in affectableEntities } + } + + private suspend fun doesIssueAffectComponentRelatedEntity(issue: Issue, component: Component): Boolean { + val relatedAffectedEntities = componentRelatedEntities(component) + return issue.affects(cache).any { it in relatedAffectedEntities } + } + + private suspend fun componentRelatedEntities(component: Component): Set { + val affected = mutableSetOf() + affected += component + for (version in component.versions(cache)) { + affected += version + for (interfaceDefinition in version.interfaceDefinitions(cache)) { + val inter = interfaceDefinition.visibleInterface(cache).value + if (inter != null) { + affected += inter + val specificationVersion = interfaceDefinition.interfaceSpecificationVersion(cache).value + affected += specificationVersion + affected += specificationVersion.interfaceSpecification(cache).value + affected += specificationVersion.activeParts(cache) + } + } + } + return affected + } + + private suspend fun createOrUpdateAggregatedIssues(relationPartner: RelationPartner, issues: Set) { + val aggregatedIssues = relationPartner.aggregatedIssues(cache) + val aggregatedIssueLookup = aggregatedIssues.associateBy { + it.type(cache).value to it.isOpen + }.toMutableMap() + for (issue in issues) { + val state = issue.state(cache).value + val type = issue.type(cache).value + aggregatedIssueLookup.getOrPut(type to state.isOpen) { + val aggregatedIssue = AggregatedIssue(0, state.isOpen) + aggregatedIssue + }.let { + if (it.issues(cache).add(issue)) { + it.count++ + } + internalUpdatedNodes += it + } + } + } + + private suspend fun relationPartnerDeleted(relationPartner: RelationPartner) { + relationPartner.aggregatedIssues(cache).forEach { + deleteAggregatedIssue(it) + } + relationPartner.affectingIssues(cache).forEach { issue -> + issue.affects(cache).remove(relationPartner) + } + } + + private suspend fun deleteAggregatedIssue(aggregatedIssue: AggregatedIssue) { + deletedNodes += aggregatedIssue + deletedNodes += aggregatedIssue.incomingRelations(cache) + deletedNodes += aggregatedIssue.outgoingRelations(cache) + } + + private suspend fun aggregateIssueOnComponentIfNecessary(issue: Issue, component: Component) { + if (component !in issue.trackables(cache)) { + return + } + if (!doesIssueAffectComponentRelatedEntity(issue, component)) { + for (componentVersion in component.versions(cache)) { + createOrUpdateAggregatedIssues(componentVersion, setOf(issue)) + } + } + } + + private suspend fun unaggregateIssueOnComponentIfNecessary(issue: Issue, component: Component) { + if (component in issue.trackables(cache)) { + return + } + if (doesIssueAffectComponentRelatedEntity(issue, component)) { + for (componentVersion in component.versions(cache)) { + removeIssueFromAggregatedIssueOnRelationPartner(issue, componentVersion) + } + } + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/issue/IssueService.kt b/core/src/main/kotlin/gropius/service/issue/IssueService.kt index 1e08eea3..d6f95a4f 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueService.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueService.kt @@ -287,6 +287,9 @@ class IssueService( updateIssueRelationsAfterTemplateUpdate( issue, event, issueRelationTypeMapping, atTime.plusNanos(timeOffset), byUser ) + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.changedIssueStateOrType(issue, state ?: issue.state().value, type ?: issue.type().value) + aggregationUpdater.save(nodeRepository) } return event } @@ -466,6 +469,9 @@ class IssueService( "delete Issues on a Trackable the Issue is on" ) } + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.deletedIssue(issue) + aggregationUpdater.save(nodeRepository) nodeRepository.deleteAll(prepareIssueDeletion(issue)).awaitSingleOrNull() } @@ -565,9 +571,13 @@ class IssueService( val trackable = trackableRepository.findById(input.trackable) checkManageIssuesPermission(trackable, authorizationContext) return if (trackable in issue.trackables()) { - timelineItemRepository.save( + val event = timelineItemRepository.save( removeIssueFromTrackable(issue, trackable, OffsetDateTime.now(), getUser(authorizationContext)) ).awaitSingle() + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.removedIssueFromTrackable(issue, trackable) + aggregationUpdater.save(nodeRepository) + event } else { null } @@ -1261,6 +1271,9 @@ class IssueService( event.newState().value = newState event.oldState().value = oldState changeIssueProperty(issue, newState, atTime, byUser, issue.state()::value, event) + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.changedIssueStateOrType(issue, oldState, issue.type().value) + aggregationUpdater.save(nodeRepository) return event } @@ -1321,6 +1334,9 @@ class IssueService( event.newType().value = newType event.oldType().value = oldType changeIssueProperty(issue, newType, atTime, byUser, issue.type()::value, event) + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.changedIssueStateOrType(issue, issue.state().value, oldType) + aggregationUpdater.save(nodeRepository) return event } @@ -1397,6 +1413,9 @@ class IssueService( ) { it.removedAffectedEntity().value == affectedEntity } ) { issue.affects() += affectedEntity + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.addedAffectedEntity(issue, affectedEntity) + aggregationUpdater.save(nodeRepository) } return event } @@ -1464,6 +1483,9 @@ class IssueService( ) { it.addedAffectedEntity().value == affectedEntity } ) { issue.affects() -= affectedEntity + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.removedAffectedEntity(issue, affectedEntity) + aggregationUpdater.save(nodeRepository) } return event } From 2edf9e44d3841ba543cc00efe012527e0ea46677 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Tue, 19 Sep 2023 04:28:58 +0200 Subject: [PATCH 02/30] filter for AffectedbyIssue related to Trackable --- .../gropius/graphql/GraphQLConfiguration.kt | 18 ++++++-- .../AffectedByIssueRelatedToFilterEntry.kt | 31 ++++++++++++++ ...edByIssueRelatedToFilterEntryDefinition.kt | 41 +++++++++++++++++++ .../model/architecture/AffectedByIssue.kt | 13 +++--- .../model/architecture/ComponentVersion.kt | 1 + .../gropius/model/architecture/Interface.kt | 1 + .../model/architecture/InterfaceDefinition.kt | 1 + .../model/architecture/InterfacePart.kt | 1 + .../architecture/InterfaceSpecification.kt | 1 + .../InterfaceSpecificationVersion.kt | 1 + .../gropius/model/architecture/Trackable.kt | 2 +- .../user/permission/TrackablePermission.kt | 7 ++++ .../InterfaceSpecificationService.kt | 1 + 13 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt create mode 100644 api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntryDefinition.kt diff --git a/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt b/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt index cc3f85c9..7b484965 100644 --- a/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt +++ b/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt @@ -16,19 +16,20 @@ import graphql.scalars.regex.RegexScalar import graphql.schema.* import gropius.authorization.checkPermission import gropius.authorization.gropiusAuthorizationContext -import gropius.graphql.filter.DateTimeFilterDefinition -import gropius.graphql.filter.DurationFilterDefinition -import gropius.graphql.filter.NodePermissionFilterEntryDefinition -import gropius.graphql.filter.TemplatedFieldsFilterEntryDefinition +import gropius.graphql.filter.* +import gropius.model.architecture.RELATED_TO_FILTER_BEAN import gropius.model.common.PERMISSION_FIELD_BEAN import gropius.model.template.TEMPLATED_FIELDS_FILTER_BEAN import gropius.model.template.TemplatedNode import gropius.model.user.GropiusUser import gropius.model.user.NODE_PERMISSION_FILTER_BEAN import gropius.model.user.permission.ALL_PERMISSION_ENTRY_NAME +import gropius.model.architecture.Trackable +import gropius.model.architecture.AffectedByIssue import gropius.util.JsonNodeMapper import io.github.graphglue.authorization.Permission import io.github.graphglue.connection.filter.TypeFilterDefinitionEntry +import io.github.graphglue.connection.filter.definition.SubFilterGenerator import io.github.graphglue.connection.filter.definition.scalars.StringFilterDefinition import io.github.graphglue.definition.ExtensionFieldDefinition import io.github.graphglue.definition.NodeDefinition @@ -160,6 +161,15 @@ class GraphQLConfiguration { @Bean(NODE_PERMISSION_FILTER_BEAN) fun nodePermissionFilter(nodeDefinitionCollection: NodeDefinitionCollection) = NodePermissionFilterEntryDefinition(nodeDefinitionCollection) + /** + * Filter for [AffectedByIssue]s which are related to a specific [Trackable] + * + * @param nodeDefinitionCollection used to get the node definition + * @return the generated filter definition + */ + @Bean(RELATED_TO_FILTER_BEAN) + fun relatedToFilter(nodeDefinitionCollection: NodeDefinitionCollection) = AffectedByIssueRelatedToFilterEntryDefinition(nodeDefinitionCollection) + /** * Provides the permission field for all nodes * diff --git a/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt b/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt new file mode 100644 index 00000000..d8fbc217 --- /dev/null +++ b/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt @@ -0,0 +1,31 @@ +package gropius.graphql.filter + +import gropius.model.user.permission.TrackablePermission +import io.github.graphglue.authorization.Permission +import io.github.graphglue.connection.filter.model.Filter +import io.github.graphglue.connection.filter.model.FilterEntry +import org.neo4j.cypherdsl.core.Condition +import org.neo4j.cypherdsl.core.Conditions +import org.neo4j.cypherdsl.core.Cypher +import org.neo4j.cypherdsl.core.Node + +class AffectedByIssueRelatedToFilterEntry( + val filter: String, + private val relatedToFilterEntryDefinition: AffectedByIssueRelatedToFilterEntryDefinition, + private val permission: Permission? + +) : FilterEntry(relatedToFilterEntryDefinition) { + + override fun generateCondition(node: Node): Condition { + val relatedNode = Cypher.anyNode(node.requiredSymbolicName.value + "_") + val relationship = node.relationshipTo(relatedNode).min(0) + .withProperties(mapOf(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY to Cypher.literalTrue())) + val authCondition = if (permission != null) { + relatedToFilterEntryDefinition.generateAuthorizationCondition(permission).generateCondition(relatedNode) + } else { + Conditions.isTrue() + } + return Cypher.match(relationship).where(authCondition).asCondition() + } + +} \ No newline at end of file diff --git a/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntryDefinition.kt b/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntryDefinition.kt new file mode 100644 index 00000000..4f2afbc8 --- /dev/null +++ b/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntryDefinition.kt @@ -0,0 +1,41 @@ +package gropius.graphql.filter + +import graphql.Scalars +import graphql.schema.GraphQLInputType +import gropius.model.architecture.Trackable +import io.github.graphglue.authorization.Permission +import io.github.graphglue.connection.filter.definition.FilterEntryDefinition +import io.github.graphglue.connection.filter.definition.SubFilterGenerator +import io.github.graphglue.connection.filter.definition.generateFilterDefinition +import io.github.graphglue.connection.filter.model.FilterEntry +import io.github.graphglue.data.execution.CypherConditionGenerator +import io.github.graphglue.definition.NodeDefinitionCollection +import io.github.graphglue.util.CacheMap + +class AffectedByIssueRelatedToFilterEntryDefinition( + private val nodeDefinitionCollection: NodeDefinitionCollection, +) : FilterEntryDefinition("relatedTo", "Filters for AffectedByIssues which are related to a Trackable") { + + /** + * Provides a condition generator used to filter for Trackables which the Permissions allows to access + * + * @param permission the current read permission, used to only consider nodes in filters which match the permission + * @return the generated condition generator + */ + fun generateAuthorizationCondition(permission: Permission): CypherConditionGenerator { + return nodeDefinitionCollection.generateAuthorizationCondition( + nodeDefinitionCollection.getNodeDefinition(), + permission + ) + } + + override fun parseEntry(value: Any?, permission: Permission?): FilterEntry { + return AffectedByIssueRelatedToFilterEntry( + value!! as String, + this, + permission + ) + } + + override fun toGraphQLType(inputTypeCache: CacheMap): GraphQLInputType = Scalars.GraphQLID +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/architecture/AffectedByIssue.kt b/core/src/main/kotlin/gropius/model/architecture/AffectedByIssue.kt index c168f030..d50ac4d7 100644 --- a/core/src/main/kotlin/gropius/model/architecture/AffectedByIssue.kt +++ b/core/src/main/kotlin/gropius/model/architecture/AffectedByIssue.kt @@ -3,17 +3,20 @@ package gropius.model.architecture import com.expediagroup.graphql.generator.annotations.GraphQLDescription import gropius.model.common.NamedNode import gropius.model.issue.Issue -import io.github.graphglue.model.Direction -import io.github.graphglue.model.DomainNode -import io.github.graphglue.model.FilterProperty -import io.github.graphglue.model.NodeRelationship +import io.github.graphglue.model.* -@DomainNode +/** + * Name of the bean defining the relatedTo filter + */ +const val RELATED_TO_FILTER_BEAN = "relatedToFilter" + +@DomainNode(searchQueryName = "searchAffectedByIssues") @GraphQLDescription( """Entities that can be affected by an Issue, meaning that this entity is in some regard impacted by e.g. a bug described by an issue, or the non-present of a feature described by an issue. """ ) +@AdditionalFilter(RELATED_TO_FILTER_BEAN) abstract class AffectedByIssue(name: String, description: String) : NamedNode(name, description) { @NodeRelationship(Issue.AFFECTS, Direction.INCOMING) diff --git a/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt b/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt index 6483ce1c..9cf6c988 100644 --- a/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt +++ b/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt @@ -26,6 +26,7 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty @Authorization(ComponentPermission.RELATE_FROM_COMPONENT, allowFromRelated = ["component"]) @Authorization(ComponentPermission.ADD_TO_PROJECTS, allowFromRelated = ["component"]) @Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["component"]) +@Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["component"]) class ComponentVersion( name: String, description: String, diff --git a/core/src/main/kotlin/gropius/model/architecture/Interface.kt b/core/src/main/kotlin/gropius/model/architecture/Interface.kt index 33238b6f..bc648389 100644 --- a/core/src/main/kotlin/gropius/model/architecture/Interface.kt +++ b/core/src/main/kotlin/gropius/model/architecture/Interface.kt @@ -25,6 +25,7 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty @Authorization(NodePermission.ADMIN, allowFromRelated = ["interfaceDefinition"]) @Authorization(ComponentPermission.RELATE_FROM_COMPONENT, allowFromRelated = ["interfaceDefinition"]) @Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["interfaceDefinition"]) +@Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["interfaceDefinition"]) class Interface( name: String, description: String, diff --git a/core/src/main/kotlin/gropius/model/architecture/InterfaceDefinition.kt b/core/src/main/kotlin/gropius/model/architecture/InterfaceDefinition.kt index f7cfcc69..afc0ed07 100644 --- a/core/src/main/kotlin/gropius/model/architecture/InterfaceDefinition.kt +++ b/core/src/main/kotlin/gropius/model/architecture/InterfaceDefinition.kt @@ -24,6 +24,7 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty @Authorization(NodePermission.ADMIN, allowFromRelated = ["componentVersion"]) @Authorization(ComponentPermission.RELATE_FROM_COMPONENT, allowFromRelated = ["componentVersion"]) @Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["componentVersion"]) +@Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["componentVersion"]) class InterfaceDefinition( @property:GraphQLDescription( """If true, `interfaceSpecificationVersion`is self-defined visible on the `componentVersion`""" diff --git a/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt b/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt index def2c5bf..60777a62 100644 --- a/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt +++ b/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt @@ -23,6 +23,7 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty @Authorization(NodePermission.READ, allowFromRelated = ["definedOn"]) @Authorization(NodePermission.ADMIN, allowFromRelated = ["definedOn"]) @Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["definedOn"]) +@Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["activeOn"]) class InterfacePart( name: String, description: String, diff --git a/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecification.kt b/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecification.kt index fa6ffb9f..fbcbbba7 100644 --- a/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecification.kt +++ b/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecification.kt @@ -22,6 +22,7 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty @Authorization(NodePermission.READ, allowFromRelated = ["component", "versions"]) @Authorization(NodePermission.ADMIN, allowFromRelated = ["component"]) @Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["component"]) +@Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["versions"]) class InterfaceSpecification( name: String, description: String, diff --git a/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecificationVersion.kt b/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecificationVersion.kt index 0373660c..24facb7d 100644 --- a/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecificationVersion.kt +++ b/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecificationVersion.kt @@ -27,6 +27,7 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty ) @Authorization(NodePermission.ADMIN, allowFromRelated = ["interfaceSpecification"]) @Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["interfaceSpecification"]) +@Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["interfaceDefinitions"]) class InterfaceSpecificationVersion( name: String, description: String, diff --git a/core/src/main/kotlin/gropius/model/architecture/Trackable.kt b/core/src/main/kotlin/gropius/model/architecture/Trackable.kt index c2cfb8c9..2a1150a4 100644 --- a/core/src/main/kotlin/gropius/model/architecture/Trackable.kt +++ b/core/src/main/kotlin/gropius/model/architecture/Trackable.kt @@ -10,7 +10,7 @@ import gropius.model.user.permission.TrackablePermission import io.github.graphglue.model.* import java.net.URI -@DomainNode +@DomainNode(searchQueryName = "searchTrackables") @GraphQLDescription( """An entity which can have Issues, Labels and Artefacts. Has pinned issues. diff --git a/core/src/main/kotlin/gropius/model/user/permission/TrackablePermission.kt b/core/src/main/kotlin/gropius/model/user/permission/TrackablePermission.kt index 655dd05d..8ce450bd 100644 --- a/core/src/main/kotlin/gropius/model/user/permission/TrackablePermission.kt +++ b/core/src/main/kotlin/gropius/model/user/permission/TrackablePermission.kt @@ -8,6 +8,7 @@ import gropius.model.issue.Artefact import gropius.model.issue.Issue import gropius.model.issue.Label import gropius.model.issue.timeline.Comment +import gropius.model.architecture.AffectedByIssue import io.github.graphglue.model.DomainNode /** @@ -89,6 +90,12 @@ abstract class TrackablePermission( * Permission to check if the user can add [Label]s on the [Trackable] to other [Trackable]s */ const val EXPORT_LABELS = "EXPORT_LABELS" + + /** + * Used to track [AffectedByIssue] entities relative to a trackable + * Not used for permission checking, rather for filtering + */ + const val RELATED_ISSUE_AFFECTED_ENTITY = "RELATED_ISSUE_AFFECTED_ENTITY" } } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationService.kt b/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationService.kt index e9a3049d..213c6248 100644 --- a/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationService.kt @@ -86,6 +86,7 @@ class InterfaceSpecificationService( val templatedFields = templatedNodeService.validateInitialTemplatedFields(template, input) val interfaceSpecification = InterfaceSpecification(input.name, input.description, templatedFields) interfaceSpecification.template().value = template + interfaceSpecification.component().value = component createdExtensibleNode(interfaceSpecification, input) input.versions.ifPresent { inputs -> interfaceSpecification.versions() += inputs.map { From 3a58f84b79944f5e13a8de6e8bc49b304b3e1d21 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sat, 23 Sep 2023 20:26:25 +0200 Subject: [PATCH 03/30] update issue graph, bugfixes, filter by associated project --- .../gropius/graphql/GraphQLConfiguration.kt | 21 ++++++---- .../AffectedByIssueRelatedToFilterEntry.kt | 5 ++- .../filter/PartOfProjectFilterEntry.kt | 33 ++++++++++++++++ .../PartOfProjectFilterEntryDefinition.kt | 39 +++++++++++++++++++ .../AddComponentVersionToProjectPayload.kt | 16 ++++++++ .../gropius/dto/payload/DeleteNodePayload.kt | 6 ++- .../schema/mutation/ArchitectureMutations.kt | 7 ++-- .../model/architecture/ComponentVersion.kt | 2 + .../gropius/model/architecture/Interface.kt | 2 + .../model/architecture/InterfaceDefinition.kt | 2 + .../model/architecture/RelationPartner.kt | 11 ++++-- .../user/permission/ProjectPermission.kt | 6 +++ .../architecture/ComponentGraphUpdater.kt | 2 + .../architecture/ComponentVersionService.kt | 3 +- .../service/architecture/ProjectService.kt | 11 ++++-- .../service/issue/IssueAggregationUpdater.kt | 2 + 16 files changed, 145 insertions(+), 23 deletions(-) create mode 100644 api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntry.kt create mode 100644 api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntryDefinition.kt create mode 100644 api-public/src/main/kotlin/gropius/dto/payload/AddComponentVersionToProjectPayload.kt diff --git a/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt b/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt index 7b484965..cf568a81 100644 --- a/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt +++ b/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt @@ -17,19 +17,16 @@ import graphql.schema.* import gropius.authorization.checkPermission import gropius.authorization.gropiusAuthorizationContext import gropius.graphql.filter.* -import gropius.model.architecture.RELATED_TO_FILTER_BEAN +import gropius.model.architecture.* import gropius.model.common.PERMISSION_FIELD_BEAN import gropius.model.template.TEMPLATED_FIELDS_FILTER_BEAN import gropius.model.template.TemplatedNode import gropius.model.user.GropiusUser import gropius.model.user.NODE_PERMISSION_FILTER_BEAN import gropius.model.user.permission.ALL_PERMISSION_ENTRY_NAME -import gropius.model.architecture.Trackable -import gropius.model.architecture.AffectedByIssue import gropius.util.JsonNodeMapper import io.github.graphglue.authorization.Permission import io.github.graphglue.connection.filter.TypeFilterDefinitionEntry -import io.github.graphglue.connection.filter.definition.SubFilterGenerator import io.github.graphglue.connection.filter.definition.scalars.StringFilterDefinition import io.github.graphglue.definition.ExtensionFieldDefinition import io.github.graphglue.definition.NodeDefinition @@ -159,7 +156,8 @@ class GraphQLConfiguration { * @return the generated filter definition */ @Bean(NODE_PERMISSION_FILTER_BEAN) - fun nodePermissionFilter(nodeDefinitionCollection: NodeDefinitionCollection) = NodePermissionFilterEntryDefinition(nodeDefinitionCollection) + fun nodePermissionFilter(nodeDefinitionCollection: NodeDefinitionCollection) = + NodePermissionFilterEntryDefinition(nodeDefinitionCollection) /** * Filter for [AffectedByIssue]s which are related to a specific [Trackable] @@ -168,7 +166,15 @@ class GraphQLConfiguration { * @return the generated filter definition */ @Bean(RELATED_TO_FILTER_BEAN) - fun relatedToFilter(nodeDefinitionCollection: NodeDefinitionCollection) = AffectedByIssueRelatedToFilterEntryDefinition(nodeDefinitionCollection) + fun relatedToFilter(nodeDefinitionCollection: NodeDefinitionCollection) = + AffectedByIssueRelatedToFilterEntryDefinition(nodeDefinitionCollection) + + /** + * Filter for [RelationPartner]s which part of the graph of a specific [Project] + */ + @Bean(PART_OF_PROJECT_FILTER) + fun partOfProjectFilter(nodeDefinitionCollection: NodeDefinitionCollection) = + PartOfProjectFilterEntryDefinition(nodeDefinitionCollection) /** * Provides the permission field for all nodes @@ -200,8 +206,7 @@ class GraphQLConfiguration { ): Expression { return if (dfe.checkPermission) { val conditionGenerator = nodeDefinitionCollection.generateAuthorizationCondition( - nodeDefinition, - Permission(arguments["permission"] as String, dfe.gropiusAuthorizationContext) + nodeDefinition, Permission(arguments["permission"] as String, dfe.gropiusAuthorizationContext) ) val condition = conditionGenerator.generateCondition(node) condition diff --git a/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt b/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt index d8fbc217..f09b70ca 100644 --- a/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt +++ b/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt @@ -23,9 +23,10 @@ class AffectedByIssueRelatedToFilterEntry( val authCondition = if (permission != null) { relatedToFilterEntryDefinition.generateAuthorizationCondition(permission).generateCondition(relatedNode) } else { - Conditions.isTrue() + Conditions.noCondition() } - return Cypher.match(relationship).where(authCondition).asCondition() + val idCondition = relatedNode.property("id").isEqualTo(Cypher.anonParameter(filter)) + return Cypher.match(relationship).where(idCondition.and(authCondition)).asCondition() } } \ No newline at end of file diff --git a/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntry.kt b/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntry.kt new file mode 100644 index 00000000..5eb60b26 --- /dev/null +++ b/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntry.kt @@ -0,0 +1,33 @@ +package gropius.graphql.filter + +import gropius.model.user.permission.ProjectPermission +import gropius.model.user.permission.TrackablePermission +import io.github.graphglue.authorization.Permission +import io.github.graphglue.connection.filter.model.Filter +import io.github.graphglue.connection.filter.model.FilterEntry +import org.neo4j.cypherdsl.core.Condition +import org.neo4j.cypherdsl.core.Conditions +import org.neo4j.cypherdsl.core.Cypher +import org.neo4j.cypherdsl.core.Node + +class PartOfProjectFilterEntry( + val filter: String, + private val partOfProjectFilterEntryDefinition: PartOfProjectFilterEntryDefinition, + private val permission: Permission? + +) : FilterEntry(partOfProjectFilterEntryDefinition) { + + override fun generateCondition(node: Node): Condition { + val relatedNode = Cypher.anyNode(node.requiredSymbolicName.value + "_") + val relationship = node.relationshipTo(relatedNode).min(0) + .withProperties(mapOf(ProjectPermission.PART_OF_PROJECT to Cypher.literalTrue())) + val authCondition = if (permission != null) { + partOfProjectFilterEntryDefinition.generateAuthorizationCondition(permission).generateCondition(relatedNode) + } else { + Conditions.noCondition() + } + val idCondition = relatedNode.property("id").isEqualTo(Cypher.anonParameter(filter)) + return Cypher.match(relationship).where(idCondition.and(authCondition)).asCondition() + } + +} \ No newline at end of file diff --git a/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntryDefinition.kt b/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntryDefinition.kt new file mode 100644 index 00000000..6f747fa9 --- /dev/null +++ b/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntryDefinition.kt @@ -0,0 +1,39 @@ +package gropius.graphql.filter + +import graphql.Scalars +import graphql.schema.GraphQLInputType +import gropius.model.architecture.Project +import io.github.graphglue.authorization.Permission +import io.github.graphglue.connection.filter.definition.FilterEntryDefinition +import io.github.graphglue.connection.filter.model.FilterEntry +import io.github.graphglue.data.execution.CypherConditionGenerator +import io.github.graphglue.definition.NodeDefinitionCollection +import io.github.graphglue.util.CacheMap + +class PartOfProjectFilterEntryDefinition( + private val nodeDefinitionCollection: NodeDefinitionCollection, +) : FilterEntryDefinition("partOfProject", "Filters for RelationPartners which are part of a Project's component graph") { + + /** + * Provides a condition generator used to filter for Projects which the Permissions allows to access + * + * @param permission the current read permission, used to only consider nodes in filters which match the permission + * @return the generated condition generator + */ + fun generateAuthorizationCondition(permission: Permission): CypherConditionGenerator { + return nodeDefinitionCollection.generateAuthorizationCondition( + nodeDefinitionCollection.getNodeDefinition(), + permission + ) + } + + override fun parseEntry(value: Any?, permission: Permission?): FilterEntry { + return PartOfProjectFilterEntry( + value!! as String, + this, + permission + ) + } + + override fun toGraphQLType(inputTypeCache: CacheMap): GraphQLInputType = Scalars.GraphQLID +} \ No newline at end of file diff --git a/api-public/src/main/kotlin/gropius/dto/payload/AddComponentVersionToProjectPayload.kt b/api-public/src/main/kotlin/gropius/dto/payload/AddComponentVersionToProjectPayload.kt new file mode 100644 index 00000000..bf79580e --- /dev/null +++ b/api-public/src/main/kotlin/gropius/dto/payload/AddComponentVersionToProjectPayload.kt @@ -0,0 +1,16 @@ +package gropius.dto.payload + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import gropius.model.architecture.ComponentVersion +import gropius.model.architecture.Project + +/** + * Payload type for the addComponentVersionToProject mutation + */ +@GraphQLDescription("Payload type for the addComponentVersionToProject mutation") +class AddComponentVersionToProjectPayload( + @GraphQLDescription("The updated project") + val project: Project, + @GraphQLDescription("The added component version") + val componentVersion: ComponentVersion +) \ No newline at end of file diff --git a/api-public/src/main/kotlin/gropius/dto/payload/DeleteNodePayload.kt b/api-public/src/main/kotlin/gropius/dto/payload/DeleteNodePayload.kt index 22f0f6de..1115c0f8 100644 --- a/api-public/src/main/kotlin/gropius/dto/payload/DeleteNodePayload.kt +++ b/api-public/src/main/kotlin/gropius/dto/payload/DeleteNodePayload.kt @@ -6,4 +6,8 @@ import com.expediagroup.graphql.generator.scalars.ID /** * Payload type for delete node mutations */ -class DeleteNodePayload(@GraphQLDescription("The id of the deleted Node") val id: ID) \ No newline at end of file +@GraphQLDescription("Payload type for delete node mutations") +class DeleteNodePayload( + @GraphQLDescription("The id of the deleted Node") + val id: ID +) \ No newline at end of file diff --git a/api-public/src/main/kotlin/gropius/schema/mutation/ArchitectureMutations.kt b/api-public/src/main/kotlin/gropius/schema/mutation/ArchitectureMutations.kt index 2de5eaee..32422d05 100644 --- a/api-public/src/main/kotlin/gropius/schema/mutation/ArchitectureMutations.kt +++ b/api-public/src/main/kotlin/gropius/schema/mutation/ArchitectureMutations.kt @@ -7,6 +7,7 @@ import graphql.schema.DataFetchingEnvironment import gropius.authorization.gropiusAuthorizationContext import gropius.dto.input.architecture.* import gropius.dto.input.common.DeleteNodeInput +import gropius.dto.payload.AddComponentVersionToProjectPayload import gropius.dto.payload.DeleteNodePayload import gropius.graphql.AutoPayloadType import gropius.model.architecture.* @@ -346,12 +347,12 @@ class ArchitectureMutations( with the ComponentVersion """ ) - @AutoPayloadType("The updated Project") suspend fun addComponentVersionToProject( @GraphQLDescription("Defines which ComponentVersion to add to which Project") input: AddComponentVersionToProjectInput, dfe: DataFetchingEnvironment - ): Project { - return projectService.addComponentVersionToProject(dfe.gropiusAuthorizationContext, input) + ): AddComponentVersionToProjectPayload { + val (project, componentVersion) = projectService.addComponentVersionToProject(dfe.gropiusAuthorizationContext, input) + return AddComponentVersionToProjectPayload(project, componentVersion) } @GraphQLDescription( diff --git a/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt b/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt index 9cf6c988..03a27d04 100644 --- a/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt +++ b/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt @@ -8,6 +8,7 @@ import gropius.model.template.MutableTemplatedNode import gropius.model.template.RelationPartnerTemplate import gropius.model.user.permission.ComponentPermission import gropius.model.user.permission.NodePermission +import gropius.model.user.permission.ProjectPermission import gropius.model.user.permission.TrackablePermission import io.github.graphglue.model.* import io.github.graphglue.model.property.NodeCache @@ -27,6 +28,7 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty @Authorization(ComponentPermission.ADD_TO_PROJECTS, allowFromRelated = ["component"]) @Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["component"]) @Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["component"]) +@Authorization(ProjectPermission.PART_OF_PROJECT, allowFromRelated = ["includingProjects"]) class ComponentVersion( name: String, description: String, diff --git a/core/src/main/kotlin/gropius/model/architecture/Interface.kt b/core/src/main/kotlin/gropius/model/architecture/Interface.kt index bc648389..16dfd1a9 100644 --- a/core/src/main/kotlin/gropius/model/architecture/Interface.kt +++ b/core/src/main/kotlin/gropius/model/architecture/Interface.kt @@ -8,6 +8,7 @@ import gropius.model.template.MutableTemplatedNode import gropius.model.template.RelationPartnerTemplate import gropius.model.user.permission.ComponentPermission import gropius.model.user.permission.NodePermission +import gropius.model.user.permission.ProjectPermission import gropius.model.user.permission.TrackablePermission import io.github.graphglue.model.* import io.github.graphglue.model.property.NodeCache @@ -26,6 +27,7 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty @Authorization(ComponentPermission.RELATE_FROM_COMPONENT, allowFromRelated = ["interfaceDefinition"]) @Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["interfaceDefinition"]) @Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["interfaceDefinition"]) +@Authorization(ProjectPermission.PART_OF_PROJECT, allowFromRelated = ["interfaceDefinition"]) class Interface( name: String, description: String, diff --git a/core/src/main/kotlin/gropius/model/architecture/InterfaceDefinition.kt b/core/src/main/kotlin/gropius/model/architecture/InterfaceDefinition.kt index afc0ed07..e1c0e324 100644 --- a/core/src/main/kotlin/gropius/model/architecture/InterfaceDefinition.kt +++ b/core/src/main/kotlin/gropius/model/architecture/InterfaceDefinition.kt @@ -8,6 +8,7 @@ import gropius.model.template.InterfaceDefinitionTemplate import gropius.model.template.MutableTemplatedNode import gropius.model.user.permission.ComponentPermission import gropius.model.user.permission.NodePermission +import gropius.model.user.permission.ProjectPermission import gropius.model.user.permission.TrackablePermission import io.github.graphglue.model.* import org.springframework.data.neo4j.core.schema.CompositeProperty @@ -25,6 +26,7 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty @Authorization(ComponentPermission.RELATE_FROM_COMPONENT, allowFromRelated = ["componentVersion"]) @Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["componentVersion"]) @Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["componentVersion"]) +@Authorization(ProjectPermission.PART_OF_PROJECT, allowFromRelated = ["componentVersion"]) class InterfaceDefinition( @property:GraphQLDescription( """If true, `interfaceSpecificationVersion`is self-defined visible on the `componentVersion`""" diff --git a/core/src/main/kotlin/gropius/model/architecture/RelationPartner.kt b/core/src/main/kotlin/gropius/model/architecture/RelationPartner.kt index ba967f89..f6ff7bb6 100644 --- a/core/src/main/kotlin/gropius/model/architecture/RelationPartner.kt +++ b/core/src/main/kotlin/gropius/model/architecture/RelationPartner.kt @@ -5,14 +5,17 @@ import com.expediagroup.graphql.generator.annotations.GraphQLIgnore import gropius.model.issue.AggregatedIssue import gropius.model.template.RelationPartnerTemplate import gropius.model.template.TemplatedNode -import io.github.graphglue.model.Direction -import io.github.graphglue.model.DomainNode -import io.github.graphglue.model.FilterProperty -import io.github.graphglue.model.NodeRelationship +import io.github.graphglue.model.* import io.github.graphglue.model.property.NodeCache +/** + * Name of the bean defining the partOfProject filter + */ +const val PART_OF_PROJECT_FILTER = "partOfProject" + @DomainNode @GraphQLDescription("Entity which can be used as start / end of Relations. Can be affected by Issues.") +@AdditionalFilter(PART_OF_PROJECT_FILTER) abstract class RelationPartner(name: String, description: String) : AffectedByIssue(name, description), TemplatedNode { companion object { const val INCOMING_RELATION = "INCOMING_RELATION" diff --git a/core/src/main/kotlin/gropius/model/user/permission/ProjectPermission.kt b/core/src/main/kotlin/gropius/model/user/permission/ProjectPermission.kt index dc8d90b3..2ccbf0fc 100644 --- a/core/src/main/kotlin/gropius/model/user/permission/ProjectPermission.kt +++ b/core/src/main/kotlin/gropius/model/user/permission/ProjectPermission.kt @@ -4,6 +4,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDescription import gropius.graphql.TypeGraphQLType import gropius.model.architecture.ComponentVersion import gropius.model.architecture.Project +import gropius.model.architecture.RelationPartner import io.github.graphglue.model.DomainNode /** @@ -22,6 +23,11 @@ class ProjectPermission( * Permission to check if a user is allowed to add / remove [ComponentVersion]s to / from the [Project] */ const val MANAGE_COMPONENTS = "MANAGE_COMPONENTS" + + /** + * Used to track [RelationPartner]s which are part of the graph of a [Project] + */ + const val PART_OF_PROJECT = "PART_OF_PROJECT" } @GraphQLDescription(ENTRIES_DESCRIPTION) diff --git a/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt b/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt index d489ab72..0ed70f82 100644 --- a/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt +++ b/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt @@ -223,6 +223,7 @@ class ComponentGraphUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateCon definition.invisibleSelfDefined = true } internalUpdatedNodes += definition + handleUpdatedInterfaceDefinition(definition) addForUpdatedComponentVersion(componentVersion, setOf(definition)) } @@ -584,6 +585,7 @@ class ComponentGraphUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateCon newInterface.template(cache).value = interfaceTemplate internalUpdatedNodes += definition definition.visibleInterface(cache).value = newInterface + newInterface.interfaceDefinition(cache).value = definition issueAggregationUpdater.createdInterface(newInterface) } diff --git a/core/src/main/kotlin/gropius/service/architecture/ComponentVersionService.kt b/core/src/main/kotlin/gropius/service/architecture/ComponentVersionService.kt index a2c4fcf5..ea7846d9 100644 --- a/core/src/main/kotlin/gropius/service/architecture/ComponentVersionService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/ComponentVersionService.kt @@ -118,8 +118,7 @@ class ComponentVersionService( } val graphUpdater = ComponentGraphUpdater() updateFunction(graphUpdater, componentVersion, interfaceSpecificationVersion) - nodeRepository.saveAll(graphUpdater.updatedNodes).collectList().awaitSingle() - nodeRepository.deleteAll(graphUpdater.deletedNodes).awaitSingleOrNull() + graphUpdater.save(nodeRepository) } /** diff --git a/core/src/main/kotlin/gropius/service/architecture/ProjectService.kt b/core/src/main/kotlin/gropius/service/architecture/ProjectService.kt index 7a474fa0..b98a13ed 100644 --- a/core/src/main/kotlin/gropius/service/architecture/ProjectService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/ProjectService.kt @@ -14,6 +14,7 @@ import gropius.model.user.permission.NodePermission import gropius.model.user.permission.ProjectPermission import gropius.repository.architecture.ComponentVersionRepository import gropius.repository.architecture.ProjectRepository +import gropius.repository.common.NodeRepository import gropius.repository.findById import gropius.service.user.permission.ProjectPermissionService import io.github.graphglue.authorization.Permission @@ -105,11 +106,11 @@ class ProjectService( * * @param authorizationContext used to check for the required permission * @param input defines which [ComponentVersion] to add to which [Project] - * @return the updated [Project] + * @return the updated [Project] and the added [ComponentVersion] */ suspend fun addComponentVersionToProject( authorizationContext: GropiusAuthorizationContext, input: AddComponentVersionToProjectInput - ): Project { + ): Pair { input.validate() val project = repository.findById(input.project) checkPermission( @@ -124,7 +125,11 @@ class ProjectService( "add the ComponentVersion to Projects" ) project.components() += componentVersion - return repository.save(project).awaitSingle() + val savedNodes = nodeRepository.saveAll(listOf(project, componentVersion)).collectList().awaitSingle() + return Pair( + savedNodes.first { it is Project } as Project, + savedNodes.first { it is ComponentVersion } as ComponentVersion + ) } /** diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index 1bc19ff7..152b0745 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -367,6 +367,8 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC val type = issue.type(cache).value aggregatedIssueLookup.getOrPut(type to state.isOpen) { val aggregatedIssue = AggregatedIssue(0, state.isOpen) + aggregatedIssue.relationPartner(cache).value = relationPartner + aggregatedIssue.type(cache).value = type aggregatedIssue }.let { if (it.issues(cache).add(issue)) { From de7248b8f1a798b5eb32f897da29981aaa61368a Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 25 Sep 2023 04:33:52 +0200 Subject: [PATCH 04/30] working styles --- .../template/CreateComponentTemplateInput.kt | 2 +- ...eateInterfaceSpecificationTemplateInput.kt | 4 +-- .../CreateRelationPartnerTemplateInput.kt | 30 +++++++++++++++++ .../template/CreateRelationTemplateInput.kt | 11 ++++++- ...ceSpecificationDerivationConditionInput.kt | 2 +- .../input/template/style/FillStyleInput.kt | 10 ++++++ .../input/template/style/StrokeStyleInput.kt | 15 +++++++++ .../model/template/ComponentTemplate.kt | 10 ++++-- .../InterfaceSpecificationTemplate.kt | 10 ++++-- .../model/template/RelationPartnerTemplate.kt | 26 ++++++++++++++- .../model/template/RelationTemplate.kt | 17 +++++++++- .../gropius/model/template/style/BaseStyle.kt | 12 +++++++ .../gropius/model/template/style/FillStyle.kt | 11 +++++++ .../model/template/style/MarkerType.kt | 21 ++++++++++++ .../gropius/model/template/style/ShapeType.kt | 21 ++++++++++++ .../model/template/style/StrokeStyle.kt | 15 +++++++++ .../template/ComponentTemplateService.kt | 5 ++- .../InterfaceSpecificationTemplateService.kt | 32 +++++++++---------- .../RelationPartnerTemplateService.kt | 14 ++++++-- .../template/RelationTemplateService.kt | 8 ++++- 20 files changed, 244 insertions(+), 32 deletions(-) create mode 100644 core/src/main/kotlin/gropius/dto/input/template/CreateRelationPartnerTemplateInput.kt create mode 100644 core/src/main/kotlin/gropius/dto/input/template/style/FillStyleInput.kt create mode 100644 core/src/main/kotlin/gropius/dto/input/template/style/StrokeStyleInput.kt create mode 100644 core/src/main/kotlin/gropius/model/template/style/BaseStyle.kt create mode 100644 core/src/main/kotlin/gropius/model/template/style/FillStyle.kt create mode 100644 core/src/main/kotlin/gropius/model/template/style/MarkerType.kt create mode 100644 core/src/main/kotlin/gropius/model/template/style/ShapeType.kt create mode 100644 core/src/main/kotlin/gropius/model/template/style/StrokeStyle.kt diff --git a/core/src/main/kotlin/gropius/dto/input/template/CreateComponentTemplateInput.kt b/core/src/main/kotlin/gropius/dto/input/template/CreateComponentTemplateInput.kt index 710ec23c..bc384955 100644 --- a/core/src/main/kotlin/gropius/dto/input/template/CreateComponentTemplateInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/template/CreateComponentTemplateInput.kt @@ -6,7 +6,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDescription class CreateComponentTemplateInput( @GraphQLDescription("SubTemplate for all ComponentVersions of a Component with the created Template") val componentVersionTemplate: SubTemplateInput -) : CreateTemplateInput() { +) : CreateRelationPartnerTemplateInput() { override fun validate() { super.validate() diff --git a/core/src/main/kotlin/gropius/dto/input/template/CreateInterfaceSpecificationTemplateInput.kt b/core/src/main/kotlin/gropius/dto/input/template/CreateInterfaceSpecificationTemplateInput.kt index 922c0d41..7ab2a41f 100644 --- a/core/src/main/kotlin/gropius/dto/input/template/CreateInterfaceSpecificationTemplateInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/template/CreateInterfaceSpecificationTemplateInput.kt @@ -16,8 +16,8 @@ class CreateInterfaceSpecificationTemplateInput( @GraphQLDescription("SubTemplate for all Interfaces of a InterfaceSpecification with the created Template") val interfaceTemplate: NullableSubTemplateInput, @GraphQLDescription("SubTemplate for all InterfacesDefinitions of a InterfaceSpecification with the created Template") - val interfaceDefinitionTemplate: NullableSubTemplateInput -) : CreateTemplateInput() { + val interfaceDefinitionTemplate: NullableSubTemplateInput, +) : CreateRelationPartnerTemplateInput() { override fun validate() { super.validate() diff --git a/core/src/main/kotlin/gropius/dto/input/template/CreateRelationPartnerTemplateInput.kt b/core/src/main/kotlin/gropius/dto/input/template/CreateRelationPartnerTemplateInput.kt new file mode 100644 index 00000000..e6460d96 --- /dev/null +++ b/core/src/main/kotlin/gropius/dto/input/template/CreateRelationPartnerTemplateInput.kt @@ -0,0 +1,30 @@ +package gropius.dto.input.template + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.execution.OptionalInput +import gropius.dto.input.ifPresent +import gropius.dto.input.template.style.FillStyleInput +import gropius.dto.input.template.style.StrokeStyleInput +import gropius.model.template.style.ShapeType +import kotlin.properties.Delegates + +abstract class CreateRelationPartnerTemplateInput : CreateTemplateInput() { + + @GraphQLDescription("Style of the fill") + var fill: OptionalInput by Delegates.notNull() + + @GraphQLDescription("Style of the stroke") + var stroke: OptionalInput by Delegates.notNull() + + @GraphQLDescription("The corner radius of the shape, ignored for circle/ellipse") + var shapeRadius: OptionalInput by Delegates.notNull() + + @GraphQLDescription("The type of the shape") + var shapeType: ShapeType by Delegates.notNull() + + override fun validate() { + super.validate() + fill.ifPresent { it.validate() } + stroke.ifPresent { it.validate() } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/template/CreateRelationTemplateInput.kt b/core/src/main/kotlin/gropius/dto/input/template/CreateRelationTemplateInput.kt index 55fbf734..3bc1aaec 100644 --- a/core/src/main/kotlin/gropius/dto/input/template/CreateRelationTemplateInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/template/CreateRelationTemplateInput.kt @@ -1,15 +1,24 @@ package gropius.dto.input.template import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.execution.OptionalInput +import gropius.dto.input.ifPresent +import gropius.dto.input.template.style.StrokeStyleInput +import gropius.model.template.style.MarkerType @GraphQLDescription("Input for the createRelationTemplate mutation") class CreateRelationTemplateInput( @GraphQLDescription("Defines which Relations can use the created Template, at least one RelationCondition has to match (logical OR)") - val relationConditions: List + val relationConditions: List, + @GraphQLDescription("The type of the marker at the end of the relation.") + val markerType: MarkerType, + @GraphQLDescription("Style of the stroke") + val stroke: OptionalInput ) : CreateTemplateInput() { override fun validate() { super.validate() relationConditions.forEach { it.validate() } + stroke.ifPresent { it.validate() } } } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/template/InterfaceSpecificationDerivationConditionInput.kt b/core/src/main/kotlin/gropius/dto/input/template/InterfaceSpecificationDerivationConditionInput.kt index 24203d1c..76d7c0e3 100644 --- a/core/src/main/kotlin/gropius/dto/input/template/InterfaceSpecificationDerivationConditionInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/template/InterfaceSpecificationDerivationConditionInput.kt @@ -17,4 +17,4 @@ class InterfaceSpecificationDerivationConditionInput( val isVisibleDerived: Boolean, @GraphQLDescription("If true InterfaceSpecifications are invisible derived") val isInvisibleDerived: Boolean -) : CreateExtensibleNodeInput() \ No newline at end of file +) : CreateRelationPartnerTemplateInput() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/template/style/FillStyleInput.kt b/core/src/main/kotlin/gropius/dto/input/template/style/FillStyleInput.kt new file mode 100644 index 00000000..bfebd238 --- /dev/null +++ b/core/src/main/kotlin/gropius/dto/input/template/style/FillStyleInput.kt @@ -0,0 +1,10 @@ +package gropius.dto.input.template.style + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import gropius.dto.input.common.Input + +@GraphQLDescription("Input to create a FillStyle") +class FillStyleInput( + @GraphQLDescription("The color of the fill") + val color: String +) : Input() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/template/style/StrokeStyleInput.kt b/core/src/main/kotlin/gropius/dto/input/template/style/StrokeStyleInput.kt new file mode 100644 index 00000000..5ed44ab6 --- /dev/null +++ b/core/src/main/kotlin/gropius/dto/input/template/style/StrokeStyleInput.kt @@ -0,0 +1,15 @@ +package gropius.dto.input.template.style + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.execution.OptionalInput +import gropius.dto.input.common.Input + +@GraphQLDescription("Input to create a StrokeStyle") +class StrokeStyleInput( + @GraphQLDescription("The color of the stroke") + val color: OptionalInput, + @GraphQLDescription("The width of the stroke") + val width: OptionalInput, + @GraphQLDescription("The dash pattern of the stroke") + val dash: OptionalInput> +) : Input() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/template/ComponentTemplate.kt b/core/src/main/kotlin/gropius/model/template/ComponentTemplate.kt index 405f6c46..169ae2c6 100644 --- a/core/src/main/kotlin/gropius/model/template/ComponentTemplate.kt +++ b/core/src/main/kotlin/gropius/model/template/ComponentTemplate.kt @@ -2,6 +2,7 @@ package gropius.model.template import com.expediagroup.graphql.generator.annotations.GraphQLDescription import gropius.model.architecture.Component +import gropius.model.template.style.ShapeType import io.github.graphglue.model.Direction import io.github.graphglue.model.DomainNode import io.github.graphglue.model.FilterProperty @@ -15,9 +16,14 @@ import io.github.graphglue.model.NodeRelationship """ ) class ComponentTemplate( - name: String, description: String, templateFieldSpecifications: MutableMap, isDeprecated: Boolean + name: String, + description: String, + templateFieldSpecifications: MutableMap, + isDeprecated: Boolean, + shapeRadius: Double?, + shapeType: ShapeType, ) : RelationPartnerTemplate( - name, description, templateFieldSpecifications, isDeprecated + name, description, templateFieldSpecifications, isDeprecated, shapeRadius, shapeType ) { companion object { diff --git a/core/src/main/kotlin/gropius/model/template/InterfaceSpecificationTemplate.kt b/core/src/main/kotlin/gropius/model/template/InterfaceSpecificationTemplate.kt index 91f2b2f1..b423a3e7 100644 --- a/core/src/main/kotlin/gropius/model/template/InterfaceSpecificationTemplate.kt +++ b/core/src/main/kotlin/gropius/model/template/InterfaceSpecificationTemplate.kt @@ -2,6 +2,7 @@ package gropius.model.template import com.expediagroup.graphql.generator.annotations.GraphQLDescription import gropius.model.architecture.InterfaceSpecification +import gropius.model.template.style.ShapeType import io.github.graphglue.model.Direction import io.github.graphglue.model.DomainNode import io.github.graphglue.model.FilterProperty @@ -16,9 +17,14 @@ import io.github.graphglue.model.NodeRelationship """ ) class InterfaceSpecificationTemplate( - name: String, description: String, templateFieldSpecifications: MutableMap, isDeprecated: Boolean + name: String, + description: String, + templateFieldSpecifications: MutableMap, + isDeprecated: Boolean, + shapeRadius: Double?, + shapeType: ShapeType, ) : RelationPartnerTemplate( - name, description, templateFieldSpecifications, isDeprecated + name, description, templateFieldSpecifications, isDeprecated, shapeRadius, shapeType ) { @NodeRelationship(ComponentTemplate.VISIBLE_INTERFACE_SPECIFICATION, Direction.INCOMING) diff --git a/core/src/main/kotlin/gropius/model/template/RelationPartnerTemplate.kt b/core/src/main/kotlin/gropius/model/template/RelationPartnerTemplate.kt index 14fc77a6..de7c3f7f 100644 --- a/core/src/main/kotlin/gropius/model/template/RelationPartnerTemplate.kt +++ b/core/src/main/kotlin/gropius/model/template/RelationPartnerTemplate.kt @@ -1,14 +1,30 @@ package gropius.model.template import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import gropius.model.template.style.FillStyle +import gropius.model.template.style.ShapeType +import gropius.model.template.style.StrokeStyle import io.github.graphglue.model.* @DomainNode @GraphQLDescription("Template for RelationPartners.") abstract class RelationPartnerTemplate>( - name: String, description: String, templateFieldSpecifications: MutableMap, isDeprecated: Boolean + name: String, + description: String, + templateFieldSpecifications: MutableMap, + isDeprecated: Boolean, + @GraphQLDescription("The corner radius of the shape, ignored for circle/ellipse.") + val shapeRadius: Double?, + @GraphQLDescription("The type of the shape.") + val shapeType: ShapeType, ) : Template(name, description, templateFieldSpecifications, isDeprecated) where T : Node, T : TemplatedNode { + companion object { + const val FILL_STYLE = "FILL_STYLE" + const val STROKE_STYLE = "STROKE_STYLE" + } + + @NodeRelationship(RelationCondition.FROM, Direction.INCOMING) @GraphQLDescription("RelationConditions which allow this template for the start of the relation.") @FilterProperty @@ -19,4 +35,12 @@ abstract class RelationPartnerTemplate>( @FilterProperty val possibleEndOfRelations by NodeSetProperty() + @NodeRelationship(FILL_STYLE, Direction.OUTGOING) + @GraphQLDescription("Style of the fill") + val fill by NodeProperty() + + @NodeRelationship(STROKE_STYLE, Direction.OUTGOING) + @GraphQLDescription("Style of the stroke") + val stroke by NodeProperty() + } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/template/RelationTemplate.kt b/core/src/main/kotlin/gropius/model/template/RelationTemplate.kt index 083e5478..3d46dc44 100644 --- a/core/src/main/kotlin/gropius/model/template/RelationTemplate.kt +++ b/core/src/main/kotlin/gropius/model/template/RelationTemplate.kt @@ -2,6 +2,8 @@ package gropius.model.template import com.expediagroup.graphql.generator.annotations.GraphQLDescription import gropius.model.architecture.Relation +import gropius.model.template.style.MarkerType +import gropius.model.template.style.StrokeStyle import io.github.graphglue.model.Direction import io.github.graphglue.model.DomainNode import io.github.graphglue.model.FilterProperty @@ -16,12 +18,25 @@ import io.github.graphglue.model.NodeRelationship """ ) class RelationTemplate( - name: String, description: String, templateFieldSpecifications: MutableMap, isDeprecated: Boolean + name: String, + description: String, + templateFieldSpecifications: MutableMap, + isDeprecated: Boolean, + @GraphQLDescription("The type of the marker at the end of the relation.") + val markerType: MarkerType, ) : Template(name, description, templateFieldSpecifications, isDeprecated) { + companion object { + const val STROKE_STYLE = "STROKE_STYLE" + } + @NodeRelationship(RelationCondition.PART_OF, Direction.INCOMING) @GraphQLDescription("Defines which Relations can use this template, at least one RelationCondition has to match") @FilterProperty val relationConditions by NodeSetProperty() + @NodeRelationship(STROKE_STYLE, Direction.OUTGOING) + @GraphQLDescription("Style of the stroke") + val stroke by NodeProperty() + } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/template/style/BaseStyle.kt b/core/src/main/kotlin/gropius/model/template/style/BaseStyle.kt new file mode 100644 index 00000000..a987eebb --- /dev/null +++ b/core/src/main/kotlin/gropius/model/template/style/BaseStyle.kt @@ -0,0 +1,12 @@ +package gropius.model.template.style + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import gropius.model.user.permission.NodePermission +import io.github.graphglue.model.Authorization +import io.github.graphglue.model.DomainNode +import io.github.graphglue.model.Node + +@DomainNode +@GraphQLIgnore +@Authorization(NodePermission.READ, allowAll = true) +abstract class BaseStyle : Node() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/template/style/FillStyle.kt b/core/src/main/kotlin/gropius/model/template/style/FillStyle.kt new file mode 100644 index 00000000..dfdf75bf --- /dev/null +++ b/core/src/main/kotlin/gropius/model/template/style/FillStyle.kt @@ -0,0 +1,11 @@ +package gropius.model.template.style + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import io.github.graphglue.model.DomainNode + +@DomainNode +@GraphQLDescription("Fill style of a shape") +class FillStyle( + @GraphQLDescription("The color of the fill") + val color: String +) : BaseStyle() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/template/style/MarkerType.kt b/core/src/main/kotlin/gropius/model/template/style/MarkerType.kt new file mode 100644 index 00000000..aa6b3ab2 --- /dev/null +++ b/core/src/main/kotlin/gropius/model/template/style/MarkerType.kt @@ -0,0 +1,21 @@ +package gropius.model.template.style + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription + +@GraphQLDescription("Type of a Relation marker") +enum class MarkerType { + @GraphQLDescription("A regular arrow") + ARROW, + + @GraphQLDescription("A diamond") + DIAMOND, + + @GraphQLDescription("A filled diamond") + FILLED_DIAMOND, + + @GraphQLDescription("A triangle") + TRIANGLE, + + @GraphQLDescription("A filled triangle") + FILLED_TRIANGLE +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/template/style/ShapeType.kt b/core/src/main/kotlin/gropius/model/template/style/ShapeType.kt new file mode 100644 index 00000000..ef53e159 --- /dev/null +++ b/core/src/main/kotlin/gropius/model/template/style/ShapeType.kt @@ -0,0 +1,21 @@ +package gropius.model.template.style + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription + +@GraphQLDescription("Type of a Shape") +enum class ShapeType { + @GraphQLDescription("A Rectangle") + RECT, + + @GraphQLDescription("A Circle") + CIRCLE, + + @GraphQLDescription("An Ellipse") + ELLIPSE, + + @GraphQLDescription("A Rhombus") + RHOMBUS, + + @GraphQLDescription("A Hexagon") + HEXAGON, +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/template/style/StrokeStyle.kt b/core/src/main/kotlin/gropius/model/template/style/StrokeStyle.kt new file mode 100644 index 00000000..cd990598 --- /dev/null +++ b/core/src/main/kotlin/gropius/model/template/style/StrokeStyle.kt @@ -0,0 +1,15 @@ +package gropius.model.template.style + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import io.github.graphglue.model.DomainNode + +@DomainNode +@GraphQLDescription("Style of the stroke") +class StrokeStyle( + @GraphQLDescription("The color of the stroke") + val color: String?, + @GraphQLDescription("The width of the stroke") + val width: Double?, + @GraphQLDescription("The dash pattern of the stroke") + val dash: List? +) : BaseStyle() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/template/ComponentTemplateService.kt b/core/src/main/kotlin/gropius/service/template/ComponentTemplateService.kt index bb31f8bf..353f4f42 100644 --- a/core/src/main/kotlin/gropius/service/template/ComponentTemplateService.kt +++ b/core/src/main/kotlin/gropius/service/template/ComponentTemplateService.kt @@ -1,6 +1,7 @@ package gropius.service.template import gropius.authorization.GropiusAuthorizationContext +import gropius.dto.input.orElse import gropius.dto.input.template.CreateComponentTemplateInput import gropius.model.template.ComponentTemplate import gropius.model.template.ComponentVersionTemplate @@ -36,7 +37,9 @@ class ComponentTemplateService( ): ComponentTemplate { input.validate() checkCreateTemplatePermission(authorizationContext) - val template = ComponentTemplate(input.name, input.description, mutableMapOf(), false) + val template = ComponentTemplate( + input.name, input.description, mutableMapOf(), false, input.shapeRadius.orElse(null), input.shapeType + ) createdRelationPartnerTemplate(template, input) template.componentVersionTemplate().value = subTemplateService.createSubTemplate(::ComponentVersionTemplate, input.componentVersionTemplate, diff --git a/core/src/main/kotlin/gropius/service/template/InterfaceSpecificationTemplateService.kt b/core/src/main/kotlin/gropius/service/template/InterfaceSpecificationTemplateService.kt index 5802563c..31e476af 100644 --- a/core/src/main/kotlin/gropius/service/template/InterfaceSpecificationTemplateService.kt +++ b/core/src/main/kotlin/gropius/service/template/InterfaceSpecificationTemplateService.kt @@ -1,6 +1,7 @@ package gropius.service.template import gropius.authorization.GropiusAuthorizationContext +import gropius.dto.input.orElse import gropius.dto.input.template.CreateInterfaceSpecificationTemplateInput import gropius.model.template.* import gropius.repository.findAllById @@ -36,7 +37,9 @@ class InterfaceSpecificationTemplateService( ): InterfaceSpecificationTemplate { input.validate() checkCreateTemplatePermission(authorizationContext) - val template = InterfaceSpecificationTemplate(input.name, input.description, mutableMapOf(), false) + val template = InterfaceSpecificationTemplate( + input.name, input.description, mutableMapOf(), false, input.shapeRadius.orElse(null), input.shapeType + ) createdRelationPartnerTemplate(template, input) template.canBeVisibleOnComponents() += componentTemplateRepository.findAllById(input.canBeVisibleOnComponents) template.canBeVisibleOnComponents() += template.extends().flatMap { it.canBeVisibleOnComponents() } @@ -57,26 +60,21 @@ class InterfaceSpecificationTemplateService( template: InterfaceSpecificationTemplate, input: CreateInterfaceSpecificationTemplateInput ) { val extendedTemplates = template.extends() - template.interfaceSpecificationVersionTemplate().value = subTemplateService.createSubTemplate( - ::InterfaceSpecificationVersionTemplate, - input.interfaceSpecificationVersionTemplate, - extendedTemplates.map { it.interfaceSpecificationVersionTemplate().value } - ) - template.interfacePartTemplate().value = subTemplateService.createSubTemplate( - ::InterfacePartTemplate, + template.interfaceSpecificationVersionTemplate().value = + subTemplateService.createSubTemplate(::InterfaceSpecificationVersionTemplate, + input.interfaceSpecificationVersionTemplate, + extendedTemplates.map { it.interfaceSpecificationVersionTemplate().value }) + template.interfacePartTemplate().value = subTemplateService.createSubTemplate(::InterfacePartTemplate, input.interfacePartTemplate, - extendedTemplates.map { it.interfacePartTemplate().value } - ) + extendedTemplates.map { it.interfacePartTemplate().value }) template.interfaceTemplate().value = subTemplateService.createSubTemplate( ::InterfaceTemplate, input.interfaceTemplate, - extendedTemplates.map { it.interfaceTemplate().value } - ) - template.interfaceDefinitionTemplate().value = subTemplateService.createSubTemplate( - ::InterfaceDefinitionTemplate, - input.interfaceDefinitionTemplate, - extendedTemplates.map { it.interfaceDefinitionTemplate().value } - ) + extendedTemplates.map { it.interfaceTemplate().value }) + template.interfaceDefinitionTemplate().value = + subTemplateService.createSubTemplate(::InterfaceDefinitionTemplate, + input.interfaceDefinitionTemplate, + extendedTemplates.map { it.interfaceDefinitionTemplate().value }) } } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/template/RelationPartnerTemplateService.kt b/core/src/main/kotlin/gropius/service/template/RelationPartnerTemplateService.kt index ad0741ff..41aedca5 100644 --- a/core/src/main/kotlin/gropius/service/template/RelationPartnerTemplateService.kt +++ b/core/src/main/kotlin/gropius/service/template/RelationPartnerTemplateService.kt @@ -1,7 +1,11 @@ package gropius.service.template -import gropius.dto.input.template.CreateTemplateInput +import gropius.dto.input.ifPresent +import gropius.dto.input.orElse +import gropius.dto.input.template.CreateRelationPartnerTemplateInput import gropius.model.template.RelationPartnerTemplate +import gropius.model.template.style.FillStyle +import gropius.model.template.style.StrokeStyle import gropius.repository.GropiusRepository /** @@ -24,10 +28,16 @@ abstract class RelationPartnerTemplateService, * @param template the [RelationPartnerTemplate] to update * @param input specifies added templateFieldSpecifications */ - suspend fun createdRelationPartnerTemplate(template: T, input: CreateTemplateInput) { + suspend fun createdRelationPartnerTemplate(template: T, input: CreateRelationPartnerTemplateInput) { createdTemplate(template, input) template.possibleStartOfRelations() += template.extends().flatMap { it.possibleStartOfRelations() } template.possibleEndOfRelations() += template.extends().flatMap { it.possibleEndOfRelations() } + input.fill.ifPresent { + template.fill().value = FillStyle(it.color) + } + input.stroke.ifPresent { + template.stroke().value = StrokeStyle(it.color.orElse(null), it.width.orElse(null), it.dash.orElse(null)) + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/template/RelationTemplateService.kt b/core/src/main/kotlin/gropius/service/template/RelationTemplateService.kt index 9641d6ea..e27593d4 100644 --- a/core/src/main/kotlin/gropius/service/template/RelationTemplateService.kt +++ b/core/src/main/kotlin/gropius/service/template/RelationTemplateService.kt @@ -1,12 +1,15 @@ package gropius.service.template import gropius.authorization.GropiusAuthorizationContext +import gropius.dto.input.ifPresent +import gropius.dto.input.orElse import gropius.dto.input.template.CreateRelationTemplateInput import gropius.dto.input.template.RelationConditionInput import gropius.model.template.InterfaceSpecificationDerivationCondition import gropius.model.template.RelationCondition import gropius.model.template.RelationPartnerTemplate import gropius.model.template.RelationTemplate +import gropius.model.template.style.StrokeStyle import gropius.repository.findAllById import gropius.repository.template.RelationPartnerTemplateRepository import gropius.repository.template.RelationTemplateRepository @@ -38,7 +41,10 @@ class RelationTemplateService( ): RelationTemplate { input.validate() checkCreateTemplatePermission(authorizationContext) - val template = RelationTemplate(input.name, input.description, mutableMapOf(), false) + val template = RelationTemplate(input.name, input.description, mutableMapOf(), false, input.markerType) + input.stroke.ifPresent { + template.stroke().value = StrokeStyle(it.color.orElse(null), it.width.orElse(null), it.dash.orElse(null)) + } createdTemplate(template, input) template.relationConditions() += input.relationConditions.map { createRelationCondition(it) From 001e7c578831fc9ef9a3764439e8169381d7d554 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 25 Sep 2023 05:27:08 +0200 Subject: [PATCH 05/30] bugfix --- .../gropius/service/issue/IssueAggregationUpdater.kt | 7 +++++++ .../main/kotlin/gropius/service/issue/IssueService.kt | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index 152b0745..b5159d56 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -38,6 +38,13 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + suspend fun addedIssueToTrackable(issue: Issue, trackable: Trackable) { + if (trackable !is Component) { + return + } + aggregateIssueOnComponentIfNecessary(issue, trackable) + } + suspend fun removedIssueFromTrackable(issue: Issue, trackable: Trackable) { if (trackable !is Component) { return diff --git a/core/src/main/kotlin/gropius/service/issue/IssueService.kt b/core/src/main/kotlin/gropius/service/issue/IssueService.kt index d6f95a4f..fe63313a 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueService.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueService.kt @@ -549,6 +549,9 @@ class IssueService( ) { it.removedFromTrackable().value == trackable } ) { issue.trackables() += trackable + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.addedIssueToTrackable(issue, trackable) + aggregationUpdater.save(nodeRepository) } return event } @@ -574,9 +577,6 @@ class IssueService( val event = timelineItemRepository.save( removeIssueFromTrackable(issue, trackable, OffsetDateTime.now(), getUser(authorizationContext)) ).awaitSingle() - val aggregationUpdater = IssueAggregationUpdater() - aggregationUpdater.removedIssueFromTrackable(issue, trackable) - aggregationUpdater.save(nodeRepository) event } else { null @@ -624,6 +624,9 @@ class IssueService( .map { removeArtefactFromIssue(issue, it, atTime.plusNanos(timeOffset++), byUser) } event.childItems() += issue.labels().filter { Collections.disjoint(issue.trackables(), it.trackables()) } .map { removeLabelFromIssue(issue, it, atTime.plusNanos(timeOffset++), byUser) } + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.removedIssueFromTrackable(issue, trackable) + aggregationUpdater.save(nodeRepository) } return event } From ac5b73b777b0f36b989fd9e07469f98f24919c13 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 25 Sep 2023 17:10:38 +0200 Subject: [PATCH 06/30] small improvements, documentation --- .../service/issue/IssueAggregationUpdater.kt | 246 +++++++++++++++--- 1 file changed, 213 insertions(+), 33 deletions(-) diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index b5159d56..3a1b5210 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -8,9 +8,22 @@ import gropius.model.template.IssueType import gropius.service.NodeBatchUpdateContext import gropius.service.NodeBatchUpdater -class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateContext()) : - NodeBatchUpdater by updateContext { - +/** + * Helper class to handle anything AggregatedIssue related. + * After using, nodes from [deletedNodes] must be deleted, and nodes from [updatedNodes] must be saved + */ +class IssueAggregationUpdater( + updateContext: NodeBatchUpdater = NodeBatchUpdateContext() +) : NodeBatchUpdater by updateContext { + + /** + * Should be called when the state or type of an issue was changed. + * Musts be called AFTER the change was applied to the issue. + * + * @param issue the issue that was changed + * @param oldState the old state of the issue + * @param oldType the old type of the issue + */ suspend fun changedIssueStateOrType(issue: Issue, oldState: IssueState, oldType: IssueType) { if (issue.type(cache).value == oldType && issue.state(cache).value.isOpen == oldState.isOpen) { return @@ -25,12 +38,24 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Should be called when an [Issue] was deleted. + * Must be called BEFORE the issue is deleted from the database. + * + * @param issue the issue that was deleted + */ suspend fun deletedIssue(issue: Issue) { issue.aggregatedBy(cache).forEach { aggregatedBy -> removeIssueFromAggregatedIssue(issue, aggregatedBy) } } + /** + * Should be called when a [ComponentVersion] is deleted. + * Must be called BEFORE the component version is deleted from the database. + * + * @param componentVersion the component version that was deleted + */ suspend fun deletedComponentVersion(componentVersion: ComponentVersion) { relationPartnerDeleted(componentVersion) for (issue in componentVersion.affectingIssues(cache)) { @@ -38,6 +63,13 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Should be called when an [Issue] is added to a [Trackable]. + * Must be called AFTER the issue was added to the trackable. + * + * @param issue the issue that was added + * @param trackable the trackable the issue was added to + */ suspend fun addedIssueToTrackable(issue: Issue, trackable: Trackable) { if (trackable !is Component) { return @@ -45,6 +77,13 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC aggregateIssueOnComponentIfNecessary(issue, trackable) } + /** + * Should be called when an [Issue] is removed from a [Trackable]. + * Must be called AFTER the issue is removed from the trackable. + * + * @param issue the issue that was removed + * @param trackable the trackable the issue was removed from + */ suspend fun removedIssueFromTrackable(issue: Issue, trackable: Trackable) { if (trackable !is Component) { return @@ -63,13 +102,16 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC aggregatedBy.removeAll(removed) } + /** + * Should be called when an [Interface] is created + * + * @param createdInterface the created interface + */ suspend fun createdInterface(createdInterface: Interface) { val definition = createdInterface.interfaceDefinition(cache).value val specificationVersion = definition.interfaceSpecificationVersion(cache).value val affectableEntities = listOf( - createdInterface, - specificationVersion, - specificationVersion.interfaceSpecification(cache).value + createdInterface, specificationVersion, specificationVersion.interfaceSpecification(cache).value ) + specificationVersion.activeParts(cache) val affectedIssues = affectableEntities.flatMap { it.affectingIssues(cache) }.toSet() createOrUpdateAggregatedIssues(createdInterface, affectedIssues) @@ -79,6 +121,12 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Should be called when an [Interface] is deleted + * Must be called BEFORE the interface is deleted from the database. + * + * @param deletedInterface the deleted interface + */ suspend fun deletedInterface(deletedInterface: Interface) { relationPartnerDeleted(deletedInterface) val component = @@ -88,6 +136,12 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Should be called when an [InterfacePart] is deleted + * Must be called BEFORE the interface part is deleted from the database. + * + * @param interfacePart the deleted interface part + */ suspend fun deletedInterfacePart(interfacePart: InterfacePart) { val interfaces = interfacePart.activeOn(cache).flatMap { version -> version.interfaceDefinitions(cache).mapNotNull { @@ -110,6 +164,14 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Should be called when the active parts of an [InterfaceSpecificationVersion] were updated. + * Must be called AFTER the active parts were updated. + * + * @param interfaceSpecificationVersion the interface specification version that was updated + * @param addedParts the parts that were added + * @param removedParts the parts that were removed + */ suspend fun updatedActiveParts( interfaceSpecificationVersion: InterfaceSpecificationVersion, addedParts: Set, @@ -141,6 +203,13 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Should be called when an [Issue] affects an additional entity. + * Must be called AFTER the affected entity was added to the issue. + * + * @param issue the issue that now affects [affectedEntity] + * @param affectedEntity the entity that is now affected by [issue] + */ suspend fun addedAffectedEntity(issue: Issue, affectedEntity: AffectedByIssue) { when (affectedEntity) { is Component -> { @@ -180,10 +249,20 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Should be called when an [Issue] does not affect an entity anymore. + * Must be called AFTER the affected entity was removed from the issue. + * + * @param issue the issue that does not affect [affectedEntity] anymore + * @param affectedEntity the entity that is not affected by [issue] anymore + */ suspend fun removedAffectedEntity(issue: Issue, affectedEntity: AffectedByIssue) { when (affectedEntity) { is Component -> { - if (affectedEntity !in issue.trackables(cache) || doesIssueAffectComponentRelatedEntity(issue, affectedEntity)) { + if (affectedEntity !in issue.trackables(cache) || doesIssueAffectComponentRelatedEntity( + issue, affectedEntity + ) + ) { affectedEntity.versions(cache).forEach { if (it !in issue.affects(cache)) { removeIssueFromAggregatedIssueOnRelationPartner(issue, it) @@ -235,9 +314,16 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Handles the case of an [Issue] affecting an interface related entity ([InterfacePart], [InterfaceSpecificationVersion], [InterfaceSpecification]). + * All relevant [InterfaceSpecificationVersion]s must be provided, each related [Interface] will be updated. + * Relevance depends on the type of affected entity. + * + * @param issue the issue that affects the entity + * @param interfaceSpecificationVersions the interface related entities that are affected by the issue + */ private suspend fun addedAffectedInterfaceRelatedEntity( - issue: Issue, - interfaceSpecificationVersions: Set + issue: Issue, interfaceSpecificationVersions: Set ) { val interfaces = interfaceSpecificationVersions.flatMap { version -> version.interfaceDefinitions(cache).mapNotNull { it.visibleInterface(cache).value } @@ -245,9 +331,14 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC addedAffectedInterfaceRelatedEntity(issue, interfaces) } + /** + * Handles the case of an [Issue] affecting [Interface]s, directly or indirectly. + * + * @param issue the issue that affects the entity + * @param interfaces the interfaces that are affected by the issue + */ private suspend fun addedAffectedInterfaceRelatedEntity( - issue: Issue, - interfaces: Collection + issue: Issue, interfaces: Collection ) { for (inter in interfaces) { createOrUpdateAggregatedIssues(inter, setOf(issue)) @@ -260,9 +351,16 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Handles the case of an [Issue] no longer affecting an interface related entity ([InterfacePart], [InterfaceSpecificationVersion], [InterfaceSpecification]). + * All relevant [InterfaceSpecificationVersion]s must be provided, each related [Interface] will be updated. + * Relevance depends on the type of affected entity. + * + * @param issue the issue that no longer affects the entity + * @param interfaceSpecificationVersions the interface related entities that may no longer be affected by the issue + */ private suspend fun removedAffectedInterfaceRelatedEntity( - issue: Issue, - interfaceSpecificationVersions: Set + issue: Issue, interfaceSpecificationVersions: Set ) { val interfaces = interfaceSpecificationVersions.flatMap { version -> version.interfaceDefinitions(cache).mapNotNull { it.visibleInterface(cache).value } @@ -270,9 +368,15 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC removedAffectedInterfaceRelatedEntity(issue, interfaces) } + /** + * Handles the case of an [Issue] no longer affecting [Interface]s, directly or indirectly. + * Handles the case that the [Issue] still affects the [Interface]s. + * + * @param issue the issue that no longer affects the entity + * @param interfaces the interfaces that may no longer be affected by the issue + */ private suspend fun removedAffectedInterfaceRelatedEntity( - issue: Issue, - interfaces: Collection + issue: Issue, interfaces: Collection ) { val addToComponentPotentially = mutableSetOf() for (inter in interfaces) { @@ -288,9 +392,15 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Removes an issue from an [AggregatedIssue] on a [RelationPartner]. + * Handles the case of the [Issue] not being aggregated on the [RelationPartner]. + * + * @param issue the issue to remove + * @param relationPartner the relation partner to remove the issue from + */ private suspend fun removeIssueFromAggregatedIssueOnRelationPartner( - issue: Issue, - relationPartner: RelationPartner + issue: Issue, relationPartner: RelationPartner ) { val type = issue.type(cache).value val isOpen = issue.state(cache).value.isOpen @@ -300,21 +410,33 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC removeIssueFromAggregatedIssue(issue, aggregatedIssue) } + /** + * Removes an [Issue] from an [AggregatedIssue]. + * Handles the case of the [Issue] not being aggregated on the [AggregatedIssue]. + * + * @param issue the issue to remove + * @param aggregatedIssue the aggregated issue to remove the issue from + */ private suspend fun removeIssueFromAggregatedIssue( - issue: Issue, - aggregatedIssue: AggregatedIssue + issue: Issue, aggregatedIssue: AggregatedIssue ) { - aggregatedIssue.issues(cache).remove(issue) - aggregatedIssue.count-- - internalUpdatedNodes += aggregatedIssue - if (aggregatedIssue.issues(cache).isEmpty()) { - deleteAggregatedIssue(aggregatedIssue) + if (aggregatedIssue.issues(cache).remove(issue)) { + aggregatedIssue.count-- + internalUpdatedNodes += aggregatedIssue + if (aggregatedIssue.issues(cache).isEmpty()) { + deleteAggregatedIssue(aggregatedIssue) + } } } + /** + * Checks if an [Issue] is still aggregated by a [ComponentVersion]. + * + * @param issue the issue to check + * @param componentVersion the component version to check + */ private suspend fun isIssueStillAggregatedByComponentVersion( - issue: Issue, - componentVersion: ComponentVersion + issue: Issue, componentVersion: ComponentVersion ): Boolean { val affected = issue.affects(cache) if (componentVersion in affected || componentVersion.component(cache).value in affected) { @@ -327,24 +449,47 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC return false } + /** + * Checks if an [Issue] is still aggregated by an [Interface]. + * + * @param issue the issue to check + * @param inter the interface to check + * @return true if the issue is still aggregated by the interface, false otherwise + */ private suspend fun isIssueStillAggregatedByInterface( - issue: Issue, - inter: Interface + issue: Issue, inter: Interface ): Boolean { val specificationVersion = inter.interfaceDefinition(cache).value.interfaceSpecificationVersion(cache).value val affectableEntities = listOf( - inter, - specificationVersion, - specificationVersion.interfaceSpecification(cache).value + inter, specificationVersion, specificationVersion.interfaceSpecification(cache).value ) + specificationVersion.activeParts(cache) return issue.affects(cache).any { it in affectableEntities } } + /** + * Checks if an [Issue] affects an entity related to a [Component]. + * + * @param issue the issue to check + * @param component the component to check + * @return true if the issue affects an entity related to the component, false otherwise + */ private suspend fun doesIssueAffectComponentRelatedEntity(issue: Issue, component: Component): Boolean { val relatedAffectedEntities = componentRelatedEntities(component) return issue.affects(cache).any { it in relatedAffectedEntities } } + /** + * Gets all entities related to a [Component]. + * This includes + * - the [Component] itself + * - all [ComponentVersion]s of the [Component] + * - all [Interface]s of all [ComponentVersion]s + * - all [InterfaceSpecificationVersion]s of all [Interface]s + * - all [InterfaceSpecification]s of all used [InterfaceSpecificationVersion]s + * + * @param component the component to get the related entities of + * @return a set of all related entities + */ private suspend fun componentRelatedEntities(component: Component): Set { val affected = mutableSetOf() affected += component @@ -364,6 +509,13 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC return affected } + /** + * Aggregates a set of [Issue]s on a [RelationPartner]. + * Handles the case of an [Issue] already being aggregated on the [RelationPartner]. + * + * @param relationPartner the relation partner to aggregate the issues on + * @param issues the issues to aggregate + */ private suspend fun createOrUpdateAggregatedIssues(relationPartner: RelationPartner, issues: Set) { val aggregatedIssues = relationPartner.aggregatedIssues(cache) val aggregatedIssueLookup = aggregatedIssues.associateBy { @@ -386,6 +538,12 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Handles the deletion of an [Interface] or [ComponentVersion] + * Deletes all associated [AggregatedIssue]s and relations. + * + * @param relationPartner the relation partner that was deleted + */ private suspend fun relationPartnerDeleted(relationPartner: RelationPartner) { relationPartner.aggregatedIssues(cache).forEach { deleteAggregatedIssue(it) @@ -395,12 +553,24 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Deletes an [AggregatedIssue] and all relations. + * + * @param aggregatedIssue the aggregated issue to delete + */ private suspend fun deleteAggregatedIssue(aggregatedIssue: AggregatedIssue) { deletedNodes += aggregatedIssue deletedNodes += aggregatedIssue.incomingRelations(cache) deletedNodes += aggregatedIssue.outgoingRelations(cache) } + /** + * Aggregates an [Issue] on a [Component] if necessary. + * It is necessary only if the issue is on the [Component] and does not affect an entity related to said [Component]. + * + * @param issue the issue to aggregate + * @param component the component to aggregate the issue on + */ private suspend fun aggregateIssueOnComponentIfNecessary(issue: Issue, component: Component) { if (component !in issue.trackables(cache)) { return @@ -412,13 +582,23 @@ class IssueAggregationUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateC } } + /** + * Unaggregates an [Issue] from a [ComponentVersion] if necessary. + * It is necessary only if the issue is on the [Component] and affects an entity related to said [Component]. + * It is only unaggregated on [ComponentVersion]s where the issue does not affect the [Component] or [ComponentVersion] itself. + * + * @param issue the issue to unaggregate + * @param component the component to unaggregate the issue from + */ private suspend fun unaggregateIssueOnComponentIfNecessary(issue: Issue, component: Component) { if (component in issue.trackables(cache)) { return } - if (doesIssueAffectComponentRelatedEntity(issue, component)) { + if (doesIssueAffectComponentRelatedEntity(issue, component) && component !in issue.affects(cache)) { for (componentVersion in component.versions(cache)) { - removeIssueFromAggregatedIssueOnRelationPartner(issue, componentVersion) + if (componentVersion !in issue.affects(cache)) { + removeIssueFromAggregatedIssueOnRelationPartner(issue, componentVersion) + } } } } From dc456a3e999c9ea899494c5dcc1c840f2dfce4d8 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 25 Sep 2023 17:23:43 +0200 Subject: [PATCH 07/30] more documentation --- .../filter/AffectedByIssueRelatedToFilterEntry.kt | 12 +++++++++--- ...AffectedByIssueRelatedToFilterEntryDefinition.kt | 6 ++++++ .../graphql/filter/PartOfProjectFilterEntry.kt | 13 +++++++++---- .../filter/PartOfProjectFilterEntryDefinition.kt | 6 ++++++ 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt b/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt index f09b70ca..aa3c76ce 100644 --- a/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt +++ b/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntry.kt @@ -2,15 +2,21 @@ package gropius.graphql.filter import gropius.model.user.permission.TrackablePermission import io.github.graphglue.authorization.Permission -import io.github.graphglue.connection.filter.model.Filter import io.github.graphglue.connection.filter.model.FilterEntry import org.neo4j.cypherdsl.core.Condition import org.neo4j.cypherdsl.core.Conditions import org.neo4j.cypherdsl.core.Cypher import org.neo4j.cypherdsl.core.Node +/** + * Parsed filter entry of a [AffectedByIssueRelatedToFilterEntryDefinition] + * + * @param trackableId the id of the Trackable to which the entity must be related to + * @param permission the node permission to check + * @param relatedToFilterEntryDefinition [AffectedByIssueRelatedToFilterEntryDefinition] used to create this entry + */ class AffectedByIssueRelatedToFilterEntry( - val filter: String, + val trackableId: String, private val relatedToFilterEntryDefinition: AffectedByIssueRelatedToFilterEntryDefinition, private val permission: Permission? @@ -25,7 +31,7 @@ class AffectedByIssueRelatedToFilterEntry( } else { Conditions.noCondition() } - val idCondition = relatedNode.property("id").isEqualTo(Cypher.anonParameter(filter)) + val idCondition = relatedNode.property("id").isEqualTo(Cypher.anonParameter(trackableId)) return Cypher.match(relationship).where(idCondition.and(authCondition)).asCondition() } diff --git a/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntryDefinition.kt b/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntryDefinition.kt index 4f2afbc8..32c727c3 100644 --- a/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntryDefinition.kt +++ b/api-common/src/main/kotlin/gropius/graphql/filter/AffectedByIssueRelatedToFilterEntryDefinition.kt @@ -12,6 +12,12 @@ import io.github.graphglue.data.execution.CypherConditionGenerator import io.github.graphglue.definition.NodeDefinitionCollection import io.github.graphglue.util.CacheMap +/** + * Filter definition entry for affected by issue related to a Trackable. + * Takes the id of the Trackable to which the entities must be related. + * + * @param nodeDefinitionCollection the [NodeDefinitionCollection] to use for authorization + */ class AffectedByIssueRelatedToFilterEntryDefinition( private val nodeDefinitionCollection: NodeDefinitionCollection, ) : FilterEntryDefinition("relatedTo", "Filters for AffectedByIssues which are related to a Trackable") { diff --git a/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntry.kt b/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntry.kt index 5eb60b26..ae64a603 100644 --- a/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntry.kt +++ b/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntry.kt @@ -1,17 +1,22 @@ package gropius.graphql.filter import gropius.model.user.permission.ProjectPermission -import gropius.model.user.permission.TrackablePermission import io.github.graphglue.authorization.Permission -import io.github.graphglue.connection.filter.model.Filter import io.github.graphglue.connection.filter.model.FilterEntry import org.neo4j.cypherdsl.core.Condition import org.neo4j.cypherdsl.core.Conditions import org.neo4j.cypherdsl.core.Cypher import org.neo4j.cypherdsl.core.Node +/** + * Parsed filter entry of a [AffectedByIssueRelatedToFilterEntryDefinition] + * + * @param projectId the id of the Project to which the entity must be related to + * @param permission the node permission to check + * @param partOfProjectFilterEntryDefinition [PartOfProjectFilterEntryDefinition] used to create this entry + */ class PartOfProjectFilterEntry( - val filter: String, + val projectId: String, private val partOfProjectFilterEntryDefinition: PartOfProjectFilterEntryDefinition, private val permission: Permission? @@ -26,7 +31,7 @@ class PartOfProjectFilterEntry( } else { Conditions.noCondition() } - val idCondition = relatedNode.property("id").isEqualTo(Cypher.anonParameter(filter)) + val idCondition = relatedNode.property("id").isEqualTo(Cypher.anonParameter(projectId)) return Cypher.match(relationship).where(idCondition.and(authCondition)).asCondition() } diff --git a/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntryDefinition.kt b/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntryDefinition.kt index 6f747fa9..eab9f3a9 100644 --- a/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntryDefinition.kt +++ b/api-common/src/main/kotlin/gropius/graphql/filter/PartOfProjectFilterEntryDefinition.kt @@ -10,6 +10,12 @@ import io.github.graphglue.data.execution.CypherConditionGenerator import io.github.graphglue.definition.NodeDefinitionCollection import io.github.graphglue.util.CacheMap +/** + * Filter definition entry for RelationPartners which are part of a Project. + * Takes the id of the Project of which the RelationPartners must be part of the graph. + * + * @param nodeDefinitionCollection the [NodeDefinitionCollection] to use for authorization + */ class PartOfProjectFilterEntryDefinition( private val nodeDefinitionCollection: NodeDefinitionCollection, ) : FilterEntryDefinition("partOfProject", "Filters for RelationPartners which are part of a Project's component graph") { From c0434c4994ed20dd7a9a5dcabec33088f59b773b Mon Sep 17 00:00:00 2001 From: nk-coding Date: Fri, 20 Oct 2023 17:58:12 +0200 Subject: [PATCH 08/30] domain model changes progress --- .../architecture/CreateInterfacePartInput.kt | 4 +-- .../InterfaceSpecificationInput.kt | 6 ---- .../InterfaceSpecificationVersionInput.kt | 12 ++++--- ...pdateInterfaceSpecificationVersionInput.kt | 5 --- .../gropius/model/architecture/Component.kt | 11 ++++++ .../model/architecture/InterfacePart.kt | 13 ++----- .../architecture/InterfaceSpecification.kt | 11 +----- .../InterfaceSpecificationVersion.kt | 10 +++--- .../model/issue/AggregatedIssueRelation.kt | 11 ++++++ .../issue/MetaAggregatedIssueRelation.kt | 31 +++++++++++++++++ .../model/issue/timeline/IssueRelation.kt | 12 +++++++ .../architecture/InterfacePartService.kt | 34 +++++-------------- .../InterfaceSpecificationService.kt | 12 ------- .../InterfaceSpecificationVersionService.kt | 21 ++++-------- ...ComponentDependencySpecificationService.kt | 2 +- .../service/architecture/RelationService.kt | 2 +- .../service/issue/IssueAggregationUpdater.kt | 12 +++---- 17 files changed, 104 insertions(+), 105 deletions(-) create mode 100644 core/src/main/kotlin/gropius/model/issue/MetaAggregatedIssueRelation.kt diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/CreateInterfacePartInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/CreateInterfacePartInput.kt index 7024994f..91e0464d 100644 --- a/core/src/main/kotlin/gropius/dto/input/architecture/CreateInterfacePartInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/architecture/CreateInterfacePartInput.kt @@ -5,6 +5,6 @@ import com.expediagroup.graphql.generator.scalars.ID @GraphQLDescription("Input for the createInterfacePart mutation") class CreateInterfacePartInput( - @GraphQLDescription("The id of the InterfaceSpecification the created InterfacePart is part of") - val interfaceSpecification: ID + @GraphQLDescription("The id of the InterfaceSpecificationVersion the created InterfacePart is part of") + val interfaceSpecificationVersion: ID ) : InterfacePartInput() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/InterfaceSpecificationInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/InterfaceSpecificationInput.kt index 3057f4b5..2574ae10 100644 --- a/core/src/main/kotlin/gropius/dto/input/architecture/InterfaceSpecificationInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/architecture/InterfaceSpecificationInput.kt @@ -17,9 +17,6 @@ open class InterfaceSpecificationInput : CreateNamedNodeInput(), CreateTemplated @GraphQLDescription("Initial versions of the InterfaceSpecification") var versions: OptionalInput> by Delegates.notNull() - @GraphQLDescription("Initial defined InterfaceParts") - var definedParts: OptionalInput> by Delegates.notNull() - @GraphQLDescription("Initial values for all templatedFields") override var templatedFields: List by Delegates.notNull() @@ -31,9 +28,6 @@ open class InterfaceSpecificationInput : CreateNamedNodeInput(), CreateTemplated versions.ifPresent { it.forEach(Input::validate) } - definedParts.ifPresent { - it.forEach(Input::validate) - } templatedFields.validateAndEnsureNoDuplicates() } } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/InterfaceSpecificationVersionInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/InterfaceSpecificationVersionInput.kt index e8532146..0cca7492 100644 --- a/core/src/main/kotlin/gropius/dto/input/architecture/InterfaceSpecificationVersionInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/architecture/InterfaceSpecificationVersionInput.kt @@ -2,10 +2,11 @@ package gropius.dto.input.architecture import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.execution.OptionalInput -import com.expediagroup.graphql.generator.scalars.ID import gropius.dto.input.common.CreateNamedNodeInput +import gropius.dto.input.common.Input import gropius.dto.input.common.JSONFieldInput import gropius.dto.input.common.validateAndEnsureNoDuplicates +import gropius.dto.input.ifPresent import gropius.dto.input.template.CreateTemplatedNodeInput import kotlin.properties.Delegates @@ -15,10 +16,8 @@ open class InterfaceSpecificationVersionInput : CreateNamedNodeInput(), CreateTe @GraphQLDescription("Initial values for all templatedFields") override var templatedFields: List by Delegates.notNull() - @GraphQLDescription( - """Ids of InterfaceParts of the associated InterfaceSpecification which should be the initial `activeParts`""" - ) - var activeParts: OptionalInput> by Delegates.notNull() + @GraphQLDescription("Initial InterfaceParts") + var parts: OptionalInput> by Delegates.notNull() @GraphQLDescription("The version of the created InterfaceSpecificationVersion") var version: String by Delegates.notNull() @@ -26,6 +25,9 @@ open class InterfaceSpecificationVersionInput : CreateNamedNodeInput(), CreateTe override fun validate() { super.validate() templatedFields.validateAndEnsureNoDuplicates() + parts.ifPresent { + it.forEach(Input::validate) + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/UpdateInterfaceSpecificationVersionInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/UpdateInterfaceSpecificationVersionInput.kt index 8f7c7f6b..fac1d2ea 100644 --- a/core/src/main/kotlin/gropius/dto/input/architecture/UpdateInterfaceSpecificationVersionInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/architecture/UpdateInterfaceSpecificationVersionInput.kt @@ -14,10 +14,6 @@ import gropius.dto.input.template.UpdateTemplatedNodeInput class UpdateInterfaceSpecificationVersionInput( @GraphQLDescription("Values for templatedFields to update") override val templatedFields: OptionalInput>, - @GraphQLDescription("Ids of InterfaceParts defined by the associated InterfaceSpecification to add to `activeParts`") - val addedActiveParts: OptionalInput>, - @GraphQLDescription("Ids of InterfaceParts defined by the associated InterfaceSpecification to remove from `activeParts`") - val removedActiveParts: OptionalInput>, @GraphQLDescription("New version of the InterfaceSpecificationVersion") val version: OptionalInput ) : UpdateNamedNodeInput(), UpdateTemplatedNodeInput { @@ -27,6 +23,5 @@ class UpdateInterfaceSpecificationVersionInput( templatedFields.ifPresent { it.validateAndEnsureNoDuplicates() } - ::addedActiveParts ensureDisjoint ::removedActiveParts } } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/architecture/Component.kt b/core/src/main/kotlin/gropius/model/architecture/Component.kt index 95c64cea..f0c83fee 100644 --- a/core/src/main/kotlin/gropius/model/architecture/Component.kt +++ b/core/src/main/kotlin/gropius/model/architecture/Component.kt @@ -2,6 +2,7 @@ package gropius.model.architecture import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import gropius.model.issue.MetaAggregatedIssueRelation import gropius.model.template.BaseTemplate import gropius.model.template.ComponentTemplate import gropius.model.template.MutableTemplatedNode @@ -44,6 +45,8 @@ class Component( companion object { const val VERSION = "VERSION" + const val INCOMING_META_AGGREGATED_ISSUE_RELATION = "INCOMING_META_AGGREGATED_ISSUE_RELATION" + const val OUTGOING_META_AGGREGATED_ISSUE_RELATION = "OUTGOING_META_AGGREGATED_ISSUE_RELATION" } @NodeRelationship(BaseTemplate.USED_IN, Direction.INCOMING) @@ -70,4 +73,12 @@ class Component( @FilterProperty override val permissions by NodeSetProperty() + @NodeRelationship(INCOMING_META_AGGREGATED_ISSUE_RELATION, Direction.INCOMING) + @GraphQLIgnore + val incomingMetaAggregatedIssueRelations by NodeSetProperty() + + @NodeRelationship(OUTGOING_META_AGGREGATED_ISSUE_RELATION, Direction.INCOMING) + @GraphQLIgnore + val outgoingMetaAggregatedIssueRelations by NodeSetProperty() + } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt b/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt index 60777a62..3e4ff981 100644 --- a/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt +++ b/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt @@ -32,10 +32,6 @@ class InterfacePart( override val templatedFields: MutableMap ) : AffectedByIssue(name, description), MutableTemplatedNode { - companion object { - const val DEFINED_ON = "DEFINED_ON" - } - @NodeRelationship(BaseTemplate.USED_IN, Direction.INCOMING) @GraphQLDescription("The Template of this InterfacePart") @FilterProperty @@ -56,14 +52,9 @@ class InterfacePart( @FilterProperty val includingIntraComponentDependencyParticipants by NodeSetProperty() - @NodeRelationship(InterfaceSpecificationVersion.ACTIVE_PART, Direction.INCOMING) + @NodeRelationship(InterfaceSpecificationVersion.PART, Direction.INCOMING) @GraphQLDescription("InterfaceSpecificationVersions where this InterfacePart is active.") @FilterProperty - val activeOn by NodeSetProperty() - - @NodeRelationship(DEFINED_ON, Direction.OUTGOING) - @GraphQLDescription("InterfaceSpecification which defines this InterfacePart") - @FilterProperty - val definedOn by NodeProperty() + val partOf by NodeProperty() } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecification.kt b/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecification.kt index fbcbbba7..4dbbc83b 100644 --- a/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecification.kt +++ b/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecification.kt @@ -48,17 +48,8 @@ class InterfaceSpecification( @FilterProperty val versions by NodeSetProperty() - @NodeRelationship(InterfacePart.DEFINED_ON, Direction.INCOMING) - @GraphQLDescription( - """InterfaceParts defined by this InterfaceSpecification. - Note that active parts depend on the InterfaceSpecificationVersion - """ - ) - @FilterProperty - val definedParts by NodeSetProperty() - @NodeRelationship(COMPONENT, Direction.OUTGOING) - @GraphQLDescription("The Component this InterfaceSpecificaton is part of.") + @GraphQLDescription("The Component this InterfaceSpecification is part of.") @FilterProperty val component by NodeProperty() diff --git a/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecificationVersion.kt b/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecificationVersion.kt index 24facb7d..ec3fb289 100644 --- a/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecificationVersion.kt +++ b/core/src/main/kotlin/gropius/model/architecture/InterfaceSpecificationVersion.kt @@ -41,7 +41,7 @@ class InterfaceSpecificationVersion( ) : AffectedByIssue(name, description), Versioned, MutableTemplatedNode { companion object { - const val ACTIVE_PART = "ACTIVE_PART" + const val PART = "PART" } @NodeRelationship(BaseTemplate.USED_IN, Direction.INCOMING) @@ -49,15 +49,13 @@ class InterfaceSpecificationVersion( @FilterProperty override val template by NodeProperty() - @NodeRelationship(ACTIVE_PART, Direction.OUTGOING) + @NodeRelationship(PART, Direction.OUTGOING) @GraphQLDescription( - """InterfaceParts which are active on this InterfaceSpecificationVersion - Semantically, only the active parts on an InterfaceSpecificationVersion exist on the Interfaces - defined by the InterfaceSpecificationVersion. + """InterfaceParts which are part of this InterfaceSpecificationVersion """ ) @FilterProperty - val activeParts by NodeSetProperty() + val parts by NodeSetProperty() @NodeRelationship(InterfaceSpecification.VERSION, Direction.INCOMING) @GraphQLDescription("The InterfaceSpecification this is part of.") diff --git a/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt b/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt index e13fd051..7e628aa9 100644 --- a/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt +++ b/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt @@ -1,6 +1,8 @@ package gropius.model.issue import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import gropius.model.issue.timeline.IssueRelation import gropius.model.user.permission.NodePermission import io.github.graphglue.model.* @@ -13,6 +15,10 @@ import io.github.graphglue.model.* @Authorization(NodePermission.READ, allowFromRelated = ["start"]) class AggregatedIssueRelation(var count: Int) : Node() { + companion object { + const val ISSUE_RELATION = "ISSUE_RELATION" + } + @NodeRelationship(AggregatedIssue.OUTGOING_RELATION, Direction.INCOMING) @GraphQLDescription("The start of this AggregatedIssueRelation.") @FilterProperty @@ -23,4 +29,9 @@ class AggregatedIssueRelation(var count: Int) : Node() { @FilterProperty val end by NodeProperty() + @NodeRelationship(ISSUE_RELATION, Direction.OUTGOING) + @GraphQLDescription("The IssueRelations aggregated by this AggregatedIssueRelation.") + @FilterProperty + val issueRelations by NodeSetProperty() + } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/issue/MetaAggregatedIssueRelation.kt b/core/src/main/kotlin/gropius/model/issue/MetaAggregatedIssueRelation.kt new file mode 100644 index 00000000..72d36552 --- /dev/null +++ b/core/src/main/kotlin/gropius/model/issue/MetaAggregatedIssueRelation.kt @@ -0,0 +1,31 @@ +package gropius.model.issue + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import gropius.model.architecture.Component +import gropius.model.issue.timeline.IssueRelation +import gropius.model.user.permission.NodePermission +import io.github.graphglue.model.* + +@DomainNode +@GraphQLDescription( + """An aggregated IssueRelation. + IssueRelations are aggregated by both start and end Issue. + """ +) +class MetaAggregatedIssueRelation(var count: Int) : Node() { + + companion object { + const val META_ISSUE_RELATION = "META_ISSUE_RELATION" + } + + @NodeRelationship(Component.OUTGOING_META_AGGREGATED_ISSUE_RELATION, Direction.INCOMING) + val start by NodeProperty() + + @NodeRelationship(Component.INCOMING_META_AGGREGATED_ISSUE_RELATION, Direction.INCOMING) + val end by NodeProperty() + + @NodeRelationship(META_ISSUE_RELATION, Direction.OUTGOING) + val issueRelations by NodeSetProperty() + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/issue/timeline/IssueRelation.kt b/core/src/main/kotlin/gropius/model/issue/timeline/IssueRelation.kt index 93806ffe..9ba5a451 100644 --- a/core/src/main/kotlin/gropius/model/issue/timeline/IssueRelation.kt +++ b/core/src/main/kotlin/gropius/model/issue/timeline/IssueRelation.kt @@ -1,7 +1,10 @@ package gropius.model.issue.timeline import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import gropius.model.issue.AggregatedIssueRelation import gropius.model.issue.Issue +import gropius.model.issue.MetaAggregatedIssueRelation import gropius.model.template.IssueRelationType import io.github.graphglue.model.Direction import io.github.graphglue.model.DomainNode @@ -38,4 +41,13 @@ class IssueRelation( @FilterProperty val relatedIssue by NodeProperty() + @NodeRelationship(AggregatedIssueRelation.ISSUE_RELATION, Direction.INCOMING) + @GraphQLDescription("The AggregatedIssueRelations this IssueRelation is aggregated by.") + @FilterProperty + val aggregatedBy by NodeSetProperty() + + @NodeRelationship(AggregatedIssueRelation.ISSUE_RELATION, Direction.OUTGOING) + @GraphQLIgnore + val metaAggregatedBy by NodeSetProperty() + } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/architecture/InterfacePartService.kt b/core/src/main/kotlin/gropius/service/architecture/InterfacePartService.kt index 823be995..6c12e8d4 100644 --- a/core/src/main/kotlin/gropius/service/architecture/InterfacePartService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/InterfacePartService.kt @@ -8,9 +8,11 @@ import gropius.dto.input.architecture.UpdateInterfacePartInput import gropius.dto.input.common.DeleteNodeInput import gropius.model.architecture.InterfacePart import gropius.model.architecture.InterfaceSpecification +import gropius.model.architecture.InterfaceSpecificationVersion import gropius.model.user.permission.NodePermission import gropius.repository.architecture.InterfacePartRepository import gropius.repository.architecture.InterfaceSpecificationRepository +import gropius.repository.architecture.InterfaceSpecificationVersionRepository import gropius.repository.common.NodeRepository import gropius.repository.findAllById import gropius.repository.findById @@ -25,6 +27,7 @@ import org.springframework.stereotype.Service * * @param repository the associated repository used for CRUD functionality * @param interfaceSpecificationRepository used get [InterfaceSpecification] by id + * @param interfaceSpecificationVersionRepository used get [InterfaceSpecificationVersion] by id * @param templatedNodeService used to update templatedFields * @param nodeRepository used to update/delete nodes */ @@ -32,6 +35,7 @@ import org.springframework.stereotype.Service class InterfacePartService( repository: InterfacePartRepository, private val interfaceSpecificationRepository: InterfaceSpecificationRepository, + private val interfaceSpecificationVersionRepository: InterfaceSpecificationVersionRepository, private val templatedNodeService: TemplatedNodeService, private val nodeRepository: NodeRepository ) : AffectedByIssueService(repository) { @@ -48,19 +52,20 @@ class InterfacePartService( authorizationContext: GropiusAuthorizationContext, input: CreateInterfacePartInput ): InterfacePart { input.validate() - val interfaceSpecification = interfaceSpecificationRepository.findById(input.interfaceSpecification) + val interfaceSpecificationVersion = interfaceSpecificationVersionRepository.findById(input.interfaceSpecificationVersion) + val interfaceSpecification = interfaceSpecificationVersion.interfaceSpecification().value checkPermission( interfaceSpecification, Permission(NodePermission.ADMIN, authorizationContext), "create InterfaceParts on the InterfaceSpecification" ) val interfacePart = createInterfacePart(interfaceSpecification, input) - interfacePart.definedOn().value = interfaceSpecification + interfacePart.partOf().value = interfaceSpecificationVersion return repository.save(interfacePart).awaitSingle() } /** - * Creates a new [InterfacePart] based on the provided [input] on [interfaceSpecification] + * Creates a new [InterfacePart] based on the provided [input] * Does not check the authorization status, does not save the created nodes * Validates the [input] * @@ -122,27 +127,4 @@ class InterfacePartService( repository.delete(interfacePart).awaitSingle() } - /** - * Gets [InterfacePart]s by id and validates that all are part of [interfaceSpecification] - * - * @param ids the ids of the [InterfacePart]s to get, can be empty - * @param interfaceSpecification all returned [InterfacePart]s must be part of this - * @return the found[InterfacePart]s - * @throws IllegalArgumentException if any [InterfacePart] was not defined by [interfaceSpecification] - */ - suspend fun findPartsByIdAndValidatePartOfInterfaceSpecification( - ids: Collection, interfaceSpecification: InterfaceSpecification - ): Set { - if (ids.isEmpty()) { - return emptySet() - } - val parts = repository.findAllById(ids) - parts.forEach { - if (it.definedOn().value != interfaceSpecification) { - throw IllegalArgumentException("InterfacePart ${it.rawId} is not part of ${interfaceSpecification.rawId}") - } - } - return parts.toSet() - } - } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationService.kt b/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationService.kt index 213c6248..40b69437 100644 --- a/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationService.kt @@ -95,13 +95,6 @@ class InterfaceSpecificationService( ) } } - input.definedParts.ifPresent { inputs -> - interfaceSpecification.definedParts() += inputs.map { - interfacePartService.createInterfacePart( - interfaceSpecification, it - ) - } - } return interfaceSpecification } @@ -146,11 +139,6 @@ class InterfaceSpecificationService( val template = interfaceSpecificationTemplateRepository.findById(templateId) interfaceSpecification.template().value = template updateInterfaceSpecificationVersionTemplate(interfaceSpecification, input, template) - val interfacePartTemplate = template.interfacePartTemplate().value - interfaceSpecification.definedParts().forEach { - it.template().value = interfacePartTemplate - templatedNodeService.updateTemplatedFields(it, input.interfacePartTemplatedFields, true) - } val graphUpdater = ComponentGraphUpdater() graphUpdater.updateInterfaceSpecificationTemplate(interfaceSpecification) nodeRepository.deleteAll(graphUpdater.deletedNodes).awaitSingleOrNull() diff --git a/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationVersionService.kt b/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationVersionService.kt index 3d3c0bd4..f394450c 100644 --- a/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationVersionService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/InterfaceSpecificationVersionService.kt @@ -87,10 +87,13 @@ class InterfaceSpecificationVersionService( val interfaceSpecificationVersion = InterfaceSpecificationVersion(input.name, input.description, input.version, templatedFields) interfaceSpecificationVersion.template().value = template - input.activeParts.ifPresent { - interfaceSpecificationVersion.activeParts().addAll( - interfacePartService.findPartsByIdAndValidatePartOfInterfaceSpecification(it, interfaceSpecification) - ) + + input.parts.ifPresent { inputs -> + interfaceSpecificationVersion.parts() += inputs.map { + interfacePartService.createInterfacePart( + interfaceSpecification, it + ) + } } createdExtensibleNode(interfaceSpecificationVersion, input) return interfaceSpecificationVersion @@ -114,20 +117,10 @@ class InterfaceSpecificationVersionService( Permission(NodePermission.ADMIN, authorizationContext), "update the InterfaceSpecificationVersion" ) - val interfaceSpecification = interfaceSpecificationVersion.interfaceSpecification().value - val addedParts = interfacePartService.findPartsByIdAndValidatePartOfInterfaceSpecification( - input.addedActiveParts.orElse(emptySet()), interfaceSpecification - ) - interfaceSpecificationVersion.activeParts().addAll(addedParts) - val removedParts = interfacePartService.findPartsByIdAndValidatePartOfInterfaceSpecification( - input.removedActiveParts.orElse(emptySet()), interfaceSpecification - ) - interfaceSpecificationVersion.activeParts().removeAll(removedParts) input.version.ifPresent { interfaceSpecificationVersion.version = it } templatedNodeService.updateTemplatedFields(interfaceSpecificationVersion, input, false) updateNamedNode(interfaceSpecificationVersion, input) val issueAggregationUpdater = IssueAggregationUpdater() - issueAggregationUpdater.updatedActiveParts(interfaceSpecificationVersion, addedParts, removedParts) issueAggregationUpdater.save(nodeRepository) return repository.save(interfaceSpecificationVersion).awaitSingle() } diff --git a/core/src/main/kotlin/gropius/service/architecture/IntraComponentDependencySpecificationService.kt b/core/src/main/kotlin/gropius/service/architecture/IntraComponentDependencySpecificationService.kt index a6090a2e..abc89737 100644 --- a/core/src/main/kotlin/gropius/service/architecture/IntraComponentDependencySpecificationService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/IntraComponentDependencySpecificationService.kt @@ -170,7 +170,7 @@ class IntraComponentDependencySpecificationService( } val parts = interfacePartRepository.findAllById(input.includedParts.orElse(emptyList())) for (part in parts) { - if (interfaceDefinition.interfaceSpecificationVersion().value !in part.activeOn()) { + if (interfaceDefinition.interfaceSpecificationVersion().value !in part.partOf()) { throw IllegalArgumentException("Specified includedParts must be active on the specified Interface") } } diff --git a/core/src/main/kotlin/gropius/service/architecture/RelationService.kt b/core/src/main/kotlin/gropius/service/architecture/RelationService.kt index 61f81526..8dc59381 100644 --- a/core/src/main/kotlin/gropius/service/architecture/RelationService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/RelationService.kt @@ -123,7 +123,7 @@ class RelationService( relationPartner.interfaceDefinition().value.interfaceSpecificationVersion().value val parts = partIds.map { interfacePartRepository.findById(it) }.toSet() parts.forEach { - if (interfaceSpecificationVersion !in it.activeOn()) { + if (interfaceSpecificationVersion !in it.partOf()) { throw IllegalArgumentException("InterfacePart must be active on the used InterfaceSpecificationVersion") } } diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index 3a1b5210..89baa6fc 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -112,7 +112,7 @@ class IssueAggregationUpdater( val specificationVersion = definition.interfaceSpecificationVersion(cache).value val affectableEntities = listOf( createdInterface, specificationVersion, specificationVersion.interfaceSpecification(cache).value - ) + specificationVersion.activeParts(cache) + ) + specificationVersion.parts(cache) val affectedIssues = affectableEntities.flatMap { it.affectingIssues(cache) }.toSet() createOrUpdateAggregatedIssues(createdInterface, affectedIssues) val component = definition.componentVersion(cache).value.component(cache).value @@ -143,7 +143,7 @@ class IssueAggregationUpdater( * @param interfacePart the deleted interface part */ suspend fun deletedInterfacePart(interfacePart: InterfacePart) { - val interfaces = interfacePart.activeOn(cache).flatMap { version -> + val interfaces = interfacePart.partOf(cache).flatMap { version -> version.interfaceDefinitions(cache).mapNotNull { it.visibleInterface(cache).value } @@ -228,7 +228,7 @@ class IssueAggregationUpdater( } is InterfacePart -> { - addedAffectedInterfaceRelatedEntity(issue, affectedEntity.activeOn(cache)) + addedAffectedInterfaceRelatedEntity(issue, affectedEntity.partOf(cache)) } is InterfaceSpecificationVersion -> { @@ -293,7 +293,7 @@ class IssueAggregationUpdater( } is InterfacePart -> { - removedAffectedInterfaceRelatedEntity(issue, affectedEntity.activeOn(cache)) + removedAffectedInterfaceRelatedEntity(issue, affectedEntity.partOf(cache)) } is InterfaceSpecificationVersion -> { @@ -462,7 +462,7 @@ class IssueAggregationUpdater( val specificationVersion = inter.interfaceDefinition(cache).value.interfaceSpecificationVersion(cache).value val affectableEntities = listOf( inter, specificationVersion, specificationVersion.interfaceSpecification(cache).value - ) + specificationVersion.activeParts(cache) + ) + specificationVersion.parts(cache) return issue.affects(cache).any { it in affectableEntities } } @@ -502,7 +502,7 @@ class IssueAggregationUpdater( val specificationVersion = interfaceDefinition.interfaceSpecificationVersion(cache).value affected += specificationVersion affected += specificationVersion.interfaceSpecification(cache).value - affected += specificationVersion.activeParts(cache) + affected += specificationVersion.parts(cache) } } } From 1d628c509209cb4a8b6f8a6c66d2f1d13dd57580 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sun, 22 Oct 2023 03:30:07 +0200 Subject: [PATCH 09/30] meta aggregated issue relations --- .../issue/MetaAggregatedIssueRelation.kt | 4 +- .../architecture/ComponentGraphUpdater.kt | 3 +- .../service/issue/IssueAggregationUpdater.kt | 153 ++++++++++++------ .../gropius/service/issue/IssueService.kt | 10 ++ 4 files changed, 121 insertions(+), 49 deletions(-) diff --git a/core/src/main/kotlin/gropius/model/issue/MetaAggregatedIssueRelation.kt b/core/src/main/kotlin/gropius/model/issue/MetaAggregatedIssueRelation.kt index 72d36552..e5667029 100644 --- a/core/src/main/kotlin/gropius/model/issue/MetaAggregatedIssueRelation.kt +++ b/core/src/main/kotlin/gropius/model/issue/MetaAggregatedIssueRelation.kt @@ -20,10 +20,10 @@ class MetaAggregatedIssueRelation(var count: Int) : Node() { } @NodeRelationship(Component.OUTGOING_META_AGGREGATED_ISSUE_RELATION, Direction.INCOMING) - val start by NodeProperty() + val start by NodeProperty() @NodeRelationship(Component.INCOMING_META_AGGREGATED_ISSUE_RELATION, Direction.INCOMING) - val end by NodeProperty() + val end by NodeProperty() @NodeRelationship(META_ISSUE_RELATION, Direction.OUTGOING) val issueRelations by NodeSetProperty() diff --git a/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt b/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt index 0ed70f82..cdfd32a4 100644 --- a/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt +++ b/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt @@ -41,6 +41,7 @@ class ComponentGraphUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateCon suspend fun deleteComponent(component: Component) { cache.add(component) deletedNodes += component + issueAggregationUpdater.deletedComponent(component) component.interfaceSpecifications(cache).forEach { deleteInterfaceSpecification(it) } @@ -98,7 +99,6 @@ class ComponentGraphUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateCon suspend fun deleteInterfaceSpecification(interfaceSpecification: InterfaceSpecification) { cache.add(interfaceSpecification) deletedNodes += interfaceSpecification - deletedNodes += interfaceSpecification.definedParts(cache) interfaceSpecification.versions(cache).forEach { deleteInterfaceSpecificationVersion(it) } @@ -112,6 +112,7 @@ class ComponentGraphUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateCon suspend fun deleteInterfaceSpecificationVersion(interfaceSpecificationVersion: InterfaceSpecificationVersion) { cache.add(interfaceSpecificationVersion) deletedNodes += interfaceSpecificationVersion + deletedNodes += interfaceSpecificationVersion.parts(cache) interfaceSpecificationVersion.interfaceDefinitions(cache).toSet().forEach { deleteInterfaceDefinition(it) } diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index 89baa6fc..34b8a377 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -3,6 +3,8 @@ package gropius.service.issue import gropius.model.architecture.* import gropius.model.issue.AggregatedIssue import gropius.model.issue.Issue +import gropius.model.issue.MetaAggregatedIssueRelation +import gropius.model.issue.timeline.IssueRelation import gropius.model.template.IssueState import gropius.model.template.IssueType import gropius.service.NodeBatchUpdateContext @@ -63,6 +65,17 @@ class IssueAggregationUpdater( } } + /** + * Should be called when a [Component] is deleted. + * Must be called BEFORE the component is deleted from the database. + * + * @param component the component that was deleted + */ + suspend fun deletedComponent(component: Component) { + deletedNodes += component.incomingMetaAggregatedIssueRelations(cache) + deletedNodes += component.outgoingMetaAggregatedIssueRelations(cache) + } + /** * Should be called when an [Issue] is added to a [Trackable]. * Must be called AFTER the issue was added to the trackable. @@ -75,6 +88,18 @@ class IssueAggregationUpdater( return } aggregateIssueOnComponentIfNecessary(issue, trackable) + issue.incomingRelations(cache).forEach { relation -> + val other = relation.issue(cache).value + other.trackables(cache).forEach { + addToMetaAggregatedRelation(it, trackable, relation) + } + } + issue.outgoingRelations(cache).forEach { relation -> + val other = relation.relatedIssue(cache).value + other?.trackables?.let { it(cache) }?.forEach { + addToMetaAggregatedRelation(trackable, it, relation) + } + } } /** @@ -92,14 +117,31 @@ class IssueAggregationUpdater( val removed = mutableSetOf() aggregatedBy.forEach { val target = it.relationPartner(cache).value - if (target is ComponentVersion && target.component(cache).value == trackable) { - if (!isIssueStillAggregatedByComponentVersion(issue, target)) { - removeIssueFromAggregatedIssue(issue, it) - removed += it - } + val targetComponent = if (target is ComponentVersion) { + target.component(cache).value + } else if (target is Interface) { + target.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value + } else { + error("Unknown relation partner") + } + if (targetComponent == trackable) { + removeIssueFromAggregatedIssue(issue, it) + removed += it } } aggregatedBy.removeAll(removed) + issue.incomingRelations(cache).forEach { relation -> + val other = relation.issue(cache).value + other.trackables(cache).forEach { + removeFromMetaAggregatedRelation(it, trackable, relation) + } + } + issue.outgoingRelations(cache).forEach { relation -> + val other = relation.relatedIssue(cache).value + other?.trackables?.let { it(cache) }?.forEach { + removeFromMetaAggregatedRelation(trackable, it, relation) + } + } } /** @@ -143,10 +185,8 @@ class IssueAggregationUpdater( * @param interfacePart the deleted interface part */ suspend fun deletedInterfacePart(interfacePart: InterfacePart) { - val interfaces = interfacePart.partOf(cache).flatMap { version -> - version.interfaceDefinitions(cache).mapNotNull { - it.visibleInterface(cache).value - } + val interfaces = interfacePart.partOf(cache).value.interfaceDefinitions(cache).mapNotNull { + it.visibleInterface(cache).value } val components = interfaces.map { it.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value @@ -164,41 +204,26 @@ class IssueAggregationUpdater( } } - /** - * Should be called when the active parts of an [InterfaceSpecificationVersion] were updated. - * Must be called AFTER the active parts were updated. - * - * @param interfaceSpecificationVersion the interface specification version that was updated - * @param addedParts the parts that were added - * @param removedParts the parts that were removed - */ - suspend fun updatedActiveParts( - interfaceSpecificationVersion: InterfaceSpecificationVersion, - addedParts: Set, - removedParts: Set - ) { - val interfaces = interfaceSpecificationVersion.interfaceDefinitions(cache).mapNotNull { - it.visibleInterface(cache).value - } - val newAffectingIssues = addedParts.flatMap { it.affectingIssues(cache) }.toSet() - val potentialIssuesToRemove = removedParts.flatMap { it.affectingIssues(cache) }.toSet() - for (inter in interfaces) { - createOrUpdateAggregatedIssues(inter, newAffectingIssues) - for (issue in potentialIssuesToRemove) { - if (!isIssueStillAggregatedByInterface(issue, inter)) { - removeIssueFromAggregatedIssueOnRelationPartner(issue, inter) - } + suspend fun createdIssueRelation(issueRelation: IssueRelation) { + val issue = issueRelation.issue(cache).value + val relatedIssue = issueRelation.relatedIssue(cache).value + val trackables = issue.trackables(cache) + val relatedTrackables = relatedIssue?.trackables?.let { it(cache) } ?: emptySet() + for (trackable in trackables) { + for (relatedTrackable in relatedTrackables) { + addToMetaAggregatedRelation(trackable, relatedTrackable, issueRelation) } } - val components = interfaces.map { - it.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value - }.toSet() - for (component in components) { - for (issue in newAffectingIssues) { - unaggregateIssueOnComponentIfNecessary(issue, component) - } - for (issue in potentialIssuesToRemove) { - aggregateIssueOnComponentIfNecessary(issue, component) + } + + suspend fun deletedIssueRelation(issueRelation: IssueRelation) { + val issue = issueRelation.issue(cache).value + val relatedIssue = issueRelation.relatedIssue(cache).value + val trackables = issue.trackables(cache) + val relatedTrackables = relatedIssue?.trackables?.let { it(cache) } ?: emptySet() + for (trackable in trackables) { + for (relatedTrackable in relatedTrackables) { + removeFromMetaAggregatedRelation(trackable, relatedTrackable, issueRelation) } } } @@ -228,7 +253,7 @@ class IssueAggregationUpdater( } is InterfacePart -> { - addedAffectedInterfaceRelatedEntity(issue, affectedEntity.partOf(cache)) + addedAffectedInterfaceRelatedEntity(issue, setOf(affectedEntity.partOf(cache).value)) } is InterfaceSpecificationVersion -> { @@ -293,7 +318,7 @@ class IssueAggregationUpdater( } is InterfacePart -> { - removedAffectedInterfaceRelatedEntity(issue, affectedEntity.partOf(cache)) + removedAffectedInterfaceRelatedEntity(issue, setOf(affectedEntity.partOf(cache).value)) } is InterfaceSpecificationVersion -> { @@ -326,7 +351,9 @@ class IssueAggregationUpdater( issue: Issue, interfaceSpecificationVersions: Set ) { val interfaces = interfaceSpecificationVersions.flatMap { version -> - version.interfaceDefinitions(cache).mapNotNull { it.visibleInterface(cache).value } + version.interfaceDefinitions(cache).filter { + it.componentVersion(cache).value.component(cache).value in issue.trackables(cache) + }.mapNotNull { it.visibleInterface(cache).value } } addedAffectedInterfaceRelatedEntity(issue, interfaces) } @@ -474,6 +501,9 @@ class IssueAggregationUpdater( * @return true if the issue affects an entity related to the component, false otherwise */ private suspend fun doesIssueAffectComponentRelatedEntity(issue: Issue, component: Component): Boolean { + if (component !in issue.trackables(cache)) { + return false + } val relatedAffectedEntities = componentRelatedEntities(component) return issue.affects(cache).any { it in relatedAffectedEntities } } @@ -591,7 +621,7 @@ class IssueAggregationUpdater( * @param component the component to unaggregate the issue from */ private suspend fun unaggregateIssueOnComponentIfNecessary(issue: Issue, component: Component) { - if (component in issue.trackables(cache)) { + if (component !in issue.trackables(cache) || component in issue.affects(cache)) { return } if (doesIssueAffectComponentRelatedEntity(issue, component) && component !in issue.affects(cache)) { @@ -603,4 +633,35 @@ class IssueAggregationUpdater( } } + private suspend fun addToMetaAggregatedRelation(from: Trackable, to: Trackable, issueRelation: IssueRelation) { + if (from !is Component || to !is Component) { + return + } + val metaAggregatedRelation = from.outgoingMetaAggregatedIssueRelations(cache).find { + it.end(cache).value == to + } ?: MetaAggregatedIssueRelation(0).also { + it.start(cache).value = from + it.end(cache).value = to + } + internalUpdatedNodes += metaAggregatedRelation + if (metaAggregatedRelation.issueRelations(cache).add(issueRelation)) { + metaAggregatedRelation.count++ + } + } + + private suspend fun removeFromMetaAggregatedRelation(from: Trackable, to: Trackable, issueRelation: IssueRelation) { + if (from !is Component || to !is Component) { + return + } + val metaAggregatedRelation = from.outgoingMetaAggregatedIssueRelations(cache).find { + it.end(cache).value == to + } ?: return + if (metaAggregatedRelation.issueRelations(cache).remove(issueRelation)) { + metaAggregatedRelation.count-- + } + if (metaAggregatedRelation.issueRelations(cache).isEmpty()) { + deletedNodes += metaAggregatedRelation + } + } + } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/issue/IssueService.kt b/core/src/main/kotlin/gropius/service/issue/IssueService.kt index fe63313a..b5721398 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueService.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueService.kt @@ -1791,6 +1791,11 @@ class IssueService( relatedEvent.relation().value = relation createdTimelineItemOnRelatedIssue(relatedIssue, relatedEvent, atTime, byUser) relatedIssue.incomingRelations() += relation + + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.createdIssueRelation(relation) + aggregationUpdater.save(nodeRepository) + return relation } @@ -1940,6 +1945,11 @@ class IssueService( createdTimelineItemOnRelatedIssue(relatedIssue!!, relatedEvent, atTime, byUser) issue.outgoingRelations() -= issueRelation relatedIssue.incomingRelations() -= issueRelation + + val aggregationUpdater = IssueAggregationUpdater() + aggregationUpdater.createdIssueRelation(issueRelation) + aggregationUpdater.save(nodeRepository) + return event } From ac7185738ce1e612f9f07eed1db20651e124fe41 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sun, 22 Oct 2023 18:16:46 +0200 Subject: [PATCH 10/30] aggregated issue relation progress --- .../model/issue/AggregatedIssueRelation.kt | 8 + .../service/issue/IssueAggregationUpdater.kt | 207 +++++++++++++++--- .../gropius/service/issue/IssueService.kt | 2 +- 3 files changed, 188 insertions(+), 29 deletions(-) diff --git a/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt b/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt index 7e628aa9..478fe034 100644 --- a/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt +++ b/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt @@ -3,6 +3,8 @@ package gropius.model.issue import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.annotations.GraphQLIgnore import gropius.model.issue.timeline.IssueRelation +import gropius.model.template.IssueRelationType +import gropius.model.template.IssueType import gropius.model.user.permission.NodePermission import io.github.graphglue.model.* @@ -17,6 +19,7 @@ class AggregatedIssueRelation(var count: Int) : Node() { companion object { const val ISSUE_RELATION = "ISSUE_RELATION" + const val TYPE = "TYPE" } @NodeRelationship(AggregatedIssue.OUTGOING_RELATION, Direction.INCOMING) @@ -29,6 +32,11 @@ class AggregatedIssueRelation(var count: Int) : Node() { @FilterProperty val end by NodeProperty() + @NodeRelationship(TYPE, Direction.OUTGOING) + @GraphQLDescription("The IssueType of this AggregatedIssue.") + @FilterProperty + val type by NodeProperty() + @NodeRelationship(ISSUE_RELATION, Direction.OUTGOING) @GraphQLDescription("The IssueRelations aggregated by this AggregatedIssueRelation.") @FilterProperty diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index 34b8a377..8460dc66 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -2,6 +2,7 @@ package gropius.service.issue import gropius.model.architecture.* import gropius.model.issue.AggregatedIssue +import gropius.model.issue.AggregatedIssueRelation import gropius.model.issue.Issue import gropius.model.issue.MetaAggregatedIssueRelation import gropius.model.issue.timeline.IssueRelation @@ -36,7 +37,7 @@ class IssueAggregationUpdater( } issue.aggregatedBy(cache).clear() relationPartners.forEach { - createOrUpdateAggregatedIssues(it, setOf(issue)) + createOrUpdateAggregatedIssue(it, issue) } } @@ -88,6 +89,24 @@ class IssueAggregationUpdater( return } aggregateIssueOnComponentIfNecessary(issue, trackable) + val interfaceSpecificationVersions = issue.affects(cache).flatMap { + when (it) { + is InterfaceSpecificationVersion -> listOf(it) + is InterfaceSpecification -> it.versions(cache) + is InterfacePart -> listOf(it.partOf(cache).value) + else -> emptyList() + } + } + val interfaces = interfaceSpecificationVersions.flatMap { version -> + version.interfaceDefinitions(cache).filter { + it.componentVersion(cache).value.component(cache).value == trackable + }.mapNotNull { it.visibleInterface(cache).value } + } + issue.affects(cache).filterIsInstance().filter { + it.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value == trackable + } + interfaces.forEach { + createOrUpdateAggregatedIssue(it, issue) + } issue.incomingRelations(cache).forEach { relation -> val other = relation.issue(cache).value other.trackables(cache).forEach { @@ -151,13 +170,17 @@ class IssueAggregationUpdater( */ suspend fun createdInterface(createdInterface: Interface) { val definition = createdInterface.interfaceDefinition(cache).value + val component = definition.componentVersion(cache).value.component(cache).value val specificationVersion = definition.interfaceSpecificationVersion(cache).value val affectableEntities = listOf( createdInterface, specificationVersion, specificationVersion.interfaceSpecification(cache).value ) + specificationVersion.parts(cache) - val affectedIssues = affectableEntities.flatMap { it.affectingIssues(cache) }.toSet() - createOrUpdateAggregatedIssues(createdInterface, affectedIssues) - val component = definition.componentVersion(cache).value.component(cache).value + val affectedIssues = affectableEntities.flatMap { it.affectingIssues(cache) }.filter { + component in it.trackables(cache) + }.toSet() + affectedIssues.forEach { + createOrUpdateAggregatedIssue(createdInterface, it) + } for (issue in affectedIssues) { unaggregateIssueOnComponentIfNecessary(issue, component) } @@ -204,6 +227,12 @@ class IssueAggregationUpdater( } } + /** + * Should be called when an [IssueRelation] is created. + * Must be called AFTER the relation was created. + * + * @param issueRelation the created issue relation + */ suspend fun createdIssueRelation(issueRelation: IssueRelation) { val issue = issueRelation.issue(cache).value val relatedIssue = issueRelation.relatedIssue(cache).value @@ -216,6 +245,11 @@ class IssueAggregationUpdater( } } + /** + * Should be called when an [IssueRelation] is deleted. + * + * @param issueRelation the deleted issue relation + */ suspend fun deletedIssueRelation(issueRelation: IssueRelation) { val issue = issueRelation.issue(cache).value val relatedIssue = issueRelation.relatedIssue(cache).value @@ -228,6 +262,19 @@ class IssueAggregationUpdater( } } + /** + * Should be called when an [Relation] is created. + * + * @param relation the created relation + */ + suspend fun createdRelation(relation: Relation) { + + } + + suspend fun deletedRelation(relation: Relation) { + + } + /** * Should be called when an [Issue] affects an additional entity. * Must be called AFTER the affected entity was added to the issue. @@ -239,12 +286,12 @@ class IssueAggregationUpdater( when (affectedEntity) { is Component -> { affectedEntity.versions(cache).forEach { - createOrUpdateAggregatedIssues(it, setOf(issue)) + createOrUpdateAggregatedIssue(it, issue) } } is ComponentVersion -> { - createOrUpdateAggregatedIssues(affectedEntity, setOf(issue)) + createOrUpdateAggregatedIssue(affectedEntity, issue) unaggregateIssueOnComponentIfNecessary(issue, affectedEntity.component(cache).value) } @@ -305,7 +352,7 @@ class IssueAggregationUpdater( } else { if (component in issue.trackables(cache)) { for (componentVersion in component.versions(cache)) { - createOrUpdateAggregatedIssues(componentVersion, setOf(issue)) + createOrUpdateAggregatedIssue(componentVersion, issue) } } else { removeIssueFromAggregatedIssueOnRelationPartner(issue, affectedEntity) @@ -367,10 +414,15 @@ class IssueAggregationUpdater( private suspend fun addedAffectedInterfaceRelatedEntity( issue: Issue, interfaces: Collection ) { - for (inter in interfaces) { - createOrUpdateAggregatedIssues(inter, setOf(issue)) + val filteredInterfaces = interfaces.filter { + it.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value in issue.trackables( + cache + ) } - val components = interfaces.map { + for (inter in filteredInterfaces) { + createOrUpdateAggregatedIssue(inter, issue) + } + val components = filteredInterfaces.map { it.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value }.toSet() for (component in components) { @@ -453,6 +505,34 @@ class IssueAggregationUpdater( if (aggregatedIssue.issues(cache).isEmpty()) { deleteAggregatedIssue(aggregatedIssue) } + issue.outgoingRelations(cache).forEach { relation -> + val aggregatedBy = relation.aggregatedBy(cache).filter { + it.start(cache).value == aggregatedIssue + } + aggregatedBy.forEach { + removeIssueRelationFromAggregatedIssueRelation(relation, it) + } + } + issue.incomingRelations(cache).forEach { relation -> + val aggregatedBy = relation.aggregatedBy(cache).filter { + it.end(cache).value == aggregatedIssue + } + aggregatedBy.forEach { + removeIssueRelationFromAggregatedIssueRelation(relation, it) + } + } + } + } + + private suspend fun removeIssueRelationFromAggregatedIssueRelation( + issueRelation: IssueRelation, aggregatedIssueRelation: AggregatedIssueRelation + ) { + if (aggregatedIssueRelation.issueRelations(cache).remove(issueRelation)) { + aggregatedIssueRelation.count-- + internalUpdatedNodes += aggregatedIssueRelation + if (aggregatedIssueRelation.issueRelations(cache).isEmpty()) { + deletedNodes += aggregatedIssueRelation + } } } @@ -544,30 +624,57 @@ class IssueAggregationUpdater( * Handles the case of an [Issue] already being aggregated on the [RelationPartner]. * * @param relationPartner the relation partner to aggregate the issues on - * @param issues the issues to aggregate + * @param issue the issue to aggregate */ - private suspend fun createOrUpdateAggregatedIssues(relationPartner: RelationPartner, issues: Set) { + private suspend fun createOrUpdateAggregatedIssue(relationPartner: RelationPartner, issue: Issue) { val aggregatedIssues = relationPartner.aggregatedIssues(cache) - val aggregatedIssueLookup = aggregatedIssues.associateBy { - it.type(cache).value to it.isOpen - }.toMutableMap() - for (issue in issues) { - val state = issue.state(cache).value - val type = issue.type(cache).value - aggregatedIssueLookup.getOrPut(type to state.isOpen) { - val aggregatedIssue = AggregatedIssue(0, state.isOpen) - aggregatedIssue.relationPartner(cache).value = relationPartner - aggregatedIssue.type(cache).value = type - aggregatedIssue - }.let { - if (it.issues(cache).add(issue)) { - it.count++ + val aggregatedIssue = aggregatedIssues.firstOrNull { + it.type(cache).value == issue.type(cache).value && it.isOpen == issue.state(cache).value.isOpen + } ?: AggregatedIssue(0, issue.state(cache).value.isOpen).also { + it.relationPartner(cache).value = relationPartner + it.type(cache).value = issue.type(cache).value + aggregatedIssues += it + } + if (aggregatedIssue.issues(cache).add(issue)) { + aggregatedIssue.count++ + } + internalUpdatedNodes += aggregatedIssue + val incomingRelationPartners = findOutgoingRelationPartners(relationPartner) + val outgoingRelationPartners = findIncomingRelationPartners(relationPartner) + val graphRelationPartners = incomingRelationPartners + outgoingRelationPartners + issue.outgoingRelations(cache).forEach {relation -> + val relatedIssue = relation.relatedIssue(cache).value ?: return@forEach + for (relatedAggregatedIssue in relatedIssue.aggregatedBy(cache)) { + if (relatedAggregatedIssue.relationPartner(cache).value in graphRelationPartners) { + createOrUpdateAggregatedIssueRelation(aggregatedIssue, relatedAggregatedIssue, relation) + } + } + } + issue.incomingRelations(cache).forEach {relation -> + val relatedIssue = relation.issue(cache).value + for (relatedAggregatedIssue in relatedIssue.aggregatedBy(cache)) { + if (relatedAggregatedIssue.relationPartner(cache).value in graphRelationPartners) { + createOrUpdateAggregatedIssueRelation(relatedAggregatedIssue, aggregatedIssue, relation) } - internalUpdatedNodes += it } } } + private suspend fun createOrUpdateAggregatedIssueRelation( + from: AggregatedIssue, to: AggregatedIssue, issueRelation: IssueRelation + ) { + val aggregatedIssueRelation = from.outgoingRelations(cache).find { + it.end(cache).value == to + } ?: AggregatedIssueRelation(0).also { + it.start(cache).value = from + it.end(cache).value = to + } + internalUpdatedNodes += aggregatedIssueRelation + if (aggregatedIssueRelation.issueRelations(cache).add(issueRelation)) { + aggregatedIssueRelation.count++ + } + } + /** * Handles the deletion of an [Interface] or [ComponentVersion] * Deletes all associated [AggregatedIssue]s and relations. @@ -607,7 +714,7 @@ class IssueAggregationUpdater( } if (!doesIssueAffectComponentRelatedEntity(issue, component)) { for (componentVersion in component.versions(cache)) { - createOrUpdateAggregatedIssues(componentVersion, setOf(issue)) + createOrUpdateAggregatedIssue(componentVersion, issue) } } } @@ -664,4 +771,48 @@ class IssueAggregationUpdater( } } + private suspend fun findOutgoingRelationPartners(relationPartner: RelationPartner): Set { + val result = mutableSetOf() + val toExplore = ArrayDeque(listOf(relationPartner)) + while (toExplore.isNotEmpty()) { + val next = toExplore.removeFirst() + if (result.add(next)) { + toExplore += next.outgoingRelations(cache).map { it.end(cache).value } + when (next) { + is ComponentVersion -> { + toExplore += next.component(cache).value.versions(cache) + } + is Interface -> { + toExplore += next.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value.versions( + cache + ) + } + } + } + } + return result + } + + private suspend fun findIncomingRelationPartners(relationPartner: RelationPartner): Set { + val result = mutableSetOf() + val toExplore = ArrayDeque(listOf(relationPartner)) + while (toExplore.isNotEmpty()) { + val next = toExplore.removeFirst() + if (result.add(next)) { + toExplore += next.incomingRelations(cache).map { it.start(cache).value } + when (next) { + is ComponentVersion -> { + toExplore += next.component(cache).value.versions(cache) + } + is Interface -> { + toExplore += next.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value.versions( + cache + ) + } + } + } + } + return result + } + } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/issue/IssueService.kt b/core/src/main/kotlin/gropius/service/issue/IssueService.kt index b5721398..2206aaec 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueService.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueService.kt @@ -1947,7 +1947,7 @@ class IssueService( relatedIssue.incomingRelations() -= issueRelation val aggregationUpdater = IssueAggregationUpdater() - aggregationUpdater.createdIssueRelation(issueRelation) + aggregationUpdater.deletedIssueRelation(issueRelation) aggregationUpdater.save(nodeRepository) return event From f2116201f2143f1b75ebe9c596ac064932d54ad7 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 23 Oct 2023 03:24:51 +0200 Subject: [PATCH 11/30] first version of IssueRelation aggregation logic --- .../architecture/ComponentGraphUpdater.kt | 2 + .../service/issue/IssueAggregationUpdater.kt | 273 ++++++++++++++++-- 2 files changed, 256 insertions(+), 19 deletions(-) diff --git a/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt b/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt index cdfd32a4..9e3d7e1f 100644 --- a/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt +++ b/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt @@ -88,6 +88,7 @@ class ComponentGraphUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateCon if (startNode is ComponentVersion) { validateComponentVersion(startNode) } + issueAggregationUpdater.deletedRelation(relation) } } @@ -236,6 +237,7 @@ class ComponentGraphUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateCon */ suspend fun createRelation(relation: Relation) { cache.add(relation) + issueAggregationUpdater.createdRelation(relation) addForUpdatedRelationTransitive(relation) } diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index 8460dc66..4b95701f 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -243,6 +243,16 @@ class IssueAggregationUpdater( addToMetaAggregatedRelation(trackable, relatedTrackable, issueRelation) } } + if (relatedIssue != null) { + val endAggregatedIssues = relatedIssue.aggregatedBy(cache).associateBy { it.relationPartner(cache).value } + for (aggregatedIssue in issue.aggregatedBy(cache)) { + val startRelationPartner = aggregatedIssue.relationPartner(cache).value + val connected = connectedRelationPartners(startRelationPartner, endAggregatedIssues.keys) + connected.forEach { + createOrUpdateAggregatedIssueRelation(aggregatedIssue, endAggregatedIssues[it]!!, issueRelation) + } + } + } } /** @@ -260,6 +270,9 @@ class IssueAggregationUpdater( removeFromMetaAggregatedRelation(trackable, relatedTrackable, issueRelation) } } + issueRelation.aggregatedBy(cache).forEach { aggregatedBy -> + removeIssueRelationFromAggregatedIssueRelation(issueRelation, aggregatedBy) + } } /** @@ -268,11 +281,118 @@ class IssueAggregationUpdater( * @param relation the created relation */ suspend fun createdRelation(relation: Relation) { + val start = relation.start(cache).value + val end = relation.end(cache).value + val outgoingRelationPartners = findOutgoingRelationPartners(end) + end + val outgoingComponents = + outgoingRelationPartners.filterIsInstance().map { it.component(cache).value }.toSet() + val incomingRelationPartners = findIncomingRelationPartners(start) + start + val incomingComponents = + incomingRelationPartners.filterIsInstance().map { it.component(cache).value }.toSet() + for (component in incomingComponents) { + for (metaRelation in component.outgoingMetaAggregatedIssueRelations(cache)) { + val metaEnd = metaRelation.end(cache).value + if (metaEnd in outgoingComponents) { + updateAggregatedRelationBasedOnMetaRelations( + metaRelation, component, metaEnd + ) + } + } + } + for (component in outgoingComponents) { + for (metaRelation in component.incomingMetaAggregatedIssueRelations(cache)) { + val metaStart = metaRelation.start(cache).value + if (metaStart in incomingComponents) { + updateAggregatedRelationBasedOnMetaRelations( + metaRelation, metaStart, component + ) + } + } + } + } + /** + * Updates [AggregatedIssueRelation]s based on information in a [MetaAggregatedIssueRelation]. + * Should be called when a [Relation] is created and thus new [AggregatedIssueRelation]s may be created. + * Does currently NOT check if the aggregation is required. + * + * @param metaRelation the [MetaAggregatedIssueRelation] providing which [IssueRelation]s may now be aggregated + * @param startComponent the start component of the [metaRelation] + * @param endComponent the end component of the [metaRelation] + */ + private suspend fun updateAggregatedRelationBasedOnMetaRelations( + metaRelation: MetaAggregatedIssueRelation, startComponent: Component, endComponent: Component + ) { + for (issueRelation in metaRelation.issueRelations(cache)) { + val startIssue = issueRelation.issue(cache).value + val endIssue = issueRelation.relatedIssue(cache).value + if (endIssue != null) { + startIssue.aggregatedBy(cache) + .filter { isPartOfComponent(it.relationPartner(cache).value, startComponent) } + .forEach { startAggregatedBy -> + endIssue.aggregatedBy(cache) + .filter { isPartOfComponent(it.relationPartner(cache).value, endComponent) } + .forEach { endAggregatedBy -> + createOrUpdateAggregatedIssueRelation( + startAggregatedBy, endAggregatedBy, issueRelation + ) + } + } + } + } } + /** + * Should be called when an [Relation] is deleted. + * Must be called BEFORE the relation is deleted from the database. + * + * @param relation the deleted relation + */ suspend fun deletedRelation(relation: Relation) { + val start = relation.start(cache).value + val end = relation.end(cache).value + val outgoingRelationPartners = findOutgoingRelationPartners(end) + end + val incomingRelationPartners = findIncomingRelationPartners(start) + start + deleteUnconnectedAggregatedIssueRelations(outgoingRelationPartners, incomingRelationPartners) + deleteUnconnectedAggregatedIssueRelations(incomingRelationPartners, outgoingRelationPartners) + } + /** + * Helper to delete [AggregatedIssueRelation]s when a [Relation] is deleted. + * Deletes all [AggregatedIssueRelation]s originating at a [RelationPartner] in [startRelationPartners] + * and ending at a [RelationPartner] in [endRelationPartners] that are not connected to each other. + * Considers both incoming and outgoing [AggregatedIssueRelation]s. + * + * @param startRelationPartners the [RelationPartner]s to start from + * @param endRelationPartners the [RelationPartner]s to end at + */ + private suspend fun deleteUnconnectedAggregatedIssueRelations( + startRelationPartners: Set, endRelationPartners: Set + ) { + for (relationPartner in startRelationPartners) { + val aggregatedIssues = relationPartner.aggregatedIssues(cache) + val outgoingAggregatedRelationsByRelationPartner = + aggregatedIssues.flatMap { it.outgoingRelations(cache) }.groupBy { + it.end(cache).value.relationPartner(cache).value + } + val incomingAggregatedRelationsByRelationPartner = + aggregatedIssues.flatMap { it.incomingRelations(cache) }.groupBy { + it.start(cache).value.relationPartner(cache).value + } + val relevantRelationPartners = + (outgoingAggregatedRelationsByRelationPartner.keys + incomingAggregatedRelationsByRelationPartner.keys) intersect endRelationPartners + val connected = connectedRelationPartners(relationPartner, relevantRelationPartners) + incomingAggregatedRelationsByRelationPartner.forEach { (toRelationPartner, aggregatedRelations) -> + if (toRelationPartner !in connected) { + deletedNodes += aggregatedRelations + } + } + outgoingAggregatedRelationsByRelationPartner.forEach { (toRelationPartner, aggregatedRelations) -> + if (toRelationPartner !in connected) { + deletedNodes += aggregatedRelations + } + } + } } /** @@ -524,6 +644,13 @@ class IssueAggregationUpdater( } } + /** + * Removes an [IssueRelation] from an [AggregatedIssueRelation]. + * Deletes the [AggregatedIssueRelation] if it is no longer required. + * + * @param issueRelation the issue relation to remove from the aggregated issue relation + * @param aggregatedIssueRelation the aggregated issue relation to remove the issue relation from + */ private suspend fun removeIssueRelationFromAggregatedIssueRelation( issueRelation: IssueRelation, aggregatedIssueRelation: AggregatedIssueRelation ) { @@ -642,7 +769,7 @@ class IssueAggregationUpdater( val incomingRelationPartners = findOutgoingRelationPartners(relationPartner) val outgoingRelationPartners = findIncomingRelationPartners(relationPartner) val graphRelationPartners = incomingRelationPartners + outgoingRelationPartners - issue.outgoingRelations(cache).forEach {relation -> + issue.outgoingRelations(cache).forEach { relation -> val relatedIssue = relation.relatedIssue(cache).value ?: return@forEach for (relatedAggregatedIssue in relatedIssue.aggregatedBy(cache)) { if (relatedAggregatedIssue.relationPartner(cache).value in graphRelationPartners) { @@ -650,7 +777,7 @@ class IssueAggregationUpdater( } } } - issue.incomingRelations(cache).forEach {relation -> + issue.incomingRelations(cache).forEach { relation -> val relatedIssue = relation.issue(cache).value for (relatedAggregatedIssue in relatedIssue.aggregatedBy(cache)) { if (relatedAggregatedIssue.relationPartner(cache).value in graphRelationPartners) { @@ -660,6 +787,14 @@ class IssueAggregationUpdater( } } + /** + * Creates or updates an [AggregatedIssueRelation] between two [AggregatedIssue]s. + * Handles the case of an [IssueRelation] already being aggregated on the [AggregatedIssueRelation]. + * + * @param from the start aggregated issue relation + * @param to the end aggregated issue relation + * @param issueRelation the issue relation to aggregate + */ private suspend fun createOrUpdateAggregatedIssueRelation( from: AggregatedIssue, to: AggregatedIssue, issueRelation: IssueRelation ) { @@ -740,6 +875,13 @@ class IssueAggregationUpdater( } } + /** + * Adds an [IssueRelation] to a [MetaAggregatedIssueRelation]. + * + * @param from the start of the meta aggregated issue relation + * @param to the end of the meta aggregated issue relation + * @param issueRelation the issue relation to add + */ private suspend fun addToMetaAggregatedRelation(from: Trackable, to: Trackable, issueRelation: IssueRelation) { if (from !is Component || to !is Component) { return @@ -756,6 +898,14 @@ class IssueAggregationUpdater( } } + /** + * Removes an [IssueRelation] from a [MetaAggregatedIssueRelation]. + * Deletes the [MetaAggregatedIssueRelation] if it is no longer required. + * + * @param from the start of the meta aggregated issue relation + * @param to the end of the meta aggregated issue relation + * @param issueRelation the issue relation to remove from the meta aggregated issue relation + */ private suspend fun removeFromMetaAggregatedRelation(from: Trackable, to: Trackable, issueRelation: IssueRelation) { if (from !is Component || to !is Component) { return @@ -771,6 +921,13 @@ class IssueAggregationUpdater( } } + /** + * Finds all [RelationPartner]s that are connected to a [RelationPartner] via outgoing [Relation]s. + * Also considers [Interface]s of [ComponentVersion]s. + * + * @param relationPartner the relation partner to start from + * @return a set of all connected relation partners + */ private suspend fun findOutgoingRelationPartners(relationPartner: RelationPartner): Set { val result = mutableSetOf() val toExplore = ArrayDeque(listOf(relationPartner)) @@ -778,21 +935,19 @@ class IssueAggregationUpdater( val next = toExplore.removeFirst() if (result.add(next)) { toExplore += next.outgoingRelations(cache).map { it.end(cache).value } - when (next) { - is ComponentVersion -> { - toExplore += next.component(cache).value.versions(cache) - } - is Interface -> { - toExplore += next.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value.versions( - cache - ) - } - } + expandRelationPartner(next, toExplore) } } return result } + /** + * Finds all [RelationPartner]s that are connected to a [RelationPartner] via incoming [Relation]s. + * Also considers [Interface]s of [ComponentVersion]s. + * + * @param relationPartner the relation partner to start from + * @return a set of all connected relation partners + */ private suspend fun findIncomingRelationPartners(relationPartner: RelationPartner): Set { val result = mutableSetOf() val toExplore = ArrayDeque(listOf(relationPartner)) @@ -800,19 +955,99 @@ class IssueAggregationUpdater( val next = toExplore.removeFirst() if (result.add(next)) { toExplore += next.incomingRelations(cache).map { it.start(cache).value } - when (next) { - is ComponentVersion -> { - toExplore += next.component(cache).value.versions(cache) + expandRelationPartner(next, toExplore) + } + } + return result + } + + /** + * Finds the subset of [RelationPartner]s in [ends] that are connected to [start] via incoming or outgoing [Relation]s. + * Also considers [Interface]s of [ComponentVersion]s. + * + * @param start the relation partner to start from + * @param ends the relation partners to check + * @return a set of all connected relation partners + */ + private suspend fun connectedRelationPartners( + start: RelationPartner, ends: Set + ): Set { + val result = mutableSetOf() + val incomingToExplore = ArrayDeque(listOf(start)) + val incomingExplored = mutableSetOf() + val outgoingToExplore = ArrayDeque(listOf(start)) + val outgoingExplored = mutableSetOf() + val remainingEnds = ends.toMutableSet() + while ((incomingToExplore.isNotEmpty() || outgoingToExplore.isNotEmpty()) && remainingEnds.isNotEmpty()) { + if (incomingToExplore.isNotEmpty()) { + val next = incomingToExplore.removeFirst() + if (incomingExplored.add(next)) { + if (next in remainingEnds) { + result += next + remainingEnds.remove(next) } - is Interface -> { - toExplore += next.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value.versions( - cache - ) + incomingToExplore += next.incomingRelations(cache).map { it.start(cache).value } + expandRelationPartner(next, incomingToExplore) + } + } + if (outgoingToExplore.isNotEmpty()) { + val next = outgoingToExplore.removeFirst() + if (outgoingExplored.add(next)) { + if (next in remainingEnds) { + result += next + remainingEnds.remove(next) } + outgoingToExplore += next.outgoingRelations(cache).map { it.end(cache).value } + expandRelationPartner(next, outgoingToExplore) } } } return result } + /** + * Helper to + * - add the [ComponentVersion] of an [Interface] to the stack + * - add the [Interface]s of a [ComponentVersion] to the stack + * + * @param relationPartner the relation partner to expand + * @param stack the stack to add the expanded relation partners to + */ + private suspend fun expandRelationPartner(relationPartner: RelationPartner, stack: ArrayDeque) { + when (relationPartner) { + is ComponentVersion -> { + stack += relationPartner.component(cache).value.versions(cache) + } + + is Interface -> { + stack += relationPartner.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value.versions( + cache + ) + } + } + } + + /** + * Checks if a [RelationPartner] is part of a [Component]. + * + * @param relationPartner the relation partner to check + * @param component the component to check + * @return true if the relation partner is part of the component, false otherwise + */ + private suspend fun isPartOfComponent(relationPartner: RelationPartner, component: Component): Boolean { + return when (relationPartner) { + is ComponentVersion -> { + relationPartner.component(cache).value == component + } + + is Interface -> { + relationPartner.interfaceDefinition(cache).value.componentVersion(cache).value.component(cache).value == component + } + + else -> { + false + } + } + } + } \ No newline at end of file From 3e9b5a4f54ee2bc9d6b79363a31581bfb0078b9a Mon Sep 17 00:00:00 2001 From: nk-coding Date: Mon, 23 Oct 2023 04:38:08 +0200 Subject: [PATCH 12/30] bugfixes --- .../IntraComponentDependencySpecificationService.kt | 5 ----- .../gropius/service/architecture/RelationService.kt | 10 +--------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/core/src/main/kotlin/gropius/service/architecture/IntraComponentDependencySpecificationService.kt b/core/src/main/kotlin/gropius/service/architecture/IntraComponentDependencySpecificationService.kt index abc89737..93584197 100644 --- a/core/src/main/kotlin/gropius/service/architecture/IntraComponentDependencySpecificationService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/IntraComponentDependencySpecificationService.kt @@ -169,11 +169,6 @@ class IntraComponentDependencySpecificationService( throw IllegalArgumentException("The specified Interface is not part of the same ComponentVersion") } val parts = interfacePartRepository.findAllById(input.includedParts.orElse(emptyList())) - for (part in parts) { - if (interfaceDefinition.interfaceSpecificationVersion().value !in part.partOf()) { - throw IllegalArgumentException("Specified includedParts must be active on the specified Interface") - } - } val intraComponentDependencyParticipant = IntraComponentDependencyParticipant() intraComponentDependencyParticipant.`interface`().value = relatedInterface intraComponentDependencyParticipant.includedParts() += parts diff --git a/core/src/main/kotlin/gropius/service/architecture/RelationService.kt b/core/src/main/kotlin/gropius/service/architecture/RelationService.kt index 8dc59381..2b3cdd19 100644 --- a/core/src/main/kotlin/gropius/service/architecture/RelationService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/RelationService.kt @@ -119,15 +119,7 @@ class RelationService( if (relationPartner !is Interface) { throw IllegalArgumentException("InterfaceParts can only be provided if the side of the Relation uses an Interface") } - val interfaceSpecificationVersion = - relationPartner.interfaceDefinition().value.interfaceSpecificationVersion().value - val parts = partIds.map { interfacePartRepository.findById(it) }.toSet() - parts.forEach { - if (interfaceSpecificationVersion !in it.partOf()) { - throw IllegalArgumentException("InterfacePart must be active on the used InterfaceSpecificationVersion") - } - } - return parts + return partIds.map { interfacePartRepository.findById(it) }.toSet() } return emptySet() } From b890e7cb35e4918829a62fa125f85972b07f3c76 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Tue, 24 Oct 2023 06:27:35 +0200 Subject: [PATCH 13/30] partially working --- .../kotlin/gropius/model/architecture/InterfacePart.kt | 8 ++++---- .../kotlin/gropius/model/issue/AggregatedIssueRelation.kt | 2 +- .../gropius/service/issue/IssueAggregationUpdater.kt | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt b/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt index 3e4ff981..af7d9013 100644 --- a/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt +++ b/core/src/main/kotlin/gropius/model/architecture/InterfacePart.kt @@ -20,10 +20,10 @@ import org.springframework.data.neo4j.core.schema.CompositeProperty READ is granted if READ is granted on `definedOn`. """ ) -@Authorization(NodePermission.READ, allowFromRelated = ["definedOn"]) -@Authorization(NodePermission.ADMIN, allowFromRelated = ["definedOn"]) -@Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["definedOn"]) -@Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["activeOn"]) +@Authorization(NodePermission.READ, allowFromRelated = ["partOf"]) +@Authorization(NodePermission.ADMIN, allowFromRelated = ["partOf"]) +@Authorization(TrackablePermission.AFFECT_ENTITIES_WITH_ISSUES, allowFromRelated = ["partOf"]) +@Authorization(TrackablePermission.RELATED_ISSUE_AFFECTED_ENTITY, allowFromRelated = ["partOf"]) class InterfacePart( name: String, description: String, diff --git a/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt b/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt index 478fe034..e5784cd9 100644 --- a/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt +++ b/core/src/main/kotlin/gropius/model/issue/AggregatedIssueRelation.kt @@ -35,7 +35,7 @@ class AggregatedIssueRelation(var count: Int) : Node() { @NodeRelationship(TYPE, Direction.OUTGOING) @GraphQLDescription("The IssueType of this AggregatedIssue.") @FilterProperty - val type by NodeProperty() + val type by NodeProperty() @NodeRelationship(ISSUE_RELATION, Direction.OUTGOING) @GraphQLDescription("The IssueRelations aggregated by this AggregatedIssueRelation.") diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index 4b95701f..9c13c40c 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -798,11 +798,13 @@ class IssueAggregationUpdater( private suspend fun createOrUpdateAggregatedIssueRelation( from: AggregatedIssue, to: AggregatedIssue, issueRelation: IssueRelation ) { + val type = issueRelation.type(cache).value val aggregatedIssueRelation = from.outgoingRelations(cache).find { - it.end(cache).value == to + it.end(cache).value == to && it.type(cache).value == type } ?: AggregatedIssueRelation(0).also { it.start(cache).value = from it.end(cache).value = to + it.type(cache).value = type } internalUpdatedNodes += aggregatedIssueRelation if (aggregatedIssueRelation.issueRelations(cache).add(issueRelation)) { From 2741d2b474f04aaad041f0dca32c4a245e283f11 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Tue, 24 Oct 2023 16:11:57 +0200 Subject: [PATCH 14/30] remove width from stroke style --- .../kotlin/gropius/dto/input/template/style/StrokeStyleInput.kt | 2 -- .../src/main/kotlin/gropius/model/template/style/StrokeStyle.kt | 2 -- .../gropius/service/template/RelationPartnerTemplateService.kt | 2 +- .../kotlin/gropius/service/template/RelationTemplateService.kt | 2 +- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/gropius/dto/input/template/style/StrokeStyleInput.kt b/core/src/main/kotlin/gropius/dto/input/template/style/StrokeStyleInput.kt index 5ed44ab6..adfe1601 100644 --- a/core/src/main/kotlin/gropius/dto/input/template/style/StrokeStyleInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/template/style/StrokeStyleInput.kt @@ -8,8 +8,6 @@ import gropius.dto.input.common.Input class StrokeStyleInput( @GraphQLDescription("The color of the stroke") val color: OptionalInput, - @GraphQLDescription("The width of the stroke") - val width: OptionalInput, @GraphQLDescription("The dash pattern of the stroke") val dash: OptionalInput> ) : Input() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/template/style/StrokeStyle.kt b/core/src/main/kotlin/gropius/model/template/style/StrokeStyle.kt index cd990598..a56ba9a5 100644 --- a/core/src/main/kotlin/gropius/model/template/style/StrokeStyle.kt +++ b/core/src/main/kotlin/gropius/model/template/style/StrokeStyle.kt @@ -8,8 +8,6 @@ import io.github.graphglue.model.DomainNode class StrokeStyle( @GraphQLDescription("The color of the stroke") val color: String?, - @GraphQLDescription("The width of the stroke") - val width: Double?, @GraphQLDescription("The dash pattern of the stroke") val dash: List? ) : BaseStyle() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/template/RelationPartnerTemplateService.kt b/core/src/main/kotlin/gropius/service/template/RelationPartnerTemplateService.kt index 41aedca5..ba0af08c 100644 --- a/core/src/main/kotlin/gropius/service/template/RelationPartnerTemplateService.kt +++ b/core/src/main/kotlin/gropius/service/template/RelationPartnerTemplateService.kt @@ -36,7 +36,7 @@ abstract class RelationPartnerTemplateService, template.fill().value = FillStyle(it.color) } input.stroke.ifPresent { - template.stroke().value = StrokeStyle(it.color.orElse(null), it.width.orElse(null), it.dash.orElse(null)) + template.stroke().value = StrokeStyle(it.color.orElse(null), it.dash.orElse(null)) } } diff --git a/core/src/main/kotlin/gropius/service/template/RelationTemplateService.kt b/core/src/main/kotlin/gropius/service/template/RelationTemplateService.kt index e27593d4..c2d69835 100644 --- a/core/src/main/kotlin/gropius/service/template/RelationTemplateService.kt +++ b/core/src/main/kotlin/gropius/service/template/RelationTemplateService.kt @@ -43,7 +43,7 @@ class RelationTemplateService( checkCreateTemplatePermission(authorizationContext) val template = RelationTemplate(input.name, input.description, mutableMapOf(), false, input.markerType) input.stroke.ifPresent { - template.stroke().value = StrokeStyle(it.color.orElse(null), it.width.orElse(null), it.dash.orElse(null)) + template.stroke().value = StrokeStyle(it.color.orElse(null), it.dash.orElse(null)) } createdTemplate(template, input) template.relationConditions() += input.relationConditions.map { From 31b48a91fee89ba5ec1d8b19cadbb4a8ccf01a02 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Tue, 24 Oct 2023 16:15:10 +0200 Subject: [PATCH 15/30] no AggregatedIssueRelations between the same AggregatedIssue --- .../gropius/service/issue/IssueAggregationUpdater.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index 9c13c40c..c9a28591 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -798,6 +798,9 @@ class IssueAggregationUpdater( private suspend fun createOrUpdateAggregatedIssueRelation( from: AggregatedIssue, to: AggregatedIssue, issueRelation: IssueRelation ) { + if (from == to) { + return + } val type = issueRelation.type(cache).value val aggregatedIssueRelation = from.outgoingRelations(cache).find { it.end(cache).value == to && it.type(cache).value == type @@ -885,7 +888,7 @@ class IssueAggregationUpdater( * @param issueRelation the issue relation to add */ private suspend fun addToMetaAggregatedRelation(from: Trackable, to: Trackable, issueRelation: IssueRelation) { - if (from !is Component || to !is Component) { + if (from !is Component || to !is Component || from == to) { return } val metaAggregatedRelation = from.outgoingMetaAggregatedIssueRelations(cache).find { @@ -909,7 +912,7 @@ class IssueAggregationUpdater( * @param issueRelation the issue relation to remove from the meta aggregated issue relation */ private suspend fun removeFromMetaAggregatedRelation(from: Trackable, to: Trackable, issueRelation: IssueRelation) { - if (from !is Component || to !is Component) { + if (from !is Component || to !is Component || from == to) { return } val metaAggregatedRelation = from.outgoingMetaAggregatedIssueRelations(cache).find { From 8369e305a1acf089c908d4d427fa274f2f801b5f Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sat, 28 Oct 2023 04:21:29 +0200 Subject: [PATCH 16/30] make components and component versions searchable --- core/src/main/kotlin/gropius/model/architecture/Component.kt | 2 +- .../main/kotlin/gropius/model/architecture/ComponentVersion.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/gropius/model/architecture/Component.kt b/core/src/main/kotlin/gropius/model/architecture/Component.kt index f0c83fee..2ee3b395 100644 --- a/core/src/main/kotlin/gropius/model/architecture/Component.kt +++ b/core/src/main/kotlin/gropius/model/architecture/Component.kt @@ -14,7 +14,7 @@ import io.github.graphglue.model.* import org.springframework.data.neo4j.core.schema.CompositeProperty import java.net.URI -@DomainNode("components") +@DomainNode("components", searchQueryName = "searchComponents") @GraphQLDescription( """Entity which represents a software component, e.g. a library, a microservice, or a deployment platform, .... The type of software component is defined by the template. diff --git a/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt b/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt index 03a27d04..aec88797 100644 --- a/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt +++ b/core/src/main/kotlin/gropius/model/architecture/ComponentVersion.kt @@ -14,7 +14,7 @@ import io.github.graphglue.model.* import io.github.graphglue.model.property.NodeCache import org.springframework.data.neo4j.core.schema.CompositeProperty -@DomainNode +@DomainNode(searchQueryName = "searchComponentVersions") @GraphQLDescription( """Version of a component. Can specifies visible/invisible InterfaceSpecifications. From 02a5c720294e52e01ac390cf94356988e18a9f3f Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sun, 29 Oct 2023 20:06:26 +0100 Subject: [PATCH 17/30] upgrade graphglue --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ca72b68d..bf950c0f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ javaVersion = 17 # dependencies springBootVersion = 3.1.3 -graphglueVersion = 5.1.1 +graphglueVersion = 5.1.2 graphqlJavaVersion = 20.2 apolloVersion = 3.8.2 kosonVersion = 1.2.8 From 95abc1f94eebf1363e60392b777f02116fe0a47c Mon Sep 17 00:00:00 2001 From: nk-coding Date: Tue, 31 Oct 2023 13:20:23 +0100 Subject: [PATCH 18/30] make relation templates searchable --- core/src/main/kotlin/gropius/model/template/RelationTemplate.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/gropius/model/template/RelationTemplate.kt b/core/src/main/kotlin/gropius/model/template/RelationTemplate.kt index 3d46dc44..1a3e69cb 100644 --- a/core/src/main/kotlin/gropius/model/template/RelationTemplate.kt +++ b/core/src/main/kotlin/gropius/model/template/RelationTemplate.kt @@ -9,7 +9,7 @@ import io.github.graphglue.model.DomainNode import io.github.graphglue.model.FilterProperty import io.github.graphglue.model.NodeRelationship -@DomainNode("relationTemplates") +@DomainNode("relationTemplates", searchQueryName = "searchRelationTemplates") @GraphQLDescription( """Template for Relations. Defines templated fields with specific types (defined using JSON schema). From 8fa2f288728ff38849aad4dace6ed6cc65ea26cf Mon Sep 17 00:00:00 2001 From: nk-coding Date: Tue, 31 Oct 2023 21:42:49 +0100 Subject: [PATCH 19/30] fix wrong permission --- .../kotlin/gropius/model/user/permission/TrackablePermission.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/gropius/model/user/permission/TrackablePermission.kt b/core/src/main/kotlin/gropius/model/user/permission/TrackablePermission.kt index 8ce450bd..bd74cb27 100644 --- a/core/src/main/kotlin/gropius/model/user/permission/TrackablePermission.kt +++ b/core/src/main/kotlin/gropius/model/user/permission/TrackablePermission.kt @@ -40,7 +40,7 @@ abstract class TrackablePermission( * Permission to check if the user can affect entities part of this [Trackable], * e.g. the [Trackable] itself or [InterfaceSpecification]s with [Issue]s */ - const val AFFECT_ENTITIES_WITH_ISSUES = "LINK_FROM_ISSUES" + const val AFFECT_ENTITIES_WITH_ISSUES = "AFFECT_ENTITIES_WITH_ISSUES" /** * Permission to check if the user can moderate [Issue]s on the [Trackable] From 8549499fbee9067da62ea118e57eafa9e988faff Mon Sep 17 00:00:00 2001 From: nk-coding Date: Wed, 1 Nov 2023 01:45:13 +0100 Subject: [PATCH 20/30] misc bugfixes --- .../main/kotlin/gropius/graphql/GraphQLConfiguration.kt | 2 +- .../main/kotlin/gropius/model/architecture/Component.kt | 9 +++++---- .../gropius/service/issue/IssueAggregationUpdater.kt | 6 ++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt b/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt index cf568a81..b202c5cf 100644 --- a/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt +++ b/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt @@ -191,7 +191,7 @@ class GraphQLConfiguration { ALL_PERMISSION_ENTRY_NAME ) ) - }.type(Scalars.GraphQLBoolean).build() + }.type(GraphQLNonNull(Scalars.GraphQLBoolean)).build() val nodeDefinitionCollection by lazy { beanFactory.getBean(NodeDefinitionCollection::class.java) diff --git a/core/src/main/kotlin/gropius/model/architecture/Component.kt b/core/src/main/kotlin/gropius/model/architecture/Component.kt index 2ee3b395..6878dd9a 100644 --- a/core/src/main/kotlin/gropius/model/architecture/Component.kt +++ b/core/src/main/kotlin/gropius/model/architecture/Component.kt @@ -2,6 +2,7 @@ package gropius.model.architecture import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import gropius.authorization.RELATED_TO_NODE_PERMISSION_RULE import gropius.model.issue.MetaAggregatedIssueRelation import gropius.model.template.BaseTemplate import gropius.model.template.ComponentTemplate @@ -28,11 +29,11 @@ import java.net.URI @Authorization(NodePermission.READ, allowFromRelated = ["versions"]) @Authorization( ComponentPermission.RELATE_FROM_COMPONENT, - allow = [Rule(COMPONENT_PERMISSION_ENTRY_NAME, options = [NodePermission.ADMIN])] + allow = [Rule(RELATED_TO_NODE_PERMISSION_RULE, options = [NodePermission.ADMIN])] ) @Authorization( ComponentPermission.ADD_TO_PROJECTS, - allow = [Rule(COMPONENT_PERMISSION_ENTRY_NAME, options = [NodePermission.ADMIN])] + allow = [Rule(RELATED_TO_NODE_PERMISSION_RULE, options = [NodePermission.ADMIN])] ) class Component( name: String, @@ -73,11 +74,11 @@ class Component( @FilterProperty override val permissions by NodeSetProperty() - @NodeRelationship(INCOMING_META_AGGREGATED_ISSUE_RELATION, Direction.INCOMING) + @NodeRelationship(INCOMING_META_AGGREGATED_ISSUE_RELATION, Direction.OUTGOING) @GraphQLIgnore val incomingMetaAggregatedIssueRelations by NodeSetProperty() - @NodeRelationship(OUTGOING_META_AGGREGATED_ISSUE_RELATION, Direction.INCOMING) + @NodeRelationship(OUTGOING_META_AGGREGATED_ISSUE_RELATION, Direction.OUTGOING) @GraphQLIgnore val outgoingMetaAggregatedIssueRelations by NodeSetProperty() diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index c9a28591..2d4ca2e6 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -281,7 +281,7 @@ class IssueAggregationUpdater( * @param relation the created relation */ suspend fun createdRelation(relation: Relation) { - val start = relation.start(cache).value +git dif val start = relation.start(cache).value val end = relation.end(cache).value val outgoingRelationPartners = findOutgoingRelationPartners(end) + end val outgoingComponents = @@ -298,11 +298,9 @@ class IssueAggregationUpdater( ) } } - } - for (component in outgoingComponents) { for (metaRelation in component.incomingMetaAggregatedIssueRelations(cache)) { val metaStart = metaRelation.start(cache).value - if (metaStart in incomingComponents) { + if (metaStart in outgoingComponents) { updateAggregatedRelationBasedOnMetaRelations( metaRelation, metaStart, component ) From e46785f9a147e1b6b21856c1a21ea891b0aba505 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Wed, 1 Nov 2023 01:49:47 +0100 Subject: [PATCH 21/30] ... --- .../kotlin/gropius/service/issue/IssueAggregationUpdater.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt index 2d4ca2e6..7e7d2e3c 100644 --- a/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt +++ b/core/src/main/kotlin/gropius/service/issue/IssueAggregationUpdater.kt @@ -281,7 +281,7 @@ class IssueAggregationUpdater( * @param relation the created relation */ suspend fun createdRelation(relation: Relation) { -git dif val start = relation.start(cache).value + val start = relation.start(cache).value val end = relation.end(cache).value val outgoingRelationPartners = findOutgoingRelationPartners(end) + end val outgoingComponents = From 713a19cb1c5efce889713fee9b8c73e1e612f76c Mon Sep 17 00:00:00 2001 From: nk-coding Date: Thu, 2 Nov 2023 18:47:01 +0100 Subject: [PATCH 22/30] make component template searchable --- .../src/main/kotlin/gropius/model/template/ComponentTemplate.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/gropius/model/template/ComponentTemplate.kt b/core/src/main/kotlin/gropius/model/template/ComponentTemplate.kt index 169ae2c6..d96f76fc 100644 --- a/core/src/main/kotlin/gropius/model/template/ComponentTemplate.kt +++ b/core/src/main/kotlin/gropius/model/template/ComponentTemplate.kt @@ -8,7 +8,7 @@ import io.github.graphglue.model.DomainNode import io.github.graphglue.model.FilterProperty import io.github.graphglue.model.NodeRelationship -@DomainNode("componentTemplates") +@DomainNode("componentTemplates", searchQueryName = "searchComponentTemplates") @GraphQLDescription( """Template for Components. Defines templated fields with specific types (defined using JSON schema). From 71c1ed99ae5e64eb7098344ed7cf78bf6b62186e Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sun, 12 Nov 2023 19:56:53 +0100 Subject: [PATCH 23/30] update login service generated code --- login-service/src/model/graphql/generated.ts | 254 ++++++++++++++++--- 1 file changed, 213 insertions(+), 41 deletions(-) diff --git a/login-service/src/model/graphql/generated.ts b/login-service/src/model/graphql/generated.ts index c90d7756..4db21f78 100644 --- a/login-service/src/model/graphql/generated.ts +++ b/login-service/src/model/graphql/generated.ts @@ -35,6 +35,8 @@ export type AffectedByIssueFilterInput = { not?: InputMaybe; /** Connects all subformulas via or */ or?: InputMaybe>; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; }; /** Used to filter by a connection-based property. Fields are joined by AND */ @@ -63,6 +65,102 @@ export enum AffectedByIssueOrderField { Name = 'NAME' } +/** Filter used to filter AggregatedIssue */ +export type AggregatedIssueFilterInput = { + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filter by count */ + count?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by incomingRelations */ + incomingRelations?: InputMaybe; + /** Filter by isOpen */ + isOpen?: InputMaybe; + /** Filter by issues */ + issues?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filter by outgoingRelations */ + outgoingRelations?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + relationPartner?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + type?: InputMaybe; +}; + +/** Used to filter by a connection-based property. Fields are joined by AND */ +export type AggregatedIssueListFilterInput = { + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; +}; + +/** Defines the order of a AggregatedIssue list */ +export type AggregatedIssueOrder = { + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; +}; + +/** Fields a list of AggregatedIssue can be sorted by */ +export enum AggregatedIssueOrderField { + /** Order by count */ + Count = 'COUNT', + /** Order by id */ + Id = 'ID' +} + +/** Filter used to filter AggregatedIssueRelation */ +export type AggregatedIssueRelationFilterInput = { + /** Connects all subformulas via and */ + and?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + end?: InputMaybe; + /** Filter by id */ + id?: InputMaybe; + /** Filter by issueRelations */ + issueRelations?: InputMaybe; + /** Negates the subformula */ + not?: InputMaybe; + /** Connects all subformulas via or */ + or?: InputMaybe>; + /** Filters for nodes where the related node match this filter */ + start?: InputMaybe; + /** Filters for nodes where the related node match this filter */ + type?: InputMaybe; +}; + +/** Used to filter by a connection-based property. Fields are joined by AND */ +export type AggregatedIssueRelationListFilterInput = { + /** Filters for nodes where all of the related nodes match this filter */ + all?: InputMaybe; + /** Filters for nodes where any of the related nodes match this filter */ + any?: InputMaybe; + /** Filters for nodes where none of the related nodes match this filter */ + none?: InputMaybe; +}; + +/** Defines the order of a AggregatedIssueRelation list */ +export type AggregatedIssueRelationOrder = { + /** The direction to order by, defaults to ASC */ + direction?: InputMaybe; + /** The field to order by, defaults to ID */ + field?: InputMaybe; +}; + +/** Fields a list of AggregatedIssueRelation can be sorted by */ +export enum AggregatedIssueRelationOrderField { + /** Order by id */ + Id = 'ID' +} + /** Non global permission entries */ export enum AllPermissionEntry { /** @@ -73,6 +171,16 @@ export enum AllPermissionEntry { AddToProjects = 'ADD_TO_PROJECTS', /** Grants all other permissions on the Node except READ. */ Admin = 'ADMIN', + /** + * Allows affecting entities part of this Trackable with any Issues. + * Affectable entitites include + * - the Trackable itself + * - in case the Trackable is a Component + * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) + * - Interfaces on the Component + * - ComponentVersions of the Component + */ + AffectEntitiesWithIssues = 'AFFECT_ENTITIES_WITH_ISSUES', /** * Allows to create Comments on Issues on this Trackable. * Also allows editing of your own Comments. @@ -87,16 +195,6 @@ export enum AllPermissionEntry { ExportIssues = 'EXPORT_ISSUES', /** Allows adding Labels on this Trackable to other Trackables. */ ExportLabels = 'EXPORT_LABELS', - /** - * Allows affecting entities part of this Trackable with any Issues. - * Affectable entitites include - * - the Trackable itself - * - in case the Trackable is a Component - * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) - * - Interfaces on the Component - * - ComponentVersions of the Component - */ - LinkFromIssues = 'LINK_FROM_ISSUES', /** Allows to add, remove, and update Artefacts on this Trackable. */ ManageArtefacts = 'MANAGE_ARTEFACTS', /** Allows to add / remove ComponentVersions to / from this Project. */ @@ -566,6 +664,8 @@ export type ComponentFilterInput = { permissions?: InputMaybe; /** Filter by pinnedIssues */ pinnedIssues?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; /** Filter by repositoryURL */ repositoryURL?: InputMaybe; /** Filter by syncsTo */ @@ -614,6 +714,16 @@ export enum ComponentPermissionEntry { AddToProjects = 'ADD_TO_PROJECTS', /** Grants all other permissions on the Node except READ. */ Admin = 'ADMIN', + /** + * Allows affecting entities part of this Trackable with any Issues. + * Affectable entitites include + * - the Trackable itself + * - in case the Trackable is a Component + * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) + * - Interfaces on the Component + * - ComponentVersions of the Component + */ + AffectEntitiesWithIssues = 'AFFECT_ENTITIES_WITH_ISSUES', /** * Allows to create Comments on Issues on this Trackable. * Also allows editing of your own Comments. @@ -628,16 +738,6 @@ export enum ComponentPermissionEntry { ExportIssues = 'EXPORT_ISSUES', /** Allows adding Labels on this Trackable to other Trackables. */ ExportLabels = 'EXPORT_LABELS', - /** - * Allows affecting entities part of this Trackable with any Issues. - * Affectable entitites include - * - the Trackable itself - * - in case the Trackable is a Component - * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) - * - Interfaces on the Component - * - ComponentVersions of the Component - */ - LinkFromIssues = 'LINK_FROM_ISSUES', /** Allows to add, remove, and update Artefacts on this Trackable. */ ManageArtefacts = 'MANAGE_ARTEFACTS', /** @@ -793,6 +893,8 @@ export enum ComponentTemplateOrderField { export type ComponentVersionFilterInput = { /** Filter by affectingIssues */ affectingIssues?: InputMaybe; + /** Filter by aggregatedIssues */ + aggregatedIssues?: InputMaybe; /** Connects all subformulas via and */ and?: InputMaybe>; /** Filters for nodes where the related node match this filter */ @@ -817,6 +919,10 @@ export type ComponentVersionFilterInput = { or?: InputMaybe>; /** Filter by outgoingRelations */ outgoingRelations?: InputMaybe; + /** Filters for RelationPartners which are part of a Project's component graph */ + partOfProject?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; /** Filters for nodes where the related node match this filter */ template?: InputMaybe; /** Filter for templated fields with matching key and values. Entries are joined by AND */ @@ -1366,6 +1472,22 @@ export type ImsUserTemplateFilterInput = { or?: InputMaybe>; }; +/** Filter which can be used to filter for Nodes with a specific Int field */ +export type IntFilterInput = { + /** Matches values which are equal to the provided value */ + eq?: InputMaybe; + /** Matches values which are greater than the provided value */ + gt?: InputMaybe; + /** Matches values which are greater than or equal to the provided value */ + gte?: InputMaybe; + /** Matches values which are equal to any of the provided values */ + in?: InputMaybe>; + /** Matches values which are lesser than the provided value */ + lt?: InputMaybe; + /** Matches values which are lesser than or equal to the provided value */ + lte?: InputMaybe; +}; + /** Filter used to filter InterfaceDefinition */ export type InterfaceDefinitionFilterInput = { /** Connects all subformulas via and */ @@ -1444,6 +1566,8 @@ export type InterfaceDefinitionTemplateFilterInput = { export type InterfaceFilterInput = { /** Filter by affectingIssues */ affectingIssues?: InputMaybe; + /** Filter by aggregatedIssues */ + aggregatedIssues?: InputMaybe; /** Connects all subformulas via and */ and?: InputMaybe>; /** Filter by description */ @@ -1464,6 +1588,10 @@ export type InterfaceFilterInput = { or?: InputMaybe>; /** Filter by outgoingRelations */ outgoingRelations?: InputMaybe; + /** Filters for RelationPartners which are part of a Project's component graph */ + partOfProject?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; /** Filters for nodes where the related node match this filter */ template?: InputMaybe; /** Filter for templated fields with matching key and values. Entries are joined by AND */ @@ -1488,14 +1616,10 @@ export enum InterfaceOrderField { /** Filter used to filter InterfacePart */ export type InterfacePartFilterInput = { - /** Filter by activeOn */ - activeOn?: InputMaybe; /** Filter by affectingIssues */ affectingIssues?: InputMaybe; /** Connects all subformulas via and */ and?: InputMaybe>; - /** Filters for nodes where the related node match this filter */ - definedOn?: InputMaybe; /** Filter by description */ description?: InputMaybe; /** Filter by id */ @@ -1513,6 +1637,10 @@ export type InterfacePartFilterInput = { /** Connects all subformulas via or */ or?: InputMaybe>; /** Filters for nodes where the related node match this filter */ + partOf?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; + /** Filters for nodes where the related node match this filter */ template?: InputMaybe; /** Filter for templated fields with matching key and values. Entries are joined by AND */ templatedFields?: InputMaybe>>; @@ -1620,8 +1748,6 @@ export type InterfaceSpecificationFilterInput = { and?: InputMaybe>; /** Filters for nodes where the related node match this filter */ component?: InputMaybe; - /** Filter by definedParts */ - definedParts?: InputMaybe; /** Filter by description */ description?: InputMaybe; /** Filter by id */ @@ -1632,6 +1758,8 @@ export type InterfaceSpecificationFilterInput = { not?: InputMaybe; /** Connects all subformulas via or */ or?: InputMaybe>; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; /** Filters for nodes where the related node match this filter */ template?: InputMaybe; /** Filter for templated fields with matching key and values. Entries are joined by AND */ @@ -1726,8 +1854,6 @@ export enum InterfaceSpecificationTemplateOrderField { /** Filter used to filter InterfaceSpecificationVersion */ export type InterfaceSpecificationVersionFilterInput = { - /** Filter by activeParts */ - activeParts?: InputMaybe; /** Filter by affectingIssues */ affectingIssues?: InputMaybe; /** Connects all subformulas via and */ @@ -1746,6 +1872,10 @@ export type InterfaceSpecificationVersionFilterInput = { not?: InputMaybe; /** Connects all subformulas via or */ or?: InputMaybe>; + /** Filter by parts */ + parts?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; /** Filters for nodes where the related node match this filter */ template?: InputMaybe; /** Filter for templated fields with matching key and values. Entries are joined by AND */ @@ -1978,6 +2108,8 @@ export enum IssueCommentOrderField { export type IssueFilterInput = { /** Filter by affects */ affects?: InputMaybe; + /** Filter by aggregatedBy */ + aggregatedBy?: InputMaybe; /** Connects all subformulas via and */ and?: InputMaybe>; /** Filter by artefacts */ @@ -2134,6 +2266,8 @@ export enum IssuePriorityOrderField { /** Filter used to filter IssueRelation */ export type IssueRelationFilterInput = { + /** Filter by aggregatedBy */ + aggregatedBy?: InputMaybe; /** Connects all subformulas via and */ and?: InputMaybe>; /** Filter by createdAt */ @@ -2458,6 +2592,20 @@ export enum LabelOrderField { Name = 'NAME' } +/** Type of a Relation marker */ +export enum MarkerType { + /** A regular arrow */ + Arrow = 'ARROW', + /** A diamond */ + Diamond = 'DIAMOND', + /** A filled diamond */ + FilledDiamond = 'FILLED_DIAMOND', + /** A filled triangle */ + FilledTriangle = 'FILLED_TRIANGLE', + /** A triangle */ + Triangle = 'TRIANGLE' +} + export type NodePermissionFilterEntry = { /** The node where the user must have the permission */ node: Scalars['ID']; @@ -2619,6 +2767,8 @@ export type ProjectFilterInput = { permissions?: InputMaybe; /** Filter by pinnedIssues */ pinnedIssues?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; /** Filter by repositoryURL */ repositoryURL?: InputMaybe; /** Filter by syncsTo */ @@ -2655,6 +2805,16 @@ export enum ProjectOrderField { export enum ProjectPermissionEntry { /** Grants all other permissions on the Node except READ. */ Admin = 'ADMIN', + /** + * Allows affecting entities part of this Trackable with any Issues. + * Affectable entitites include + * - the Trackable itself + * - in case the Trackable is a Component + * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) + * - Interfaces on the Component + * - ComponentVersions of the Component + */ + AffectEntitiesWithIssues = 'AFFECT_ENTITIES_WITH_ISSUES', /** * Allows to create Comments on Issues on this Trackable. * Also allows editing of your own Comments. @@ -2669,16 +2829,6 @@ export enum ProjectPermissionEntry { ExportIssues = 'EXPORT_ISSUES', /** Allows adding Labels on this Trackable to other Trackables. */ ExportLabels = 'EXPORT_LABELS', - /** - * Allows affecting entities part of this Trackable with any Issues. - * Affectable entitites include - * - the Trackable itself - * - in case the Trackable is a Component - * - InterfaceSpecifications, their InterfaceSpecificationVersions and their InterfaceParts of the Component (not inherited ones) - * - Interfaces on the Component - * - ComponentVersions of the Component - */ - LinkFromIssues = 'LINK_FROM_ISSUES', /** Allows to add, remove, and update Artefacts on this Trackable. */ ManageArtefacts = 'MANAGE_ARTEFACTS', /** Allows to add / remove ComponentVersions to / from this Project. */ @@ -2869,6 +3019,8 @@ export enum RelationOrderField { export type RelationPartnerFilterInput = { /** Filter by affectingIssues */ affectingIssues?: InputMaybe; + /** Filter by aggregatedIssues */ + aggregatedIssues?: InputMaybe; /** Connects all subformulas via and */ and?: InputMaybe>; /** Filter by description */ @@ -2885,6 +3037,10 @@ export type RelationPartnerFilterInput = { or?: InputMaybe>; /** Filter by outgoingRelations */ outgoingRelations?: InputMaybe; + /** Filters for RelationPartners which are part of a Project's component graph */ + partOfProject?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; /** Filter for templated fields with matching key and values. Entries are joined by AND */ templatedFields?: InputMaybe>>; }; @@ -2987,6 +3143,20 @@ export enum RelationTemplateOrderField { Name = 'NAME' } +/** Type of a Shape */ +export enum ShapeType { + /** A Circle */ + Circle = 'CIRCLE', + /** An Ellipse */ + Ellipse = 'ELLIPSE', + /** A Hexagon */ + Hexagon = 'HEXAGON', + /** A Rectangle */ + Rect = 'RECT', + /** A Rhombus */ + Rhombus = 'RHOMBUS' +} + /** Filter which can be used to filter for Nodes with a specific String field */ export type StringFilterInput = { /** Matches Strings which contain the provided value */ @@ -3087,6 +3257,8 @@ export type TrackableFilterInput = { or?: InputMaybe>; /** Filter by pinnedIssues */ pinnedIssues?: InputMaybe; + /** Filters for AffectedByIssues which are related to a Trackable */ + relatedTo?: InputMaybe; /** Filter by repositoryURL */ repositoryURL?: InputMaybe; /** Filter by syncsTo */ @@ -3202,7 +3374,7 @@ export type GetImsUserDetailsQueryVariables = Exact<{ }>; -export type GetImsUserDetailsQuery = { __typename?: 'Query', node?: { __typename?: 'AddedAffectedEntityEvent' } | { __typename?: 'AddedArtefactEvent' } | { __typename?: 'AddedLabelEvent' } | { __typename?: 'AddedToPinnedIssuesEvent' } | { __typename?: 'AddedToTrackableEvent' } | { __typename?: 'Artefact' } | { __typename?: 'ArtefactTemplate' } | { __typename?: 'Assignment' } | { __typename?: 'AssignmentType' } | { __typename?: 'AssignmentTypeChangedEvent' } | { __typename?: 'Body' } | { __typename?: 'Component' } | { __typename?: 'ComponentPermission' } | { __typename?: 'ComponentTemplate' } | { __typename?: 'ComponentVersion' } | { __typename?: 'ComponentVersionTemplate' } | { __typename?: 'DueDateChangedEvent' } | { __typename?: 'EstimatedTimeChangedEvent' } | { __typename?: 'GlobalPermission' } | { __typename?: 'GropiusUser' } | { __typename?: 'IMS' } | { __typename?: 'IMSIssue' } | { __typename?: 'IMSIssueTemplate' } | { __typename?: 'IMSPermission' } | { __typename?: 'IMSProject' } | { __typename?: 'IMSProjectTemplate' } | { __typename?: 'IMSTemplate' } | { __typename: 'IMSUser', id: string, username?: string | null, displayName: string, email?: string | null, templatedFields: Array<{ __typename: 'JSONField', name: string, value?: any | null }>, ims: { __typename: 'IMS', id: string, name: string, description: string, templatedFields: Array<{ __typename: 'JSONField', name: string, value?: any | null }> } } | { __typename?: 'IMSUserTemplate' } | { __typename?: 'IncomingRelationTypeChangedEvent' } | { __typename?: 'Interface' } | { __typename?: 'InterfaceDefinition' } | { __typename?: 'InterfaceDefinitionTemplate' } | { __typename?: 'InterfacePart' } | { __typename?: 'InterfacePartTemplate' } | { __typename?: 'InterfaceSpecification' } | { __typename?: 'InterfaceSpecificationDerivationCondition' } | { __typename?: 'InterfaceSpecificationTemplate' } | { __typename?: 'InterfaceSpecificationVersion' } | { __typename?: 'InterfaceSpecificationVersionTemplate' } | { __typename?: 'InterfaceTemplate' } | { __typename?: 'IntraComponentDependencyParticipant' } | { __typename?: 'IntraComponentDependencySpecification' } | { __typename?: 'Issue' } | { __typename?: 'IssueComment' } | { __typename?: 'IssuePriority' } | { __typename?: 'IssueRelation' } | { __typename?: 'IssueRelationType' } | { __typename?: 'IssueState' } | { __typename?: 'IssueTemplate' } | { __typename?: 'IssueType' } | { __typename?: 'Label' } | { __typename?: 'OutgoingRelationTypeChangedEvent' } | { __typename?: 'PriorityChangedEvent' } | { __typename?: 'Project' } | { __typename?: 'ProjectPermission' } | { __typename?: 'RelatedByIssueEvent' } | { __typename?: 'Relation' } | { __typename?: 'RelationCondition' } | { __typename?: 'RelationTemplate' } | { __typename?: 'RemovedAffectedEntityEvent' } | { __typename?: 'RemovedArtefactEvent' } | { __typename?: 'RemovedAssignmentEvent' } | { __typename?: 'RemovedFromPinnedIssuesEvent' } | { __typename?: 'RemovedFromTrackableEvent' } | { __typename?: 'RemovedIncomingRelationEvent' } | { __typename?: 'RemovedLabelEvent' } | { __typename?: 'RemovedOutgoingRelationEvent' } | { __typename?: 'RemovedTemplatedFieldEvent' } | { __typename?: 'SpentTimeChangedEvent' } | { __typename?: 'StartDateChangedEvent' } | { __typename?: 'StateChangedEvent' } | { __typename?: 'TemplateChangedEvent' } | { __typename?: 'TemplatedFieldChangedEvent' } | { __typename?: 'TitleChangedEvent' } | { __typename?: 'TypeChangedEvent' } | null }; +export type GetImsUserDetailsQuery = { __typename?: 'Query', node?: { __typename?: 'AddedAffectedEntityEvent' } | { __typename?: 'AddedArtefactEvent' } | { __typename?: 'AddedLabelEvent' } | { __typename?: 'AddedToPinnedIssuesEvent' } | { __typename?: 'AddedToTrackableEvent' } | { __typename?: 'AggregatedIssue' } | { __typename?: 'AggregatedIssueRelation' } | { __typename?: 'Artefact' } | { __typename?: 'ArtefactTemplate' } | { __typename?: 'Assignment' } | { __typename?: 'AssignmentType' } | { __typename?: 'AssignmentTypeChangedEvent' } | { __typename?: 'Body' } | { __typename?: 'Component' } | { __typename?: 'ComponentPermission' } | { __typename?: 'ComponentTemplate' } | { __typename?: 'ComponentVersion' } | { __typename?: 'ComponentVersionTemplate' } | { __typename?: 'DueDateChangedEvent' } | { __typename?: 'EstimatedTimeChangedEvent' } | { __typename?: 'FillStyle' } | { __typename?: 'GlobalPermission' } | { __typename?: 'GropiusUser' } | { __typename?: 'IMS' } | { __typename?: 'IMSIssue' } | { __typename?: 'IMSIssueTemplate' } | { __typename?: 'IMSPermission' } | { __typename?: 'IMSProject' } | { __typename?: 'IMSProjectTemplate' } | { __typename?: 'IMSTemplate' } | { __typename: 'IMSUser', id: string, username?: string | null, displayName: string, email?: string | null, templatedFields: Array<{ __typename: 'JSONField', name: string, value?: any | null }>, ims: { __typename: 'IMS', id: string, name: string, description: string, templatedFields: Array<{ __typename: 'JSONField', name: string, value?: any | null }> } } | { __typename?: 'IMSUserTemplate' } | { __typename?: 'IncomingRelationTypeChangedEvent' } | { __typename?: 'Interface' } | { __typename?: 'InterfaceDefinition' } | { __typename?: 'InterfaceDefinitionTemplate' } | { __typename?: 'InterfacePart' } | { __typename?: 'InterfacePartTemplate' } | { __typename?: 'InterfaceSpecification' } | { __typename?: 'InterfaceSpecificationDerivationCondition' } | { __typename?: 'InterfaceSpecificationTemplate' } | { __typename?: 'InterfaceSpecificationVersion' } | { __typename?: 'InterfaceSpecificationVersionTemplate' } | { __typename?: 'InterfaceTemplate' } | { __typename?: 'IntraComponentDependencyParticipant' } | { __typename?: 'IntraComponentDependencySpecification' } | { __typename?: 'Issue' } | { __typename?: 'IssueComment' } | { __typename?: 'IssuePriority' } | { __typename?: 'IssueRelation' } | { __typename?: 'IssueRelationType' } | { __typename?: 'IssueState' } | { __typename?: 'IssueTemplate' } | { __typename?: 'IssueType' } | { __typename?: 'Label' } | { __typename?: 'MetaAggregatedIssueRelation' } | { __typename?: 'OutgoingRelationTypeChangedEvent' } | { __typename?: 'PriorityChangedEvent' } | { __typename?: 'Project' } | { __typename?: 'ProjectPermission' } | { __typename?: 'RelatedByIssueEvent' } | { __typename?: 'Relation' } | { __typename?: 'RelationCondition' } | { __typename?: 'RelationTemplate' } | { __typename?: 'RemovedAffectedEntityEvent' } | { __typename?: 'RemovedArtefactEvent' } | { __typename?: 'RemovedAssignmentEvent' } | { __typename?: 'RemovedFromPinnedIssuesEvent' } | { __typename?: 'RemovedFromTrackableEvent' } | { __typename?: 'RemovedIncomingRelationEvent' } | { __typename?: 'RemovedLabelEvent' } | { __typename?: 'RemovedOutgoingRelationEvent' } | { __typename?: 'RemovedTemplatedFieldEvent' } | { __typename?: 'SpentTimeChangedEvent' } | { __typename?: 'StartDateChangedEvent' } | { __typename?: 'StateChangedEvent' } | { __typename?: 'StrokeStyle' } | { __typename?: 'TemplateChangedEvent' } | { __typename?: 'TemplatedFieldChangedEvent' } | { __typename?: 'TitleChangedEvent' } | { __typename?: 'TypeChangedEvent' } | null }; export type GetImsUsersByTemplatedFieldValuesQueryVariables = Exact<{ imsFilterInput: ImsFilterInput; @@ -3224,7 +3396,7 @@ export type GetBasicGropiusUserDataQueryVariables = Exact<{ }>; -export type GetBasicGropiusUserDataQuery = { __typename?: 'Query', node?: { __typename?: 'AddedAffectedEntityEvent' } | { __typename?: 'AddedArtefactEvent' } | { __typename?: 'AddedLabelEvent' } | { __typename?: 'AddedToPinnedIssuesEvent' } | { __typename?: 'AddedToTrackableEvent' } | { __typename?: 'Artefact' } | { __typename?: 'ArtefactTemplate' } | { __typename?: 'Assignment' } | { __typename?: 'AssignmentType' } | { __typename?: 'AssignmentTypeChangedEvent' } | { __typename?: 'Body' } | { __typename?: 'Component' } | { __typename?: 'ComponentPermission' } | { __typename?: 'ComponentTemplate' } | { __typename?: 'ComponentVersion' } | { __typename?: 'ComponentVersionTemplate' } | { __typename?: 'DueDateChangedEvent' } | { __typename?: 'EstimatedTimeChangedEvent' } | { __typename?: 'GlobalPermission' } | { __typename: 'GropiusUser', id: string, username: string, displayName: string, email?: string | null } | { __typename?: 'IMS' } | { __typename?: 'IMSIssue' } | { __typename?: 'IMSIssueTemplate' } | { __typename?: 'IMSPermission' } | { __typename?: 'IMSProject' } | { __typename?: 'IMSProjectTemplate' } | { __typename?: 'IMSTemplate' } | { __typename?: 'IMSUser' } | { __typename?: 'IMSUserTemplate' } | { __typename?: 'IncomingRelationTypeChangedEvent' } | { __typename?: 'Interface' } | { __typename?: 'InterfaceDefinition' } | { __typename?: 'InterfaceDefinitionTemplate' } | { __typename?: 'InterfacePart' } | { __typename?: 'InterfacePartTemplate' } | { __typename?: 'InterfaceSpecification' } | { __typename?: 'InterfaceSpecificationDerivationCondition' } | { __typename?: 'InterfaceSpecificationTemplate' } | { __typename?: 'InterfaceSpecificationVersion' } | { __typename?: 'InterfaceSpecificationVersionTemplate' } | { __typename?: 'InterfaceTemplate' } | { __typename?: 'IntraComponentDependencyParticipant' } | { __typename?: 'IntraComponentDependencySpecification' } | { __typename?: 'Issue' } | { __typename?: 'IssueComment' } | { __typename?: 'IssuePriority' } | { __typename?: 'IssueRelation' } | { __typename?: 'IssueRelationType' } | { __typename?: 'IssueState' } | { __typename?: 'IssueTemplate' } | { __typename?: 'IssueType' } | { __typename?: 'Label' } | { __typename?: 'OutgoingRelationTypeChangedEvent' } | { __typename?: 'PriorityChangedEvent' } | { __typename?: 'Project' } | { __typename?: 'ProjectPermission' } | { __typename?: 'RelatedByIssueEvent' } | { __typename?: 'Relation' } | { __typename?: 'RelationCondition' } | { __typename?: 'RelationTemplate' } | { __typename?: 'RemovedAffectedEntityEvent' } | { __typename?: 'RemovedArtefactEvent' } | { __typename?: 'RemovedAssignmentEvent' } | { __typename?: 'RemovedFromPinnedIssuesEvent' } | { __typename?: 'RemovedFromTrackableEvent' } | { __typename?: 'RemovedIncomingRelationEvent' } | { __typename?: 'RemovedLabelEvent' } | { __typename?: 'RemovedOutgoingRelationEvent' } | { __typename?: 'RemovedTemplatedFieldEvent' } | { __typename?: 'SpentTimeChangedEvent' } | { __typename?: 'StartDateChangedEvent' } | { __typename?: 'StateChangedEvent' } | { __typename?: 'TemplateChangedEvent' } | { __typename?: 'TemplatedFieldChangedEvent' } | { __typename?: 'TitleChangedEvent' } | { __typename?: 'TypeChangedEvent' } | null }; +export type GetBasicGropiusUserDataQuery = { __typename?: 'Query', node?: { __typename?: 'AddedAffectedEntityEvent' } | { __typename?: 'AddedArtefactEvent' } | { __typename?: 'AddedLabelEvent' } | { __typename?: 'AddedToPinnedIssuesEvent' } | { __typename?: 'AddedToTrackableEvent' } | { __typename?: 'AggregatedIssue' } | { __typename?: 'AggregatedIssueRelation' } | { __typename?: 'Artefact' } | { __typename?: 'ArtefactTemplate' } | { __typename?: 'Assignment' } | { __typename?: 'AssignmentType' } | { __typename?: 'AssignmentTypeChangedEvent' } | { __typename?: 'Body' } | { __typename?: 'Component' } | { __typename?: 'ComponentPermission' } | { __typename?: 'ComponentTemplate' } | { __typename?: 'ComponentVersion' } | { __typename?: 'ComponentVersionTemplate' } | { __typename?: 'DueDateChangedEvent' } | { __typename?: 'EstimatedTimeChangedEvent' } | { __typename?: 'FillStyle' } | { __typename?: 'GlobalPermission' } | { __typename: 'GropiusUser', id: string, username: string, displayName: string, email?: string | null } | { __typename?: 'IMS' } | { __typename?: 'IMSIssue' } | { __typename?: 'IMSIssueTemplate' } | { __typename?: 'IMSPermission' } | { __typename?: 'IMSProject' } | { __typename?: 'IMSProjectTemplate' } | { __typename?: 'IMSTemplate' } | { __typename?: 'IMSUser' } | { __typename?: 'IMSUserTemplate' } | { __typename?: 'IncomingRelationTypeChangedEvent' } | { __typename?: 'Interface' } | { __typename?: 'InterfaceDefinition' } | { __typename?: 'InterfaceDefinitionTemplate' } | { __typename?: 'InterfacePart' } | { __typename?: 'InterfacePartTemplate' } | { __typename?: 'InterfaceSpecification' } | { __typename?: 'InterfaceSpecificationDerivationCondition' } | { __typename?: 'InterfaceSpecificationTemplate' } | { __typename?: 'InterfaceSpecificationVersion' } | { __typename?: 'InterfaceSpecificationVersionTemplate' } | { __typename?: 'InterfaceTemplate' } | { __typename?: 'IntraComponentDependencyParticipant' } | { __typename?: 'IntraComponentDependencySpecification' } | { __typename?: 'Issue' } | { __typename?: 'IssueComment' } | { __typename?: 'IssuePriority' } | { __typename?: 'IssueRelation' } | { __typename?: 'IssueRelationType' } | { __typename?: 'IssueState' } | { __typename?: 'IssueTemplate' } | { __typename?: 'IssueType' } | { __typename?: 'Label' } | { __typename?: 'MetaAggregatedIssueRelation' } | { __typename?: 'OutgoingRelationTypeChangedEvent' } | { __typename?: 'PriorityChangedEvent' } | { __typename?: 'Project' } | { __typename?: 'ProjectPermission' } | { __typename?: 'RelatedByIssueEvent' } | { __typename?: 'Relation' } | { __typename?: 'RelationCondition' } | { __typename?: 'RelationTemplate' } | { __typename?: 'RemovedAffectedEntityEvent' } | { __typename?: 'RemovedArtefactEvent' } | { __typename?: 'RemovedAssignmentEvent' } | { __typename?: 'RemovedFromPinnedIssuesEvent' } | { __typename?: 'RemovedFromTrackableEvent' } | { __typename?: 'RemovedIncomingRelationEvent' } | { __typename?: 'RemovedLabelEvent' } | { __typename?: 'RemovedOutgoingRelationEvent' } | { __typename?: 'RemovedTemplatedFieldEvent' } | { __typename?: 'SpentTimeChangedEvent' } | { __typename?: 'StartDateChangedEvent' } | { __typename?: 'StateChangedEvent' } | { __typename?: 'StrokeStyle' } | { __typename?: 'TemplateChangedEvent' } | { __typename?: 'TemplatedFieldChangedEvent' } | { __typename?: 'TitleChangedEvent' } | { __typename?: 'TypeChangedEvent' } | null }; export type GetUserByNameQueryVariables = Exact<{ username: Scalars['String']; @@ -3238,7 +3410,7 @@ export type CheckUserIsAdminQueryVariables = Exact<{ }>; -export type CheckUserIsAdminQuery = { __typename?: 'Query', node?: { __typename: 'AddedAffectedEntityEvent' } | { __typename: 'AddedArtefactEvent' } | { __typename: 'AddedLabelEvent' } | { __typename: 'AddedToPinnedIssuesEvent' } | { __typename: 'AddedToTrackableEvent' } | { __typename: 'Artefact' } | { __typename: 'ArtefactTemplate' } | { __typename: 'Assignment' } | { __typename: 'AssignmentType' } | { __typename: 'AssignmentTypeChangedEvent' } | { __typename: 'Body' } | { __typename: 'Component' } | { __typename: 'ComponentPermission' } | { __typename: 'ComponentTemplate' } | { __typename: 'ComponentVersion' } | { __typename: 'ComponentVersionTemplate' } | { __typename: 'DueDateChangedEvent' } | { __typename: 'EstimatedTimeChangedEvent' } | { __typename: 'GlobalPermission' } | { __typename: 'GropiusUser', id: string, isAdmin: boolean } | { __typename: 'IMS' } | { __typename: 'IMSIssue' } | { __typename: 'IMSIssueTemplate' } | { __typename: 'IMSPermission' } | { __typename: 'IMSProject' } | { __typename: 'IMSProjectTemplate' } | { __typename: 'IMSTemplate' } | { __typename: 'IMSUser' } | { __typename: 'IMSUserTemplate' } | { __typename: 'IncomingRelationTypeChangedEvent' } | { __typename: 'Interface' } | { __typename: 'InterfaceDefinition' } | { __typename: 'InterfaceDefinitionTemplate' } | { __typename: 'InterfacePart' } | { __typename: 'InterfacePartTemplate' } | { __typename: 'InterfaceSpecification' } | { __typename: 'InterfaceSpecificationDerivationCondition' } | { __typename: 'InterfaceSpecificationTemplate' } | { __typename: 'InterfaceSpecificationVersion' } | { __typename: 'InterfaceSpecificationVersionTemplate' } | { __typename: 'InterfaceTemplate' } | { __typename: 'IntraComponentDependencyParticipant' } | { __typename: 'IntraComponentDependencySpecification' } | { __typename: 'Issue' } | { __typename: 'IssueComment' } | { __typename: 'IssuePriority' } | { __typename: 'IssueRelation' } | { __typename: 'IssueRelationType' } | { __typename: 'IssueState' } | { __typename: 'IssueTemplate' } | { __typename: 'IssueType' } | { __typename: 'Label' } | { __typename: 'OutgoingRelationTypeChangedEvent' } | { __typename: 'PriorityChangedEvent' } | { __typename: 'Project' } | { __typename: 'ProjectPermission' } | { __typename: 'RelatedByIssueEvent' } | { __typename: 'Relation' } | { __typename: 'RelationCondition' } | { __typename: 'RelationTemplate' } | { __typename: 'RemovedAffectedEntityEvent' } | { __typename: 'RemovedArtefactEvent' } | { __typename: 'RemovedAssignmentEvent' } | { __typename: 'RemovedFromPinnedIssuesEvent' } | { __typename: 'RemovedFromTrackableEvent' } | { __typename: 'RemovedIncomingRelationEvent' } | { __typename: 'RemovedLabelEvent' } | { __typename: 'RemovedOutgoingRelationEvent' } | { __typename: 'RemovedTemplatedFieldEvent' } | { __typename: 'SpentTimeChangedEvent' } | { __typename: 'StartDateChangedEvent' } | { __typename: 'StateChangedEvent' } | { __typename: 'TemplateChangedEvent' } | { __typename: 'TemplatedFieldChangedEvent' } | { __typename: 'TitleChangedEvent' } | { __typename: 'TypeChangedEvent' } | null }; +export type CheckUserIsAdminQuery = { __typename?: 'Query', node?: { __typename: 'AddedAffectedEntityEvent' } | { __typename: 'AddedArtefactEvent' } | { __typename: 'AddedLabelEvent' } | { __typename: 'AddedToPinnedIssuesEvent' } | { __typename: 'AddedToTrackableEvent' } | { __typename: 'AggregatedIssue' } | { __typename: 'AggregatedIssueRelation' } | { __typename: 'Artefact' } | { __typename: 'ArtefactTemplate' } | { __typename: 'Assignment' } | { __typename: 'AssignmentType' } | { __typename: 'AssignmentTypeChangedEvent' } | { __typename: 'Body' } | { __typename: 'Component' } | { __typename: 'ComponentPermission' } | { __typename: 'ComponentTemplate' } | { __typename: 'ComponentVersion' } | { __typename: 'ComponentVersionTemplate' } | { __typename: 'DueDateChangedEvent' } | { __typename: 'EstimatedTimeChangedEvent' } | { __typename: 'FillStyle' } | { __typename: 'GlobalPermission' } | { __typename: 'GropiusUser', id: string, isAdmin: boolean } | { __typename: 'IMS' } | { __typename: 'IMSIssue' } | { __typename: 'IMSIssueTemplate' } | { __typename: 'IMSPermission' } | { __typename: 'IMSProject' } | { __typename: 'IMSProjectTemplate' } | { __typename: 'IMSTemplate' } | { __typename: 'IMSUser' } | { __typename: 'IMSUserTemplate' } | { __typename: 'IncomingRelationTypeChangedEvent' } | { __typename: 'Interface' } | { __typename: 'InterfaceDefinition' } | { __typename: 'InterfaceDefinitionTemplate' } | { __typename: 'InterfacePart' } | { __typename: 'InterfacePartTemplate' } | { __typename: 'InterfaceSpecification' } | { __typename: 'InterfaceSpecificationDerivationCondition' } | { __typename: 'InterfaceSpecificationTemplate' } | { __typename: 'InterfaceSpecificationVersion' } | { __typename: 'InterfaceSpecificationVersionTemplate' } | { __typename: 'InterfaceTemplate' } | { __typename: 'IntraComponentDependencyParticipant' } | { __typename: 'IntraComponentDependencySpecification' } | { __typename: 'Issue' } | { __typename: 'IssueComment' } | { __typename: 'IssuePriority' } | { __typename: 'IssueRelation' } | { __typename: 'IssueRelationType' } | { __typename: 'IssueState' } | { __typename: 'IssueTemplate' } | { __typename: 'IssueType' } | { __typename: 'Label' } | { __typename: 'MetaAggregatedIssueRelation' } | { __typename: 'OutgoingRelationTypeChangedEvent' } | { __typename: 'PriorityChangedEvent' } | { __typename: 'Project' } | { __typename: 'ProjectPermission' } | { __typename: 'RelatedByIssueEvent' } | { __typename: 'Relation' } | { __typename: 'RelationCondition' } | { __typename: 'RelationTemplate' } | { __typename: 'RemovedAffectedEntityEvent' } | { __typename: 'RemovedArtefactEvent' } | { __typename: 'RemovedAssignmentEvent' } | { __typename: 'RemovedFromPinnedIssuesEvent' } | { __typename: 'RemovedFromTrackableEvent' } | { __typename: 'RemovedIncomingRelationEvent' } | { __typename: 'RemovedLabelEvent' } | { __typename: 'RemovedOutgoingRelationEvent' } | { __typename: 'RemovedTemplatedFieldEvent' } | { __typename: 'SpentTimeChangedEvent' } | { __typename: 'StartDateChangedEvent' } | { __typename: 'StateChangedEvent' } | { __typename: 'StrokeStyle' } | { __typename: 'TemplateChangedEvent' } | { __typename: 'TemplatedFieldChangedEvent' } | { __typename: 'TitleChangedEvent' } | { __typename: 'TypeChangedEvent' } | null }; export type CreateNewUserMutationVariables = Exact<{ input: CreateGropiusUserInput; From a6ebb9ca44df44ded9bf92bfa292b7def72039bd Mon Sep 17 00:00:00 2001 From: Niklas Krieger <53957498+nk-coding@users.noreply.github.com> Date: Sun, 12 Nov 2023 20:39:11 +0100 Subject: [PATCH 24/30] Add debug git status to action --- .github/workflows/validate-generated-code.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-generated-code.yml b/.github/workflows/validate-generated-code.yml index b21e9149..49f754ce 100644 --- a/.github/workflows/validate-generated-code.yml +++ b/.github/workflows/validate-generated-code.yml @@ -50,10 +50,11 @@ jobs: echo "Failed to generate model" exit 1 fi + git status if [[ `git status --porcelain` ]]; then echo "Outdated generated code in login-service" exit 1 else echo "login-service up to date" fi - kill $gradlew_pid \ No newline at end of file + kill $gradlew_pid From 6f2a7b40504ce3a41fe78a74b28776349e086515 Mon Sep 17 00:00:00 2001 From: Niklas Krieger <53957498+nk-coding@users.noreply.github.com> Date: Sun, 12 Nov 2023 20:43:50 +0100 Subject: [PATCH 25/30] Add debug git diff to action --- .github/workflows/validate-generated-code.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/validate-generated-code.yml b/.github/workflows/validate-generated-code.yml index 49f754ce..3ecc95d4 100644 --- a/.github/workflows/validate-generated-code.yml +++ b/.github/workflows/validate-generated-code.yml @@ -51,6 +51,7 @@ jobs: exit 1 fi git status + git diff if [[ `git status --porcelain` ]]; then echo "Outdated generated code in login-service" exit 1 From 496be77ef2138491c4fd4515f56a520b5ccfb1d1 Mon Sep 17 00:00:00 2001 From: Niklas Krieger <53957498+nk-coding@users.noreply.github.com> Date: Sun, 12 Nov 2023 21:15:36 +0100 Subject: [PATCH 26/30] upgrade checkout action (in hope this does sth) --- .github/workflows/validate-generated-code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-generated-code.yml b/.github/workflows/validate-generated-code.yml index 3ecc95d4..5b53eaa3 100644 --- a/.github/workflows/validate-generated-code.yml +++ b/.github/workflows/validate-generated-code.yml @@ -13,7 +13,7 @@ jobs: name: Validate generated code runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: distribution: temurin From 2ff5926d60e62bd4194e544f2ce975becb0c8111 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sun, 12 Nov 2023 21:30:44 +0100 Subject: [PATCH 27/30] fix pipeline --- login-service/src/model/graphql/generated.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/login-service/src/model/graphql/generated.ts b/login-service/src/model/graphql/generated.ts index 2f4425ec..aa7f2273 100644 --- a/login-service/src/model/graphql/generated.ts +++ b/login-service/src/model/graphql/generated.ts @@ -3374,7 +3374,7 @@ export type GetBasicImsUserDataQueryVariables = Exact<{ }>; -export type GetBasicImsUserDataQuery = { __typename?: 'Query', node?: { __typename: 'AddedAffectedEntityEvent', id: string } | { __typename: 'AddedArtefactEvent', id: string } | { __typename: 'AddedLabelEvent', id: string } | { __typename: 'AddedToPinnedIssuesEvent', id: string } | { __typename: 'AddedToTrackableEvent', id: string } | { __typename: 'Artefact', id: string } | { __typename: 'ArtefactTemplate', id: string } | { __typename: 'Assignment', id: string } | { __typename: 'AssignmentType', id: string } | { __typename: 'AssignmentTypeChangedEvent', id: string } | { __typename: 'Body', id: string } | { __typename: 'Component', id: string } | { __typename: 'ComponentPermission', id: string } | { __typename: 'ComponentTemplate', id: string } | { __typename: 'ComponentVersion', id: string } | { __typename: 'ComponentVersionTemplate', id: string } | { __typename: 'DueDateChangedEvent', id: string } | { __typename: 'EstimatedTimeChangedEvent', id: string } | { __typename: 'GlobalPermission', id: string } | { __typename: 'GropiusUser', id: string } | { __typename: 'IMS', id: string } | { __typename: 'IMSIssue', id: string } | { __typename: 'IMSIssueTemplate', id: string } | { __typename: 'IMSPermission', id: string } | { __typename: 'IMSProject', id: string } | { __typename: 'IMSProjectTemplate', id: string } | { __typename: 'IMSTemplate', id: string } | { __typename: 'IMSUser', id: string } | { __typename: 'IMSUserTemplate', id: string } | { __typename: 'IncomingRelationTypeChangedEvent', id: string } | { __typename: 'Interface', id: string } | { __typename: 'InterfaceDefinition', id: string } | { __typename: 'InterfaceDefinitionTemplate', id: string } | { __typename: 'InterfacePart', id: string } | { __typename: 'InterfacePartTemplate', id: string } | { __typename: 'InterfaceSpecification', id: string } | { __typename: 'InterfaceSpecificationDerivationCondition', id: string } | { __typename: 'InterfaceSpecificationTemplate', id: string } | { __typename: 'InterfaceSpecificationVersion', id: string } | { __typename: 'InterfaceSpecificationVersionTemplate', id: string } | { __typename: 'InterfaceTemplate', id: string } | { __typename: 'IntraComponentDependencyParticipant', id: string } | { __typename: 'IntraComponentDependencySpecification', id: string } | { __typename: 'Issue', id: string } | { __typename: 'IssueComment', id: string } | { __typename: 'IssuePriority', id: string } | { __typename: 'IssueRelation', id: string } | { __typename: 'IssueRelationType', id: string } | { __typename: 'IssueState', id: string } | { __typename: 'IssueTemplate', id: string } | { __typename: 'IssueType', id: string } | { __typename: 'Label', id: string } | { __typename: 'OutgoingRelationTypeChangedEvent', id: string } | { __typename: 'PriorityChangedEvent', id: string } | { __typename: 'Project', id: string } | { __typename: 'ProjectPermission', id: string } | { __typename: 'RelatedByIssueEvent', id: string } | { __typename: 'Relation', id: string } | { __typename: 'RelationCondition', id: string } | { __typename: 'RelationTemplate', id: string } | { __typename: 'RemovedAffectedEntityEvent', id: string } | { __typename: 'RemovedArtefactEvent', id: string } | { __typename: 'RemovedAssignmentEvent', id: string } | { __typename: 'RemovedFromPinnedIssuesEvent', id: string } | { __typename: 'RemovedFromTrackableEvent', id: string } | { __typename: 'RemovedIncomingRelationEvent', id: string } | { __typename: 'RemovedLabelEvent', id: string } | { __typename: 'RemovedOutgoingRelationEvent', id: string } | { __typename: 'RemovedTemplatedFieldEvent', id: string } | { __typename: 'SpentTimeChangedEvent', id: string } | { __typename: 'StartDateChangedEvent', id: string } | { __typename: 'StateChangedEvent', id: string } | { __typename: 'TemplateChangedEvent', id: string } | { __typename: 'TemplatedFieldChangedEvent', id: string } | { __typename: 'TitleChangedEvent', id: string } | { __typename: 'TypeChangedEvent', id: string } | null }; +export type GetBasicImsUserDataQuery = { __typename?: 'Query', node?: { __typename: 'AddedAffectedEntityEvent', id: string } | { __typename: 'AddedArtefactEvent', id: string } | { __typename: 'AddedLabelEvent', id: string } | { __typename: 'AddedToPinnedIssuesEvent', id: string } | { __typename: 'AddedToTrackableEvent', id: string } | { __typename: 'AggregatedIssue', id: string } | { __typename: 'AggregatedIssueRelation', id: string } | { __typename: 'Artefact', id: string } | { __typename: 'ArtefactTemplate', id: string } | { __typename: 'Assignment', id: string } | { __typename: 'AssignmentType', id: string } | { __typename: 'AssignmentTypeChangedEvent', id: string } | { __typename: 'Body', id: string } | { __typename: 'Component', id: string } | { __typename: 'ComponentPermission', id: string } | { __typename: 'ComponentTemplate', id: string } | { __typename: 'ComponentVersion', id: string } | { __typename: 'ComponentVersionTemplate', id: string } | { __typename: 'DueDateChangedEvent', id: string } | { __typename: 'EstimatedTimeChangedEvent', id: string } | { __typename: 'FillStyle', id: string } | { __typename: 'GlobalPermission', id: string } | { __typename: 'GropiusUser', id: string } | { __typename: 'IMS', id: string } | { __typename: 'IMSIssue', id: string } | { __typename: 'IMSIssueTemplate', id: string } | { __typename: 'IMSPermission', id: string } | { __typename: 'IMSProject', id: string } | { __typename: 'IMSProjectTemplate', id: string } | { __typename: 'IMSTemplate', id: string } | { __typename: 'IMSUser', id: string } | { __typename: 'IMSUserTemplate', id: string } | { __typename: 'IncomingRelationTypeChangedEvent', id: string } | { __typename: 'Interface', id: string } | { __typename: 'InterfaceDefinition', id: string } | { __typename: 'InterfaceDefinitionTemplate', id: string } | { __typename: 'InterfacePart', id: string } | { __typename: 'InterfacePartTemplate', id: string } | { __typename: 'InterfaceSpecification', id: string } | { __typename: 'InterfaceSpecificationDerivationCondition', id: string } | { __typename: 'InterfaceSpecificationTemplate', id: string } | { __typename: 'InterfaceSpecificationVersion', id: string } | { __typename: 'InterfaceSpecificationVersionTemplate', id: string } | { __typename: 'InterfaceTemplate', id: string } | { __typename: 'IntraComponentDependencyParticipant', id: string } | { __typename: 'IntraComponentDependencySpecification', id: string } | { __typename: 'Issue', id: string } | { __typename: 'IssueComment', id: string } | { __typename: 'IssuePriority', id: string } | { __typename: 'IssueRelation', id: string } | { __typename: 'IssueRelationType', id: string } | { __typename: 'IssueState', id: string } | { __typename: 'IssueTemplate', id: string } | { __typename: 'IssueType', id: string } | { __typename: 'Label', id: string } | { __typename: 'MetaAggregatedIssueRelation', id: string } | { __typename: 'OutgoingRelationTypeChangedEvent', id: string } | { __typename: 'PriorityChangedEvent', id: string } | { __typename: 'Project', id: string } | { __typename: 'ProjectPermission', id: string } | { __typename: 'RelatedByIssueEvent', id: string } | { __typename: 'Relation', id: string } | { __typename: 'RelationCondition', id: string } | { __typename: 'RelationTemplate', id: string } | { __typename: 'RemovedAffectedEntityEvent', id: string } | { __typename: 'RemovedArtefactEvent', id: string } | { __typename: 'RemovedAssignmentEvent', id: string } | { __typename: 'RemovedFromPinnedIssuesEvent', id: string } | { __typename: 'RemovedFromTrackableEvent', id: string } | { __typename: 'RemovedIncomingRelationEvent', id: string } | { __typename: 'RemovedLabelEvent', id: string } | { __typename: 'RemovedOutgoingRelationEvent', id: string } | { __typename: 'RemovedTemplatedFieldEvent', id: string } | { __typename: 'SpentTimeChangedEvent', id: string } | { __typename: 'StartDateChangedEvent', id: string } | { __typename: 'StateChangedEvent', id: string } | { __typename: 'StrokeStyle', id: string } | { __typename: 'TemplateChangedEvent', id: string } | { __typename: 'TemplatedFieldChangedEvent', id: string } | { __typename: 'TitleChangedEvent', id: string } | { __typename: 'TypeChangedEvent', id: string } | null }; export type GetImsUserDetailsQueryVariables = Exact<{ imsUserId: Scalars['ID']; From 38f3d4a2ef272ec66d4af79a34cc8f1b62ceae58 Mon Sep 17 00:00:00 2001 From: Niklas Krieger <53957498+nk-coding@users.noreply.github.com> Date: Sun, 12 Nov 2023 21:36:27 +0100 Subject: [PATCH 28/30] spelling error --- .../src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt b/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt index b202c5cf..4bc98806 100644 --- a/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt +++ b/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt @@ -170,7 +170,7 @@ class GraphQLConfiguration { AffectedByIssueRelatedToFilterEntryDefinition(nodeDefinitionCollection) /** - * Filter for [RelationPartner]s which part of the graph of a specific [Project] + * Filter for [RelationPartner]s which are part of a specific [Project]'s graph. */ @Bean(PART_OF_PROJECT_FILTER) fun partOfProjectFilter(nodeDefinitionCollection: NodeDefinitionCollection) = @@ -267,4 +267,4 @@ class GraphQLConfiguration { } } -} \ No newline at end of file +} From 6882b11ac0e26968a83a73ec818cb436244bf0b5 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sun, 12 Nov 2023 22:08:31 +0100 Subject: [PATCH 29/30] add (filled) circle marker type --- .../kotlin/gropius/model/template/style/MarkerType.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/gropius/model/template/style/MarkerType.kt b/core/src/main/kotlin/gropius/model/template/style/MarkerType.kt index aa6b3ab2..310b9734 100644 --- a/core/src/main/kotlin/gropius/model/template/style/MarkerType.kt +++ b/core/src/main/kotlin/gropius/model/template/style/MarkerType.kt @@ -17,5 +17,11 @@ enum class MarkerType { TRIANGLE, @GraphQLDescription("A filled triangle") - FILLED_TRIANGLE + FILLED_TRIANGLE, + + @GraphQLDescription("A circle") + CIRCLE, + + @GraphQLDescription("A filled circle") + FILLED_CIRCLE } \ No newline at end of file From 9c3864a093663dfced17516abd534575c32c34f8 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sun, 12 Nov 2023 22:57:25 +0100 Subject: [PATCH 30/30] update generated code --- login-service/src/model/graphql/generated.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/login-service/src/model/graphql/generated.ts b/login-service/src/model/graphql/generated.ts index aa7f2273..02667287 100644 --- a/login-service/src/model/graphql/generated.ts +++ b/login-service/src/model/graphql/generated.ts @@ -2596,8 +2596,12 @@ export enum LabelOrderField { export enum MarkerType { /** A regular arrow */ Arrow = 'ARROW', + /** A circle */ + Circle = 'CIRCLE', /** A diamond */ Diamond = 'DIAMOND', + /** A filled circle */ + FilledCircle = 'FILLED_CIRCLE', /** A filled diamond */ FilledDiamond = 'FILLED_DIAMOND', /** A filled triangle */