Open History to see the last commit
1/**
2 * @title Getting started with GraphQL clients
3 *
4 * Define a query, build a typed client service over HTTP with an auth
5 * middleware, and page through a cursor connection.
6 */
7import { Config, Context, Effect, Layer, Redacted, Schema, Stream } from "effect"
8import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLMiddleware, GraphQLProtocol } from "effect/graphql"
9import { FetchHttpClient } from "effect/http"
10
11// Operations are usually generated: write them in `.graphql` files and run
12// `graphqlgen` from `@effect/graphql-generator`, which writes a `.graphql.ts`
13// module like the one below next to each document.
14//
15// query RepoIssues($owner: String!, $name: String!, $after: String) {
16// repository(owner: $owner, name: $name) {
17// issues(first: 50, after: $after) {
18// pageInfo { hasNextPage endCursor }
19// nodes { number title }
20// }
21// }
22// }
23//
24// Each nested object selection has a Schema.Struct const and a same-named
25// interface for its decoded type. The const handles decoding, encoding and
26// `make`; use the interface in explicit type annotations when a named type
27// is useful. Inferred types and editor hovers can expand structurally rather
28// than showing the names, unlike the previous Schema.Opaque classes.
29export const PageInfo = Schema.Struct({
30 hasNextPage: Schema.Boolean,
31 endCursor: Schema.NullOr(Schema.String)
32})
33export interface PageInfo extends Schema.Struct.Type<typeof PageInfo.fields> {}
34
35export const Issue = Schema.Struct({ number: Schema.Int, title: Schema.String })
36export interface Issue extends Schema.Struct.Type<typeof Issue.fields> {}
37
38export const IssueConnection = Schema.Struct({
39 pageInfo: PageInfo,
40 nodes: Schema.NullOr(Schema.Array(Schema.NullOr(Issue)))
41})
42export interface IssueConnection extends Schema.Struct.Type<typeof IssueConnection.fields> {}
43
44export const Repository = Schema.Struct({ issues: IssueConnection })
45export interface Repository extends Schema.Struct.Type<typeof Repository.fields> {}
46
47export const RepoIssues = GraphQL.query("RepoIssues", {
48 document:
49 "query RepoIssues($owner:String!,$name:String!,$after:String){repository(owner:$owner,name:$name){issues(first:50,after:$after){pageInfo{hasNextPage endCursor}nodes{number title}}}}",
50 variables: {
51 owner: Schema.String,
52 name: Schema.String,
53 after: Schema.optional(Schema.NullOr(Schema.String))
54 },
55 result: Schema.Struct({
56 repository: Schema.NullOr(Repository)
57 })
58})
59
60// A middleware is a service that wraps every operation it is attached to.
61// `mapRequest` builds one from a single request transform.
62export class GitHubAuth extends GraphQLMiddleware.Service<GitHubAuth>()("app/GitHubAuth") {
63 static readonly layer = Layer.effect(
64 GitHubAuth,
65 Effect.gen(function*() {
66 const token = yield* Config.Redacted("GITHUB_TOKEN")
67 return GraphQLMiddleware.mapRequest((request) =>
68 Effect.succeed({
69 ...request,
70 headers: { ...request.headers, authorization: `Bearer ${Redacted.value(token)}` }
71 })
72 )
73 })
74 )
75}
76
77// A group is the set of operations a client exposes; the generator emits one
78// per file. Attaching the middleware makes `GraphQLClient.make` require it.
79export const GitHubGraphQLGroup = GraphQLGroup.make(RepoIssues).middleware(GitHubAuth)
80
81// The client has one method per operation, named after it.
82export class GitHub extends Context.Service<
83 GitHub,
84 GraphQLClient.FromGroup<typeof GitHubGraphQLGroup>
85>()("app/GitHub") {
86 static readonly layerTest = GraphQLClient.layerTest(GitHub, GitHubGraphQLGroup)
87
88 static readonly layer = Layer.effect(GitHub, GraphQLClient.make(GitHubGraphQLGroup)).pipe(
89 Layer.provide(GitHubAuth.layer),
90 // Queries and mutations are sent as `POST`. Subscriptions use graphql-sse
91 // here, or graphql-ws with `GraphQLProtocol.layerWebSocket`.
92 Layer.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })),
93 Layer.provide(FetchHttpClient.layer)
94 )
95}
96
97export const program = Effect.gen(function*() {
98 const github = yield* GitHub
99
100 // Variables are encoded and the result decoded through the Schemas. Any
101 // `errors` in the response fail with a `GraphQLClientError`; pass
102 // `{ partial: true }` as the second argument to get them with the data.
103 const { repository } = yield* github.RepoIssues({ owner: "Effect-TS", name: "effect" })
104 yield* Effect.log(`First page has ${repository?.issues.nodes?.length ?? 0} issues`)
105
106 // `items` pages through the connection by driving `$after`, one request at
107 // a time and only as the stream is pulled.
108 const titles = yield* GraphQLClient.items(github.RepoIssues, {
109 variables: { owner: "Effect-TS", name: "effect" },
110 connection: (result) => result.repository?.issues
111 }).pipe(
112 Stream.take(120),
113 Stream.map((issue) => issue?.title),
114 Stream.runCollect
115 )
116 yield* Effect.log(`Fetched ${titles.length} issues`)
117}).pipe(Effect.provide(GitHub.layer))