Skip to content

Instantly share code, notes, and snippets.

@lagenorhynque
Last active November 11, 2022 07:03
Show Gist options
  • Save lagenorhynque/b2faa4f51617ba0dafc7bdd86af68e79 to your computer and use it in GitHub Desktop.
Save lagenorhynque/b2faa4f51617ba0dafc7bdd86af68e79 to your computer and use it in GitHub Desktop.
GraphQL API in Clojure

GraphQL API in Clojure


(defprofile lagénorhynque
  :aliases      [カマイルカ🐬]

  :languages    [Clojure Common-Lisp Scheme Haskell
                 English français]
  :interests    [programming Love-Live!
                 language-learning/linguistics law mathematics]

  :committing   [github.com/lagenorhynque/duct.module.pedestal]
  :contributing [github.com/japan-clojurians/clojure-site-ja])

twitter icon


  1. GraphQL

    • GraphQL query

    • GraphQL schema

  2. API implementation

    • project structure

    • API server

    • GraphQL implementation


ClojureのLaciniaでGraphQL API開発してみた

qiita lacinia


example

lagenorhynque/aqoursql: AqoursQL, an example GraphQL API based on Lacinia-Pedestal & Duct

aqoursql db


GraphQL

venia & edn


GraphQL query


query: member_by_id

{
  member_by_id(id: 1) {
    id
    name
    organization_id
    organization_name
  }
}

response (JSON)

{
  "data": {
    "member_by_id": {
      "id": 1,
      "name": "高海 千歌",
      "organization_id": 1,
      "organization_name": "浦の星女学院"
    }
  }
}

GraphiQL

query member_by_id


Clojure REPL

dev> (q #:venia{:queries [[:member_by_id {:id 1}
                           [:id
                            :name
                            :organization_id
                            :organization_name]]]})
{:data
 {:member_by_id
  {:id 1, :name "高海 千歌", :organization_id 1,
   :organization_name "浦の星女学院"}}}

utility function q

(defn q
  ([query] (q query nil))
  ([query variables]
   (lacinia/execute (:aqoursql.graphql/schema system)
                    (venia/graphql-query query)
                    variables
                    {:db (:duct.database.sql/hikaricp system)})))

query: songs

{
  songs {
    name
    artist {
      name
      members {
        name
      }
    }
  }
}

response (JSON)

{
  "data": {
    "songs": [
      {
        "name": "君のこころは輝いてるかい?",
        "artist": {
          "name": "Aqours",
          "members": [
            {
              "name": "高海 千歌"
            }, ...
          ]
        }
      }, ...
    ]
  }
}

GraphiQL

query songs


Clojure REPL

dev> (q #:venia{:queries [[:songs
                           [:name
                            [:artist
                             [:name
                              [:members
                               [:name]]]]]]]})
{:data
 {:songs
  ({:name "君のこころは輝いてるかい?",
    :artist
    {:name "Aqours",
     :members
     ({:name "高海 千歌"}
      ...

GraphQL schema


object definition: Member

"""メンバー"""
type Member {
  """メンバーID"""
  id: Int!
  """メンバー名"""
  name: String!
  """所属組織ID"""
  organization_id: Int!
  """所属組織名"""
  organization_name: String!
}

resources/aqoursql/graphql-schema.edn

{:objects
 {:Member
  {:description "メンバー"
   :fields
   {:id {:type (non-null Int)
         :description "メンバーID"}
    :name {:type (non-null String)
           :description "メンバー名"}
    :organization_id {:type (non-null Int)
                      :description "所属組織ID"}
    :organization_name {:type (non-null String)
                        :description "所属組織名"}}}}}

object definition: Song

"""楽曲"""
type Song {
  """楽曲ID"""
  id: Int!
  """楽曲名"""
  name: String!
  """"アーティストID""
  artist_id: Int!
  """アーティスト"""
  artist: Artist!
  """リリース日 (YYYY-MM-DD)"""
  release_date: String!
}

resources/aqoursql/graphql-schema.edn

{:objects
 {:Song
  {:description "楽曲"
   :fields
   {:id {:type (non-null Int)
         :description "楽曲ID"}
    :name {:type (non-null String)
           :description "楽曲名"}
    :artist_id {:type (non-null Int)
                :description "アーティストID"}
    :artist {:type (non-null :Artist)
             :description "アーティスト"}
    :release_date {:type (non-null String)
                   :description "リリース日 (YYYY-MM-DD)"}}}}}

query definition: member_by_id

type Query {
  """IDによるメンバー取得"""
  member_by_id(
    "メンバーID"
    id: Int!
  ): Member
}

resources/aqoursql/graphql-schema.edn

{:queries
 {:member_by_id
  {:type :Member
   :description "IDによるメンバー取得"
   :args
   {:id {:type (non-null Int)
         :description "メンバーID"}}
   :resolve :query/member-by-id}}}

query definition: songs

type Query {
  """楽曲一覧取得"""
  songs(
    "楽曲名 (部分一致)"
    name: String
  ): [Song]
}

{:queries
 {:songs
  {:type (list :Song)
   :description "楽曲一覧取得"
   :args
   {:name {:type String
           :description "楽曲名 (部分一致)"}}
   :resolve :query/songs}}}

API implementation

Duct + Pedestal + Lacinia


project structure


ClojureサーバサイドフレームワークDuctガイド

qiita duct


resources/aqoursql/config.edn

{:duct.profile/base
 {:duct.core/project-ns  aqoursql
  :duct.server/pedestal { ... }
  :aqoursql.graphql/schema {}
  :aqoursql.graphql/service { ... }}

 :duct.profile/dev #duct/include "dev"
 :duct.profile/test #duct/include "test"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod {}

 :duct.module/logging {}
 :duct.module/sql { ... }
 :duct.module/pedestal {}}

API server


Clojureサービス開発ライブラリPedestal入門

qiita pedestal


resources/aqoursql/config.edn

{:duct.profile/base
 {:duct.core/project-ns  aqoursql

  :duct.server/pedestal
  {:base-service #ig/ref :aqoursql.graphql/service
   :service #:io.pedestal.http{:join? true
                               :host #duct/env "SERVER_HOST"
                               :port #duct/env ["SERVER_PORT" Int
                                                :or 8888]}}
  ... }

 ...

 :duct.module/pedestal {}}

GraphQL implementation


resources/aqoursql/config.edn

{:duct.profile/base
 {:duct.core/project-ns  aqoursql

  ...

  :aqoursql.graphql/schema {}

  :aqoursql.graphql/service
  {:schema #ig/ref :aqoursql.graphql/schema
   :options {:graphiql true
             :app-context {:db #ig/ref :duct.database/sql}
             :env :prod}}}

 ... }

src/aqoursql/graphql.clj

(defmethod ig/init-key ::schema
  [_ _]
  (-> (io/resource "aqoursql/graphql-schema.edn")
      slurp
      edn/read-string
      (util/attach-resolvers resolver-map)
      schema/compile))

(defmethod ig/init-key ::service
  [_ {:keys [schema options]}]
  (lacinia/service-map schema options))

resolver (function)

src/aqoursql/graphql.clj

(def resolver-map
  {:query/artist-by-id artists/fetch-artist-by-id
   :query/artists artists/list-artists
   :query/member-by-id members/fetch-member-by-id
   :query/members members/list-members
   :query/song-by-id songs/fetch-song-by-id
   :query/songs songs/list-songs})

src/aqoursql/resolver/members.clj

(defn fetch-member-by-id [{:keys [db]} {:keys [id]} _]
  (db.member/find-member-by-id db id))

resolver function spec

(s/fdef resolver
  :args (s/cat :app-context map?
               :arguments (s/nilable map?)
               :resovled-value (s/nilable map?)))

boundary (DB)

src/aqoursql/boundary/db/member.clj

(s/def ::id nat-int?)
(s/def ::name string?)
(s/def ::organization_id ::organization/id)

(s/def ::organization_name ::organization/name)
(s/def ::artist_id ::artist/id)
(s/def ::artist_ids (s/coll-of ::artist/id))

(s/def ::member
  (s/keys :req-un [::id
                   ::name
                   ::organization_id]
          :opt-un [::organization_name
                   ::artist_id]))

(s/fdef find-member-by-id
  :args (s/cat :db ::db/db
               :id ::id)
  :ret (s/nilable ::member))

...

(defprotocol Member
  (find-member-by-id [db id])
  ... )

dev> (aqoursql.boundary.db.member/find-member-by-id
      (:duct.database.sql/hikaricp system) 1)
{:id 1, :name "高海 千歌", :organization_id 1,
 :organization_name "浦の星女学院"}
dev> (aqoursql.resolver.members/fetch-member-by-id
      {:db (:duct.database.sql/hikaricp system)}
      {:id 1} nil)
{:id 1, :name "高海 千歌", :organization_id 1,
 :organization_name "浦の星女学院"}
dev> (q #:venia{:queries [[:member_by_id {:id 1}
                           [:name
                            :organization_name]]]})
{:data {:member_by_id {:name "高海 千歌",
 :organization_name "浦の星女学院"}}}

Further Reading

Lacinia


Duct


Pedestal


GraphQL

View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment