All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.graphglue.definition.NodeDefinition.kt Maven / Gradle / Ivy

Go to download

A library to develop annotation-based code-first GraphQL servers using GraphQL Kotlin, Spring Boot and Neo4j - excluding Spring GraphQL server dependencies

The newest version!
package io.github.graphglue.definition

import io.github.graphglue.authorization.MergedAuthorization
import io.github.graphglue.definition.extensions.firstTypeArgument
import io.github.graphglue.graphql.extensions.getSimpleName
import io.github.graphglue.graphql.extensions.springFindAnnotation
import io.github.graphglue.graphql.extensions.springFindRepeatableAnnotations
import io.github.graphglue.graphql.extensions.springGetRepeatableAnnotations
import io.github.graphglue.model.*
import io.github.graphglue.model.property.NODE_PROPERTY_TYPE
import io.github.graphglue.model.property.NODE_SET_PROPERTY_TYPE
import io.github.graphglue.model.property.NodePropertyDelegate
import org.neo4j.cypherdsl.core.Cypher
import org.neo4j.cypherdsl.core.Expression
import org.neo4j.cypherdsl.core.SymbolicName
import org.springframework.data.neo4j.core.mapping.Constants
import org.springframework.data.neo4j.core.mapping.CypherGenerator
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.KTypeParameter
import kotlin.reflect.full.*
import kotlin.reflect.jvm.javaField

/**
 * Definition of a [Node]
 * Used to find relationships, get the priamry label, create a Cypher-DSL return expressions
 *
 * @param nodeType the class associated with the definition
 * @param persistentEntity defines Neo4j's view on the node
 * @param extensionFieldDefinitions all known [ExtensionFieldDefinition] beans
 */
class NodeDefinition(
    val nodeType: KClass,
    val persistentEntity: Neo4jPersistentEntity<*>,
    extensionFieldDefinitions: Map
) {
    /**
     * map of all found authorizations defined on this type from authorization name to [Authorization]
     */
    val authorizations: Map = generateAuthorizations()

    /**
     * map of all found authorizations defined on this type and all supertypes from authorization name to [Authorization]
     */
    val mergedAuthorizations: Map = generateMergedAuthorizations()

    /**
     * All [ExtensionFieldDefinition]s
     */
    val extensionFieldDefinitions =
        generateExtensionFieldDefinitions(extensionFieldDefinitions).associateBy { it.graphQLName }

    /**
     * All one [RelationshipDefinition]s
     */
    private val oneRelationshipDefinitions: List = generateOneRelationshipDefinitions()

    /**
     * All many [RelationshipDefinition]s
     */
    private val manyRelationshipDefinitions: List = generateManyRelationshipDefinitions()

    /**
     * Map of all relationship definitions by GraphQL name
     */
    val relationshipDefinitions =
        (oneRelationshipDefinitions + manyRelationshipDefinitions).associateBy { it.graphQLName }

    /**
     * map of all relationship definitions by defining property
     * Name of property as key
     */
    internal val relationshipDefinitionsByProperty =
        (oneRelationshipDefinitions + manyRelationshipDefinitions).associateBy { it.property.name }

    /**
     * Lookup for [RelationshipDefinition]s by its inverse
     */
    private val relationshipDefinitionByInverse: MutableMap =
        mutableMapOf()

    /**
     * Set of all extension fields GraphQL names
     * Can be used to check if a field is an extension field
     */
    val extensionFieldGraphQLNames =
        this.extensionFieldDefinitions.values.map(ExtensionFieldDefinition::graphQLName).toSet()

    /**
     * Set of all relationship GraphQL names
     * Can be used to check if a field is a relationship
     */
    val relationshipGraphQLNames = relationshipDefinitions.values.map(RelationshipDefinition::graphQLName).toSet()

    /**
     * Expression which can be used when creating a query using Cypher-DSL
     * Fetches all necessary data to map the result to a [Node]
     */
    val returnExpression: Expression

    /**
     * Name of the return value in the [returnExpression]
     */
    val returnNodeName: SymbolicName

    /**
     * The primary label of the [Node]
     */
    val primaryLabel: String get() = persistentEntity.primaryLabel

    /**
     * GraphQL type name
     */
    val name get() = nodeType.getSimpleName()

    /**
     * The name of the search index, if existing
     */
    val searchIndexName: String?

    init {
        val expressions = CypherGenerator.INSTANCE.createReturnStatementForMatch(persistentEntity)
        if (expressions.size != 1) {
            throw IllegalStateException("Cannot get return expression for $nodeType, probably due to cyclic references: $expressions")
        }
        returnExpression = expressions.first()
        returnNodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(persistentEntity)
        searchIndexName = if (nodeType.springFindAnnotation()?.searchQueryName?.isNotBlank() == true) {
            "${name}SearchIndex"
        } else {
            null
        }
    }

    /**
     * Creates all authorizations by search for [Authorization] annotations on [nodeType] and all super types
     *
     * @return the map of found merged authorizations by authorization name
     */
    private fun generateMergedAuthorizations(): Map {
        val allAuthorizations = nodeType.springFindRepeatableAnnotations()
        return mergeAuthorizations(allAuthorizations)
    }

    /**
     * Creates all authorizations by search for [Authorization] annotations on [nodeType]
     *
     * @return the map of found merged authorizations by authorization name
     */
    private fun generateAuthorizations(): Map {
        val allAuthorizations = nodeType.springGetRepeatableAnnotations()
        return mergeAuthorizations(allAuthorizations)
    }

    /**
     * Merges a list of [Authorization]s
     *
     * @return the merged [Authorization]
     */
    private fun mergeAuthorizations(authorizations: Collection) =
        authorizations.groupBy { it.name }
            .mapValues { (name, authorizations) ->
                val authorization = MergedAuthorization(
                    name,
                    authorizations.flatMap { it.allow.toList() }.toSet(),
                    authorizations.flatMap { it.allowFromRelated.toList() }.toSet(),
                    authorizations.flatMap { it.disallow.toList() }.toSet(),
                    authorizations.any { it.allowAll }
                )
                authorization
            }


    /**
     * Finds the [ExtensionFieldDefinition]s for this [NodeDefinition] from the provided list of all definitions
     *
     * @param extensionFieldGenerators all known [ExtensionFieldDefinition] beans
     * @return the list of found [ExtensionFieldDefinition]s
     */
    private fun generateExtensionFieldDefinitions(extensionFieldGenerators: Map): List {
        val allExtensionFields = nodeType.springFindRepeatableAnnotations()
        return allExtensionFields.mapNotNull {
            extensionFieldGenerators[it.beanName]
        }
    }

    /**
     * Generates the [OneRelationshipDefinition]s for this [NodeDefinition]
     *
     * @return the list of generated relationship definitions
     */
    private fun generateOneRelationshipDefinitions(): List {
        val properties = nodeType.memberProperties.filter { it.returnType.isSubtypeOf(NODE_PROPERTY_TYPE) }
            .filter { it.returnType.firstTypeArgument.classifier !is KTypeParameter }.filter { !it.isAbstract }

        return properties.map {
            val field = it.javaField
            if (field == null || !field.type.kotlin.isSubclassOf(NodePropertyDelegate::class)) {
                throw NodeSchemaException("Property of type Node is not backed by a NodeProperty: $it")
            }
            val annotation = it.findAnnotation()
                ?: throw NodeSchemaException("Property of type Node is not annotated with NodeRelationship: $it")
            OneRelationshipDefinition(
                it,
                annotation.type,
                annotation.direction,
                nodeType,
                generateRelationshipAuthorizationNames(it)
            )
        }
    }

    /**
     * Generates the [ManyRelationshipDefinition]s for this [NodeDefinition]
     *
     * @return the list of generated relationship definitions
     */
    private fun generateManyRelationshipDefinitions(): List {
        val properties = nodeType.memberProperties.filter { it.returnType.isSubtypeOf(NODE_SET_PROPERTY_TYPE) }.filter {
            it.returnType.firstTypeArgument.classifier !is KTypeParameter
        }.filter { !it.isAbstract }
        return properties.map {
            val annotation = it.findAnnotation()
                ?: throw NodeSchemaException("Property of type Node is not annotated with NodeRelationship: $it")
            ManyRelationshipDefinition(
                it,
                annotation.type,
                annotation.direction,
                nodeType,
                generateRelationshipAuthorizationNames(it)
            )
        }
    }

    /**
     * Gets the set of authorization names which allow via a Relation defined by a property
     *
     * @param property the property defining the relation
     * @return a set with all authorization names allowing via the relation
     */
    private fun generateRelationshipAuthorizationNames(
        property: KProperty1<*, *>
    ): Set {
        return mergedAuthorizations.filterValues {
            it.allowFromRelated.contains(property.name)
        }.keys
    }

    /**
     * Gets the Neo4j name of a property
     *
     * @param property the property to get the name of
     * @return the name used by Neo4j
     */
    fun getNeo4jNameOfProperty(property: KProperty1<*, *>): String {
        return persistentEntity.getGraphProperty(property.name).orElseThrow().propertyName
    }

    /**
     * Gets the [RelationshipDefinition] by property
     *
     * @param property the property to get the relation of
     * @return the found [RelationshipDefinition]
     * @throws Exception if property is not used as relation
     */
    fun getRelationshipDefinitionOfProperty(property: KProperty1<*, *>): RelationshipDefinition {
        return relationshipDefinitionsByProperty[property.name]!!
    }

    /**
     * Gets the [RelationshipDefinition] by property
     *
     * @param property the property to get the relation of
     * @return the found [RelationshipDefinition] or null if none was found
     */
    fun getRelationshipDefinitionOfPropertyOrNull(property: KProperty1<*, *>): RelationshipDefinition? {
        return relationshipDefinitionsByProperty[property.name]
    }

    /**
     * Generates a new CypherDSL node with the necessary labels
     */
    fun node(): org.neo4j.cypherdsl.core.Node {
        return Cypher.node(persistentEntity.primaryLabel, persistentEntity.additionalLabels)
    }

    /**
     * Gets a [RelationshipDefinition] by the [inverse]
     */
    fun getRelationshipDefinitionByInverse(inverse: RelationshipDefinition): RelationshipDefinition? {
        return relationshipDefinitionByInverse.computeIfAbsent(inverse) {
            relationshipDefinitions.values.firstOrNull {
                it.type == inverse.type && it.direction != inverse.direction && it.nodeKClass.isSuperclassOf(inverse.parentKClass)
            }
        }
    }

    override fun toString() = nodeType.getSimpleName()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy