Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scala + Caliban で作るGraphQL バックエンド / Making GraphQL Backend with Scala + Caliban

AGAWA Koji
October 01, 2021

Scala + Caliban で作るGraphQL バックエンド / Making GraphQL Backend with Scala + Caliban

AGAWA Koji

October 01, 2021
Tweet

More Decks by AGAWA Koji

Other Decks in Programming

Transcript

  1. GraphQL in Scala Sangria Caliban Since 2015 2019 Effect System

    Future ZIO Boilerplate 多い 少ない Resolver 密結合 疎結合
  2. case class User( id: UUID, name: String, age: Option[Int] )

    スキーマ定義 type User { id: ID! name: String! age: Int }
  3. case class UserArgs(id: UUID) case class Query( user: UserArgs =>

    Option[User], users: Seq[User] ) スキーマ定義 type Query { user(id: ID): User users: [User!]! }
  4. trait UserResolver { def getUser(id: UUID): Option[User] def getAllUsers(): Seq[User]

    } val query = Query( args => userResolver.getUser(args.id), userResolver.getAllUsers ) val api = graphQL(RootResolver(query)) リゾルバ定義
  5. case class GetGroup(id: Int) extends Request[Throwable, Group] val GroupDataSource =

    DataSource.fromFunctionBatchedM("GroupDataSource")( requests => dao.getGroups(requests.map(_.id)) ) def getGroup(id: Int): ZQuery[Any, Throwable, Group] = ZQuery.fromRequest(GetGroup(id))(GroupDataSource) ZQuery
  6. val routesM: IO[HttpRoutes[RIO[AppEnv, *]]] = for { a = Http4sAdapter.makeHttpService

    [AppEnv with Has[Auth.Service], CalibanError](interpreter) route = new Auth[AppEnv].apply(a) } yield Router[RIO[AppEnv, *]]( "/graphql" -> route ) http4s router
  7. class Auth[R <: Has[_] with Database]() { def apply(route: HttpRoutes[RIO[R

    with Has[Auth.Service, *]]): HttpRoutes[RIO[R, *]] = Http4sAdapter.provideSomeLayerFromRequest[R with Database, Has[Auth.Service]]( route, req => makeService(req).toLayer ) def makeService(req: Request[RIO[R, *]]): RIO[Database, Auth.Service] = ??? } object Auth { trait Service { def authUser: Option[AuthUser] } } Authentication service
  8. case class Query( getAllUsers: ZIO[Has[Auth.Service], Throwable, User] ) val resolver

    = Query( ZIO.access[Has[Auth.Service]] { _.get.authUser match { case Some(authUser) => ??? case None => ??? } ) Usage
  9. # An object with a Globally Unique ID interface Node

    { # The ID of the object. id: ID! } type User implements Node { id: ID! # Full name name: String! } Node Interface
  10. sealed trait Node { def id: String } case class

    User(id: String, name: String) extends Node Node Interface in Caliban (?)
  11. sealed trait Node { def id: String } case class

    User(id: String, name: String) extends Node Not scalable ...
  12. def lookupUser(id: String): UQuery[Option[User]] = ZQuery.some(User(id)) def lookupGroup(id: String): UQuery[Option[Group]]

    = ZQuery.some(Group(id)) val node = Node.instance .withType(lookupUser) .withType(lookupGroup) implicit val nodeSchema: Schema[Any, Node] = node.build case class Query( node: NodeArg => IO[CalibanError, Node] ) Solution
  13. object PaginatedQuery { def apply[A: Get]( fullTableQuery: Fragment )( cursor:

    Cursor, orderBy: Fragment = Fragment.empty ): TranzactIO[PageInfoDto[A]] = ??? } Relay-based Pagination の一般化
  14. case class Cursor( first: Option[Int], after: Option[String], last: Option[Int], before:

    Option[String] ) Cursor / PageInfo case class PageInfoDto[A]( totalCount: Long, hasPreviousPage: Boolean, hasNextPage: Boolean, value: Vector[A] )
  15. WITH _SOURCE AS ($table), _T AS ( SELECT ROW_NUMBER() OVER

    (ORDER BY ${orderBy}) AS n, ${idColumn} AS id FROM _SOURCE ORDER BY ${orderBy} ), _afterN AS (SELECT n FROM _T WHERE _T.id = ${after}), _beforeN AS (SELECT n FROM _T WHERE _T.id = ${before}), _totalN AS (SELECT COUNT(1) AS total FROM _T), _page AS ( SELECT n, _T.id FROM _T WHERE (${cursor.after} IS NULL OR n > COALESCE((TABLE _afterN), 0)) AND (${cursor.first} IS NULL OR n <= COALESCE((TABLE _afterN), 0) + ${cursor.first}) AND (${cursor.before} IS NULL OR n < COALESCE((TABLE _beforeN), (TABLE _totalN) + 1)) AND (${cursor.last} IS NULL OR n >= COALESCE((TABLE _beforeN), (TABLE _totalN) + 1) - ${cursor.last}) ), _minN AS (SELECT MIN(n) FROM _page), _maxN AS (SELECT MAX(n) FROM _page) SELECT _totalN.total, CASE WHEN (TABLE _minN) > 1 THEN true ELSE false END AS has_previous_page, CASE WHEN (TABLE _maxN) < (TABLE _totalN) THEN true ELSE false END AS has_next_page, _page.id FROM _page, _totalN Pagination SQL (!)
  16. val nodes = ZQuery.foreachPar( pageInfo.value.map(id => GetSignageRequest(GRI(id.asString))) )( ZQuery.fromRequest(_)(getSignageDataSource.dataSource) )

    SignageConnection( edges = nodes.map(_.map(n => SignageEdge(n.databaseId.asString, n))), nodes = nodes, pageInfo = PageInfo( hasNextPage = pageInfo.hasNextPage, hasPreviousPage = pageInfo.hasPreviousPage, startCursor = pageInfo.value.headOption.map(_.asString), endCursor = pageInfo.value.lastOption.map(_.asString) ), totalCount = pageInfo.totalCount ) Resolver