import * as Cause from "../Cause.ts";
import * as Context from "../Context.ts";
import * as Effect from "../Effect.ts";
import * as Request from "../Request.ts";
import * as RequestResolver from "../RequestResolver.ts";
import type * as Types from "../Types.ts";
import type * as GraphQL from "./GraphQL.ts";
import type { OperationMethod } from "./GraphQLClient.ts";
type NonNull = Exclude;
type Step = K extends keyof NonNull ? NonNull[K] : never;
type CheckPath = NonNull extends ReadonlyArray ? readonly [] : P extends readonly [infer Head, ...infer Tail] ? readonly [Head extends keyof NonNull ? Head : keyof NonNull, ...CheckPath, Tail>] : readonly [keyof NonNull];
type Path = P extends CheckPath ? P : CheckPath;
type AtPath = P extends readonly [infer Head, ...infer Tail] ? AtPath, Tail> : A;
type Item = NonNull> extends ReadonlyArray ? NonNull : never;
type Typenames = Types.IsUnion extends true ? I extends {
readonly __typename: infer T extends string;
} ? T : never : never;
type Narrow = [T] extends [never] ? I : Extract;
type BatchKey = [GraphQL.Services] extends [never] ? {
readonly batchKey?: never;
} : {
readonly batchKey: (context: Context.Context>) => unknown;
};
interface KeyRequest extends Request.Request {
readonly key: K;
/** The protocol URL {@link request} resolved for this lookup, if it could. */
readonly url: string | undefined;
}
/**
* Looks up a key with an {@link ordered}, {@link findById} or {@link aliased}
* resolver, using `request(resolver)(key)` or `request(key, resolver)`.
*
* **Details**
*
* Concurrent lookups are batched. Keys equal under `Equal` and `Hash`
* are sent once per batch and share the result.
*
* **Example** (Looking up repositories by id)
*
* ```ts import.meta.vitest
* import { Effect, Layer, Schema, Stream } from "effect"
* import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol, GraphQLResolver } from "effect/graphql"
*
* const RepoNodes = GraphQL.query("RepoNodes", {
* document: "query RepoNodes($ids:[ID!]!){nodes(ids:$ids){... on Repository{name}}}",
* variables: { ids: Schema.Array(Schema.String) },
* result: Schema.Struct({ nodes: Schema.Array(Schema.NullOr(Schema.Struct({ name: Schema.String }))) })
* })
*
* let calls = 0
* const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
* execute: (request) => {
* calls++
* const { ids } = request.variables as { ids: Array }
* return Effect.succeed({ body: { data: { nodes: ids.map((id) => ({ name: `repo-${id}` })) } } })
* },
* subscribe: () => Stream.empty
* })
*
* const program = Effect.gen(function*() {
* const client = yield* GraphQLClient.make(GraphQLGroup.make(RepoNodes))
* const getRepo = GraphQLResolver.request(GraphQLResolver.ordered(client.RepoNodes, {
* variables: (ids: ReadonlyArray) => ({ ids }),
* path: ["nodes"]
* }))
* const repos = yield* Effect.forEach(["1", "2", "1"], getRepo, { concurrency: "unbounded" })
* return [repos.map((repo) => repo.name), calls]
* })
*
* await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => [["repo-1", "repo-2", "repo-1"], 1]
* ```
*
* @stability experimental
* @category running
* @since 4.0.0
*/
export declare const request: {
/**
* Looks up a key with an {@link ordered}, {@link findById} or {@link aliased}
* resolver, using `request(resolver)(key)` or `request(key, resolver)`.
*
* **Details**
*
* Concurrent lookups are batched. Keys equal under `Equal` and `Hash`
* are sent once per batch and share the result.
*
* **Example** (Looking up repositories by id)
*
* ```ts import.meta.vitest
* import { Effect, Layer, Schema, Stream } from "effect"
* import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol, GraphQLResolver } from "effect/graphql"
*
* const RepoNodes = GraphQL.query("RepoNodes", {
* document: "query RepoNodes($ids:[ID!]!){nodes(ids:$ids){... on Repository{name}}}",
* variables: { ids: Schema.Array(Schema.String) },
* result: Schema.Struct({ nodes: Schema.Array(Schema.NullOr(Schema.Struct({ name: Schema.String }))) })
* })
*
* let calls = 0
* const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
* execute: (request) => {
* calls++
* const { ids } = request.variables as { ids: Array }
* return Effect.succeed({ body: { data: { nodes: ids.map((id) => ({ name: `repo-${id}` })) } } })
* },
* subscribe: () => Stream.empty
* })
*
* const program = Effect.gen(function*() {
* const client = yield* GraphQLClient.make(GraphQLGroup.make(RepoNodes))
* const getRepo = GraphQLResolver.request(GraphQLResolver.ordered(client.RepoNodes, {
* variables: (ids: ReadonlyArray) => ({ ids }),
* path: ["nodes"]
* }))
* const repos = yield* Effect.forEach(["1", "2", "1"], getRepo, { concurrency: "unbounded" })
* return [repos.map((repo) => repo.name), calls]
* })
*
* await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => [["repo-1", "repo-2", "repo-1"], 1]
* ```
*
* @stability experimental
* @category running
* @since 4.0.0
*/
(resolver: RequestResolver.RequestResolver>): (key: K) => Effect.Effect;
/**
* Looks up a key with an {@link ordered}, {@link findById} or {@link aliased}
* resolver, using `request(resolver)(key)` or `request(key, resolver)`.
*
* **Details**
*
* Concurrent lookups are batched. Keys equal under `Equal` and `Hash`
* are sent once per batch and share the result.
*
* **Example** (Looking up repositories by id)
*
* ```ts import.meta.vitest
* import { Effect, Layer, Schema, Stream } from "effect"
* import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol, GraphQLResolver } from "effect/graphql"
*
* const RepoNodes = GraphQL.query("RepoNodes", {
* document: "query RepoNodes($ids:[ID!]!){nodes(ids:$ids){... on Repository{name}}}",
* variables: { ids: Schema.Array(Schema.String) },
* result: Schema.Struct({ nodes: Schema.Array(Schema.NullOr(Schema.Struct({ name: Schema.String }))) })
* })
*
* let calls = 0
* const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
* execute: (request) => {
* calls++
* const { ids } = request.variables as { ids: Array }
* return Effect.succeed({ body: { data: { nodes: ids.map((id) => ({ name: `repo-${id}` })) } } })
* },
* subscribe: () => Stream.empty
* })
*
* const program = Effect.gen(function*() {
* const client = yield* GraphQLClient.make(GraphQLGroup.make(RepoNodes))
* const getRepo = GraphQLResolver.request(GraphQLResolver.ordered(client.RepoNodes, {
* variables: (ids: ReadonlyArray) => ({ ids }),
* path: ["nodes"]
* }))
* const repos = yield* Effect.forEach(["1", "2", "1"], getRepo, { concurrency: "unbounded" })
* return [repos.map((repo) => repo.name), calls]
* })
*
* await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => [["repo-1", "repo-2", "repo-1"], 1]
* ```
*
* @stability experimental
* @category running
* @since 4.0.0
*/
(key: K, resolver: RequestResolver.RequestResolver>): Effect.Effect;
};
/**
* Batches lookups for fields such as `nodes(ids:)` that return one entry per
* key in input order.
*
* **Details**
*
* `variables` maps batch keys to operation variables; annotate its parameter
* to set the key type. `path` selects a list through object fields, with
* autocomplete at each level. For `__typename` unions, `typename` narrows
* the success type and treats other types as missing.
*
* Null entries, typename mismatches and null parents fail with
* `Cause.NoSuchElementError`. GraphQL errors whose paths start with
* `[...path, i]` fail only entry `i`; its `ResponseError` contains those errors
* and the response status. Other GraphQL errors fail the batch with the same
* `ResponseError` as a non-partial call. Transport, encode and decode errors
* also fail the batch. A list length mismatch fails with `DecodeError`.
*
* `pageInfo.hasNextPage: true` on the list's parent fails the batch with
* `DecodeError` to avoid reporting unreturned entries as missing. Set the
* operation's page size at least as large as `RequestResolver.batchN`.
*
* A batch uses one lookup's services. Methods requiring services also require
* `batchKey`; only lookups with keys equal under `Equal` share a batch. Return
* the value that distinguishes those services, for example
* `(context) => Context.get(context, CurrentUser).tenantId`. Methods without
* services use one batch and do not accept `batchKey`.
*
* **Gotchas**
*
* Don't return the whole context from `batchKey`. It also holds the caller's
* span, so most lookups would get a batch of their own.
*
* A middleware that reads a `Context.Reference` with a default value doesn't
* add it to the method's services. `batchKey` isn't required for it, and
* lookups with different values can share a batch.
*
* **Example** (Looking up repositories in a `Node` union)
*
* ```ts import.meta.vitest
* import { Cause, Effect, Layer, RequestResolver, Schema, Stream } from "effect"
* import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol, GraphQLResolver } from "effect/graphql"
*
* const Repository = Schema.Struct({ __typename: Schema.Literal("Repository"), nameWithOwner: Schema.String })
* const User = Schema.Struct({ __typename: Schema.Literal("User"), login: Schema.String })
*
* const RepoNodes = GraphQL.query("RepoNodes", {
* document: "query RepoNodes($ids:[ID!]!){nodes(ids:$ids){__typename ...on Repository{nameWithOwner} ...on User{login}}}",
* variables: { ids: Schema.Array(Schema.String) },
* result: Schema.Struct({ nodes: Schema.Array(Schema.NullOr(Schema.Union([Repository, User]))) })
* })
*
* const store: Record = {
* R_1: { __typename: "Repository", nameWithOwner: "Effect-TS/effect" },
* U_1: { __typename: "User", login: "tim" }
* }
* const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
* execute: (request) => {
* const { ids } = request.variables as { ids: Array }
* return Effect.succeed({ body: { data: { nodes: ids.map((id) => store[id] ?? null) } } })
* },
* subscribe: () => Stream.empty
* })
*
* const program = Effect.gen(function*() {
* const client = yield* GraphQLClient.make(GraphQLGroup.make(RepoNodes))
* const RepoById = GraphQLResolver.ordered(client.RepoNodes, {
* variables: (ids: ReadonlyArray) => ({ ids }),
* path: ["nodes"],
* typename: "Repository"
* }).pipe(RequestResolver.batchN(100))
* const getRepo = GraphQLResolver.request(RepoById)
* return yield* Effect.forEach(
* ["R_1", "U_1", "R_2"],
* (id) => getRepo(id).pipe(
* Effect.map((repo) => repo.nameWithOwner),
* Effect.catchIf(Cause.isNoSuchElementError, () => Effect.succeed("missing"))
* ),
* { concurrency: "unbounded" }
* )
* })
*
* await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => ["Effect-TS/effect", "missing", "missing"]
* ```
*
* @stability experimental
* @category resolvers
* @since 4.0.0
*/
export declare const ordered: , T extends Typenames- , P>> = never>(method: OperationMethod, options: {
readonly variables: (keys: ReadonlyArray) => GraphQL.Variables;
readonly path: Path, P>;
readonly typename?: T | undefined;
} & BatchKey) => RequestResolver.RequestResolver, P>, T>, GraphQL.Error | Cause.NoSuchElementError, GraphQL.Services>>;
/**
* Batches lookups by key for searches and collections that return results
* in any order.
*
* **Details**
*
* Write `key` before `variables` to infer the key type from its return type.
* `variables` maps batch keys to operation variables. Keys are compared with
* `Equal` and `Hash`. `path` and `typename` work as in {@link ordered};
* `key` receives the narrowed item type.
*
* Missing keys, null parents, pagination and `batchKey` behave as in
* {@link ordered}.
* Any GraphQL error fails the batch with the same `ResponseError` as a
* non-partial call, since error paths cannot be matched to keys.
*
* **Example** (Finding repositories through search)
*
* ```ts import.meta.vitest
* import { Effect, Layer, Schema, Stream } from "effect"
* import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol, GraphQLResolver } from "effect/graphql"
*
* const Repository = Schema.Struct({ __typename: Schema.Literal("Repository"), nameWithOwner: Schema.String })
* const User = Schema.Struct({ __typename: Schema.Literal("User"), login: Schema.String })
*
* const SearchRepos = GraphQL.query("SearchRepos", {
* document: "query SearchRepos($query:String!){search(query:$query,type:REPOSITORY,first:100){pageInfo{hasNextPage} nodes{__typename ...on Repository{nameWithOwner} ...on User{login}}}}",
* variables: { query: Schema.String },
* result: Schema.Struct({
* search: Schema.Struct({
* pageInfo: Schema.Struct({ hasNextPage: Schema.Boolean }),
* nodes: Schema.NullOr(Schema.Array(Schema.NullOr(Schema.Union([Repository, User]))))
* })
* })
* })
*
* const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
* execute: (request) => {
* const { query } = request.variables as { query: string }
* const nodes = query.split(" ").map((term) => ({ __typename: "Repository", nameWithOwner: term.slice(5) }))
* return Effect.succeed({ body: { data: { search: { pageInfo: { hasNextPage: false }, nodes: nodes.reverse() } } } })
* },
* subscribe: () => Stream.empty
* })
*
* const program = Effect.gen(function*() {
* const client = yield* GraphQLClient.make(GraphQLGroup.make(SearchRepos))
* const RepoByName = GraphQLResolver.findById(client.SearchRepos, {
* key: (repo) => repo.nameWithOwner,
* variables: (names) => ({ query: names.map((name) => `repo:${name}`).join(" ") }),
* path: ["search", "nodes"],
* typename: "Repository"
* })
* const repos = yield* Effect.forEach(
* ["Effect-TS/effect", "Effect-TS/website"],
* GraphQLResolver.request(RepoByName),
* { concurrency: "unbounded" }
* )
* return repos.map((repo) => repo.nameWithOwner)
* })
*
* await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => ["Effect-TS/effect", "Effect-TS/website"]
* ```
*
* @stability experimental
* @category resolvers
* @since 4.0.0
*/
export declare const findById: , T extends Typenames
- , P>> = never>(method: OperationMethod, options: {
readonly variables: (keys: ReadonlyArray) => GraphQL.Variables;
readonly path: Path, P>;
readonly key: (item: Narrow
- , P>, T>) => K;
readonly typename?: T | undefined;
} & BatchKey) => RequestResolver.RequestResolver, P>, T>, GraphQL.Error | Cause.NoSuchElementError, GraphQL.Services>>;
/**
* Batches calls to a generated query or mutation by repeating the operation
* under aliases in one request.
*
* **When to use**
*
* Use when the API has no list-key field, such as GitHub's pull requests by
* repository and number. Otherwise, {@link ordered} or {@link findById} send
* a smaller query.
*
* **Details**
*
* Use {@link request} to get `(variables) => Effect`. Variables that encode
* to the same JSON share a slice; each slice uses the operation's result Schema.
* Throws when the resolver is built if the operation has no document
* template, if an operation directive references a variable, or if a root
* selection is a fragment spread or inline fragment, since none of these can
* be repeated under aliases.
*
* Batches use `POST` even with `queryMethod: "GET"`. The default batch size
* is 10; change it with `RequestResolver.batchN`. Middleware run once per
* batch, and `batchKey` works as in {@link ordered}. Mock and custom methods
* are called once per slice instead.
*
* Errors with a matching `path[0]` fail only that slice, with the alias prefix
* removed from the error path. Errors without a matching path, transport errors
* and middleware failures fail the batch.
*
* **Gotchas**
*
* An error under a non-null root field can null the entire response, failing
* every slice. GitHub's `repository` and `pullRequest` are nullable.
*
* Middleware that hash queries must use `request.query`, not
* `operation.document`. Trusted-document allowlists don't support batching;
* query hashing does, since the same operation and slice count produce the
* same document.
*
* Mutations are allowed, but the caller cannot control root-field order.
*
* **Example** (Looking up pull requests by number)
*
* ```ts import.meta.vitest
* import { Effect, Layer, RequestResolver, Schema, Stream } from "effect"
* import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol, GraphQLResolver } from "effect/graphql"
*
* // Generated from:
* // query PullRequest($owner: String!, $name: String!, $number: Int!) {
* // repository(owner: $owner, name: $name) { pullRequest(number: $number) { title } }
* // }
* const PullRequest = GraphQL.query("PullRequest", {
* document: {
* variables: [
* { name: "owner", body: "String!" },
* { name: "name", body: "String!" },
* { name: "number", body: "Int!" }
* ],
* selections: [{
* name: "repository",
* body: ["(owner:$", "owner", ",name:$", "name", "){pullRequest(number:$", "number", "){title}}"]
* }],
* fragments: []
* },
* variables: { owner: Schema.String, name: Schema.String, number: Schema.Int },
* result: Schema.Struct({
* repository: Schema.NullOr(Schema.Struct({
* pullRequest: Schema.NullOr(Schema.Struct({ title: Schema.String }))
* }))
* })
* })
*
* // A stand-in server that answers every aliased slice
* const queries: Array = []
* const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
* execute: (request) => {
* queries.push(request.query)
* const data: Record = {}
* for (const [key, number] of Object.entries(request.variables as Record)) {
* const prefix = /^(_\d+_)number$/.exec(key)?.[1]
* if (prefix !== undefined) data[`${prefix}repository`] = { pullRequest: { title: `PR ${number}` } }
* }
* return Effect.succeed({ body: { data } })
* },
* subscribe: () => Stream.empty
* })
*
* const program = Effect.gen(function*() {
* const github = yield* GraphQLClient.make(GraphQLGroup.make(PullRequest))
* const PullRequests = GraphQLResolver.aliased(github.PullRequest).pipe(RequestResolver.batchN(20))
* const getPullRequest = GraphQLResolver.request(PullRequests)
* const results = yield* Effect.forEach(
* [1, 2, 3],
* (number) => getPullRequest({ owner: "Effect-TS", name: "effect", number }),
* { concurrency: "unbounded" }
* )
* return [results.map((result) => result.repository?.pullRequest?.title), queries.length]
* })
*
* await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => [["PR 1", "PR 2", "PR 3"], 1]
* ```
*
* @stability experimental
* @category resolvers
* @since 4.0.0
*/
export declare const aliased: (method: OperationMethod, ...[options]: {} extends BatchKey ? [options?: BatchKey] : [options: BatchKey]) => RequestResolver.RequestResolver>;
type AliasedRequest = KeyRequest, GraphQL.Result, GraphQL.Error, GraphQL.Services>;
export {};
//# sourceMappingURL=GraphQLResolver.d.ts.map