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

io.github.graphglue.model.property.NodePropertyDelegate.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.model.property

import graphql.execution.DataFetcherResult
import io.github.graphglue.data.execution.DEFAULT_PART_ID
import io.github.graphglue.data.execution.NodeQuery
import io.github.graphglue.data.execution.NodeQueryParser
import io.github.graphglue.data.execution.NodeQueryResult
import io.github.graphglue.data.repositories.RelationshipDiff
import io.github.graphglue.definition.NodeDefinition
import io.github.graphglue.definition.NodeDefinitionCollection
import io.github.graphglue.definition.RelationshipDefinition
import io.github.graphglue.definition.extensions.firstTypeArgument
import io.github.graphglue.model.Node
import io.github.graphglue.model.property.NodeSetPropertyDelegate.NodeSetProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.KTypeParameter
import kotlin.reflect.KTypeProjection
import kotlin.reflect.full.createType

/**
 * Property for the one side of a relation
 * Depending on the type of `property` may be an optional property
 *
 * @param parent see [BasePropertyDelegate.parent]
 * @param property see [BasePropertyDelegate.property]
 */
class NodePropertyDelegate(
    parent: Node, property: KProperty1<*, *>
) : BasePropertyDelegate.NodeProperty>(parent, property) {

    /**
     * The [NodeSetProperty] returned to the user
     */
    private val nodeProperty = NodeProperty()

    /**
     * If `true`, the value of this property is already loaded (either from the database or from another value)
     */
    private var isLoaded = false

    /**
     * The current value of this property, may or may not be persisted to the database
     */
    private var currentNode: T? = null

    /**
     * The related node as in the database
     * `null` if not loaded or no relation present in the database
     */
    private var persistedNode: T? = null

    /**
     * True if the [T] is marked nullable
     */
    private val supportsNull: Boolean get() {
        val type = property.returnType.firstTypeArgument
        return type.isMarkedNullable || type.classifier is KTypeParameter
    }

    override fun registerQueryResult(nodeQueryResult: NodeQueryResult) {
        super.registerQueryResult(nodeQueryResult)
        if (!isLoaded && nodeQueryResult.options.isAllQuery) {
            if (nodeQueryResult.nodes.size > 1) {
                throw IllegalArgumentException("Too many nodes for one side of relation $propertyName")
            }
            currentNode = nodeQueryResult.nodes.firstOrNull()
            persistedNode = currentNode
            isLoaded = true
        }
    }

    override fun getRelationshipDiff(
        nodeDefinition: NodeDefinition
    ): RelationshipDiff {
        val current = currentNode
        val nodesToRemove = if (current != persistedNode && persistedNode != null) {
            listOf(persistedNode!!)
        } else {
            emptyList()
        }
        val nodesToAdd = if (current != persistedNode && current != null) {
            listOf(current)
        } else {
            emptyList()
        }
        return RelationshipDiff(nodesToAdd, nodesToRemove)
    }

    override fun getRelatedNodesToSave(): Collection {
        val current = currentNode
        return if (current != persistedNode && current != null) {
            listOf(current)
        } else {
            emptyList()
        }
    }

    override fun getLoadedRelatedNodes(): Collection {
        return listOfNotNull(currentNode)
    }

    override fun constructGraphQLResult(
        result: NodeQueryResult, localContext: NodeQuery?, nodeQueryParser: NodeQueryParser
    ): DataFetcherResult<*> {
        return DataFetcherResult.newResult().data(result.nodes.firstOrNull())
            .localContext(localContext?.parts?.get(DEFAULT_PART_ID)).build()
    }

    /**
     * Ensures that this property is loaded
     *
     * @param cache used to load nodes from, if provided, not loading deleted nodes
     */
    private suspend fun ensureLoaded(cache: NodeCache?) {
        if (!isLoaded) {
            val (result, _) = parent.loadNodesOfRelationship(property)
            if (result.nodes.size > 1) {
                throw IllegalArgumentException("Too many nodes for one side of relation $propertyName")
            }
            currentNode = result.nodes.firstOrNull()
            persistedNode = currentNode
            isLoaded = true
        }
        if (cache != null && nodeCache == null) {
            nodeCache = cache
            val newNode = cache.getOrAdd(currentNode)
            if (currentNode == persistedNode) {
                persistedNode = newNode
            }
            currentNode = newNode
        }
    }

    /**
     * Sets the node from the remote side
     * Used to prevent unnecessary lazy loaded queries
     *
     * @param value the value loaded from the database
     */
    internal fun setFromRemote(value: T) {
        if (!isLoaded) {
            currentNode = value
            persistedNode = value
            isLoaded = true
        }
    }

    override suspend fun getLoadedProperty(cache: NodeCache?): NodeProperty {
        ensureLoaded(cache)
        return nodeProperty
    }

    override fun validate(
        savingNodes: Set,
        relationshipDefinition: RelationshipDefinition,
        nodeDefinitionCollection: NodeDefinitionCollection
    ) {
        if (!supportsNull) {
            val neverSetInitialRelationship = currentNode == null && parent.rawId == null
            val removedRequiredRelationship = currentNode == null && persistedNode != null
            if (neverSetInitialRelationship || removedRequiredRelationship) {
                val setByOtherSide = savingNodes.filter { relationshipDefinition.nodeKClass.isInstance(it) }
                    .any {
                        val relatedNodeDefinition = nodeDefinitionCollection.getNodeDefinition(it::class)
                        val inverseRelationshipDefinition = relatedNodeDefinition.getRelationshipDefinitionByInverse(relationshipDefinition)
                        inverseRelationshipDefinition?.getLoadedRelatedNodes(it)?.contains(parent) ?: false
                    }
                if (!setByOtherSide) {
                    throw IllegalStateException(
                        "Non-nullable property $propertyName cannot be saved, as it has value null and is not set by other side."
                    )
                }
            }
        }
    }

    /**
     * Node property representing the one side of a [Node] relation
     * [value] can be used to get/set the node
     */
    inner class NodeProperty {

        /**
         * The current value of the property
         */
        @Suppress("UNCHECKED_CAST")
        var value: T
            get() {
                assert(isLoaded)
                if (!supportsNull && currentNode == null) {
                    throw IllegalStateException("The non-nullable property $propertyName has a null value")
                }
                return currentNode as T
            }
            set(value) {
                assert(isLoaded)
                if (value != currentNode) {
                    currentNode = value
                }
            }
    }
}

/**
 * Type which can be used to check the return type of node properties
 */
val NODE_PROPERTY_TYPE = LazyLoadingDelegate::class.createType(
    listOf(
        KTypeProjection.covariant(Node::class.createType(nullable = true)), KTypeProjection.covariant(
            NodePropertyDelegate.NodeProperty::class.createType(
                listOf(
                    KTypeProjection.covariant(
                        Node::class.createType(nullable = true)
                    )
                )
            )
        )
    )
)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy