effect 4.0.2 → 4.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +24 -0
- package/CLAUDE.md +24 -0
- package/ai-docs/package.json +2 -2
- package/ai-docs/src/52_graphql/10_basics.ts +111 -0
- package/ai-docs/src/52_graphql/20_testing.ts +45 -0
- package/ai-docs/src/52_graphql/30_batching.ts +116 -0
- package/ai-docs/src/52_graphql/40_aliased-batching.ts +122 -0
- package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
- package/ai-docs/src/52_graphql/index.md +6 -0
- package/dist/Context.d.ts.map +1 -1
- package/dist/Context.js +12 -3
- package/dist/Context.js.map +1 -1
- package/dist/Crypto.d.ts +33 -5
- package/dist/Crypto.d.ts.map +1 -1
- package/dist/Crypto.js +8 -2
- package/dist/Crypto.js.map +1 -1
- package/dist/DateTime.d.ts +9 -6
- package/dist/DateTime.d.ts.map +1 -1
- package/dist/DateTime.js +9 -6
- package/dist/DateTime.js.map +1 -1
- package/dist/Duration.d.ts +6 -6
- package/dist/Duration.d.ts.map +1 -1
- package/dist/Duration.js +27 -8
- package/dist/Duration.js.map +1 -1
- package/dist/FileSystem.d.ts +44 -2
- package/dist/FileSystem.d.ts.map +1 -1
- package/dist/FileSystem.js.map +1 -1
- package/dist/RequestResolver.d.ts.map +1 -1
- package/dist/RequestResolver.js +4 -10
- package/dist/RequestResolver.js.map +1 -1
- package/dist/Scheduler.d.ts +9 -0
- package/dist/Scheduler.d.ts.map +1 -1
- package/dist/Scheduler.js +9 -0
- package/dist/Scheduler.js.map +1 -1
- package/dist/Schema.d.ts +481 -1
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +362 -4
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaGetter.d.ts +4 -0
- package/dist/SchemaGetter.d.ts.map +1 -1
- package/dist/SchemaGetter.js +52 -33
- package/dist/SchemaGetter.js.map +1 -1
- package/dist/Sink.d.ts.map +1 -1
- package/dist/Sink.js +38 -17
- package/dist/Sink.js.map +1 -1
- package/dist/Stdio.d.ts +7 -1
- package/dist/Stdio.d.ts.map +1 -1
- package/dist/Stdio.js +1 -0
- package/dist/Stdio.js.map +1 -1
- package/dist/Stream.d.ts +22 -11
- package/dist/Stream.d.ts.map +1 -1
- package/dist/Stream.js +21 -6
- package/dist/Stream.js.map +1 -1
- package/dist/Utils.js +8 -3
- package/dist/Utils.js.map +1 -1
- package/dist/ai/AnthropicStructuredOutput.js +0 -2
- package/dist/ai/AnthropicStructuredOutput.js.map +1 -1
- package/dist/ai/Chat.d.ts +2 -2
- package/dist/ai/Chat.d.ts.map +1 -1
- package/dist/ai/LanguageModel.d.ts +38 -7
- package/dist/ai/LanguageModel.d.ts.map +1 -1
- package/dist/ai/LanguageModel.js +25 -7
- package/dist/ai/LanguageModel.js.map +1 -1
- package/dist/ai/McpServer.d.ts +61 -0
- package/dist/ai/McpServer.d.ts.map +1 -1
- package/dist/ai/McpServer.js +196 -44
- package/dist/ai/McpServer.js.map +1 -1
- package/dist/ai/Prompt.d.ts.map +1 -1
- package/dist/ai/Prompt.js +28 -5
- package/dist/ai/Prompt.js.map +1 -1
- package/dist/ai/Response.d.ts +4 -3
- package/dist/ai/Response.d.ts.map +1 -1
- package/dist/ai/Response.js.map +1 -1
- package/dist/ai/internal/mcpProtocol/v2026_07_28.js +5 -0
- package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
- package/dist/ai/internal/mcpProtocol.js.map +1 -1
- package/dist/ai/internal/mcpRuntime.js +1 -0
- package/dist/ai/internal/mcpRuntime.js.map +1 -1
- package/dist/cli/CliOutput.d.ts +27 -0
- package/dist/cli/CliOutput.d.ts.map +1 -1
- package/dist/cli/CliOutput.js +33 -10
- package/dist/cli/CliOutput.js.map +1 -1
- package/dist/cli/Command.d.ts.map +1 -1
- package/dist/cli/Command.js +6 -3
- package/dist/cli/Command.js.map +1 -1
- package/dist/cli/Completions.d.ts +3 -3
- package/dist/cli/Completions.d.ts.map +1 -1
- package/dist/cli/Completions.js +8 -5
- package/dist/cli/Completions.js.map +1 -1
- package/dist/cli/GlobalFlag.d.ts +5 -4
- package/dist/cli/GlobalFlag.d.ts.map +1 -1
- package/dist/cli/GlobalFlag.js +3 -3
- package/dist/cli/GlobalFlag.js.map +1 -1
- package/dist/cli/Prompt.d.ts +17 -1
- package/dist/cli/Prompt.d.ts.map +1 -1
- package/dist/cli/Prompt.js +119 -114
- package/dist/cli/Prompt.js.map +1 -1
- package/dist/cli/internal/ansi.d.ts +0 -1
- package/dist/cli/internal/ansi.d.ts.map +1 -1
- package/dist/cli/internal/ansi.js +20 -3
- package/dist/cli/internal/ansi.js.map +1 -1
- package/dist/cli/internal/completions/powershell.d.ts +2 -0
- package/dist/cli/internal/completions/powershell.d.ts.map +1 -0
- package/dist/cli/internal/completions/powershell.js +273 -0
- package/dist/cli/internal/completions/powershell.js.map +1 -0
- package/dist/cli/internal/completions/zsh.js +3 -2
- package/dist/cli/internal/completions/zsh.js.map +1 -1
- package/dist/cli/internal/wizard.d.ts +3 -3
- package/dist/cli/internal/wizard.d.ts.map +1 -1
- package/dist/cli/internal/wizard.js +21 -18
- package/dist/cli/internal/wizard.js.map +1 -1
- package/dist/cluster/ClusterSchema.d.ts +28 -0
- package/dist/cluster/ClusterSchema.d.ts.map +1 -1
- package/dist/cluster/ClusterSchema.js +30 -0
- package/dist/cluster/ClusterSchema.js.map +1 -1
- package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
- package/dist/cluster/ClusterWorkflowEngine.js +56 -26
- package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
- package/dist/cluster/MessageStorage.d.ts +18 -3
- package/dist/cluster/MessageStorage.d.ts.map +1 -1
- package/dist/cluster/MessageStorage.js +31 -11
- package/dist/cluster/MessageStorage.js.map +1 -1
- package/dist/cluster/Runners.d.ts.map +1 -1
- package/dist/cluster/Runners.js +8 -2
- package/dist/cluster/Runners.js.map +1 -1
- package/dist/cluster/Sharding.d.ts.map +1 -1
- package/dist/cluster/Sharding.js +187 -66
- package/dist/cluster/Sharding.js.map +1 -1
- package/dist/cluster/SqlMessageStorage.d.ts +59 -19
- package/dist/cluster/SqlMessageStorage.d.ts.map +1 -1
- package/dist/cluster/SqlMessageStorage.js +84 -55
- package/dist/cluster/SqlMessageStorage.js.map +1 -1
- package/dist/cluster/SqlRunnerStorage.d.ts +68 -19
- package/dist/cluster/SqlRunnerStorage.d.ts.map +1 -1
- package/dist/cluster/SqlRunnerStorage.js +191 -113
- package/dist/cluster/SqlRunnerStorage.js.map +1 -1
- package/dist/cluster/internal/entityManager.js +104 -37
- package/dist/cluster/internal/entityManager.js.map +1 -1
- package/dist/encoding/Sse.d.ts +6 -0
- package/dist/encoding/Sse.d.ts.map +1 -1
- package/dist/encoding/Sse.js +62 -70
- package/dist/encoding/Sse.js.map +1 -1
- package/dist/graphql/GraphQL.d.ts +349 -0
- package/dist/graphql/GraphQL.d.ts.map +1 -0
- package/dist/graphql/GraphQL.js +207 -0
- package/dist/graphql/GraphQL.js.map +1 -0
- package/dist/graphql/GraphQLClient.d.ts +562 -0
- package/dist/graphql/GraphQLClient.d.ts.map +1 -0
- package/dist/graphql/GraphQLClient.js +498 -0
- package/dist/graphql/GraphQLClient.js.map +1 -0
- package/dist/graphql/GraphQLClientError.d.ts +445 -0
- package/dist/graphql/GraphQLClientError.d.ts.map +1 -0
- package/dist/graphql/GraphQLClientError.js +479 -0
- package/dist/graphql/GraphQLClientError.js.map +1 -0
- package/dist/graphql/GraphQLGroup.d.ts +118 -0
- package/dist/graphql/GraphQLGroup.d.ts.map +1 -0
- package/dist/graphql/GraphQLGroup.js +109 -0
- package/dist/graphql/GraphQLGroup.js.map +1 -0
- package/dist/graphql/GraphQLMiddleware.d.ts +249 -0
- package/dist/graphql/GraphQLMiddleware.d.ts.map +1 -0
- package/dist/graphql/GraphQLMiddleware.js +130 -0
- package/dist/graphql/GraphQLMiddleware.js.map +1 -0
- package/dist/graphql/GraphQLProtocol.d.ts +731 -0
- package/dist/graphql/GraphQLProtocol.d.ts.map +1 -0
- package/dist/graphql/GraphQLProtocol.js +719 -0
- package/dist/graphql/GraphQLProtocol.js.map +1 -0
- package/dist/graphql/GraphQLResolver.d.ts +453 -0
- package/dist/graphql/GraphQLResolver.d.ts.map +1 -0
- package/dist/graphql/GraphQLResolver.js +552 -0
- package/dist/graphql/GraphQLResolver.js.map +1 -0
- package/dist/graphql/index.d.ts +40 -0
- package/dist/graphql/index.d.ts.map +1 -0
- package/dist/graphql/index.js +41 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/http/FindMyWay/internal/router.js +3 -6
- package/dist/http/FindMyWay/internal/router.js.map +1 -1
- package/dist/http/HttpClient.d.ts +21 -0
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/http/HttpClient.js +63 -12
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/http/HttpEffect.d.ts.map +1 -1
- package/dist/http/HttpEffect.js +14 -7
- package/dist/http/HttpEffect.js.map +1 -1
- package/dist/http/HttpMiddleware.d.ts.map +1 -1
- package/dist/http/HttpMiddleware.js +31 -32
- package/dist/http/HttpMiddleware.js.map +1 -1
- package/dist/http/HttpRouter.d.ts +35 -0
- package/dist/http/HttpRouter.d.ts.map +1 -1
- package/dist/http/HttpRouter.js +35 -0
- package/dist/http/HttpRouter.js.map +1 -1
- package/dist/http/HttpServerResponse.d.ts +8 -6
- package/dist/http/HttpServerResponse.d.ts.map +1 -1
- package/dist/http/HttpServerResponse.js +27 -16
- package/dist/http/HttpServerResponse.js.map +1 -1
- package/dist/http/HttpTraceContext.d.ts.map +1 -1
- package/dist/http/HttpTraceContext.js +4 -2
- package/dist/http/HttpTraceContext.js.map +1 -1
- package/dist/http/Multipart.d.ts.map +1 -1
- package/dist/http/Multipart.js +15 -1
- package/dist/http/Multipart.js.map +1 -1
- package/dist/http-api/HttpApiScalar.d.ts +5 -0
- package/dist/http-api/HttpApiScalar.d.ts.map +1 -1
- package/dist/http-api/HttpApiScalar.js.map +1 -1
- package/dist/internal/dateTime.js +40 -35
- package/dist/internal/dateTime.js.map +1 -1
- package/dist/internal/effect.js +19 -9
- package/dist/internal/effect.js.map +1 -1
- package/dist/internal/graphql.d.ts +2 -0
- package/dist/internal/graphql.d.ts.map +1 -0
- package/dist/internal/graphql.js +195 -0
- package/dist/internal/graphql.js.map +1 -0
- package/dist/internal/graphqlHeaders.d.ts +4 -0
- package/dist/internal/graphqlHeaders.d.ts.map +1 -0
- package/dist/internal/graphqlHeaders.js +11 -0
- package/dist/internal/graphqlHeaders.js.map +1 -0
- package/dist/internal/request.js +20 -1
- package/dist/internal/request.js.map +1 -1
- package/dist/internal/schema/toCodeDocument.js +4 -1
- package/dist/internal/schema/toCodeDocument.js.map +1 -1
- package/dist/internal/schema/toJsonSchemaDocument.js +29 -5
- package/dist/internal/schema/toJsonSchemaDocument.js.map +1 -1
- package/dist/internal/version.js +1 -1
- package/dist/net/AddressResolver.d.ts +145 -0
- package/dist/net/AddressResolver.d.ts.map +1 -0
- package/dist/net/AddressResolver.js +114 -0
- package/dist/net/AddressResolver.js.map +1 -0
- package/dist/net/Dns.d.ts +596 -0
- package/dist/net/Dns.d.ts.map +1 -0
- package/dist/net/Dns.js +549 -0
- package/dist/net/Dns.js.map +1 -0
- package/dist/net/Host.d.ts +369 -0
- package/dist/net/Host.d.ts.map +1 -0
- package/dist/net/Host.js +397 -0
- package/dist/net/Host.js.map +1 -0
- package/dist/net/IpInterface.d.ts +76 -3
- package/dist/net/IpInterface.d.ts.map +1 -1
- package/dist/net/IpInterface.js +53 -4
- package/dist/net/IpInterface.js.map +1 -1
- package/dist/net/IpNetwork.d.ts +82 -8
- package/dist/net/IpNetwork.d.ts.map +1 -1
- package/dist/net/IpNetwork.js +60 -4
- package/dist/net/IpNetwork.js.map +1 -1
- package/dist/net/NetAddress.d.ts +396 -35
- package/dist/net/NetAddress.d.ts.map +1 -1
- package/dist/net/NetAddress.js +358 -34
- package/dist/net/NetAddress.js.map +1 -1
- package/dist/net/index.d.ts +15 -0
- package/dist/net/index.d.ts.map +1 -1
- package/dist/net/index.js +15 -0
- package/dist/net/index.js.map +1 -1
- package/dist/observability/OtlpMetrics.d.ts.map +1 -1
- package/dist/observability/OtlpMetrics.js +15 -21
- package/dist/observability/OtlpMetrics.js.map +1 -1
- package/dist/observability/PrometheusMetrics.js +1 -0
- package/dist/observability/PrometheusMetrics.js.map +1 -1
- package/dist/persistence/PersistedQueue.d.ts +32 -3
- package/dist/persistence/PersistedQueue.d.ts.map +1 -1
- package/dist/persistence/PersistedQueue.js +75 -16
- package/dist/persistence/PersistedQueue.js.map +1 -1
- package/dist/rpc/RpcClient.d.ts +22 -6
- package/dist/rpc/RpcClient.d.ts.map +1 -1
- package/dist/rpc/RpcClient.js +12 -5
- package/dist/rpc/RpcClient.js.map +1 -1
- package/dist/rpc/RpcServer.d.ts +6 -0
- package/dist/rpc/RpcServer.d.ts.map +1 -1
- package/dist/rpc/RpcServer.js +78 -29
- package/dist/rpc/RpcServer.js.map +1 -1
- package/dist/socket/DatagramSocket.d.ts +456 -0
- package/dist/socket/DatagramSocket.d.ts.map +1 -0
- package/dist/socket/DatagramSocket.js +535 -0
- package/dist/socket/DatagramSocket.js.map +1 -0
- package/dist/socket/Socket.d.ts +20 -1
- package/dist/socket/Socket.d.ts.map +1 -1
- package/dist/socket/Socket.js +9 -1
- package/dist/socket/Socket.js.map +1 -1
- package/dist/socket/index.d.ts +5 -0
- package/dist/socket/index.d.ts.map +1 -1
- package/dist/socket/index.js +5 -0
- package/dist/socket/index.js.map +1 -1
- package/dist/sql/Migrator.d.ts.map +1 -1
- package/dist/sql/Migrator.js +7 -2
- package/dist/sql/Migrator.js.map +1 -1
- package/dist/sql/SqlClient.d.ts +7 -1
- package/dist/sql/SqlClient.d.ts.map +1 -1
- package/dist/sql/SqlClient.js +31 -6
- package/dist/sql/SqlClient.js.map +1 -1
- package/dist/testing/TestCrypto.d.ts +73 -0
- package/dist/testing/TestCrypto.d.ts.map +1 -0
- package/dist/testing/TestCrypto.js +87 -0
- package/dist/testing/TestCrypto.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/workflow/DurableDeferred.d.ts +54 -0
- package/dist/workflow/DurableDeferred.d.ts.map +1 -1
- package/dist/workflow/DurableDeferred.js +108 -50
- package/dist/workflow/DurableDeferred.js.map +1 -1
- package/dist/workflow/WorkflowEngine.d.ts +24 -2
- package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
- package/dist/workflow/WorkflowEngine.js +13 -3
- package/dist/workflow/WorkflowEngine.js.map +1 -1
- package/package.json +6 -3
- package/src/Context.ts +12 -3
- package/src/Crypto.ts +43 -9
- package/src/DateTime.ts +9 -6
- package/src/Duration.ts +36 -16
- package/src/FileSystem.ts +48 -2
- package/src/RequestResolver.ts +4 -10
- package/src/Scheduler.ts +9 -0
- package/src/Schema.ts +677 -5
- package/src/SchemaGetter.ts +47 -35
- package/src/Sink.ts +45 -18
- package/src/Stdio.ts +9 -2
- package/src/Stream.ts +44 -15
- package/src/Utils.ts +8 -5
- package/src/ai/AnthropicStructuredOutput.ts +2 -2
- package/src/ai/Chat.ts +2 -2
- package/src/ai/LanguageModel.ts +96 -24
- package/src/ai/McpServer.ts +288 -94
- package/src/ai/Prompt.ts +24 -10
- package/src/ai/Response.ts +6 -3
- package/src/ai/internal/mcpProtocol/v2026_07_28.ts +5 -0
- package/src/ai/internal/mcpProtocol.ts +3 -0
- package/src/ai/internal/mcpRuntime.ts +2 -0
- package/src/cli/CliOutput.ts +33 -16
- package/src/cli/Command.ts +4 -3
- package/src/cli/Completions.ts +9 -6
- package/src/cli/GlobalFlag.ts +19 -11
- package/src/cli/Prompt.ts +159 -149
- package/src/cli/internal/ansi.ts +26 -3
- package/src/cli/internal/completions/powershell.ts +322 -0
- package/src/cli/internal/completions/zsh.ts +4 -2
- package/src/cli/internal/wizard.ts +34 -26
- package/src/cluster/ClusterSchema.ts +32 -0
- package/src/cluster/ClusterWorkflowEngine.ts +64 -36
- package/src/cluster/MessageStorage.ts +45 -14
- package/src/cluster/Runners.ts +8 -2
- package/src/cluster/Sharding.ts +219 -58
- package/src/cluster/SqlMessageStorage.ts +118 -84
- package/src/cluster/SqlRunnerStorage.ts +224 -128
- package/src/cluster/internal/entityManager.ts +151 -55
- package/src/encoding/Sse.ts +69 -79
- package/src/graphql/GraphQL.ts +494 -0
- package/src/graphql/GraphQLClient.ts +912 -0
- package/src/graphql/GraphQLClientError.ts +542 -0
- package/src/graphql/GraphQLGroup.ts +167 -0
- package/src/graphql/GraphQLMiddleware.ts +253 -0
- package/src/graphql/GraphQLProtocol.ts +1286 -0
- package/src/graphql/GraphQLResolver.ts +1010 -0
- package/src/graphql/index.ts +48 -0
- package/src/http/FindMyWay/internal/router.ts +4 -7
- package/src/http/HttpClient.ts +110 -12
- package/src/http/HttpEffect.ts +11 -5
- package/src/http/HttpMiddleware.ts +33 -34
- package/src/http/HttpRouter.ts +35 -0
- package/src/http/HttpServerResponse.ts +28 -16
- package/src/http/HttpTraceContext.ts +8 -2
- package/src/http/Multipart.ts +17 -1
- package/src/http-api/HttpApiScalar.ts +5 -0
- package/src/internal/dateTime.ts +13 -10
- package/src/internal/effect.ts +22 -15
- package/src/internal/graphql.ts +263 -0
- package/src/internal/graphqlHeaders.ts +11 -0
- package/src/internal/request.ts +31 -3
- package/src/internal/schema/toCodeDocument.ts +7 -1
- package/src/internal/schema/toJsonSchemaDocument.ts +34 -2
- package/src/internal/version.ts +1 -1
- package/src/net/AddressResolver.ts +222 -0
- package/src/net/Dns.ts +1035 -0
- package/src/net/Host.ts +544 -0
- package/src/net/IpInterface.ts +117 -7
- package/src/net/IpNetwork.ts +116 -17
- package/src/net/NetAddress.ts +620 -76
- package/src/net/index.ts +18 -0
- package/src/observability/OtlpMetrics.ts +12 -23
- package/src/observability/PrometheusMetrics.ts +1 -0
- package/src/persistence/PersistedQueue.ts +146 -53
- package/src/rpc/RpcClient.ts +41 -19
- package/src/rpc/RpcServer.ts +115 -40
- package/src/socket/DatagramSocket.ts +919 -0
- package/src/socket/Socket.ts +28 -2
- package/src/socket/index.ts +6 -0
- package/src/sql/Migrator.ts +21 -5
- package/src/sql/SqlClient.ts +39 -15
- package/src/testing/TestCrypto.ts +94 -0
- package/src/testing/index.ts +6 -0
- package/src/workflow/DurableDeferred.ts +160 -47
- package/src/workflow/WorkflowEngine.ts +50 -5
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A typed client with one method per operation in a `GraphQLGroup`. Queries
|
|
3
|
+
* and mutations return an `Effect`; subscriptions return a `Stream`. The
|
|
4
|
+
* client encodes variables, runs middleware, calls the `GraphQLProtocol` in
|
|
5
|
+
* context and decodes results.
|
|
6
|
+
*
|
|
7
|
+
* Retryable subscription failures restart the middleware chain on the
|
|
8
|
+
* `subscriptionRetry` schedule. {@link pages} and {@link items} provide
|
|
9
|
+
* cursor pagination. {@link makeTest} builds a mock client for tests, and
|
|
10
|
+
* {@link layerTest} provides it as a service layer. See `GraphQLProtocol`
|
|
11
|
+
* for transport options.
|
|
12
|
+
*
|
|
13
|
+
* @stability experimental
|
|
14
|
+
* @since 4.0.0
|
|
15
|
+
*/
|
|
16
|
+
import * as Cause from "../Cause.ts"
|
|
17
|
+
import * as Context from "../Context.ts"
|
|
18
|
+
import * as Duration from "../Duration.ts"
|
|
19
|
+
import * as Effect from "../Effect.ts"
|
|
20
|
+
import { identity } from "../Function.ts"
|
|
21
|
+
import * as Headers from "../http/Headers.ts"
|
|
22
|
+
import * as UrlParams from "../http/UrlParams.ts"
|
|
23
|
+
import * as internalGraphQL from "../internal/graphql.ts"
|
|
24
|
+
import { getStackTraceLimit, setStackTraceLimit } from "../internal/stackTraceLimit.ts"
|
|
25
|
+
import * as Layer from "../Layer.ts"
|
|
26
|
+
import * as Option from "../Option.ts"
|
|
27
|
+
import type * as Pull from "../Pull.ts"
|
|
28
|
+
import * as Schedule from "../Schedule.ts"
|
|
29
|
+
import * as Schema from "../Schema.ts"
|
|
30
|
+
import * as Stream from "../Stream.ts"
|
|
31
|
+
import * as Struct from "../Struct.ts"
|
|
32
|
+
import type * as Types from "../Types.ts"
|
|
33
|
+
import type * as GraphQL from "./GraphQL.ts"
|
|
34
|
+
import {
|
|
35
|
+
DecodeError,
|
|
36
|
+
GraphQLClientError,
|
|
37
|
+
type GraphQLError,
|
|
38
|
+
isGraphQLClientError,
|
|
39
|
+
PaginationError
|
|
40
|
+
} from "./GraphQLClientError.ts"
|
|
41
|
+
import type * as GraphQLGroup from "./GraphQLGroup.ts"
|
|
42
|
+
import type * as GraphQLMiddleware from "./GraphQLMiddleware.ts"
|
|
43
|
+
import { ExecutionResult, GraphQLProtocol, type GraphQLRequest } from "./GraphQLProtocol.ts"
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decoded `data` and response `errors` for a `{ partial: true }` call.
|
|
47
|
+
* The errors array may be empty.
|
|
48
|
+
*
|
|
49
|
+
* @stability experimental
|
|
50
|
+
* @category models
|
|
51
|
+
* @since 4.0.0
|
|
52
|
+
*/
|
|
53
|
+
export interface PartialResult<A> {
|
|
54
|
+
readonly data: A
|
|
55
|
+
readonly errors: ReadonlyArray<GraphQLError>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type Args<Variables, Options> = {} extends Variables ? [variables?: Variables, options?: Options]
|
|
59
|
+
: [variables: Variables, options?: Options]
|
|
60
|
+
|
|
61
|
+
type ArgsWithOptions<Variables, Options> = {} extends Variables ? [variables: Variables | undefined, options: Options]
|
|
62
|
+
: [variables: Variables, options: Options]
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The client method for a query or mutation.
|
|
66
|
+
*
|
|
67
|
+
* **Details**
|
|
68
|
+
*
|
|
69
|
+
* Called as `(variables?, options?)`. Without `partial`, or with
|
|
70
|
+
* `partial: false`, it returns the decoded result and fails with a
|
|
71
|
+
* `ResponseError` on any `errors`. With `partial: true` it returns
|
|
72
|
+
* `{ data, errors }`. `partial` must be a literal `true` or `false`, not
|
|
73
|
+
* an arbitrary `boolean`.
|
|
74
|
+
*
|
|
75
|
+
* An operation without variables is called as `client.Viewer()` or, with
|
|
76
|
+
* options, `client.Viewer(undefined, { headers })`.
|
|
77
|
+
*
|
|
78
|
+
* `headers` and `urlParams` are sent by the HTTP transport, ignored over
|
|
79
|
+
* graphql-ws, and visible to middleware either way as
|
|
80
|
+
* `GraphQLRequest.headers` and `GraphQLRequest.urlParams`. Header options
|
|
81
|
+
* accept `Headers.Input` and are normalized to `Headers.Headers` before
|
|
82
|
+
* middleware runs. `context` provides
|
|
83
|
+
* services for this call only, and removes them from the method's
|
|
84
|
+
* requirements.
|
|
85
|
+
*
|
|
86
|
+
* @stability experimental
|
|
87
|
+
* @category models
|
|
88
|
+
* @since 4.0.0
|
|
89
|
+
*/
|
|
90
|
+
export interface OperationMethod<Op extends GraphQL.Any> {
|
|
91
|
+
/** The operation this method runs. */
|
|
92
|
+
readonly operation: Op
|
|
93
|
+
<R2 = never>(
|
|
94
|
+
...args: ArgsWithOptions<GraphQL.Variables<Op>, {
|
|
95
|
+
readonly headers?: Headers.Input | undefined
|
|
96
|
+
readonly urlParams?: UrlParams.Input | undefined
|
|
97
|
+
readonly context?: Context.Context<R2> | undefined
|
|
98
|
+
readonly partial: true
|
|
99
|
+
}>
|
|
100
|
+
): Effect.Effect<PartialResult<GraphQL.Result<Op>>, GraphQL.Error<Op>, Exclude<GraphQL.Services<Op>, R2>>
|
|
101
|
+
<R2 = never>(
|
|
102
|
+
...args: Args<GraphQL.Variables<Op>, {
|
|
103
|
+
readonly headers?: Headers.Input | undefined
|
|
104
|
+
readonly urlParams?: UrlParams.Input | undefined
|
|
105
|
+
readonly context?: Context.Context<R2> | undefined
|
|
106
|
+
readonly partial?: false | undefined
|
|
107
|
+
}>
|
|
108
|
+
): Effect.Effect<GraphQL.Result<Op>, GraphQL.Error<Op>, Exclude<GraphQL.Services<Op>, R2>>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The client method for a subscription. It returns a `Stream` of decoded
|
|
113
|
+
* events; an event with `errors` fails the stream with a `ResponseError`,
|
|
114
|
+
* unless that error is retryable and the client resubscribes (see
|
|
115
|
+
* {@link make}). `headers`, `urlParams` and `context` work as for
|
|
116
|
+
* {@link OperationMethod}.
|
|
117
|
+
*
|
|
118
|
+
* @stability experimental
|
|
119
|
+
* @category models
|
|
120
|
+
* @since 4.0.0
|
|
121
|
+
*/
|
|
122
|
+
export interface SubscriptionMethod<Op extends GraphQL.Any> {
|
|
123
|
+
/** The operation this method runs. */
|
|
124
|
+
readonly operation: Op
|
|
125
|
+
<R2 = never>(
|
|
126
|
+
...args: Args<GraphQL.Variables<Op>, {
|
|
127
|
+
readonly headers?: Headers.Input | undefined
|
|
128
|
+
readonly urlParams?: UrlParams.Input | undefined
|
|
129
|
+
readonly context?: Context.Context<R2> | undefined
|
|
130
|
+
}>
|
|
131
|
+
): Stream.Stream<GraphQL.Result<Op>, GraphQL.Error<Op>, Exclude<GraphQL.Services<Op>, R2>>
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The client method type for an operation, by its kind.
|
|
136
|
+
*
|
|
137
|
+
* @stability experimental
|
|
138
|
+
* @category models
|
|
139
|
+
* @since 4.0.0
|
|
140
|
+
*/
|
|
141
|
+
export type Method<Op extends GraphQL.Any> = Op["kind"] extends "subscription" ? SubscriptionMethod<Op>
|
|
142
|
+
: OperationMethod<Op>
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A client: one method per operation, keyed by operation name.
|
|
146
|
+
*
|
|
147
|
+
* @stability experimental
|
|
148
|
+
* @category models
|
|
149
|
+
* @since 4.0.0
|
|
150
|
+
*/
|
|
151
|
+
export type GraphQLClient<Ops extends GraphQL.Any> = {
|
|
152
|
+
readonly [Op in Ops as Op["name"]]: Method<Op>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Derives the client type for all operations in a `GraphQLGroup`.
|
|
157
|
+
*
|
|
158
|
+
* @stability experimental
|
|
159
|
+
* @category utility types
|
|
160
|
+
* @since 4.0.0
|
|
161
|
+
*/
|
|
162
|
+
export type FromGroup<Group> = GraphQLClient<GraphQLGroup.Operations<Group>>
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The default subscription retry schedule: exponential from 500 milliseconds
|
|
166
|
+
* with factor 1.5, capped at 5 seconds, and never shorter than the server's
|
|
167
|
+
* `retryAfter`.
|
|
168
|
+
*
|
|
169
|
+
* @stability experimental
|
|
170
|
+
* @category constants
|
|
171
|
+
* @since 4.0.0
|
|
172
|
+
*/
|
|
173
|
+
export const defaultSubscriptionRetry: Schedule.Schedule<Duration.Duration, GraphQLClientError> = Schedule.exponential(
|
|
174
|
+
"500 millis",
|
|
175
|
+
1.5
|
|
176
|
+
).pipe(
|
|
177
|
+
Schedule.setInputType<GraphQLClientError>(),
|
|
178
|
+
Schedule.modifyDelay(({ duration, input }) => {
|
|
179
|
+
const capped = Duration.min(duration, Duration.seconds(5))
|
|
180
|
+
return Effect.succeed(input.retryAfter === undefined ? capped : Duration.max(capped, input.retryAfter))
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Builds a client from a group, requiring `GraphQLProtocol` and every
|
|
186
|
+
* middleware service attached to the group or its operations.
|
|
187
|
+
*
|
|
188
|
+
* **Details**
|
|
189
|
+
*
|
|
190
|
+
* Group middleware runs outside operation middleware. Within each level,
|
|
191
|
+
* middleware run in the order attached.
|
|
192
|
+
*
|
|
193
|
+
* `subscriptionRetry` decides how a subscription is retried after it fails
|
|
194
|
+
* with a retryable error: usually a `TransportError`, but also a
|
|
195
|
+
* `ResponseError` whose response carried a valid `Retry-After`, including an
|
|
196
|
+
* event with `errors` on a graphql-sse stream whose response had that header.
|
|
197
|
+
* It defaults to {@link defaultSubscriptionRetry}. Fatal errors are never
|
|
198
|
+
* retried, whatever the schedule. A retry starts a new subscription, so the
|
|
199
|
+
* server may send events again that the stream already emitted.
|
|
200
|
+
*
|
|
201
|
+
* **Example** (A client as a service)
|
|
202
|
+
*
|
|
203
|
+
* ```ts import.meta.vitest
|
|
204
|
+
* import { Context, Effect, Layer, Schema, Stream } from "effect"
|
|
205
|
+
* import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
|
|
206
|
+
*
|
|
207
|
+
* // What the generator emits for viewer.graphql
|
|
208
|
+
* const Viewer = GraphQL.query("Viewer", {
|
|
209
|
+
* document: "query Viewer{viewer{login}}",
|
|
210
|
+
* result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) })
|
|
211
|
+
* })
|
|
212
|
+
* const ViewerGraphQLGroup = GraphQLGroup.make(Viewer)
|
|
213
|
+
*
|
|
214
|
+
* class GitHub extends Context.Service<GitHub>()("app/GitHub", {
|
|
215
|
+
* make: GraphQLClient.make(ViewerGraphQLGroup)
|
|
216
|
+
* }) {}
|
|
217
|
+
*
|
|
218
|
+
* // A scripted transport; use GraphQLProtocol.layerHttp({ url }) for a real server
|
|
219
|
+
* const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
|
|
220
|
+
* execute: () => Effect.succeed({ body: { data: { viewer: { login: "tim" } } } }),
|
|
221
|
+
* subscribe: () => Stream.empty
|
|
222
|
+
* })
|
|
223
|
+
*
|
|
224
|
+
* const program = GitHub.use((github) => github.Viewer()).pipe(
|
|
225
|
+
* Effect.provide(Layer.effect(GitHub, GitHub.make).pipe(Layer.provide(Scripted)))
|
|
226
|
+
* )
|
|
227
|
+
*
|
|
228
|
+
* await Effect.runPromise(program) // => { viewer: { login: "tim" } }
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* **Example** (Partial results)
|
|
232
|
+
*
|
|
233
|
+
* ```ts import.meta.vitest
|
|
234
|
+
* import { Effect, Layer, Schema, Stream } from "effect"
|
|
235
|
+
* import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
|
|
236
|
+
*
|
|
237
|
+
* const Repos = GraphQL.query("Repos", {
|
|
238
|
+
* document: "query Repos{a:repository(owner:\"x\",name:\"a\"){name} b:repository(owner:\"x\",name:\"b\"){name}}",
|
|
239
|
+
* result: Schema.Struct({
|
|
240
|
+
* a: Schema.NullOr(Schema.Struct({ name: Schema.String })),
|
|
241
|
+
* b: Schema.NullOr(Schema.Struct({ name: Schema.String }))
|
|
242
|
+
* })
|
|
243
|
+
* })
|
|
244
|
+
*
|
|
245
|
+
* const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
|
|
246
|
+
* execute: () =>
|
|
247
|
+
* Effect.succeed({
|
|
248
|
+
* body: {
|
|
249
|
+
* data: { a: { name: "a" }, b: null },
|
|
250
|
+
* errors: [{ message: "Could not resolve to a Repository", path: ["b"] }]
|
|
251
|
+
* }
|
|
252
|
+
* }),
|
|
253
|
+
* subscribe: () => Stream.empty
|
|
254
|
+
* })
|
|
255
|
+
*
|
|
256
|
+
* const program = Effect.gen(function*() {
|
|
257
|
+
* const client = yield* GraphQLClient.make(GraphQLGroup.make(Repos))
|
|
258
|
+
* // The default call fails with a ResponseError because `errors` is non-empty
|
|
259
|
+
* const failed = yield* Effect.flip(client.Repos())
|
|
260
|
+
* // With partial: true the decoded data comes back together with the errors
|
|
261
|
+
* const { data, errors } = yield* client.Repos(undefined, { partial: true })
|
|
262
|
+
* return [failed.reason._tag, data.a?.name, errors[0].path?.[0]]
|
|
263
|
+
* })
|
|
264
|
+
*
|
|
265
|
+
* await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => ["ResponseError", "a", "b"]
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* @stability experimental
|
|
269
|
+
* @category constructors
|
|
270
|
+
* @since 4.0.0
|
|
271
|
+
*/
|
|
272
|
+
export const make = <Ops extends GraphQL.Any>(
|
|
273
|
+
group: GraphQLGroup.GraphQLGroup<Ops>,
|
|
274
|
+
options?: {
|
|
275
|
+
readonly subscriptionRetry?: Schedule.Schedule<unknown, GraphQLClientError> | undefined
|
|
276
|
+
} | undefined
|
|
277
|
+
): Effect.Effect<
|
|
278
|
+
GraphQLClient<Ops>,
|
|
279
|
+
never,
|
|
280
|
+
GraphQLProtocol | GraphQLMiddleware.Identifier<GraphQL.Middleware<Ops>>
|
|
281
|
+
> =>
|
|
282
|
+
Effect.gen(function*() {
|
|
283
|
+
const protocol = yield* GraphQLProtocol
|
|
284
|
+
const context = yield* Effect.context<never>()
|
|
285
|
+
const retryPolicy = retryableOnly(options?.subscriptionRetry ?? defaultSubscriptionRetry)
|
|
286
|
+
return fromGroup(group, (operation) =>
|
|
287
|
+
makeMethod(operation, middlewareChain(context, group, operation), protocol, retryPolicy))
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// Type boundary: a mapping over the operations cannot prove the mapped type's
|
|
291
|
+
// name/kind relationship.
|
|
292
|
+
const fromGroup = <Ops extends GraphQL.Any>(
|
|
293
|
+
group: GraphQLGroup.GraphQLGroup<Ops>,
|
|
294
|
+
f: (operation: Ops) => unknown
|
|
295
|
+
): GraphQLClient<Ops> =>
|
|
296
|
+
Object.fromEntries(group.operations.map((operation) => [operation.name, f(operation)])) as GraphQLClient<Ops>
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The middleware implementations an operation runs through, outermost first,
|
|
300
|
+
* typed with the errors and requirements its middleware declared.
|
|
301
|
+
*/
|
|
302
|
+
type MiddlewareChain<Op extends GraphQL.Any> = ReadonlyArray<
|
|
303
|
+
GraphQLMiddleware.Implementation<
|
|
304
|
+
GraphQLMiddleware.Error<GraphQL.Middleware<Op>>,
|
|
305
|
+
GraphQLMiddleware.Requires<GraphQL.Middleware<Op>>
|
|
306
|
+
>
|
|
307
|
+
>
|
|
308
|
+
|
|
309
|
+
// Runtime tags erase middleware types; `make` requires all their identifiers.
|
|
310
|
+
const middlewareChain = <Op extends GraphQL.Any>(
|
|
311
|
+
context: Context.Context<never>,
|
|
312
|
+
group: GraphQLGroup.GraphQLGroup<Op>,
|
|
313
|
+
operation: Op
|
|
314
|
+
): MiddlewareChain<Op> => [...group.middlewares, ...operation.middlewares].map((tag) => Context.getUnsafe(context, tag))
|
|
315
|
+
|
|
316
|
+
// Steps `schedule` only for retryable `GraphQLClientError`s. Middleware
|
|
317
|
+
// errors and fatal errors end the retries before reaching it, so a custom
|
|
318
|
+
// schedule can rely on its input being a `GraphQLClientError`.
|
|
319
|
+
const retryableOnly = <Output, Error, Env>(
|
|
320
|
+
schedule: Schedule.Schedule<Output, GraphQLClientError, Error, Env>
|
|
321
|
+
): Schedule.Schedule<Output | undefined, unknown, Error, Env> =>
|
|
322
|
+
Schedule.fromStep(Effect.map(
|
|
323
|
+
Schedule.toStep(schedule),
|
|
324
|
+
(step) =>
|
|
325
|
+
(now: number, input: unknown): Pull.Pull<[Output | undefined, Duration.Duration], Error, Output | undefined, Env> =>
|
|
326
|
+
isGraphQLClientError(input) && input.isRetryable ? step(now, input) : Cause.done(undefined)
|
|
327
|
+
))
|
|
328
|
+
|
|
329
|
+
// `status` and `headers` are transport metadata, never read from the body.
|
|
330
|
+
const decodeExecutionResult = Schema.decodeUnknownEffect(ExecutionResult.mapFields(Struct.omit(["status", "headers"])))
|
|
331
|
+
|
|
332
|
+
interface CallOptions<R2 = never> {
|
|
333
|
+
readonly headers?: Headers.Input | undefined
|
|
334
|
+
readonly urlParams?: UrlParams.Input | undefined
|
|
335
|
+
readonly context?: Context.Context<R2> | undefined
|
|
336
|
+
readonly partial?: boolean | undefined
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Type boundary: a call without `context` has `R2 = never` unless the caller
|
|
340
|
+
// names `R2` explicitly, so an empty context stands in for `Context<R2>`.
|
|
341
|
+
const callContext = <R2>(options: CallOptions<R2> | undefined): Context.Context<R2> =>
|
|
342
|
+
options?.context ?? (Context.empty() as Context.Context<R2>)
|
|
343
|
+
|
|
344
|
+
const makeMethod = <Op extends GraphQL.Any>(
|
|
345
|
+
operation: Op,
|
|
346
|
+
chain: MiddlewareChain<Op>,
|
|
347
|
+
protocol: GraphQLProtocol["Service"],
|
|
348
|
+
retryPolicy: Schedule.Schedule<unknown, unknown>
|
|
349
|
+
): OperationMethod<Op> | SubscriptionMethod<Op> => {
|
|
350
|
+
const { decodePartial, decodeStrict, encodeVariables, fail, schemaFail } = internalGraphQL.makeCodec(operation)
|
|
351
|
+
|
|
352
|
+
const notGraphQL = schemaFail((description) =>
|
|
353
|
+
new DecodeError({ description: `Not a GraphQL response: ${description}` })
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
const buildRequest = (variables: GraphQL.Variables<Op> | undefined, options: CallOptions | undefined) =>
|
|
357
|
+
encodeVariables(variables).pipe(
|
|
358
|
+
Effect.map((encoded): GraphQLRequest => ({
|
|
359
|
+
kind: operation.kind,
|
|
360
|
+
query: operation.document,
|
|
361
|
+
operationName: operation.name,
|
|
362
|
+
variables: encoded,
|
|
363
|
+
headers: Headers.fromInput(options?.headers),
|
|
364
|
+
urlParams: options?.urlParams === undefined ? UrlParams.empty : UrlParams.fromInput(options.urlParams)
|
|
365
|
+
}))
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const decodeResponse = ({ body, ...metadata }: {
|
|
369
|
+
readonly body: unknown
|
|
370
|
+
readonly status?: number | undefined
|
|
371
|
+
readonly headers?: Headers.Headers | undefined
|
|
372
|
+
}) =>
|
|
373
|
+
decodeExecutionResult(body).pipe(
|
|
374
|
+
Effect.map((result): ExecutionResult => ({ ...result, ...metadata })),
|
|
375
|
+
Effect.catch(notGraphQL)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
// Type boundary: a middleware sees the errors of the middleware further in
|
|
379
|
+
// as `Types.unhandled` (as in `RpcMiddleware`), and none of their
|
|
380
|
+
// requirements. Those errors are part of `GraphQL.Error<Op>`, so the chain
|
|
381
|
+
// restates its type here. Force POST only at the transport boundary.
|
|
382
|
+
const execute = (
|
|
383
|
+
index: number,
|
|
384
|
+
request: GraphQLRequest,
|
|
385
|
+
forcePost: boolean
|
|
386
|
+
): Effect.Effect<ExecutionResult, GraphQL.Error<Op>, GraphQLMiddleware.Requires<GraphQL.Middleware<Op>>> =>
|
|
387
|
+
index === chain.length
|
|
388
|
+
? protocol.execute(request).pipe(
|
|
389
|
+
forcePost ? Effect.provideService(internalGraphQL.ForcePost, true) : identity,
|
|
390
|
+
Effect.mapError(fail),
|
|
391
|
+
Effect.flatMap(decodeResponse)
|
|
392
|
+
)
|
|
393
|
+
: chain[index].execute({
|
|
394
|
+
operation,
|
|
395
|
+
request,
|
|
396
|
+
next: (request) =>
|
|
397
|
+
execute(index + 1, request, forcePost) as Effect.Effect<
|
|
398
|
+
ExecutionResult,
|
|
399
|
+
GraphQLClientError | Types.unhandled
|
|
400
|
+
>
|
|
401
|
+
}) as Effect.Effect<ExecutionResult, GraphQL.Error<Op>, GraphQLMiddleware.Requires<GraphQL.Middleware<Op>>>
|
|
402
|
+
|
|
403
|
+
const subscribe = (
|
|
404
|
+
index: number,
|
|
405
|
+
request: GraphQLRequest
|
|
406
|
+
): Stream.Stream<ExecutionResult, GraphQL.Error<Op>, GraphQLMiddleware.Requires<GraphQL.Middleware<Op>>> =>
|
|
407
|
+
index === chain.length
|
|
408
|
+
? protocol.subscribe(request).pipe(
|
|
409
|
+
Stream.mapError(fail),
|
|
410
|
+
Stream.mapEffect(decodeResponse)
|
|
411
|
+
)
|
|
412
|
+
: chain[index].subscribe({
|
|
413
|
+
operation,
|
|
414
|
+
request,
|
|
415
|
+
next: (request) =>
|
|
416
|
+
subscribe(index + 1, request) as Stream.Stream<ExecutionResult, GraphQLClientError | Types.unhandled>
|
|
417
|
+
}) as Stream.Stream<ExecutionResult, GraphQL.Error<Op>, GraphQLMiddleware.Requires<GraphQL.Middleware<Op>>>
|
|
418
|
+
|
|
419
|
+
if (operation.kind === "subscription") {
|
|
420
|
+
const subscriptionMethod = <R2 = never>(...[variables, options]: Args<GraphQL.Variables<Op>, CallOptions<R2>>) =>
|
|
421
|
+
Stream.unwrap(Effect.map(buildRequest(variables, options), (request) => subscribe(0, request))).pipe(
|
|
422
|
+
Stream.mapEffect(decodeStrict),
|
|
423
|
+
Stream.retry(retryPolicy),
|
|
424
|
+
Stream.provideContext(callContext(options))
|
|
425
|
+
)
|
|
426
|
+
return Object.assign(subscriptionMethod, { operation })
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function method<R2 = never>(
|
|
430
|
+
...args: ArgsWithOptions<GraphQL.Variables<Op>, CallOptions<R2> & { readonly partial: true }>
|
|
431
|
+
): Effect.Effect<PartialResult<GraphQL.Result<Op>>, GraphQL.Error<Op>, Exclude<GraphQL.Services<Op>, R2>>
|
|
432
|
+
function method<R2 = never>(
|
|
433
|
+
...args: Args<GraphQL.Variables<Op>, CallOptions<R2> & { readonly partial?: false | undefined }>
|
|
434
|
+
): Effect.Effect<GraphQL.Result<Op>, GraphQL.Error<Op>, Exclude<GraphQL.Services<Op>, R2>>
|
|
435
|
+
function method<R2>(
|
|
436
|
+
variables?: GraphQL.Variables<Op>,
|
|
437
|
+
options?: CallOptions<R2>
|
|
438
|
+
): Effect.Effect<
|
|
439
|
+
GraphQL.Result<Op> | PartialResult<GraphQL.Result<Op>>,
|
|
440
|
+
GraphQL.Error<Op>,
|
|
441
|
+
Exclude<GraphQL.Services<Op>, R2>
|
|
442
|
+
> {
|
|
443
|
+
return buildRequest(variables, options).pipe(
|
|
444
|
+
Effect.flatMap((request) => execute(0, request, false)),
|
|
445
|
+
Effect.flatMap(
|
|
446
|
+
(result): Effect.Effect<
|
|
447
|
+
GraphQL.Result<Op> | PartialResult<GraphQL.Result<Op>>,
|
|
448
|
+
GraphQLClientError,
|
|
449
|
+
Op["result"]["DecodingServices"]
|
|
450
|
+
> => options?.partial === true ? decodePartial(result) : decodeStrict(result)
|
|
451
|
+
),
|
|
452
|
+
Effect.provideContext(callContext(options))
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
internalGraphQL.rawExecutors.set(method, (request) => execute(0, request, true))
|
|
456
|
+
return Object.assign(method, { operation })
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The `pageInfo` a paged connection must select.
|
|
461
|
+
*
|
|
462
|
+
* @stability experimental
|
|
463
|
+
* @category paging
|
|
464
|
+
* @since 4.0.0
|
|
465
|
+
*/
|
|
466
|
+
export interface PageInfo {
|
|
467
|
+
readonly hasNextPage: boolean
|
|
468
|
+
readonly endCursor: string | null
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* What the `connection` getter of {@link pages} must return.
|
|
473
|
+
*
|
|
474
|
+
* @stability experimental
|
|
475
|
+
* @category paging
|
|
476
|
+
* @since 4.0.0
|
|
477
|
+
*/
|
|
478
|
+
export interface Connection {
|
|
479
|
+
readonly pageInfo: PageInfo
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* What the `connection` getter of {@link items} must return: a connection
|
|
484
|
+
* that also selects `nodes`.
|
|
485
|
+
*
|
|
486
|
+
* @stability experimental
|
|
487
|
+
* @category paging
|
|
488
|
+
* @since 4.0.0
|
|
489
|
+
*/
|
|
490
|
+
export interface NodesConnection<Node> extends Connection {
|
|
491
|
+
readonly nodes: ReadonlyArray<Node> | null
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The type `variables` is given when the operation has no `$after` variable.
|
|
496
|
+
* Nothing is assignable to it, so the mistake is a compile error at the call.
|
|
497
|
+
*
|
|
498
|
+
* @stability experimental
|
|
499
|
+
* @category paging
|
|
500
|
+
* @since 4.0.0
|
|
501
|
+
*/
|
|
502
|
+
export interface MissingAfterVariable {
|
|
503
|
+
readonly "effect/graphql":
|
|
504
|
+
"GraphQLClient.pages requires the operation to declare a nullable `$after: String` variable"
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* The type `variables` is given when the operation's `after` variable is
|
|
509
|
+
* required or does not accept a `string`. The helper leaves `after` out on the
|
|
510
|
+
* first page and sends the previous `endCursor` afterwards, so it can only
|
|
511
|
+
* drive an optional variable that takes a string.
|
|
512
|
+
*
|
|
513
|
+
* @stability experimental
|
|
514
|
+
* @category paging
|
|
515
|
+
* @since 4.0.0
|
|
516
|
+
*/
|
|
517
|
+
export interface InvalidAfterVariable {
|
|
518
|
+
readonly "effect/graphql": "GraphQLClient.pages requires the `after` variable to be optional and to accept a string"
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The `variables` accepted by {@link pages} and {@link items}: the method's
|
|
523
|
+
* variables without `after`, which the helper supplies.
|
|
524
|
+
*
|
|
525
|
+
* **Details**
|
|
526
|
+
*
|
|
527
|
+
* The operation's `after` variable must be optional and accept a `string`.
|
|
528
|
+
* `after?: string | null`, from a nullable `$after: String`, and
|
|
529
|
+
* `after?: string`, from a `$after: String!` with a default value, are both
|
|
530
|
+
* accepted. A required `after` gives {@link InvalidAfterVariable}, as does one
|
|
531
|
+
* that does not accept a `string`; no `after` at all gives
|
|
532
|
+
* {@link MissingAfterVariable}.
|
|
533
|
+
*
|
|
534
|
+
* @stability experimental
|
|
535
|
+
* @category paging
|
|
536
|
+
* @since 4.0.0
|
|
537
|
+
*/
|
|
538
|
+
export type PagingVariables<Variables> = "after" extends keyof Variables
|
|
539
|
+
? {} extends Pick<Variables, "after"> ? string extends Variables["after"] ? Omit<Variables, "after">
|
|
540
|
+
: InvalidAfterVariable
|
|
541
|
+
: InvalidAfterVariable
|
|
542
|
+
: MissingAfterVariable
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Pages forward through a cursor connection, emitting one connection per
|
|
546
|
+
* page.
|
|
547
|
+
*
|
|
548
|
+
* **Details**
|
|
549
|
+
*
|
|
550
|
+
* Requires an optional string `$after` variable (see {@link PagingVariables}).
|
|
551
|
+
* The first request omits `after`; subsequent requests use the previous
|
|
552
|
+
* `endCursor`. Pages are fetched only when pulled, so `Stream.take` stops
|
|
553
|
+
* further requests. Middleware runs on each page.
|
|
554
|
+
*
|
|
555
|
+
* `connection` extracts the connection from each result. `options` is
|
|
556
|
+
* forwarded to every call but cannot include `partial`: errors fail the stream.
|
|
557
|
+
* Its `headers` accept `Headers.Input`, just as for operation calls.
|
|
558
|
+
*
|
|
559
|
+
* The stream ends after a page whose `hasNextPage` is `false`. A `null`
|
|
560
|
+
* connection on the first page gives an empty stream; on a later page it
|
|
561
|
+
* fails with a `PaginationError`, as does `hasNextPage: true` with a `null`
|
|
562
|
+
* or unchanged `endCursor`.
|
|
563
|
+
*
|
|
564
|
+
* Root fields that come with every page, such as GitHub's `rateLimit`, are for
|
|
565
|
+
* middleware to read from the raw `ExecutionResult`.
|
|
566
|
+
*
|
|
567
|
+
* **Example** (Paging issues, two per page)
|
|
568
|
+
*
|
|
569
|
+
* ```ts import.meta.vitest
|
|
570
|
+
* import { Effect, Layer, Schema, Stream } from "effect"
|
|
571
|
+
* import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
|
|
572
|
+
*
|
|
573
|
+
* const RepoIssues = GraphQL.query("RepoIssues", {
|
|
574
|
+
* document: "query RepoIssues($owner:String!,$name:String!,$after:String){repository(owner:$owner,name:$name){issues(first:2,after:$after){pageInfo{hasNextPage endCursor}nodes{number}}}}",
|
|
575
|
+
* variables: { owner: Schema.String, name: Schema.String, after: Schema.optional(Schema.NullOr(Schema.String)) },
|
|
576
|
+
* result: Schema.Struct({
|
|
577
|
+
* repository: Schema.NullOr(Schema.Struct({
|
|
578
|
+
* issues: Schema.Struct({
|
|
579
|
+
* pageInfo: Schema.Struct({ hasNextPage: Schema.Boolean, endCursor: Schema.NullOr(Schema.String) }),
|
|
580
|
+
* nodes: Schema.NullOr(Schema.Array(Schema.NullOr(Schema.Struct({ number: Schema.Int }))))
|
|
581
|
+
* })
|
|
582
|
+
* }))
|
|
583
|
+
* })
|
|
584
|
+
* })
|
|
585
|
+
*
|
|
586
|
+
* // A scripted server with two pages
|
|
587
|
+
* const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
|
|
588
|
+
* execute: (request) => {
|
|
589
|
+
* const after = (request.variables as { after?: string }).after
|
|
590
|
+
* const page = after === undefined
|
|
591
|
+
* ? { pageInfo: { hasNextPage: true, endCursor: "c1" }, nodes: [{ number: 1 }, { number: 2 }] }
|
|
592
|
+
* : { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [{ number: 3 }] }
|
|
593
|
+
* return Effect.succeed({ body: { data: { repository: { issues: page } } } })
|
|
594
|
+
* },
|
|
595
|
+
* subscribe: () => Stream.empty
|
|
596
|
+
* })
|
|
597
|
+
*
|
|
598
|
+
* const program = Effect.gen(function*() {
|
|
599
|
+
* const client = yield* GraphQLClient.make(GraphQLGroup.make(RepoIssues))
|
|
600
|
+
* const pages = GraphQLClient.pages(client.RepoIssues, {
|
|
601
|
+
* variables: { owner: "Effect-TS", name: "effect" },
|
|
602
|
+
* connection: (result) => result.repository?.issues
|
|
603
|
+
* })
|
|
604
|
+
* return yield* Stream.runCollect(Stream.map(pages, (page) => page.pageInfo.endCursor))
|
|
605
|
+
* })
|
|
606
|
+
*
|
|
607
|
+
* await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => ["c1", null]
|
|
608
|
+
* ```
|
|
609
|
+
*
|
|
610
|
+
* @stability experimental
|
|
611
|
+
* @category paging
|
|
612
|
+
* @since 4.0.0
|
|
613
|
+
*/
|
|
614
|
+
export const pages = <Op extends GraphQL.Any, C extends Connection, R2 = never>(
|
|
615
|
+
method: OperationMethod<Op>,
|
|
616
|
+
options: {
|
|
617
|
+
readonly variables: PagingVariables<GraphQL.Variables<Op>>
|
|
618
|
+
readonly connection: (result: GraphQL.Result<Op>) => C | null | undefined
|
|
619
|
+
readonly options?: {
|
|
620
|
+
readonly headers?: Headers.Input | undefined
|
|
621
|
+
readonly urlParams?: UrlParams.Input | undefined
|
|
622
|
+
readonly context?: Context.Context<R2> | undefined
|
|
623
|
+
} | undefined
|
|
624
|
+
}
|
|
625
|
+
): Stream.Stream<C, GraphQL.Error<Op>, Exclude<GraphQL.Services<Op>, R2>> => {
|
|
626
|
+
// Type boundary: `method`'s strict overload, with `variables` widened.
|
|
627
|
+
// `PagingVariables` has already checked that `options.variables` plus an
|
|
628
|
+
// optional string `after` are the operation's variables, but the overloads'
|
|
629
|
+
// conditional parameter types over a generic `Op` cannot carry that here.
|
|
630
|
+
const call = method as unknown as (
|
|
631
|
+
variables: unknown,
|
|
632
|
+
options: CallOptions<R2> | undefined
|
|
633
|
+
) => Effect.Effect<GraphQL.Result<Op>, GraphQL.Error<Op>, Exclude<GraphQL.Services<Op>, R2>>
|
|
634
|
+
// The state is the cursor to send; the first page is fetched without one.
|
|
635
|
+
return Stream.paginate(undefined as string | undefined, (after) =>
|
|
636
|
+
Effect.flatMap(
|
|
637
|
+
call(after === undefined ? options.variables : { ...options.variables, after }, options.options),
|
|
638
|
+
(result): Effect.Effect<readonly [ReadonlyArray<C>, Option.Option<string>], GraphQLClientError> => {
|
|
639
|
+
const paginationError = (description: string) =>
|
|
640
|
+
Effect.fail(
|
|
641
|
+
new GraphQLClientError({
|
|
642
|
+
operation: method.operation.name,
|
|
643
|
+
reason: new PaginationError({ description, cursor: after ?? null })
|
|
644
|
+
})
|
|
645
|
+
)
|
|
646
|
+
const connection = options.connection(result)
|
|
647
|
+
if (connection === null || connection === undefined) {
|
|
648
|
+
return after === undefined
|
|
649
|
+
? Effect.succeed([[], Option.none()])
|
|
650
|
+
: paginationError("The connection was null on a page after the first")
|
|
651
|
+
}
|
|
652
|
+
const { endCursor, hasNextPage } = connection.pageInfo
|
|
653
|
+
if (!hasNextPage) return Effect.succeed([[connection], Option.none()])
|
|
654
|
+
if (endCursor === null) return paginationError("hasNextPage is true but endCursor is null")
|
|
655
|
+
if (endCursor === after) return paginationError(`endCursor "${endCursor}" did not advance`)
|
|
656
|
+
return Effect.succeed([[connection], Option.some(endCursor)])
|
|
657
|
+
}
|
|
658
|
+
))
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Like {@link pages}, but emits decoded entries from `nodes`, including
|
|
663
|
+
* `null` entries. Use `Stream.filter(Predicate.isNotNull)` to drop them.
|
|
664
|
+
* For connections selecting only `edges`, use {@link pages} with
|
|
665
|
+
* `Stream.flatMap`.
|
|
666
|
+
*
|
|
667
|
+
* **Example** (Flattening issue numbers across pages)
|
|
668
|
+
*
|
|
669
|
+
* ```ts import.meta.vitest
|
|
670
|
+
* import { Effect, Layer, Schema, Stream } from "effect"
|
|
671
|
+
* import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
|
|
672
|
+
*
|
|
673
|
+
* const RepoIssues = GraphQL.query("RepoIssues", {
|
|
674
|
+
* document: "query RepoIssues($owner:String!,$name:String!,$after:String){repository(owner:$owner,name:$name){issues(first:2,after:$after){pageInfo{hasNextPage endCursor}nodes{number}}}}",
|
|
675
|
+
* variables: { owner: Schema.String, name: Schema.String, after: Schema.optional(Schema.NullOr(Schema.String)) },
|
|
676
|
+
* result: Schema.Struct({
|
|
677
|
+
* repository: Schema.NullOr(Schema.Struct({
|
|
678
|
+
* issues: Schema.Struct({
|
|
679
|
+
* pageInfo: Schema.Struct({ hasNextPage: Schema.Boolean, endCursor: Schema.NullOr(Schema.String) }),
|
|
680
|
+
* nodes: Schema.NullOr(Schema.Array(Schema.NullOr(Schema.Struct({ number: Schema.Int }))))
|
|
681
|
+
* })
|
|
682
|
+
* }))
|
|
683
|
+
* })
|
|
684
|
+
* })
|
|
685
|
+
*
|
|
686
|
+
* const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
|
|
687
|
+
* execute: (request) => {
|
|
688
|
+
* const after = (request.variables as { after?: string }).after
|
|
689
|
+
* const page = after === undefined
|
|
690
|
+
* ? { pageInfo: { hasNextPage: true, endCursor: "c1" }, nodes: [{ number: 1 }, null] }
|
|
691
|
+
* : { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [{ number: 3 }] }
|
|
692
|
+
* return Effect.succeed({ body: { data: { repository: { issues: page } } } })
|
|
693
|
+
* },
|
|
694
|
+
* subscribe: () => Stream.empty
|
|
695
|
+
* })
|
|
696
|
+
*
|
|
697
|
+
* const program = Effect.gen(function*() {
|
|
698
|
+
* const client = yield* GraphQLClient.make(GraphQLGroup.make(RepoIssues))
|
|
699
|
+
* const items = GraphQLClient.items(client.RepoIssues, {
|
|
700
|
+
* variables: { owner: "Effect-TS", name: "effect" },
|
|
701
|
+
* connection: (result) => result.repository?.issues
|
|
702
|
+
* })
|
|
703
|
+
* return yield* Stream.runCollect(Stream.map(items, (issue) => issue?.number ?? null))
|
|
704
|
+
* })
|
|
705
|
+
*
|
|
706
|
+
* await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => [1, null, 3]
|
|
707
|
+
* ```
|
|
708
|
+
*
|
|
709
|
+
* @stability experimental
|
|
710
|
+
* @category paging
|
|
711
|
+
* @since 4.0.0
|
|
712
|
+
*/
|
|
713
|
+
export const items = <Op extends GraphQL.Any, Node, R2 = never>(
|
|
714
|
+
method: OperationMethod<Op>,
|
|
715
|
+
options: {
|
|
716
|
+
readonly variables: PagingVariables<GraphQL.Variables<Op>>
|
|
717
|
+
readonly connection: (result: GraphQL.Result<Op>) => NodesConnection<Node> | null | undefined
|
|
718
|
+
readonly options?: {
|
|
719
|
+
readonly headers?: Headers.Input | undefined
|
|
720
|
+
readonly urlParams?: UrlParams.Input | undefined
|
|
721
|
+
readonly context?: Context.Context<R2> | undefined
|
|
722
|
+
} | undefined
|
|
723
|
+
}
|
|
724
|
+
): Stream.Stream<Node, GraphQL.Error<Op>, Exclude<GraphQL.Services<Op>, R2>> =>
|
|
725
|
+
Stream.flatMap(pages(method, options), (connection) => Stream.fromArray(connection.nodes ?? []))
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* The options a {@link makeTest} handler receives: the `headers` and
|
|
729
|
+
* `urlParams` the caller passed, unchanged. Headers accept `Headers.Input`;
|
|
730
|
+
* unlike real transport requests, test call options are not normalized.
|
|
731
|
+
*
|
|
732
|
+
* @stability experimental
|
|
733
|
+
* @category testing
|
|
734
|
+
* @since 4.0.0
|
|
735
|
+
*/
|
|
736
|
+
export interface TestCallOptions {
|
|
737
|
+
readonly headers?: Headers.Input | undefined
|
|
738
|
+
readonly urlParams?: UrlParams.Input | undefined
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* A {@link makeTest} handler for one operation. Queries and mutations return
|
|
743
|
+
* an `Effect` and subscriptions a `Stream` of the decoded result type.
|
|
744
|
+
*
|
|
745
|
+
* @stability experimental
|
|
746
|
+
* @category testing
|
|
747
|
+
* @since 4.0.0
|
|
748
|
+
*/
|
|
749
|
+
export type TestHandler<Op extends GraphQL.Any> = Op["kind"] extends "subscription" ? (
|
|
750
|
+
variables: GraphQL.Variables<Op>,
|
|
751
|
+
options: TestCallOptions
|
|
752
|
+
) => Stream.Stream<GraphQL.Result<Op>, GraphQL.Error<Op>>
|
|
753
|
+
: (
|
|
754
|
+
variables: GraphQL.Variables<Op>,
|
|
755
|
+
options: TestCallOptions
|
|
756
|
+
) => Effect.Effect<GraphQL.Result<Op>, GraphQL.Error<Op>>
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* The handlers given to {@link makeTest}, keyed by operation name. Every
|
|
760
|
+
* handler is optional.
|
|
761
|
+
*
|
|
762
|
+
* @stability experimental
|
|
763
|
+
* @category testing
|
|
764
|
+
* @since 4.0.0
|
|
765
|
+
*/
|
|
766
|
+
export type TestHandlers<Ops extends GraphQL.Any> = {
|
|
767
|
+
readonly [Op in Ops as Op["name"]]?: TestHandler<Op>
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Builds a client for tests that answers each operation from a handler,
|
|
772
|
+
* without a protocol.
|
|
773
|
+
*
|
|
774
|
+
* **Details**
|
|
775
|
+
*
|
|
776
|
+
* Handlers receive the caller's `variables` and options unchanged and return
|
|
777
|
+
* values in their decoded form. Nothing is encoded, decoded or validated, and
|
|
778
|
+
* no middleware runs. Called with `{ partial: true }`, a method returns
|
|
779
|
+
* `{ data, errors: [] }`. Subscriptions are not retried. Every method carries
|
|
780
|
+
* `.operation`, so {@link pages} and {@link items} work on it.
|
|
781
|
+
*
|
|
782
|
+
* Calling an operation without a handler dies with an `UnimplementedError`
|
|
783
|
+
* defect, so `catchTag` in the code under test cannot swallow a missing stub.
|
|
784
|
+
*
|
|
785
|
+
* Methods keep the real client's types, including middleware service
|
|
786
|
+
* requirements. Tests must provide those services even though mocks do not
|
|
787
|
+
* use them.
|
|
788
|
+
*
|
|
789
|
+
* To test partial data together with `errors`, or the encode and decode path,
|
|
790
|
+
* provide a scripted `GraphQLProtocol` layer to {@link make} instead.
|
|
791
|
+
*
|
|
792
|
+
* **Example** (Replacing a client service in tests)
|
|
793
|
+
*
|
|
794
|
+
* ```ts import.meta.vitest
|
|
795
|
+
* import { Context, Effect, Layer, Schema } from "effect"
|
|
796
|
+
* import { GraphQL, GraphQLClient, GraphQLGroup } from "effect/graphql"
|
|
797
|
+
*
|
|
798
|
+
* const RepoIssues = GraphQL.query("RepoIssues", {
|
|
799
|
+
* document: "query RepoIssues($owner:String!,$name:String!){repository(owner:$owner,name:$name){issues{totalCount}}}",
|
|
800
|
+
* variables: { owner: Schema.String, name: Schema.String },
|
|
801
|
+
* result: Schema.Struct({
|
|
802
|
+
* repository: Schema.NullOr(Schema.Struct({ issues: Schema.Struct({ totalCount: Schema.Int }) }))
|
|
803
|
+
* })
|
|
804
|
+
* })
|
|
805
|
+
* const GitHubGraphQLGroup = GraphQLGroup.make(RepoIssues)
|
|
806
|
+
*
|
|
807
|
+
* class GitHub extends Context.Service<GitHub>()("app/GitHub", {
|
|
808
|
+
* make: GraphQLClient.make(GitHubGraphQLGroup)
|
|
809
|
+
* }) {}
|
|
810
|
+
*
|
|
811
|
+
* const GitHubTest = Layer.succeed(
|
|
812
|
+
* GitHub,
|
|
813
|
+
* GraphQLClient.makeTest(GitHubGraphQLGroup)({
|
|
814
|
+
* RepoIssues: ({ name }) => Effect.succeed({ repository: { issues: { totalCount: name.length } } })
|
|
815
|
+
* })
|
|
816
|
+
* )
|
|
817
|
+
*
|
|
818
|
+
* const program = GitHub.use((github) => github.RepoIssues({ owner: "Effect-TS", name: "effect" }))
|
|
819
|
+
*
|
|
820
|
+
* await Effect.runPromise(program.pipe(Effect.provide(GitHubTest))) // => { repository: { issues: { totalCount: 6 } } }
|
|
821
|
+
* ```
|
|
822
|
+
*
|
|
823
|
+
* @stability experimental
|
|
824
|
+
* @category testing
|
|
825
|
+
* @since 4.0.0
|
|
826
|
+
*/
|
|
827
|
+
export const makeTest =
|
|
828
|
+
<Ops extends GraphQL.Any>(group: GraphQLGroup.GraphQLGroup<Ops>) =>
|
|
829
|
+
(handlers: TestHandlers<Ops>): GraphQLClient<Ops> => {
|
|
830
|
+
// Type boundary: the mapped type keys handlers by name, which a lookup by a
|
|
831
|
+
// runtime name cannot follow.
|
|
832
|
+
const byName = handlers as Record<string, TestHandler<Ops> | undefined>
|
|
833
|
+
return fromGroup(
|
|
834
|
+
group,
|
|
835
|
+
(operation) =>
|
|
836
|
+
makeTestMethod(operation, Object.hasOwn(byName, operation.name) ? byName[operation.name] : undefined)
|
|
837
|
+
)
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Builds a mock layer for a client service using {@link makeTest}. Bind the
|
|
842
|
+
* service and group once to expose a `layerTest(handlers)` helper on the service.
|
|
843
|
+
* The group fixes the operation types before the handlers are checked.
|
|
844
|
+
*
|
|
845
|
+
* **Example** (Binding a mock layer to a client service)
|
|
846
|
+
*
|
|
847
|
+
* ```ts import.meta.vitest
|
|
848
|
+
* import { Context, Effect, Schema } from "effect"
|
|
849
|
+
* import { GraphQL, GraphQLClient, GraphQLGroup } from "effect/graphql"
|
|
850
|
+
*
|
|
851
|
+
* const RepoIssues = GraphQL.query("RepoIssues", {
|
|
852
|
+
* document: "query RepoIssues($owner:String!,$name:String!){repository(owner:$owner,name:$name){issues{totalCount}}}",
|
|
853
|
+
* variables: { owner: Schema.String, name: Schema.String },
|
|
854
|
+
* result: Schema.Struct({
|
|
855
|
+
* repository: Schema.NullOr(Schema.Struct({ issues: Schema.Struct({ totalCount: Schema.Int }) }))
|
|
856
|
+
* })
|
|
857
|
+
* })
|
|
858
|
+
* const GitHubGraphQLGroup = GraphQLGroup.make(RepoIssues)
|
|
859
|
+
*
|
|
860
|
+
* class GitHub extends Context.Service<GitHub>()("app/GitHub", {
|
|
861
|
+
* make: GraphQLClient.make(GitHubGraphQLGroup)
|
|
862
|
+
* }) {
|
|
863
|
+
* static readonly layerTest = GraphQLClient.layerTest(GitHub, GitHubGraphQLGroup)
|
|
864
|
+
* }
|
|
865
|
+
*
|
|
866
|
+
* const program = GitHub.use((github) => github.RepoIssues({ owner: "Effect-TS", name: "effect" }))
|
|
867
|
+
*
|
|
868
|
+
* await Effect.runPromise(program.pipe(Effect.provide(GitHub.layerTest({
|
|
869
|
+
* RepoIssues: ({ name }) => Effect.succeed({ repository: { issues: { totalCount: name.length } } })
|
|
870
|
+
* })))) // => { repository: { issues: { totalCount: 6 } } }
|
|
871
|
+
* ```
|
|
872
|
+
*
|
|
873
|
+
* @see {@link makeTest} for mock behavior and the middleware-services limitation.
|
|
874
|
+
* @stability experimental
|
|
875
|
+
* @category testing
|
|
876
|
+
* @since 4.0.0
|
|
877
|
+
*/
|
|
878
|
+
export const layerTest = <I, Service, Ops extends GraphQL.Any>(
|
|
879
|
+
service: Context.Key<I, Service>,
|
|
880
|
+
group: GraphQLGroup.GraphQLGroup<Ops> & (GraphQLClient<Ops> extends NoInfer<Service> ? unknown : never)
|
|
881
|
+
) =>
|
|
882
|
+
(handlers: TestHandlers<Ops>): Layer.Layer<I> =>
|
|
883
|
+
// The group constraint checks that the mock satisfies the service shape.
|
|
884
|
+
Layer.succeed(service, makeTest(group)(handlers) as unknown as Service)
|
|
885
|
+
|
|
886
|
+
// Handlers are called through these erased signatures; `makeTest` has
|
|
887
|
+
// already checked them against the operation.
|
|
888
|
+
type ErasedTestHandler = (variables: unknown, options: TestCallOptions) => unknown
|
|
889
|
+
|
|
890
|
+
const makeTestMethod = (operation: GraphQL.Any, handler: ErasedTestHandler | undefined) => {
|
|
891
|
+
const subscription = operation.kind === "subscription"
|
|
892
|
+
if (handler === undefined) {
|
|
893
|
+
const unimplemented = () => {
|
|
894
|
+
const prevLimit = getStackTraceLimit()
|
|
895
|
+
setStackTraceLimit(2)
|
|
896
|
+
const error = new Error(`GraphQLClient: Unimplemented operation "${operation.name}"`)
|
|
897
|
+
setStackTraceLimit(prevLimit)
|
|
898
|
+
error.name = "UnimplementedError"
|
|
899
|
+
return subscription ? Stream.die(error) : Effect.die(error)
|
|
900
|
+
}
|
|
901
|
+
return Object.assign(unimplemented, { operation })
|
|
902
|
+
}
|
|
903
|
+
const call = (variables: unknown, options: (TestCallOptions & { readonly partial?: boolean }) | undefined) => {
|
|
904
|
+
const run = () => handler(variables === undefined ? {} : variables, options ?? {})
|
|
905
|
+
if (subscription) return Stream.suspend(run as () => Stream.Stream<unknown, unknown>)
|
|
906
|
+
const result = Effect.suspend(run as () => Effect.Effect<unknown, unknown>)
|
|
907
|
+
return options?.partial === true
|
|
908
|
+
? Effect.map(result, (data): PartialResult<unknown> => ({ data, errors: [] }))
|
|
909
|
+
: result
|
|
910
|
+
}
|
|
911
|
+
return Object.assign(call, { operation })
|
|
912
|
+
}
|