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