1/**
2 * @title Batching single-item lookups with aliases
3 *
4 * When an API has no field that takes a list of keys, `GraphQLResolver.aliased`
5 * repeats a generated operation under aliases, so concurrent lookups share one
6 * request and keep their generated types.
7 */
8import { Context, Effect, Layer, Schema } from "effect"
9import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol, GraphQLResolver } from "effect/graphql"
10import { FetchHttpClient } from "effect/http"
11import { GitHubAuth } from "./10_basics.ts"
12
13// The generator's output for `pr.graphql`:
14//
15// query PullRequest($owner: String!, $name: String!, $number: Int!) {
16// repository(owner: $owner, name: $name) {
17// pullRequest(number: $number) { number title }
18// }
19// }
20//
21// Generated modules import shared codecs, such as `Int`, from the module the
22// generator writes next to the schema (`import * as Shared from
23// "../schema.graphql.ts"`); here `Shared` is declared inline.
24const Shared = { Int: Schema.Number.check(Schema.isInt32()) }
25
26export const PullRequestPullRequest = Schema.Struct({
27 number: Shared.Int,
28 title: Schema.String
29})
30export interface PullRequestPullRequest extends Schema.Struct.Type<typeof PullRequestPullRequest.fields> {}
31
32export const Repository = Schema.Struct({
33 pullRequest: Schema.NullOr(PullRequestPullRequest)
34})
35export interface Repository extends Schema.Struct.Type<typeof Repository.fields> {}
36
37// The generator gives queries and mutations their document as a
38// `GraphQL.DocumentTemplate`. The odd entries of each `body` are the names
39// `aliased` renames per slice; joined together they are the printed document.
40/** `query PullRequest` */
41export const PullRequest = GraphQL.query("PullRequest", {
42 document: {
43 variables: [
44 { name: "owner", body: "String!" },
45 { name: "name", body: "String!" },
46 { name: "number", body: "Int!" }
47 ],
48 selections: [
49 {
50 name: "repository",
51 body: ["(owner:$", "owner", " name:$", "name", "){pullRequest(number:$", "number", "){number title}}"]
52 }
53 ],
54 fragments: []
55 },
56 variables: {
57 owner: Schema.String,
58 name: Schema.String,
59 number: Shared.Int
60 },
61 result: Schema.Struct({
62 repository: Schema.NullOr(Repository)
63 })
64})
65export declare namespace PullRequest {
66 export type Variables = typeof PullRequest.variables.Type
67 export type Result = typeof PullRequest.result.Type
68}
69
70/** Every operation in pr.graphql. */
71export const PrGraphQLGroup = GraphQLGroup.make(PullRequest)
72
73// Your code: attach the middleware to the generated group.
74export const GitHubGraphQLGroup = PrGraphQLGroup.middleware(GitHubAuth)
75
76export class GitHub extends Context.Service<
77 GitHub,
78 GraphQLClient.FromGroup<typeof GitHubGraphQLGroup> & {
79 readonly getPullRequest: (
80 variables: GraphQL.Variables<typeof PullRequest>
81 ) => Effect.Effect<GraphQL.Result<typeof PullRequest>, GraphQL.Error<typeof PullRequest>>
82 }
83>()("app/GitHub") {
84 static readonly layer = Layer.effect(
85 GitHub,
86 Effect.gen(function*() {
87 const client = yield* GraphQLClient.make(GitHubGraphQLGroup)
88
89 // Concurrent calls go out as one POST whose root fields are aliased
90 // `_0_repository`, `_1_repository`, and so on. Each slice is decoded
91 // with the generated result Schema, and a GraphQL error under
92 // `_1_repository` fails only that call.
93 //
94 // A batch holds 10 slices by default, because GitHub's query cost and
95 // timeouts grow with the size of the query. Pipe
96 // `RequestResolver.batchN` to change it.
97 const PullRequests = GraphQLResolver.aliased(client.PullRequest)
98
99 // `request` turns the resolver into a lookup by the operation's
100 // variables, as for `ordered` and `findById`.
101 return { ...client, getPullRequest: GraphQLResolver.request(PullRequests) }
102 })
103 ).pipe(
104 Layer.provide(GitHubAuth.layer),
105 Layer.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })),
106 Layer.provide(FetchHttpClient.layer)
107 )
108}
109
110export const pullRequestTitles = Effect.fn("pullRequestTitles")(function*(numbers: ReadonlyArray<number>) {
111 const github = yield* GitHub
112 const results = yield* Effect.forEach(
113 numbers,
114 (number) => github.getPullRequest({ owner: "Effect-TS", name: "effect", number }),
115 { concurrency: "unbounded" }
116 )
117 // `repository` and `pullRequest` are nullable, so a missing pull request is
118 // `null` rather than an error that would fail every slice.
119 return results.map((result) => result.repository?.pullRequest?.title ?? null)
120})
121
122export const program = pullRequestTitles([1, 2, 3]).pipe(
123 Effect.provide(GitHub.layer)
124)