From e801a8ef423060e74f851ff81fc1c89b12c90cb7 Mon Sep 17 00:00:00 2001
From: Andreas Berger <andreas@berger-ecommerce.com>
Date: Mon, 3 Aug 2020 15:48:39 +0200
Subject: [PATCH] Add support for custom scalars (resolves #9)

---
 pom.xml                                       |   6 +
 readme.adoc                                   |   3 +-
 .../kotlin/org/neo4j/graphql/NoOpCoercing.kt  |  11 +
 .../kotlin/org/neo4j/graphql/SchemaBuilder.kt |  21 +-
 .../kotlin/org/neo4j/graphql/CypherTests.kt   |   5 +-
 .../translator-tests-custom-scalars.adoc      | 202 ++++++++++++++++++
 6 files changed, 244 insertions(+), 4 deletions(-)
 create mode 100644 src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt
 create mode 100644 src/test/resources/translator-tests-custom-scalars.adoc

diff --git a/pom.xml b/pom.xml
index 1b87620c..8ebe98f5 100755
--- a/pom.xml
+++ b/pom.xml
@@ -118,6 +118,12 @@
             <artifactId>neo4j-opencypher-dsl</artifactId>
             <version>1.1.0</version>
         </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.2.3</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/readme.adoc b/readme.adoc
index b27f750b..d07c5860 100644
--- a/readme.adoc
+++ b/readme.adoc
@@ -83,6 +83,7 @@ You find more usage examples in the:
 * link:src/test/resources/translator-tests1.adoc[Translator 1 TCK]
 * link:src/test/resources/translator-tests2.adoc[Translator 2 TCK]
 * link:src/test/resources/translator-tests3.adoc[Translator 3 TCK]
+* link:src/test/resources/translator-tests-custom-scalars.adoc[Translator custom scalars TCK]
 * link:src/test/resources/optimized-query-for-filter.adoc[Alternative Filter TCK]
 
 == Demo
@@ -246,6 +247,7 @@ This example doesn't handle introspection queries, but the one in the test direc
 * date(time)
 * interfaces
 * complex filter parameters, with optional query optimization strategy
+* scalars
 
 === Next
 
@@ -253,7 +255,6 @@ This example doesn't handle introspection queries, but the one in the test direc
 * sorting (nested)
 * input types
 * unions
-* scalars
 * spatial
 
 == Documentation
diff --git a/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt b/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt
new file mode 100644
index 00000000..742ce17f
--- /dev/null
+++ b/src/main/kotlin/org/neo4j/graphql/NoOpCoercing.kt
@@ -0,0 +1,11 @@
+package org.neo4j.graphql
+
+import graphql.schema.Coercing
+
+object NoOpCoercing : Coercing<Any, Any> {
+    override fun parseLiteral(input: Any?) = input
+
+    override fun serialize(dataFetcherResult: Any?) = dataFetcherResult
+
+    override fun parseValue(input: Any?) = input
+}
diff --git a/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt b/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
index 5db55533..73d6d54f 100644
--- a/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
+++ b/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
@@ -4,6 +4,7 @@ import graphql.Scalars
 import graphql.language.*
 import graphql.schema.*
 import graphql.schema.idl.RuntimeWiring
+import graphql.schema.idl.ScalarInfo.STANDARD_SCALAR_DEFINITIONS
 import graphql.schema.idl.SchemaGenerator
 import graphql.schema.idl.SchemaParser
 import graphql.schema.idl.TypeDefinitionRegistry
@@ -40,7 +41,23 @@ object SchemaBuilder {
             enhancedRegistry.add(ObjectTypeDefinition.newObjectTypeDefinition().name(QUERY).build())
         }
 
-        val builder = RuntimeWiring.newRuntimeWiring().scalar(DynamicProperties.INSTANCE)
+        val builder = RuntimeWiring.newRuntimeWiring()
+        typeDefinitionRegistry.scalars()
+            .filterNot { entry -> STANDARD_SCALAR_DEFINITIONS.containsKey(entry.key) }
+            .forEach { (name, definition) ->
+                val scalar = when (name) {
+                    "DynamicProperties" -> DynamicProperties.INSTANCE
+                    else -> GraphQLScalarType.newScalar()
+                        .name(name)
+                        .description(definition.description?.getContent() ?: "Scalar $name")
+                        .withDirectives(*definition.directives.filterIsInstance<GraphQLDirective>().toTypedArray())
+                        .definition(definition)
+                        .coercing(NoOpCoercing)
+                        .build()
+                }
+                builder.scalar(scalar)
+            }
+
 
         enhancedRegistry
             .getTypes(InterfaceTypeDefinition::class.java)
@@ -205,4 +222,4 @@ object SchemaBuilder {
         typeDefinitionRegistry.add(inputType)
         return inputName
     }
-}
\ No newline at end of file
+}
diff --git a/src/test/kotlin/org/neo4j/graphql/CypherTests.kt b/src/test/kotlin/org/neo4j/graphql/CypherTests.kt
index e8b20e50..6b9bfabe 100644
--- a/src/test/kotlin/org/neo4j/graphql/CypherTests.kt
+++ b/src/test/kotlin/org/neo4j/graphql/CypherTests.kt
@@ -29,6 +29,9 @@ class CypherTests {
     @TestFactory
     fun `translator-tests3`() = CypherTestSuite("translator-tests3.adoc").run()
 
+    @TestFactory
+    fun `translator-tests-custom-scalars`() = CypherTestSuite("translator-tests-custom-scalars.adoc").run()
+
     @TestFactory
     fun `optimized-query-for-filter`() = CypherTestSuite("optimized-query-for-filter.adoc").run()
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/translator-tests-custom-scalars.adoc b/src/test/resources/translator-tests-custom-scalars.adoc
new file mode 100644
index 00000000..1095ceed
--- /dev/null
+++ b/src/test/resources/translator-tests-custom-scalars.adoc
@@ -0,0 +1,202 @@
+:toc:
+
+= Translator Tests
+
+== Schema
+
+[source,graphql,schema=true]
+----
+scalar Date
+type Movie {
+  _id: ID!
+  title: String!
+  released: Date
+}
+----
+
+== Tests
+
+=== Create
+
+.GraphQL-Query
+[source,graphql]
+----
+mutation {
+  createMovie(title:"Forrest Gump", released: 1994) {
+    title
+    released
+  }
+}
+----
+
+.Cypher params
+[source,json]
+----
+{
+  "createMovieTitle": "Forrest Gump",
+  "createMovieReleased": 1994
+}
+----
+
+.Cypher
+[source,cypher]
+----
+CREATE (createMovie:Movie { title: $createMovieTitle, released: $createMovieReleased })
+WITH createMovie
+RETURN createMovie { .title, .released } AS createMovie
+----
+
+=== Update
+
+.GraphQL-Query
+[source,graphql]
+----
+mutation {
+  updateMovie(_id: 1, released: 1995) {
+    title
+    released
+  }
+}
+----
+
+.Cypher params
+[source,json]
+----
+{
+  "updateMovie_id": 1,
+  "updateMovieReleased": 1995
+}
+----
+
+.Cypher
+[source,cypher]
+----
+MATCH (updateMovie: Movie)
+WHERE ID(updateMovie) = toInteger($updateMovie_id)
+SET updateMovie = { released: $updateMovieReleased }
+WITH updateMovie
+RETURN updateMovie { .title, .released } AS updateMovie
+----
+
+=== Merge
+
+.GraphQL-Query
+[source,graphql]
+----
+mutation {
+  mergeMovie(_id: 1, released: 1995) {
+    title
+    released
+  }
+}
+----
+
+.Cypher params
+[source,json]
+----
+{
+  "mergeMovie_id": 1,
+  "mergeMovieReleased": 1995
+}
+----
+
+.Cypher
+[source,cypher]
+----
+MATCH (mergeMovie: Movie)
+WHERE ID(mergeMovie) = toInteger($mergeMovie_id)
+SET mergeMovie += { released: $mergeMovieReleased }
+WITH mergeMovie
+RETURN mergeMovie { .title, .released } AS mergeMovie
+----
+
+=== Merge null
+
+.GraphQL-Query
+[source,graphql]
+----
+mutation {
+  updateMovie(_id: 1, released: null) {
+    title
+    released
+  }
+}
+----
+
+.Cypher params
+[source,json]
+----
+{
+  "updateMovie_id": 1,
+  "updateMovieReleased": null
+}
+----
+
+.Cypher
+[source,cypher]
+----
+MATCH (updateMovie: Movie)
+WHERE ID(updateMovie) = toInteger($updateMovie_id)
+SET updateMovie = { released: $updateMovieReleased }
+WITH updateMovie
+RETURN updateMovie { .title, .released } AS updateMovie
+----
+
+=== Find
+
+.GraphQL-Query
+[source,graphql]
+----
+{
+  movie(released: 1994) {
+    title
+    released
+  }
+}
+----
+
+.Cypher params
+[source,json]
+----
+{
+  "movieReleased": 1994
+}
+----
+
+.Cypher
+[source,cypher]
+----
+MATCH (movie: Movie)
+WHERE movie.released = $movieReleased
+RETURN movie { .title, .released } AS movie
+----
+
+=== Filter
+
+.GraphQL-Query
+[source,graphql]
+----
+{
+  movie(filter:{released_gte: 1994}) {
+    title
+    released
+  }
+}
+----
+
+.Cypher params
+[source,json]
+----
+{
+  "filterMovieReleased_GTE": 1994
+}
+----
+
+.Cypher
+[source,cypher]
+----
+MATCH (movie: Movie)
+WHERE movie.released >= $filterMovieReleased_GTE
+RETURN movie { .title, .released } AS movie
+----
+