Skip to content

Instantly share code, notes, and snippets.

@OlegIlyenko
Last active February 10, 2022 22:05
Show Gist options
  • Save OlegIlyenko/ce3c8c4b3ab7afdd51a0faea4e3e6dc0 to your computer and use it in GitHub Desktop.
Save OlegIlyenko/ce3c8c4b3ab7afdd51a0faea4e3e6dc0 to your computer and use it in GitHub Desktop.
Example of Sangria middleware for Apollo Cache Control (https://github.com/apollographql/apollo-cache-control)
import sangria.ast._
import sangria.execution._
import sangria.schema.Context
import sangria.marshalling.queryAst._
import java.util.concurrent.ConcurrentLinkedQueue
import scala.concurrent.duration.FiniteDuration
import scala.collection.JavaConverters._
object ApolloCacheExtension extends Middleware[Any] with MiddlewareExtension[Any] with MiddlewareAfterField[Any] {
type QueryVal = ConcurrentLinkedQueue[Value]
type FieldVal = Option[CacheHint]
def beforeQuery(context: MiddlewareQueryContext[Any, _, _]) =
new ConcurrentLinkedQueue
def afterQuery(queryVal: QueryVal, context: MiddlewareQueryContext[Any, _, _]) = ()
def beforeField(queryVal: QueryVal, mctx: MiddlewareQueryContext[Any, _, _], ctx: Context[Any, _]) = {
val cacheControl = ctx.field.tags.collectFirst {case cc: CacheControl ⇒ cc}
if (cacheControl.isDefined) {
val hint = Some(new CacheHint(cacheControl))
BeforeFieldResult(hint, attachment = hint)
} else noCache
}
def afterField(queryVal: QueryVal, fieldVal: FieldVal, value: Any, mctx: MiddlewareQueryContext[Any, _, _], ctx: Context[Any, _]) = {
fieldVal
.flatMap(_.cc)
.flatMap {case CacheControl(maxAge, scope) ⇒ cacheElement(ctx.path, maxAge, scope)}
.foreach(queryVal.add)
None
}
def cacheElement(path: ExecutionPath, maxAge: Option[FiniteDuration], scope: CacheScope.Value) = {
val pathField = Vector("path" → ListValue(path.path.map(queryAstResultMarshaller.scalarNode(_, "Any", Set.empty))))
val maxAgeField = maxAge.map(ma ⇒ "maxAge" → BigIntValue(ma.toSeconds)).toVector
val scopeField = scope match {
case CacheScope.Private ⇒ Vector("scope" → StringValue("PRIVATE"))
case CacheScope.Public ⇒ Vector.empty
}
if (maxAgeField.nonEmpty || scopeField.nonEmpty)
Some(ObjectValue(pathField ++ maxAgeField ++ scopeField: _*))
else
None
}
def afterQueryExtensions(queryVal: QueryVal, context: MiddlewareQueryContext[Any, _, _]): Vector[Extension[_]] =
Vector(Extension(ObjectValue(
"cacheControl" → ObjectValue(
"version" → IntValue(1),
"hints" → ListValue(queryVal.asScala.toVector))): Value))
val noCache = BeforeFieldResult[Any, Option[CacheHint]](None, None, None)
}
import sangria.execution.{FieldTag, MiddlewareAttachment}
import sangria.schema.Context
import scala.concurrent.duration.FiniteDuration
case class CacheControl(maxAge: Option[FiniteDuration], scope: CacheScope.Value) extends FieldTag
object CacheControl {
val empty = CacheControl(None, CacheScope.Public)
def apply(maxAge: FiniteDuration, scope: CacheScope.Value = CacheScope.Public): CacheControl =
CacheControl(Some(maxAge), scope)
def apply(scope: CacheScope.Value): CacheControl =
CacheControl(None, scope)
}
object CacheScope extends Enumeration {
val Private, Public = Value
}
class CacheHint(@volatile var cc: Option[CacheControl] = None) extends MiddlewareAttachment {
def cacheControl(maxAge: Option[FiniteDuration] = None, scope: CacheScope.Value = CacheScope.Public): Unit =
cc = Some(CacheControl(maxAge, scope))
}
object CacheHint {
def cacheControl(ctx: Context[_, _], maxAge: FiniteDuration, scope: CacheScope.Value = CacheScope.Public): Unit =
ctx.attachment[CacheHint].foreach(_.cacheControl(Some(maxAge), scope))
}
import sangria.macros._
import sangria.macros.derive._
import sangria.schema._
import sangria.execution._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
case class Post(title: String, votes: Int, readByCurrentUser: Boolean)
object Post {
implicit val graphqlType = deriveObjectType[Unit, Post](
FieldTags("votes", CacheControl(30 seconds)),
FieldTags("readByCurrentUser", CacheControl(CacheScope.Private)))
}
val IdArg = Argument("id", IDType)
val schema = Schema(ObjectType("Query", fields[Unit, Unit](
Field("post", OptionType(Post.graphqlType),
tags = CacheControl(2 minutes) :: Nil,
arguments = IdArg :: Nil,
resolve = c ⇒ {
CacheHint.cacheControl(c, 60 seconds)
Post("Test post", 20, true)
}))))
val query =
graphql"""
query MyPost {
post(id: 1) {
title
votes
readByCurrentUser
}
}
"""
Executor.execute(schema, query, middleware = ApolloCacheExtension :: Nil)
{
"data": {
"post": {
"title": "Test post",
"votes": 20,
"readByCurrentUser": true
}
},
"extensions": {
"cacheControl": {
"version": 1,
"hints": [{
"path": ["post"],
"maxAge": 60
}, {
"path": ["post", "votes"],
"maxAge": 30
}, {
"path": ["post", "readByCurrentUser"],
"scope": "PRIVATE"
}]
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment