Slide 1

Slide 1 text

GraphQL Λ ϓϩμΫγϣϯಋೖͨ݁͠Ռ YAPC::Okinawa 2018 ONNASON 1

Slide 2

Slide 2 text

ࣗݾ঺հ — ਔଟݟ ྒྷʢʹͨΈ Γΐ͏ʣ @bird_tummy — Android, iOS application developer @ CyberAgent, Inc. — ❤ SHISHAMO ! ! ! YAPC::Okinawa 2018 ONNASON 2

Slide 3

Slide 3 text

ࣗݾ঺հ — Perl શ͘ॻ͍ͨ͜ͱ͋Γ·ͤΜʂʂʂʂʂ — ͔ͤͬ͘ YAPC ʹདྷͨͷͰษڧͯ͠ؼΖ͏ͱࢥ͍· ͢ʂʂʂʂ YAPC::Okinawa 2018 ONNASON 3

Slide 4

Slide 4 text

ࠓ೔࿩͢͜ͱ — ࡢ೥͔ΒΑ͘ݟ͔͚ΔΑ͏ʹͳͬͨ GraphQL — GitHub API v4 Ͱ࠾༻͞Εͨͷ͕͖͔͚ͬͷ 1 ͭ — ͳ͔ͳ͔࣮૷पΓͷ஌ݟ͕޿·͍ͬͯͳ͍Α͏ʹࢥ͏ ʢࢼͨ͠ܥهࣄ͸͋Δ͚Ͳɻɻʣ YAPC::Okinawa 2018 ONNASON 4

Slide 5

Slide 5 text

ࠓ೔࿩͢͜ͱ — ಋೖฤɺ࣮૷ฤɺӡ༻ฤͷ 3 ͭͷϑΣʔζʹ෼͚ͯͲͷΑ ͏ʹ࣮૷͍ͯ͠Δ͔Λ͓࿩͠·͢ — ΫϥΠΞϯτ੒෼ଟΊͰ͢ — ࠓޙ࣮ࡍʹಋೖ͠Α͏ͱ͍ͯ͠Δํɺݕ౼͍ͯ͠Δํͷࢀ ߟʹͳΕ͹޾͍Ͱ͢ YAPC::Okinawa 2018 ONNASON 5

Slide 6

Slide 6 text

ࠓ೔࿩͞ͳ͍͜ͱ — GraphQL ͱ REST ౳ͱͷൺֱ — GraphQL ʹ͍ͭͯͷࡉ͔͍ղઆ YAPC::Okinawa 2018 ONNASON 6

Slide 7

Slide 7 text

ΞδΣϯμ — ͦ΋ͦ΋ GraphQL ͱ͸ʁ — ಋೖฤ — GraphQL ͷ։ൃ؀ڥ — ࣮૷ฤ — GraphQL ͷαʔό / ΫϥΠΞϯτͦΕͧΕͷ࣮૷ํ๏ — ӡ༻ฤ — Ωϟογϡ౳ͷύϑΥʔϚϯε໘ʹ͍ͭͯ — ςετͷॻ͖ํ — ·ͱΊ YAPC::Okinawa 2018 ONNASON 7

Slide 8

Slide 8 text

GraphQL ͱ͸ʁ YAPC::Okinawa 2018 ONNASON 8

Slide 9

Slide 9 text

GraphQL ͱ͸ʁ — http://graphql.org — Facebook ੡ — API ༻ͷΫΤϦݴޠͰ͋Γɺطଘ ͷσʔλͰΫΤϦΛ࣮ߦ͢ΔͨΊ ͷϥϯλΠϜ YAPC::Okinawa 2018 ONNASON 9

Slide 10

Slide 10 text

YAPC::Okinawa 2018 ONNASON 10

Slide 11

Slide 11 text

GraphQL ͱ͸ʁ — Mutation ͱ Query ͕͋Δ — Mutation: REST API Ͱ͍͏ POST, PUT, DELETE — Query: REST API Ͱ͍͏ GET — Fragment Ͱಉ͡ఆٛΛ࢖͍·ΘͤΔ YAPC::Okinawa 2018 ONNASON 11

Slide 12

Slide 12 text

YAPC::Okinawa 2018 ONNASON 12

Slide 13

Slide 13 text

YAPC::Okinawa 2018 ONNASON 13

Slide 14

Slide 14 text

YAPC::Okinawa 2018 ONNASON 14

Slide 15

Slide 15 text

ಋೖฤ YAPC::Okinawa 2018 ONNASON 15

Slide 16

Slide 16 text

ಋೖฤ — ·ͣखݩͰࢼͤΔ؀ڥΛ༻ҙ͢Δ — GraphiQL (GraphiQL Feen!) — υΩϡϝϯτੜ੒ — graphdoc YAPC::Okinawa 2018 ONNASON 16

Slide 17

Slide 17 text

GraphiQL (GraphiQL Feen!) — https://github.com/graphql/graphiql — ϒϥ΢βͰಈ͘ — https://github.com/skevy/graphiql-app — Electron ੡ — https://github.com/bpatters/graphiql-feen — Chrome Extension YAPC::Okinawa 2018 ONNASON 17

Slide 18

Slide 18 text

YAPC::Okinawa 2018 ONNASON 18

Slide 19

Slide 19 text

YAPC::Okinawa 2018 ONNASON 19

Slide 20

Slide 20 text

ಋೖฤ — υΩϡϝϯτͱ࢓༷Λݟͳ͕Βɺ֤ Mutation ΍ Query Ͱ ΄͍͠΋ͷ͚ͩఆٛ͢Δ — .graphql, .gql — ఆٛ͢Δͱ͖ʹิ׬͕͖͍ͨঢ়ଶͰ΍Γ͍ͨ — ֤ΤσΟλͷϓϥάΠϯ͕࡞ΒΕ͍ͯΔ YAPC::Okinawa 2018 ONNASON 20

Slide 21

Slide 21 text

ಋೖฤ — vscode-graphql — js-graphql-intellij-plugin YAPC::Okinawa 2018 ONNASON 21

Slide 22

Slide 22 text

ಋೖฤ — intellij ͷํΛ͕ͬͭΓ࢖ͬͯΔͷͰͦͪΒΛྫʹݟͯΈΔ YAPC::Okinawa 2018 ONNASON 22

Slide 23

Slide 23 text

ಋೖฤ — js-graphql-intellij-plugin — https://github.com/jimkyndemeyer/js-graphql- intellij-plugin — graphql.config.json ʹ API ͷΤϯυϙΠϯτ΍ඞཁͳ ϔομʔΛઃఆͯ͠࢖༻ YAPC::Okinawa 2018 ONNASON 23

Slide 24

Slide 24 text

{ "schema": { "file": "graphql.schema.json", "request": { "url" : "http://--graphql-server--/path-to-schema-json-or-introspection-endpoint", "method" : "POST", "postIntrospectionQuery" : true, "README_options" : "See the 'Options' section at https://github.com/then/then-request", "options" : { "headers": { "user-agent" : "JS GraphQL" } } } }, "README_endpoints": "A list of GraphQL endpoints that can be queried from '.graphql' files in the IDE", "endpoints" : [ { "name": "Default (http://localhost:8080/graphql)", "url": "http://localhost:8080/graphql", "options" : { "headers": { "user-agent" : "JS GraphQL" } } } ] } YAPC::Okinawa 2018 ONNASON 24

Slide 25

Slide 25 text

YAPC::Okinawa 2018 ONNASON 25

Slide 26

Slide 26 text

ಋೖฤ·ͱΊ — GraphiQL Λ࢖͑͹खݩͰࢼͤΔ — graphql ϑΝΠϧʹฦΓ஋ͷఆٛΛॻ͕͘ɺॻ͘ࡍͷαϙʔτΛͯ͘͠ΕΔΤσΟλ ͷϓϥάΠϯ͕ఏڙ͞Ε͍ͯΔ — vscode — Intellij — etc. — ࠓͷͱ͜Ζ࢖͍ͬͯΔϓϥάΠϯͰࠔ͍ͬͯΔ͜ͱ͸ͳ͍͕ɺ͜Ε͕͋ͬͨΒͳʔͱ ͍͏ͱ͜Ζ͕গ͋͠Δ — graphql.config.json ͷ؅ཧ͕೉͍͠ — ߋ৽͕ࢭ·͍͍ͬͯͯͭ࢖͑ͳ͘ͳΔ͔Θ͔Βͳ͍ YAPC::Okinawa 2018 ONNASON 26

Slide 27

Slide 27 text

࣮૷ฤ YAPC::Okinawa 2018 ONNASON 27

Slide 28

Slide 28 text

αʔόαΠυ — apollo-server — https://github.com/apollographql/apollo-server — Լهͷத͔Β޷͖ͳ΋ͷΛબΜͰ࢖͏͜ͱ͕Ͱ͖Δ — Express — koa — hapi — restify — lambda — micro — azure-functions — adonis YAPC::Okinawa 2018 ONNASON 28

Slide 29

Slide 29 text

apollo-server-koa import koa from 'koa'; // koa@2 import koaRouter from 'koa-router'; // koa-router@next import koaBody from 'koa-bodyparser'; // koa-bodyparser@next import { graphqlKoa, graphiqlKoa } from 'apollo-server-koa'; const app = new koa(); const router = new koaRouter(); const PORT = 3000; // koaBody is needed just for POST. router.post('/graphql', koaBody(), graphqlKoa({ schema: myGraphQLSchema })); router.get('/graphql', graphqlKoa({ schema: myGraphQLSchema })); router.get('/graphiql', graphiqlKoa({ endpointURL: '/graphql' })); app.use(router.routes()); app.use(router.allowedMethods()); app.listen(PORT); YAPC::Okinawa 2018 ONNASON 29

Slide 30

Slide 30 text

ϑϩϯτ — @nuxtjs/apollo — https://github.com/nuxt-community/apollo-module — vue-apollo ͱ͍͏ .graphql ϑΝΠϧͷ Loader Λ nuxtjs Ͱ࢖͑ΔΑ͏ʹͨ͠΋ͷ YAPC::Okinawa 2018 ONNASON 30

Slide 31

Slide 31 text

@nuxtjs/apollo import { ApolloLink } from 'apollo-link' import { HttpLink } from 'apollo-link-http' import { InMemoryCache } from 'apollo-cache-inmemory' export default (ctx) => { const httpLink = new HttpLink({ uri: 'http://localhost:8000/graphql' }) // middleware const middlewareLink = new ApolloLink((operation, forward) => { const token = process.server ? ctx.req.session : window.__NUXT__.state.session operation.setContext({ headers: { authorization: `Bearer ${token}` } }) return forward(operation) }) const link = middlewareLink.concat(httpLink) return { link, cache: new InMemoryCache() } } YAPC::Okinawa 2018 ONNASON 31

Slide 32

Slide 32 text

iOS — apollo-ios — https://github.com/apollographql/apollo-ios — apollo-codegen — αʔόଆͰఆٛ͞Ε͍ͯΔ schema Λݩʹ Swift ϑΝ ΠϧΛੜ੒͢Δ CLI YAPC::Okinawa 2018 ONNASON 32

Slide 33

Slide 33 text

apollo-ios let queryString = "GraphQL" let configuration: URLSessionConfiguration = .default configuration.httpAdditionalHeaders = ["Authorization": "Bearer \(token)"] configuration.requestCachePolicy = .reloadIgnoringLocalCacheData // To avoid 412 let url = URL(string: "https://api.github.com/graphql")! let apollo = ApolloClient(networkTransport: HTTPNetworkTransport(url: url, configuration: configuration)) apollo.fetch(query: SearchRepositoriesQuery(query: queryString, count: 2), completionHandler: { (result, error) in if let error = error { print("Error: \(error)"); return } result?.data?.search.edges?.forEach { edge in guard let repository = edge?.node?.asRepository else { return } print("Name: \(repository.name)") print("Path: \(repository.url)") print("Owner: \(repository.owner.path)") print("Stars: \(repository.stargazers.totalCount)") } }) YAPC::Okinawa 2018 ONNASON 33

Slide 34

Slide 34 text

Android — apollo-android — https://github.com/apollographql/apollo-android — apollo-codegen — αʔόଆͰఆٛ͞Ε͍ͯΔ schema Λݩʹ Java ϑΝΠ ϧΛੜ੒͢Δ CLI YAPC::Okinawa 2018 ONNASON 34

Slide 35

Slide 35 text

apollo-android private void fetchRepositoryDetails() { ApolloCall entryDetailQuery = application.apolloClient() .query(new EntryDetailQuery(repoFullName)) .responseFetcher(ApolloResponseFetchers.CACHE_FIRST); //Example call using Rx2Support disposables.add(Rx2Apollo.from(entryDetailQuery) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeWith(new DisposableObserver>() { @Override public void onNext(Response dataResponse) { setEntryData(dataResponse.data()); } @Override public void onError(Throwable e) { Log.e(TAG, e.getMessage(), e); } @Override public void onComplete() { } })); } YAPC::Okinawa 2018 ONNASON 35

Slide 36

Slide 36 text

apollo-codegen — https://github.com/apollographql/apollo-codegen — αʔόଆͰఆٛ͞Ε͍ͯΔ schema Λݩʹ Java ϑΝΠϧ Λੜ੒͢Δ CLI — Node.js ੡ YAPC::Okinawa 2018 ONNASON 36

Slide 37

Slide 37 text

apollo-codegen $ apollo-codegen generate **/*.graphql --schema schema.json --output API.swift YAPC::Okinawa 2018 ONNASON 37

Slide 38

Slide 38 text

apollo-codegen public struct AsRepository: GraphQLConditionalFragment { public static let possibleTypes = ["Repository"] public let __typename = "Repository" public let name: String public let owner: Owner public let stargazers: Stargazer public let url: String public init(reader: GraphQLResultReader) throws { name = try reader.value(for: Field(responseName: "name")) owner = try reader.value(for: Field(responseName: "owner")) stargazers = try reader.value(for: Field(responseName: "stargazers")) url = try reader.value(for: Field(responseName: "url")) } public struct Owner: GraphQLMappable { public let __typename: String public let path: String public init(reader: GraphQLResultReader) throws { __typename = try reader.value(for: Field(responseName: "__typename")) path = try reader.value(for: Field(responseName: "path")) } } public struct Stargazer: GraphQLMappable { public let __typename = "StargazerConnection" public let totalCount: Int public init(reader: GraphQLResultReader) throws { totalCount = try reader.value(for: Field(responseName: "totalCount")) } } } YAPC::Okinawa 2018 ONNASON 38

Slide 39

Slide 39 text

࣮૷ฤ·ͱΊ — apollo ʹײँ ! — ͍͍ͩͨͷݴޠ͕αϙʔτ͞Ε͍ͯΔ — Perl ͸ͳ͔ͬͨ (´ɾТɾʆ) — Shopify Ͱ࢖ΘΕ͍ͯͯɺͦͷ։ൃऀ͕ϝϯςφϯε͍ͯ͠ ΔͷͰ҆৺ — ࡶͳ issue ΋ड͚෇͚ͯ͘ΕΔͷͰػೳͷཁ๬ͱ͔౤͛ͯ ΈΔͱ͍͍͔΋ YAPC::Okinawa 2018 ONNASON 39

Slide 40

Slide 40 text

YAPC::Okinawa 2018 ONNASON 40

Slide 41

Slide 41 text

ӡ༻ฤ YAPC::Okinawa 2018 ONNASON 41

Slide 42

Slide 42 text

ӡ༻ฤ — ΩϟογϡͬͯͲ͏ͳΔͷʁ — apollo-android Ͱ͸ 3 ͭͷํ๏Λαϙʔτ — HTTP Response Cache: ੜͷϨεϙϯεΛϑΝΠϧʹ͠ ͯΩϟογϡ — Normalized Disk Cache: SQL ʹಥͬࠐΜͰΩϟογϡ — Normalized InMemory Cache: ϝϞϦʹͭͬ͜ΜͰΩ ϟογϡ YAPC::Okinawa 2018 ONNASON 42

Slide 43

Slide 43 text

// HTTP Response Cache //Directory where cached responses will be stored File file = new File("/cache/"); //Size in bytes of the cache int size = 1024*1024; //Create the http response cache store DiskLruHttpCacheStore cacheStore = new DiskLruCacheStore(file, size); //Build the Apollo Client ApolloClient apolloClient = ApolloClient.builder() .serverUrl("/") .httpCache(new ApolloHttpCache(cacheStore)) .okHttpClient(okHttpClient) .build(); apolloClient .query( FeedQuery.builder() .limit(10) .type(FeedType.HOT) .build() ) .httpCachePolicy(HttpCachePolicy.CACHE_FIRST) .enqueue(new ApolloCall.Callback() { @Override public void onResponse(@Nonnull Response dataResponse) { ... } @Override public void onFailure(@Nonnull Throwable t) { ... } }); YAPC::Okinawa 2018 ONNASON 43

Slide 44

Slide 44 text

ӡ༻ฤ — CACHE_ONLY — Ωϟογϡ͚ͩݟʹߦ͘ɻAPI ୟ͍ͯऔಘ͠ʹ͍͔ͳ͍ɻΩϟογϡ͕ଘࡏ͠ͳ͔ͬͨ ͱ͖͸ΤϥʔʹͳΔ — NETWORK_ONLY — ΩϟογϡΛݟʹߦ͔ͣʹຖճ API Λୟ͍ͯऔಘ — CACHE_FIRST — ΩϟογϡΛ·ͣୈҰʹݟʹߦ͘ɻΩϟογϡ͕ͳ͔ͬͨΓͨ͠ͱ͖͸ API Λୟ͖ʹߦ ͘ — NETWORK_FIRST — API Λ·ͣୈҰʹୟ͖ʹߦ͘ɻAPI ϨεϙϯεͰΤϥʔ͕ฦ͖ͬͯͯɺΩϟογϡ͕·ͩ ࢒͍ͬͯΕ͹ͦΕΛ࢖͏ YAPC::Okinawa 2018 ONNASON 44

Slide 45

Slide 45 text

ӡ༻ฤ — Query ͸͍ͣΕ͔ͷํ๏Λ࢖͑͹ΩϟογϡՄೳ — Mutation ͸ෆՄ YAPC::Okinawa 2018 ONNASON 45

Slide 46

Slide 46 text

ӡ༻ฤ — ςετͷॻ͖ํ — UI ςετ͸جຊ΍Βͳ͍ — ωοτϫʔΫΤϥʔͷςετ — ಛఆͷ Type ʹͪΌΜͱύʔεͰ͖Δ͔Ͳ͏͔ YAPC::Okinawa 2018 ONNASON 46

Slide 47

Slide 47 text

@Test public void onNetworkError() throws Exception { final CountDownLatch countDownLatch = new CountDownLatch(1); final AtomicBoolean invoked = new AtomicBoolean(); final Handler callbackHandler = mockCallbackHandler(invoked); final AtomicReference exceptionRef = new AtomicReference<>(); apolloClient.query(EMPTY_QUERY).enqueue(ApolloCallback.wrap(new ApolloCall.Callback() { @Override public void onResponse(@Nonnull Response response) { countDownLatch.countDown(); } @Override public void onFailure(@Nonnull ApolloException e) { countDownLatch.countDown(); } @Override public void onNetworkError(@Nonnull ApolloNetworkException e) { exceptionRef.set(e); countDownLatch.countDown(); } }, callbackHandler)); countDownLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(invoked.get()).isTrue(); assertThat(exceptionRef.get()).isInstanceOf(ApolloNetworkException.class); } YAPC::Okinawa 2018 ONNASON 47

Slide 48

Slide 48 text

@Test public void onResponse() throws Exception { final CountDownLatch countDownLatch = new CountDownLatch(1); final AtomicBoolean invoked = new AtomicBoolean(); final Handler callbackHandler = mockCallbackHandler(invoked); final AtomicReference responseRef = new AtomicReference<>(); server.enqueue(new MockResponse().setResponseCode(200).setBody("{" + " \"errors\": [" + " {" + " \"message\": \"Cannot query field \\\"names\\\" on type \\\"Species\\\".\"," + " \"locations\": [" + " {" + " \"line\": 3," + " \"column\": 5" + " }" + " ]" + " }" + " ]" + "}")); apolloClient.query(EMPTY_QUERY).enqueue(ApolloCallback.wrap(new ApolloCall.Callback() { @Override public void onResponse(@Nonnull Response response) { responseRef.set(response); countDownLatch.countDown(); } @Override public void onFailure(@Nonnull ApolloException e) { countDownLatch.countDown(); } }, callbackHandler)); countDownLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(invoked.get()).isTrue(); assertThat(responseRef.get()).isNotNull(); } YAPC::Okinawa 2018 ONNASON 48

Slide 49

Slide 49 text

·ͱΊ — GraphQL ͷ࣮૷ʹ͍ͭͯ͝঺հ͠·ͨ͠ — ಋೖ͸ϥΠϒϥϦͷ README ͕੔͍ͬͯΔ͓͔͛ͰָʹՄೳ — ࣮૷ɾӡ༻͸ϥΠϒϥϦʹࠨӈ͞ΕΔ — ֤ϓϥοτϑΥʔϜͰศརͳϥΠϒϥϦ͕ἧ͍ͬͯΔͨΊɺී௨ʹ࣮ ૷͸Ͱ͖ͦ͏ — ·ͩ·ͩࢀߟهࣄ͕গͳ͘ɺͪΐͬͱ͸·Δͱݫ͍͠ͱ͖͕͋Δ — ͔͜͜Β੝Γ্͕͖ͬͯͯɺ஌ݟ͕ͨ͘͞ΜΞ΢τϓοτ͞ΕΔͷ Λظ଴ʂ YAPC::Okinawa 2018 ONNASON 49

Slide 50

Slide 50 text

͝੩ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ ࠙਌ձ·Ͱ͍ΔͷͰԿ͔࣭໰౳͋Ε͹੠͔͚͍ͯͩ͘͞ʂ ʢTwitter ͔ΒͰ΋ OK Ͱ͢ʣ YAPC::Okinawa 2018 ONNASON 50