Skip to content

Commit

Permalink
Merge pull request #65 from ccims/feature/project_graph
Browse files Browse the repository at this point in the history
Feature/project graph
  • Loading branch information
nk-coding authored Nov 13, 2023
2 parents d9d1dec + 9c3864a commit 2c88e27
Show file tree
Hide file tree
Showing 64 changed files with 2,139 additions and 248 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/validate-generated-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -50,10 +50,12 @@ jobs:
echo "Failed to generate model"
exit 1
fi
git status
git diff
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
kill $gradlew_pid
33 changes: 24 additions & 9 deletions api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@ 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.*
import gropius.model.common.PERMISSION_FIELD_BEAN
import gropius.model.template.TEMPLATED_FIELDS_FILTER_BEAN
import gropius.model.template.TemplatedNode
Expand Down Expand Up @@ -158,7 +156,25 @@ 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]
*
* @param nodeDefinitionCollection used to get the node definition
* @return the generated filter definition
*/
@Bean(RELATED_TO_FILTER_BEAN)
fun relatedToFilter(nodeDefinitionCollection: NodeDefinitionCollection) =
AffectedByIssueRelatedToFilterEntryDefinition(nodeDefinitionCollection)

/**
* Filter for [RelationPartner]s which are part of a specific [Project]'s graph.
*/
@Bean(PART_OF_PROJECT_FILTER)
fun partOfProjectFilter(nodeDefinitionCollection: NodeDefinitionCollection) =
PartOfProjectFilterEntryDefinition(nodeDefinitionCollection)

/**
* Provides the permission field for all nodes
Expand All @@ -175,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)
Expand All @@ -190,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
Expand Down Expand Up @@ -252,4 +267,4 @@ class GraphQLConfiguration {
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gropius.graphql.filter

import gropius.model.user.permission.TrackablePermission
import io.github.graphglue.authorization.Permission
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 trackableId: 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.noCondition()
}
val idCondition = relatedNode.property("id").isEqualTo(Cypher.anonParameter(trackableId))
return Cypher.match(relationship).where(idCondition.and(authCondition)).asCondition()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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

/**
* 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") {

/**
* 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<Trackable>(),
permission
)
}

override fun parseEntry(value: Any?, permission: Permission?): FilterEntry {
return AffectedByIssueRelatedToFilterEntry(
value!! as String,
this,
permission
)
}

override fun toGraphQLType(inputTypeCache: CacheMap<String, GraphQLInputType>): GraphQLInputType = Scalars.GraphQLID
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gropius.graphql.filter

import gropius.model.user.permission.ProjectPermission
import io.github.graphglue.authorization.Permission
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 projectId: 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(projectId))
return Cypher.match(relationship).where(idCondition.and(authCondition)).asCondition()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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

/**
* 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") {

/**
* 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<Project>(),
permission
)
}

override fun parseEntry(value: Any?, permission: Permission?): FilterEntry {
return PartOfProjectFilterEntry(
value!! as String,
this,
permission
)
}

override fun toGraphQLType(inputTypeCache: CacheMap<String, GraphQLInputType>): GraphQLInputType = Scalars.GraphQLID
}
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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)
@GraphQLDescription("Payload type for delete node mutations")
class DeleteNodePayload(
@GraphQLDescription("The id of the deleted Node")
val id: ID
)
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ open class InterfaceSpecificationInput : CreateNamedNodeInput(), CreateTemplated
@GraphQLDescription("Initial versions of the InterfaceSpecification")
var versions: OptionalInput<List<InterfaceSpecificationVersionInput>> by Delegates.notNull()

@GraphQLDescription("Initial defined InterfaceParts")
var definedParts: OptionalInput<List<InterfacePartInput>> by Delegates.notNull()

@GraphQLDescription("Initial values for all templatedFields")
override var templatedFields: List<JSONFieldInput> by Delegates.notNull()

Expand All @@ -31,9 +28,6 @@ open class InterfaceSpecificationInput : CreateNamedNodeInput(), CreateTemplated
versions.ifPresent {
it.forEach(Input::validate)
}
definedParts.ifPresent {
it.forEach(Input::validate)
}
templatedFields.validateAndEnsureNoDuplicates()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -15,17 +16,18 @@ open class InterfaceSpecificationVersionInput : CreateNamedNodeInput(), CreateTe
@GraphQLDescription("Initial values for all templatedFields")
override var templatedFields: List<JSONFieldInput> by Delegates.notNull()

@GraphQLDescription(
"""Ids of InterfaceParts of the associated InterfaceSpecification which should be the initial `activeParts`"""
)
var activeParts: OptionalInput<List<ID>> by Delegates.notNull()
@GraphQLDescription("Initial InterfaceParts")
var parts: OptionalInput<List<InterfacePartInput>> by Delegates.notNull()

@GraphQLDescription("The version of the created InterfaceSpecificationVersion")
var version: String by Delegates.notNull()

override fun validate() {
super.validate()
templatedFields.validateAndEnsureNoDuplicates()
parts.ifPresent {
it.forEach(Input::validate)
}
}

}
Loading

0 comments on commit 2c88e27

Please sign in to comment.