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 1413253a..4de6ebd3 100644 --- a/api-public/src/main/kotlin/gropius/schema/mutation/ArchitectureMutations.kt +++ b/api-public/src/main/kotlin/gropius/schema/mutation/ArchitectureMutations.kt @@ -6,11 +6,14 @@ import com.expediagroup.graphql.server.operations.Mutation import graphql.schema.DataFetchingEnvironment import gropius.authorization.gropiusAuthorizationContext import gropius.dto.input.architecture.* +import gropius.dto.input.architecture.layout.CreateViewInput +import gropius.dto.input.architecture.layout.UpdateViewInput import gropius.dto.input.common.DeleteNodeInput import gropius.dto.payload.AddComponentVersionToProjectPayload import gropius.dto.payload.DeleteNodePayload import gropius.graphql.AutoPayloadType import gropius.model.architecture.* +import gropius.model.architecture.layout.View import gropius.service.architecture.* import org.springframework.beans.factory.annotation.Autowired import org.springframework.transaction.annotation.Propagation @@ -29,6 +32,7 @@ import org.springframework.transaction.annotation.Transactional * @param imsProjectService used for IMSProject-related mutations * @param intraComponentDependencySpecificationService used for IntraComponentDependencySpecificationService-related mutations * @param syncPermissionTargetService used for SyncPermissionTarget-related mutations + * @param viewService used for View-related mutations */ @org.springframework.stereotype.Component @Transactional(propagation = Propagation.REQUIRES_NEW) @@ -43,7 +47,8 @@ class ArchitectureMutations( private val imsService: IMSService, private val imsProjectService: IMSProjectService, private val intraComponentDependencySpecificationService: IntraComponentDependencySpecificationService, - private val syncPermissionTargetService: SyncPermissionTargetService + private val syncPermissionTargetService: SyncPermissionTargetService, + private val viewService: ViewService ) : Mutation { @GraphQLDescription( @@ -121,6 +126,33 @@ class ArchitectureMutations( return DeleteNodePayload(input.id) } + @GraphQLDescription("Creates a new View, requires MANAGE_VIEWS on the project owning the view.") + @AutoPayloadType("The created View") + suspend fun createView( + @GraphQLDescription("Defines the created View") + input: CreateViewInput, dfe: DataFetchingEnvironment + ): View { + return viewService.createView(dfe.gropiusAuthorizationContext, input) + } + + @GraphQLDescription("Updates the specified View, requires MANAGE_VIEWS on the project owning the view.") + @AutoPayloadType("The updated View") + suspend fun updateView( + @GraphQLDescription("Defines which View to update and how to update it") + input: UpdateViewInput, dfe: DataFetchingEnvironment + ): View { + return viewService.updateView(dfe.gropiusAuthorizationContext, input) + } + + @GraphQLDescription("Deletes the specified View, requires MANAGE_VIEWS on the project owning the view.") + suspend fun deleteView( + @GraphQLDescription("Defines which View to delete") + input: DeleteNodeInput, dfe: DataFetchingEnvironment + ): DeleteNodePayload { + viewService.deleteView(dfe.gropiusAuthorizationContext, input) + return DeleteNodePayload(input.id) + } + @GraphQLDescription("Creates a new InterfaceSpecification, requires ADMIN on the Component.") @AutoPayloadType("The created InterfaceSpecification") suspend fun createInterfaceSpecification( diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/UpdateProjectInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/UpdateProjectInput.kt index 1257c428..3e490a4c 100644 --- a/core/src/main/kotlin/gropius/dto/input/architecture/UpdateProjectInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/architecture/UpdateProjectInput.kt @@ -1,6 +1,25 @@ 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.architecture.layout.UpdateLayoutInput +import gropius.dto.input.architecture.layout.UpdateRelationLayoutInput +import gropius.dto.input.architecture.layout.UpdateRelationPartnerLayoutInput @GraphQLDescription("Input for the updateProject mutation") -class UpdateProjectInput : UpdateTrackableInput() \ No newline at end of file +class UpdateProjectInput( + @GraphQLDescription("The default view for the project") + val defaultView: OptionalInput, + @GraphQLDescription("Defines the new layout of a set of Relations") + override val relationLayouts: OptionalInput>, + @GraphQLDescription("Defines the new layout of a set of RelationPartners") + override val relationPartnerLayouts: OptionalInput>, +) : UpdateTrackableInput(), UpdateLayoutInput { + + override fun validate() { + super.validate() + validateLayout() + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/layout/CreateViewInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/layout/CreateViewInput.kt new file mode 100644 index 00000000..c75ef2eb --- /dev/null +++ b/core/src/main/kotlin/gropius/dto/input/architecture/layout/CreateViewInput.kt @@ -0,0 +1,25 @@ +package gropius.dto.input.architecture.layout + +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 + +@GraphQLDescription("Input for the createView mutation") +class CreateViewInput( + @GraphQLDescription("Defines the new layout of a set of Relations") + override val relationLayouts: OptionalInput>, + @GraphQLDescription("Defines the new layout of a set of RelationPartners") + override val relationPartnerLayouts: OptionalInput>, + @GraphQLDescription("The new filter of the view") + val filterByTemplate: List, + @GraphQLDescription("The id of the project the view belongs to") + val project: ID +) : CreateNamedNodeInput(), UpdateLayoutInput { + + override fun validate() { + super.validate() + validateLayout() + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/layout/RelationLayoutInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/layout/RelationLayoutInput.kt new file mode 100644 index 00000000..2162f454 --- /dev/null +++ b/core/src/main/kotlin/gropius/dto/input/architecture/layout/RelationLayoutInput.kt @@ -0,0 +1,11 @@ +package gropius.dto.input.architecture.layout + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import gropius.dto.input.common.Input +import gropius.model.architecture.layout.Point + +@GraphQLDescription("Input which defines the layout of a Relation") +class RelationLayoutInput( + @GraphQLDescription("List of intermediate points of the relation") + val points: List, +) : Input() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/layout/RelationPartnerLayoutInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/layout/RelationPartnerLayoutInput.kt new file mode 100644 index 00000000..8da027a5 --- /dev/null +++ b/core/src/main/kotlin/gropius/dto/input/architecture/layout/RelationPartnerLayoutInput.kt @@ -0,0 +1,11 @@ +package gropius.dto.input.architecture.layout + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import gropius.dto.input.common.Input +import gropius.model.architecture.layout.Point + +@GraphQLDescription("Input which defines the layout of a RelationPartner") +class RelationPartnerLayoutInput( + @GraphQLDescription("The position of the RelationPartner") + val pos: Point +) : Input() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateLayoutInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateLayoutInput.kt new file mode 100644 index 00000000..5574b0d3 --- /dev/null +++ b/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateLayoutInput.kt @@ -0,0 +1,45 @@ +package gropius.dto.input.architecture.layout + +import com.expediagroup.graphql.generator.execution.OptionalInput +import gropius.dto.input.ifPresent + +/** + * Common input for updating the layout of a Project + */ +interface UpdateLayoutInput { + + /** + * The layout of the RelationPartners + */ + val relationPartnerLayouts: OptionalInput> + + /** + * The layout of the Relations + */ + val relationLayouts: OptionalInput> + + /** + * Validates the [relationPartnerLayouts] and [relationLayouts] + * Ensures that there is only one layout per RelationPartner and Relation + */ + fun validateLayout() { + relationPartnerLayouts.ifPresent { layouts -> + layouts.forEach(UpdateRelationPartnerLayoutInput::validate) + layouts.groupBy { it.relationPartner }.forEach { (id, group) -> + if (group.size > 1) { + throw IllegalArgumentException("Multiple layouts for the same RelationPartner: $id") + } + } + } + relationLayouts.ifPresent { layouts -> + layouts.forEach(UpdateRelationLayoutInput::validate) + layouts.groupBy { it.relation }.forEach { (id, group) -> + if (group.size > 1) { + throw IllegalArgumentException("Multiple layouts for the same Relation: $id") + } + } + } + + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateRelationLayoutInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateRelationLayoutInput.kt new file mode 100644 index 00000000..fde5cbb0 --- /dev/null +++ b/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateRelationLayoutInput.kt @@ -0,0 +1,20 @@ +package gropius.dto.input.architecture.layout + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.scalars.ID +import gropius.dto.input.common.Input + +@GraphQLDescription("Input to update the layout of a Relation") +class UpdateRelationLayoutInput( + @GraphQLDescription("The id of the Relation of which to update the layout") + val relation: ID, + @GraphQLDescription("The new layout of the Relation, or null if the layout should be reset") + val layout: RelationLayoutInput? +) : Input() { + + override fun validate() { + super.validate() + layout?.validate() + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateRelationPartnerLayoutInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateRelationPartnerLayoutInput.kt new file mode 100644 index 00000000..d4c7bd52 --- /dev/null +++ b/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateRelationPartnerLayoutInput.kt @@ -0,0 +1,20 @@ +package gropius.dto.input.architecture.layout + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.scalars.ID +import gropius.dto.input.common.Input + +@GraphQLDescription("Input to update the layout of a RelationPartner") +class UpdateRelationPartnerLayoutInput( + @GraphQLDescription("The id of the RelationPartner of which to update the layout") + val relationPartner: ID, + @GraphQLDescription("The new layout of the RelationPartner, or null if the layout should be reset") + val layout: RelationPartnerLayoutInput? +) : Input() { + + override fun validate() { + super.validate() + layout?.validate() + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateViewInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateViewInput.kt new file mode 100644 index 00000000..931d4cdc --- /dev/null +++ b/core/src/main/kotlin/gropius/dto/input/architecture/layout/UpdateViewInput.kt @@ -0,0 +1,23 @@ +package gropius.dto.input.architecture.layout + +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.UpdateNamedNodeInput + +@GraphQLDescription("Input for the updateView mutation") +class UpdateViewInput( + @GraphQLDescription("Defines the new layout of a set of Relations") + override val relationLayouts: OptionalInput>, + @GraphQLDescription("Defines the new layout of a set of RelationPartners") + override val relationPartnerLayouts: OptionalInput>, + @GraphQLDescription("The new filter of the view") + val filterByTemplate: OptionalInput> +) : UpdateNamedNodeInput(), UpdateLayoutInput { + + override fun validate() { + super.validate() + validateLayout() + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/architecture/Project.kt b/core/src/main/kotlin/gropius/model/architecture/Project.kt index 1335edb1..6cf62548 100644 --- a/core/src/main/kotlin/gropius/model/architecture/Project.kt +++ b/core/src/main/kotlin/gropius/model/architecture/Project.kt @@ -2,6 +2,10 @@ package gropius.model.architecture import com.expediagroup.graphql.generator.annotations.GraphQLDescription import gropius.authorization.RELATED_TO_NODE_PERMISSION_RULE +import gropius.model.architecture.layout.Layout +import gropius.model.architecture.layout.RelationLayout +import gropius.model.architecture.layout.RelationPartnerLayout +import gropius.model.architecture.layout.View import gropius.model.user.permission.NodePermission import gropius.model.user.permission.NodeWithPermissions import gropius.model.user.permission.ProjectPermission @@ -21,12 +25,20 @@ import java.net.URI ProjectPermission.MANAGE_COMPONENTS, allow = [Rule(RELATED_TO_NODE_PERMISSION_RULE, options = [NodePermission.ADMIN])] ) +@Authorization( + ProjectPermission.MANAGE_VIEWS, + allow = [Rule(RELATED_TO_NODE_PERMISSION_RULE, options = [NodePermission.ADMIN])] +) class Project( name: String, description: String, repositoryURL: URI? -) : Trackable(name, description, repositoryURL), NodeWithPermissions { +) : Trackable(name, description, repositoryURL), NodeWithPermissions, Layout { companion object { const val COMPONENT = "COMPONENT" + const val RELATION_PARTNER = "RELATION_PARTNER" + const val RELATION = "RELATION" + const val VIEW = "VIEW" + const val DEFAULT_VIEW = "DEFAULT_VIEW" } @NodeRelationship(COMPONENT, Direction.OUTGOING) @@ -34,6 +46,26 @@ class Project( @FilterProperty val components by NodeSetProperty() + @NodeRelationship(RELATION_PARTNER, Direction.OUTGOING) + @GraphQLDescription("Layouts for relation partners") + @FilterProperty + override val relationPartnerLayouts by NodeSetProperty() + + @NodeRelationship(RELATION, Direction.OUTGOING) + @GraphQLDescription("Layouts for relations") + @FilterProperty + override val relationLayouts by NodeSetProperty() + + @NodeRelationship(VIEW, Direction.OUTGOING) + @GraphQLDescription("Views on the architecture graph of this project.") + @FilterProperty + val views by NodeSetProperty() + + @NodeRelationship(DEFAULT_VIEW, Direction.OUTGOING) + @GraphQLDescription("The default view for this project.") + @FilterProperty + val defaultView by NodeProperty() + @NodeRelationship(NodePermission.NODE, Direction.INCOMING) @GraphQLDescription("Permissions for this Project.") @FilterProperty diff --git a/core/src/main/kotlin/gropius/model/architecture/Relation.kt b/core/src/main/kotlin/gropius/model/architecture/Relation.kt index 73dd57e4..712e5ec6 100644 --- a/core/src/main/kotlin/gropius/model/architecture/Relation.kt +++ b/core/src/main/kotlin/gropius/model/architecture/Relation.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.architecture.layout.RelationLayout import gropius.model.common.BaseNode import gropius.model.template.BaseTemplate import gropius.model.template.MutableTemplatedNode @@ -32,6 +33,7 @@ class Relation( companion object { const val START_PART = "START_PART" const val END_PART = "END_PART" + const val LAYOUT = "LAYOUT" } @NodeRelationship(BaseTemplate.USED_IN, Direction.INCOMING) @@ -73,4 +75,8 @@ class Relation( @GraphQLDescription("InterfaceDefinition this Relation derives invisible") @FilterProperty val derivesInvisible by NodeSetProperty() + + @NodeRelationship(LAYOUT, Direction.OUTGOING) + @GraphQLIgnore + val layouts by NodeSetProperty() } \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/architecture/RelationPartner.kt b/core/src/main/kotlin/gropius/model/architecture/RelationPartner.kt index f6ff7bb6..e36744c4 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.architecture.layout.RelationPartnerLayout import gropius.model.issue.AggregatedIssue import gropius.model.template.RelationPartnerTemplate import gropius.model.template.TemplatedNode @@ -21,6 +22,7 @@ abstract class RelationPartner(name: String, description: String) : AffectedByIs const val INCOMING_RELATION = "INCOMING_RELATION" const val OUTGOING_RELATION = "OUTGOING_RELATION" const val AGGREGATED_ISSUE = "AGGREGATED_ISSUE" + const val LAYOUT = "LAYOUT" } @NodeRelationship(INCOMING_RELATION, Direction.OUTGOING) @@ -38,6 +40,10 @@ abstract class RelationPartner(name: String, description: String) : AffectedByIs @FilterProperty val aggregatedIssues by NodeSetProperty() + @NodeRelationship(LAYOUT, Direction.OUTGOING) + @GraphQLIgnore + val layouts by NodeSetProperty() + /** * Helper function to get the associated [RelationPartnerTemplate] * diff --git a/core/src/main/kotlin/gropius/model/architecture/layout/Layout.kt b/core/src/main/kotlin/gropius/model/architecture/layout/Layout.kt new file mode 100644 index 00000000..a1838453 --- /dev/null +++ b/core/src/main/kotlin/gropius/model/architecture/layout/Layout.kt @@ -0,0 +1,24 @@ +package gropius.model.architecture.layout + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import gropius.model.architecture.Project +import io.github.graphglue.model.property.LazyLoadingDelegate +import io.github.graphglue.model.property.NodeSetPropertyDelegate + +/** + * Interface for common layout information of [Project]s and [View]s + */ +@GraphQLIgnore +interface Layout { + + /** + * Layouts for relation partners + */ + val relationPartnerLayouts: LazyLoadingDelegate.NodeSetProperty> + + /** + * Layouts for relations + */ + val relationLayouts: LazyLoadingDelegate.NodeSetProperty> + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/architecture/layout/Point.kt b/core/src/main/kotlin/gropius/model/architecture/layout/Point.kt new file mode 100644 index 00000000..a67a528d --- /dev/null +++ b/core/src/main/kotlin/gropius/model/architecture/layout/Point.kt @@ -0,0 +1,11 @@ +package gropius.model.architecture.layout + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription + +@GraphQLDescription("A point in a 2D coordinate system") +class Point( + @property:GraphQLDescription("The x coordinate of the point") + val x: Int, + @property:GraphQLDescription("The y coordinate of the point") + val y: Int +) \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/architecture/layout/RelationLayout.kt b/core/src/main/kotlin/gropius/model/architecture/layout/RelationLayout.kt new file mode 100644 index 00000000..d4e2b5f4 --- /dev/null +++ b/core/src/main/kotlin/gropius/model/architecture/layout/RelationLayout.kt @@ -0,0 +1,29 @@ +package gropius.model.architecture.layout + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import gropius.model.architecture.Relation +import gropius.model.common.BaseNode +import io.github.graphglue.model.Direction +import io.github.graphglue.model.DomainNode +import io.github.graphglue.model.FilterProperty +import io.github.graphglue.model.NodeRelationship + +@DomainNode +@GraphQLDescription("Layout for a Relation") +class RelationLayout( + @property:GraphQLIgnore + var xCoordinates: IntArray, + @property:GraphQLIgnore + var yCoordinates: IntArray +) : BaseNode() { + + @NodeRelationship(Relation.LAYOUT, Direction.INCOMING) + @GraphQLDescription("The Relation this layout is for.") + @FilterProperty + val relation by NodeProperty() + + @GraphQLDescription("The intermediate points of the Relation in the layout.") + val points get() = xCoordinates.zip(yCoordinates).map { Point(it.first, it.second) } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/architecture/layout/RelationPartnerLayout.kt b/core/src/main/kotlin/gropius/model/architecture/layout/RelationPartnerLayout.kt new file mode 100644 index 00000000..27c5c6aa --- /dev/null +++ b/core/src/main/kotlin/gropius/model/architecture/layout/RelationPartnerLayout.kt @@ -0,0 +1,29 @@ +package gropius.model.architecture.layout + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import gropius.model.architecture.RelationPartner +import gropius.model.common.BaseNode +import io.github.graphglue.model.Direction +import io.github.graphglue.model.DomainNode +import io.github.graphglue.model.FilterProperty +import io.github.graphglue.model.NodeRelationship + +@DomainNode +@GraphQLDescription("Layout for a RelationPartner (ComponentVersion or Interface)") +class RelationPartnerLayout( + @property:GraphQLIgnore + var x: Int, + @property:GraphQLIgnore + var y: Int +) : BaseNode() { + + @NodeRelationship(RelationPartner.LAYOUT, Direction.INCOMING) + @GraphQLDescription("The RelationPartner this layout is for.") + @FilterProperty + val relationPartner by NodeProperty() + + @GraphQLDescription("The position of the RelationPartner in the layout.") + val pos get() = Point(x, y) + +} diff --git a/core/src/main/kotlin/gropius/model/architecture/layout/View.kt b/core/src/main/kotlin/gropius/model/architecture/layout/View.kt new file mode 100644 index 00000000..1e3e81f2 --- /dev/null +++ b/core/src/main/kotlin/gropius/model/architecture/layout/View.kt @@ -0,0 +1,44 @@ +package gropius.model.architecture.layout + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import gropius.model.architecture.Project +import gropius.model.common.NamedNode +import gropius.model.template.ComponentTemplate +import io.github.graphglue.model.Direction +import io.github.graphglue.model.DomainNode +import io.github.graphglue.model.FilterProperty +import io.github.graphglue.model.NodeRelationship + +@DomainNode(searchQueryName = "searchViews") +@GraphQLDescription("A view on the architecture graph of a project") +class View( + name: String, description: String +) : NamedNode(name, description), Layout { + + companion object { + const val RELATION_PARTNER = "RELATION_PARTNER" + const val RELATION = "RELATION" + const val FILTER_TEMPLATE = "FILTER_TEMPLATE" + } + + @NodeRelationship(Project.VIEW, Direction.INCOMING) + @GraphQLDescription("The project this view is for") + @FilterProperty + val project by NodeProperty() + + @NodeRelationship(RELATION_PARTNER, Direction.OUTGOING) + @GraphQLDescription("Layouts for relation partners") + @FilterProperty + override val relationPartnerLayouts by NodeSetProperty() + + @NodeRelationship(RELATION, Direction.OUTGOING) + @GraphQLDescription("Layouts for relations") + @FilterProperty + override val relationLayouts by NodeSetProperty() + + @NodeRelationship(FILTER_TEMPLATE, Direction.OUTGOING) + @GraphQLDescription("Filter which ComponentVersions are shown in this view") + @FilterProperty + val filterByTemplate by NodeSetProperty() + +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/user/permission/PermissionConfiguration.kt b/core/src/main/kotlin/gropius/model/user/permission/PermissionConfiguration.kt index dbe141ba..ae1f212d 100644 --- a/core/src/main/kotlin/gropius/model/user/permission/PermissionConfiguration.kt +++ b/core/src/main/kotlin/gropius/model/user/permission/PermissionConfiguration.kt @@ -180,7 +180,12 @@ class PermissionConfiguration { ProjectPermission.MANAGE_COMPONENTS, """ Allows to add / remove ComponentVersions to / from this Project. """.trimIndent() - ) + ), + PermissionEntry( + ProjectPermission.MANAGE_VIEWS, """ + Allows to manage the views of this Project. + """.trimIndent() + ), ) ) 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 cd8dc95f..7cc67285 100644 --- a/core/src/main/kotlin/gropius/model/user/permission/ProjectPermission.kt +++ b/core/src/main/kotlin/gropius/model/user/permission/ProjectPermission.kt @@ -24,6 +24,11 @@ class ProjectPermission( */ const val MANAGE_COMPONENTS = "MANAGE_COMPONENTS" + /** + * Permission to manage the views of a [Project] + */ + const val MANAGE_VIEWS = "MANAGE_VIEWS" + /** * Used to track [RelationPartner]s which are part of the graph of a [Project] */ diff --git a/core/src/main/kotlin/gropius/repository/architecture/RelationLayoutRepository.kt b/core/src/main/kotlin/gropius/repository/architecture/RelationLayoutRepository.kt new file mode 100644 index 00000000..2aed9d63 --- /dev/null +++ b/core/src/main/kotlin/gropius/repository/architecture/RelationLayoutRepository.kt @@ -0,0 +1,11 @@ +package gropius.repository.architecture + +import gropius.model.architecture.layout.RelationLayout +import gropius.repository.GropiusRepository +import org.springframework.stereotype.Repository + +/** + * Repository for [RelationLayout] + */ +@Repository +interface RelationLayoutRepository : GropiusRepository \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/repository/architecture/RelationPartnerLayoutRepository.kt b/core/src/main/kotlin/gropius/repository/architecture/RelationPartnerLayoutRepository.kt new file mode 100644 index 00000000..2b7c26e7 --- /dev/null +++ b/core/src/main/kotlin/gropius/repository/architecture/RelationPartnerLayoutRepository.kt @@ -0,0 +1,11 @@ +package gropius.repository.architecture + +import gropius.model.architecture.layout.RelationPartnerLayout +import gropius.repository.GropiusRepository +import org.springframework.stereotype.Repository + +/** + * Repository for [RelationPartnerLayout] + */ +@Repository +interface RelationPartnerLayoutRepository : GropiusRepository \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/repository/architecture/ViewRepository.kt b/core/src/main/kotlin/gropius/repository/architecture/ViewRepository.kt new file mode 100644 index 00000000..023ce217 --- /dev/null +++ b/core/src/main/kotlin/gropius/repository/architecture/ViewRepository.kt @@ -0,0 +1,11 @@ +package gropius.repository.architecture + +import gropius.model.architecture.layout.View +import gropius.repository.GropiusRepository +import org.springframework.stereotype.Repository + +/** + * Repository for [View] + */ +@Repository +interface ViewRepository : GropiusRepository \ 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 c1458507..fc97fcc4 100644 --- a/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt +++ b/core/src/main/kotlin/gropius/service/architecture/ComponentGraphUpdater.kt @@ -59,6 +59,9 @@ class ComponentGraphUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateCon cache.add(componentVersion) lockIncomingAndOutgoingRelationPartners(componentVersion) deletedNodes += componentVersion + componentVersion.intraComponentDependencySpecifications(cache).forEach { deleteIntraComponentDependencySpecification(it) } + componentVersion.interfaceDefinitions(cache).toSet().forEach { deleteInterfaceDefinition(it) } + deletedNodes += componentVersion.layouts(cache) val relations = componentVersion.outgoingRelations(cache) + componentVersion.incomingRelations(cache) relations.forEach { deleteRelation(it) @@ -614,6 +617,7 @@ class ComponentGraphUpdater(updateContext: NodeBatchUpdater = NodeBatchUpdateCon deletedNodes += node deletedNodes += node.incomingRelations(cache) deletedNodes += node.outgoingRelations(cache) + deletedNodes += node.layouts(cache) val intraComponentDependencyParticipants = node.intraComponentDependencyParticipants(cache) deletedNodes += intraComponentDependencyParticipants for (participant in intraComponentDependencyParticipants) { diff --git a/core/src/main/kotlin/gropius/service/architecture/LayoutService.kt b/core/src/main/kotlin/gropius/service/architecture/LayoutService.kt new file mode 100644 index 00000000..4091b25c --- /dev/null +++ b/core/src/main/kotlin/gropius/service/architecture/LayoutService.kt @@ -0,0 +1,106 @@ +package gropius.service.architecture + +import gropius.dto.input.architecture.layout.UpdateLayoutInput +import gropius.dto.input.orElse +import gropius.model.architecture.layout.Layout +import gropius.model.architecture.layout.RelationLayout +import gropius.model.architecture.layout.RelationPartnerLayout +import gropius.repository.architecture.RelationPartnerRepository +import gropius.repository.architecture.RelationRepository +import gropius.repository.findById +import gropius.service.NodeBatchUpdater +import org.springframework.stereotype.Service + +/** + * Service for updating the layout of a [Layout] + * + * @param relationPartnerRepository The [RelationPartnerRepository] to use for updating the layout + * @param relationRepository The [RelationRepository] to use for updating the layout + */ +@Service +class LayoutService( + private val relationPartnerRepository: RelationPartnerRepository, + private val relationRepository: RelationRepository, +) { + + /** + * Updates the layout of a [Layout] + * + * @param layout The layout to update + * @param input Defines new layout values + * @param batchUpdater The [NodeBatchUpdater] to use for updating the layout + */ + suspend fun updateLayout(layout: Layout, input: UpdateLayoutInput, batchUpdater: NodeBatchUpdater) { + updateRelationPartnerLayouts(layout, input, batchUpdater) + updateRelationLayouts(layout, input, batchUpdater) + } + + /** + * Updates the layout of the [RelationPartnerLayout]s of a [Layout] + * + * @param layout The layout to update + * @param input Defines new layout values + * @param batchUpdater The [NodeBatchUpdater] to use for updating the layout + */ + private suspend fun updateRelationPartnerLayouts( + layout: Layout, input: UpdateLayoutInput, batchUpdater: NodeBatchUpdater, + ) { + val cache = batchUpdater.cache + val relationPartnerLayouts = + layout.relationPartnerLayouts(cache).associateBy { it.relationPartner(cache).value.rawId!! } + input.relationPartnerLayouts.orElse(emptyList()).forEach { + if (it.layout != null) { + val existingLayout = relationPartnerLayouts[it.relationPartner.value] + if (existingLayout == null) { + val newLayout = RelationPartnerLayout(it.layout.pos.x, it.layout.pos.y) + newLayout.relationPartner(cache).value = relationPartnerRepository.findById(it.relationPartner) + batchUpdater.internalUpdatedNodes += newLayout + layout.relationPartnerLayouts() += newLayout + } else { + existingLayout.x = it.layout.pos.x + existingLayout.y = it.layout.pos.y + batchUpdater.internalUpdatedNodes += existingLayout + } + } else { + relationPartnerLayouts[it.relationPartner.value]?.let { layout -> + batchUpdater.deletedNodes += layout + } + } + } + } + + /** + * Updates the layout of the [RelationLayout]s of a [Layout] + * + * @param layout The layout to update + * @param input Defines new layout values + * @param batchUpdater The [NodeBatchUpdater] to use for updating the layout + */ + private suspend fun updateRelationLayouts( + layout: Layout, input: UpdateLayoutInput, batchUpdater: NodeBatchUpdater, + ) { + val cache = batchUpdater.cache + val relationLayouts = layout.relationLayouts(cache).associateBy { it.relation(cache).value.rawId!! } + input.relationLayouts.orElse(emptyList()).forEach { + if (it.layout != null) { + val existingLayout = relationLayouts[it.relation.value] + val xCoordinates = it.layout.points.map { point -> point.x }.toIntArray() + val yCoordinates = it.layout.points.map { point -> point.y }.toIntArray() + if (existingLayout == null) { + val newLayout = RelationLayout(xCoordinates, yCoordinates) + newLayout.relation(cache).value = relationRepository.findById(it.relation) + batchUpdater.internalUpdatedNodes += newLayout + layout.relationLayouts() += newLayout + } else { + existingLayout.xCoordinates = xCoordinates + existingLayout.yCoordinates = yCoordinates + batchUpdater.internalUpdatedNodes += existingLayout + } + } else { + relationLayouts[it.relation.value]?.let { layout -> + batchUpdater.deletedNodes += layout + } + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/service/architecture/ProjectService.kt b/core/src/main/kotlin/gropius/service/architecture/ProjectService.kt index f6b6b187..b60aebdf 100644 --- a/core/src/main/kotlin/gropius/service/architecture/ProjectService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/ProjectService.kt @@ -6,6 +6,7 @@ import gropius.dto.input.architecture.CreateProjectInput import gropius.dto.input.architecture.RemoveComponentVersionFromProjectInput import gropius.dto.input.architecture.UpdateProjectInput import gropius.dto.input.common.DeleteNodeInput +import gropius.dto.input.ifPresent import gropius.model.architecture.ComponentVersion import gropius.model.architecture.Project import gropius.model.user.permission.ComponentPermission @@ -14,7 +15,9 @@ 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.architecture.ViewRepository import gropius.repository.findById +import gropius.service.NodeBatchUpdateContext import gropius.service.user.permission.ProjectPermissionService import io.github.graphglue.authorization.Permission import kotlinx.coroutines.reactor.awaitSingle @@ -27,12 +30,16 @@ import org.springframework.stereotype.Service * @param repository the associated repository used for CRUD functionality * @param projectPermissionService used to create the initial permission for a created [Project] * @param componentVersionRepository used to get [ComponentVersion]s by id + * @param layoutService used to update the layout of a [Project] + * @param viewRepository used to save the [Project] */ @Service class ProjectService( repository: ProjectRepository, private val projectPermissionService: ProjectPermissionService, - private val componentVersionRepository: ComponentVersionRepository + private val componentVersionRepository: ComponentVersionRepository, + private val layoutService: LayoutService, + private val viewRepository: ViewRepository ) : TrackableService(repository) { /** @@ -72,11 +79,16 @@ class ProjectService( checkPermission( project, Permission(NodePermission.ADMIN, authorizationContext), "update the Project" ) + input.defaultView.ifPresent { + project.defaultView().value = if (it == null) null else viewRepository.findById(it) + } projectPermissionService.updatePermissionsOfNode( project, input.addedPermissions, input.removedPermissions, authorizationContext ) updateTrackable(project, input) - return repository.save(project).awaitSingle() + val batchUpdater = NodeBatchUpdateContext() + layoutService.updateLayout(project, input, batchUpdater) + return batchUpdater.save(project, nodeRepository) } /** @@ -95,6 +107,7 @@ class ProjectService( project, Permission(NodePermission.ADMIN, authorizationContext), "delete the Project" ) beforeDeleteTrackable(project) + nodeRepository.deleteAll(project.relationLayouts() + project.relationLayouts()).awaitSingle() repository.delete(project).awaitSingleOrNull() } diff --git a/core/src/main/kotlin/gropius/service/architecture/ViewService.kt b/core/src/main/kotlin/gropius/service/architecture/ViewService.kt new file mode 100644 index 00000000..4d84e80c --- /dev/null +++ b/core/src/main/kotlin/gropius/service/architecture/ViewService.kt @@ -0,0 +1,120 @@ +package gropius.service.architecture + +import gropius.authorization.GropiusAuthorizationContext +import gropius.dto.input.architecture.layout.CreateViewInput +import gropius.dto.input.architecture.layout.UpdateViewInput +import gropius.dto.input.common.DeleteNodeInput +import gropius.dto.input.ifPresent +import gropius.model.architecture.Project +import gropius.model.architecture.layout.View +import gropius.model.template.ComponentTemplate +import gropius.model.user.permission.ProjectPermission +import gropius.repository.architecture.ProjectRepository +import gropius.repository.architecture.ViewRepository +import gropius.repository.common.NodeRepository +import gropius.repository.findAllById +import gropius.repository.findById +import gropius.repository.template.ComponentTemplateRepository +import gropius.service.NodeBatchUpdateContext +import gropius.service.common.NamedNodeService +import io.github.graphglue.authorization.Permission +import kotlinx.coroutines.reactor.awaitSingle +import org.springframework.stereotype.Service + +/** + * Service for [View]s. Provides functions to create, update and delete + * + * @param repository the associated repository used for CRUD functionality + * @param projectRepository used to get [Project]s by id + * @param layoutService used to update the layout of a [View] + * @param nodeRepository used to save the [View] + * @param componentTemplateRepository used to get [ComponentTemplate]s by id + */ +@Service +class ViewService( + repository: ViewRepository, + private val projectRepository: ProjectRepository, + private val layoutService: LayoutService, + private val nodeRepository: NodeRepository, + private val componentTemplateRepository: ComponentTemplateRepository +) : NamedNodeService(repository) { + + /** + * Creates a new [View] based on the provided [input] + * Checks the authorization status + * + * @param authorizationContext used to check for the required permission + * @param input defines the [View] + * @return the saved created [View] + */ + suspend fun createView( + authorizationContext: GropiusAuthorizationContext, input: CreateViewInput + ): View { + input.validate() + val project = projectRepository.findById(input.project) + checkManageViewsPermission(project, authorizationContext) + val view = View(input.name, input.description) + view.project().value = project + view.filterByTemplate() += componentTemplateRepository.findAllById(input.filterByTemplate) + val batchUpdater = NodeBatchUpdateContext() + layoutService.updateLayout(view, input, batchUpdater) + return batchUpdater.save(view, nodeRepository) + } + + /** + * Updates a [View] based on the provided [input] + * Checks the authorization status + * + * @param authorizationContext used to check for the required permission + * @param input defines which [View] to update and how + * @return the updated [View] + */ + suspend fun updateView( + authorizationContext: GropiusAuthorizationContext, input: UpdateViewInput + ): View { + input.validate() + val view = repository.findById(input.id) + checkManageViewsPermission(view.project().value, authorizationContext) + updateNamedNode(view, input) + input.filterByTemplate.ifPresent { + view.filterByTemplate().clear() + view.filterByTemplate() += componentTemplateRepository.findAllById(it) + } + val batchUpdater = NodeBatchUpdateContext() + layoutService.updateLayout(view, input, batchUpdater) + return batchUpdater.save(view, nodeRepository) + } + + /** + * Deletes a [View] by id + * Checks the authorization status + * + * @param authorizationContext used to check for the required permission + * @param input defines which [View] to delete + */ + suspend fun deleteView( + authorizationContext: GropiusAuthorizationContext, input: DeleteNodeInput + ) { + input.validate() + val view = repository.findById(input.id) + checkManageViewsPermission(view.project().value, authorizationContext) + nodeRepository.deleteAll(view.relationLayouts() + view.relationLayouts()).awaitSingle() + repository.delete(view).awaitSingle() + } + + /** + * Checks that the user has [ProjectPermission.MANAGE_VIEWS] on the provided [project] + * + * @param project the [Project] where the permission must be granted + * @param authorizationContext necessary for checking for the permission + * @throws IllegalArgumentException if the permission is not granted + */ + private suspend fun checkManageViewsPermission( + project: Project, authorizationContext: GropiusAuthorizationContext + ) { + checkPermission( + project, Permission(ProjectPermission.MANAGE_VIEWS, authorizationContext), "manage views" + ) + } + +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 235a2196..bc288950 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.jvmargs=-Xmx2g # dependencies springBootVersion=3.3.0 -graphglueVersion=7.0.3 +graphglueVersion=7.0.4 graphqlJavaVersion=21.0 apolloVersion=3.8.4 kosonVersion=1.2.8