@effect/platform-cloudflare 0.0.0 → 4.1.3
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 +434 -0
- package/CLAUDE.md +434 -0
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/ai-docs/README.md +44 -0
- package/ai-docs/package.json +36 -0
- package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +52 -0
- package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
- package/ai-docs/src/01_effect/01_basics/index.md +7 -0
- package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
- package/ai-docs/src/01_effect/02_schema/index.md +7 -0
- package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
- package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
- package/ai-docs/src/01_effect/03_services/index.md +5 -0
- package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
- package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
- package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
- package/ai-docs/src/01_effect/04_errors/index.md +1 -0
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
- package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
- package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
- package/ai-docs/src/01_effect/05_resources/index.md +3 -0
- package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
- package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
- package/ai-docs/src/01_effect/06_running/index.md +1 -0
- package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
- package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
- package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
- package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
- package/ai-docs/src/03_stream/30_encoding.ts +163 -0
- package/ai-docs/src/03_stream/index.md +4 -0
- package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
- package/ai-docs/src/04_integration/index.md +5 -0
- package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
- package/ai-docs/src/05_batching/index.md +3 -0
- package/ai-docs/src/06_schedule/10_schedules.ts +111 -0
- package/ai-docs/src/06_schedule/index.md +3 -0
- package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
- package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
- package/ai-docs/src/07_datetime/index.md +5 -0
- package/ai-docs/src/08_observability/10_logging.ts +66 -0
- package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
- package/ai-docs/src/08_observability/index.md +7 -0
- package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
- package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
- package/ai-docs/src/09_testing/index.md +1 -0
- package/ai-docs/src/10_predicate/01_basics.ts +14 -0
- package/ai-docs/src/10_predicate/index.md +9 -0
- package/ai-docs/src/40_sql/10_basics.ts +175 -0
- package/ai-docs/src/40_sql/index.md +6 -0
- package/ai-docs/src/50_http-client/10_basics.ts +102 -0
- package/ai-docs/src/50_http-client/index.md +3 -0
- package/ai-docs/src/51_http-server/10_basics.ts +118 -0
- package/ai-docs/src/51_http-server/20_testing.ts +100 -0
- package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
- package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
- package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
- package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
- package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
- package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
- package/ai-docs/src/51_http-server/index.md +3 -0
- package/ai-docs/src/52_graphql/10_basics.ts +117 -0
- package/ai-docs/src/52_graphql/20_testing.ts +45 -0
- package/ai-docs/src/52_graphql/30_batching.ts +118 -0
- package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -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/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
- package/ai-docs/src/60_child-process/index.md +3 -0
- package/ai-docs/src/70_cli/10_basics.ts +162 -0
- package/ai-docs/src/70_cli/index.md +5 -0
- package/ai-docs/src/71_ai/10_language-model.ts +156 -0
- package/ai-docs/src/71_ai/20_tools.ts +226 -0
- package/ai-docs/src/71_ai/30_chat.ts +158 -0
- package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
- package/ai-docs/src/71_ai/index.md +5 -0
- package/ai-docs/src/80_cluster/10_entities.ts +97 -0
- package/ai-docs/src/80_cluster/index.md +4 -0
- package/ai-docs/src/index.md +10 -0
- package/ai-docs/tsconfig.json +28 -0
- package/dist/AlchemyCloudflareCluster.d.ts +134 -0
- package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
- package/dist/AlchemyCloudflareCluster.js +111 -0
- package/dist/AlchemyCloudflareCluster.js.map +1 -0
- package/dist/CloudflareCluster.d.ts +196 -0
- package/dist/CloudflareCluster.d.ts.map +1 -0
- package/dist/CloudflareCluster.js +533 -0
- package/dist/CloudflareCluster.js.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.js +295 -0
- package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
- package/dist/CloudflareDurableObjects.d.ts +149 -0
- package/dist/CloudflareDurableObjects.d.ts.map +1 -0
- package/dist/CloudflareDurableObjects.js +337 -0
- package/dist/CloudflareDurableObjects.js.map +1 -0
- package/dist/CloudflarePersistedQueue.d.ts +36 -0
- package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
- package/dist/CloudflarePersistedQueue.js +122 -0
- package/dist/CloudflarePersistedQueue.js.map +1 -0
- package/dist/CloudflareWorkflowEngine.d.ts +72 -0
- package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
- package/dist/CloudflareWorkflowEngine.js +187 -0
- package/dist/CloudflareWorkflowEngine.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/alchemyCluster.d.ts +2 -0
- package/dist/internal/alchemyCluster.d.ts.map +1 -0
- package/dist/internal/alchemyCluster.js +75 -0
- package/dist/internal/alchemyCluster.js.map +1 -0
- package/dist/internal/boundedMap.d.ts +3 -0
- package/dist/internal/boundedMap.d.ts.map +1 -0
- package/dist/internal/boundedMap.js +15 -0
- package/dist/internal/boundedMap.js.map +1 -0
- package/dist/internal/cluster.d.ts +57 -0
- package/dist/internal/cluster.d.ts.map +1 -0
- package/dist/internal/cluster.js +2 -0
- package/dist/internal/cluster.js.map +1 -0
- package/dist/internal/clusterName.d.ts +2 -0
- package/dist/internal/clusterName.d.ts.map +1 -0
- package/dist/internal/clusterName.js +16 -0
- package/dist/internal/clusterName.js.map +1 -0
- package/dist/internal/entityKeepAlive.d.ts +2 -0
- package/dist/internal/entityKeepAlive.d.ts.map +1 -0
- package/dist/internal/entityKeepAlive.js +73 -0
- package/dist/internal/entityKeepAlive.js.map +1 -0
- package/dist/internal/entityMailbox.d.ts +2 -0
- package/dist/internal/entityMailbox.d.ts.map +1 -0
- package/dist/internal/entityMailbox.js +160 -0
- package/dist/internal/entityMailbox.js.map +1 -0
- package/dist/internal/entityRegistry.d.ts +16 -0
- package/dist/internal/entityRegistry.d.ts.map +1 -0
- package/dist/internal/entityRegistry.js +22 -0
- package/dist/internal/entityRegistry.js.map +1 -0
- package/dist/internal/entityReply.d.ts +2 -0
- package/dist/internal/entityReply.d.ts.map +1 -0
- package/dist/internal/entityReply.js +35 -0
- package/dist/internal/entityReply.js.map +1 -0
- package/dist/internal/entityRuntime.d.ts +2 -0
- package/dist/internal/entityRuntime.d.ts.map +1 -0
- package/dist/internal/entityRuntime.js +823 -0
- package/dist/internal/entityRuntime.js.map +1 -0
- package/dist/internal/entitySql.d.ts +2 -0
- package/dist/internal/entitySql.d.ts.map +1 -0
- package/dist/internal/entitySql.js +29 -0
- package/dist/internal/entitySql.js.map +1 -0
- package/dist/internal/entityStorage.d.ts +2 -0
- package/dist/internal/entityStorage.d.ts.map +1 -0
- package/dist/internal/entityStorage.js +98 -0
- package/dist/internal/entityStorage.js.map +1 -0
- package/dist/internal/entityWire.d.ts +2 -0
- package/dist/internal/entityWire.d.ts.map +1 -0
- package/dist/internal/entityWire.js +146 -0
- package/dist/internal/entityWire.js.map +1 -0
- package/dist/internal/queueRuntime.d.ts +2 -0
- package/dist/internal/queueRuntime.d.ts.map +1 -0
- package/dist/internal/queueRuntime.js +88 -0
- package/dist/internal/queueRuntime.js.map +1 -0
- package/dist/internal/queueStorage.d.ts +2 -0
- package/dist/internal/queueStorage.d.ts.map +1 -0
- package/dist/internal/queueStorage.js +129 -0
- package/dist/internal/queueStorage.js.map +1 -0
- package/dist/internal/registry.d.ts +10 -0
- package/dist/internal/registry.d.ts.map +1 -0
- package/dist/internal/registry.js +24 -0
- package/dist/internal/registry.js.map +1 -0
- package/dist/internal/singletonRegistry.d.ts +2 -0
- package/dist/internal/singletonRegistry.d.ts.map +1 -0
- package/dist/internal/singletonRegistry.js +9 -0
- package/dist/internal/singletonRegistry.js.map +1 -0
- package/dist/internal/singletonRuntime.d.ts +2 -0
- package/dist/internal/singletonRuntime.d.ts.map +1 -0
- package/dist/internal/singletonRuntime.js +42 -0
- package/dist/internal/singletonRuntime.js.map +1 -0
- package/dist/internal/singletonStorage.d.ts +2 -0
- package/dist/internal/singletonStorage.d.ts.map +1 -0
- package/dist/internal/singletonStorage.js +34 -0
- package/dist/internal/singletonStorage.js.map +1 -0
- package/dist/internal/transport.d.ts +2 -0
- package/dist/internal/transport.d.ts.map +1 -0
- package/dist/internal/transport.js +49 -0
- package/dist/internal/transport.js.map +1 -0
- package/dist/internal/workflowRegistry.d.ts +2 -0
- package/dist/internal/workflowRegistry.d.ts.map +1 -0
- package/dist/internal/workflowRegistry.js +29 -0
- package/dist/internal/workflowRegistry.js.map +1 -0
- package/dist/internal/workflowRuntime.d.ts +2 -0
- package/dist/internal/workflowRuntime.d.ts.map +1 -0
- package/dist/internal/workflowRuntime.js +322 -0
- package/dist/internal/workflowRuntime.js.map +1 -0
- package/dist/internal/workflowStorage.d.ts +2 -0
- package/dist/internal/workflowStorage.d.ts.map +1 -0
- package/dist/internal/workflowStorage.js +113 -0
- package/dist/internal/workflowStorage.js.map +1 -0
- package/dist/internal/workflowWire.d.ts +2 -0
- package/dist/internal/workflowWire.d.ts.map +1 -0
- package/dist/internal/workflowWire.js +52 -0
- package/dist/internal/workflowWire.js.map +1 -0
- package/package.json +77 -1
- package/src/AlchemyCloudflareCluster.ts +200 -0
- package/src/CloudflareCluster.ts +704 -0
- package/src/CloudflareDurableObjectPrograms.ts +543 -0
- package/src/CloudflareDurableObjects.ts +430 -0
- package/src/CloudflarePersistedQueue.ts +189 -0
- package/src/CloudflareWorkflowEngine.ts +220 -0
- package/src/index.ts +38 -0
- package/src/internal/alchemyCluster.ts +120 -0
- package/src/internal/boundedMap.ts +15 -0
- package/src/internal/cluster.ts +57 -0
- package/src/internal/clusterName.ts +19 -0
- package/src/internal/entityKeepAlive.ts +121 -0
- package/src/internal/entityMailbox.ts +324 -0
- package/src/internal/entityRegistry.ts +46 -0
- package/src/internal/entityReply.ts +54 -0
- package/src/internal/entityRuntime.ts +1318 -0
- package/src/internal/entitySql.ts +60 -0
- package/src/internal/entityStorage.ts +168 -0
- package/src/internal/entityWire.ts +204 -0
- package/src/internal/queueRuntime.ts +174 -0
- package/src/internal/queueStorage.ts +255 -0
- package/src/internal/registry.ts +31 -0
- package/src/internal/singletonRegistry.ts +21 -0
- package/src/internal/singletonRuntime.ts +88 -0
- package/src/internal/singletonStorage.ts +64 -0
- package/src/internal/transport.ts +67 -0
- package/src/internal/workflowRegistry.ts +78 -0
- package/src/internal/workflowRuntime.ts +443 -0
- package/src/internal/workflowStorage.ts +234 -0
- package/src/internal/workflowWire.ts +123 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { HttpApi, OpenApi } from "effect/http-api"
|
|
2
|
+
import { SystemApi } from "./System.ts"
|
|
3
|
+
import { UsersApiGroup } from "./Users.ts"
|
|
4
|
+
|
|
5
|
+
// Defined the root API, which combines all of the groups together. This is the
|
|
6
|
+
// API that you will serve and generate clients for. You can also annotate the
|
|
7
|
+
// API with OpenAPI metadata.
|
|
8
|
+
export class Api extends HttpApi.make("user-api")
|
|
9
|
+
.add(UsersApiGroup)
|
|
10
|
+
.add(SystemApi)
|
|
11
|
+
.annotateMerge(OpenApi.annotations({
|
|
12
|
+
title: "Acme User API"
|
|
13
|
+
}))
|
|
14
|
+
{}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Context, Schema } from "effect"
|
|
2
|
+
import { HttpApiMiddleware, HttpApiSecurity } from "effect/http-api"
|
|
3
|
+
import type { User } from "../domain/User.ts"
|
|
4
|
+
|
|
5
|
+
export class CurrentUser extends Context.Service<CurrentUser, User>()("acme/HttpApi/Authorization/CurrentUser") {}
|
|
6
|
+
|
|
7
|
+
export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
8
|
+
"Unauthorized",
|
|
9
|
+
{
|
|
10
|
+
message: Schema.String
|
|
11
|
+
},
|
|
12
|
+
// You can define error status codes directly on the error class
|
|
13
|
+
{ httpApiStatus: 401 }
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
export class Authorization extends HttpApiMiddleware.Service<Authorization, {
|
|
17
|
+
// Middleware can provide services to other middleware and endpoints, which is
|
|
18
|
+
// useful for things like authentication, where you want to inject the current
|
|
19
|
+
// user into the context for other endpoints to consume.
|
|
20
|
+
provides: CurrentUser
|
|
21
|
+
// If your middleware requires dependencies from other middleware, you can
|
|
22
|
+
// specify those as well.
|
|
23
|
+
requires: never
|
|
24
|
+
}>()("acme/HttpApi/Authorization", {
|
|
25
|
+
// This middleware requires clients to also provide an implementation, to
|
|
26
|
+
// inject a api key
|
|
27
|
+
requiredForClient: true,
|
|
28
|
+
// Middleware can optionally define security schemes, which are used to
|
|
29
|
+
// generate OpenAPI docs and decode credientials from incoming requests for
|
|
30
|
+
// you.
|
|
31
|
+
security: {
|
|
32
|
+
bearer: HttpApiSecurity.bearer
|
|
33
|
+
},
|
|
34
|
+
// Middlware can specify errors that it may raise
|
|
35
|
+
error: Unauthorized
|
|
36
|
+
}) {}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api"
|
|
2
|
+
|
|
3
|
+
// Top level groups are added to the root of the derived HttpApiClient.
|
|
4
|
+
//
|
|
5
|
+
// `client.health()`
|
|
6
|
+
export class SystemApi extends HttpApiGroup.make("system", { topLevel: true }).add(
|
|
7
|
+
HttpApiEndpoint.get("health", "/health", {
|
|
8
|
+
success: HttpApiSchema.NoContent
|
|
9
|
+
})
|
|
10
|
+
) {}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api"
|
|
3
|
+
import { User, UserId } from "../domain/User.ts"
|
|
4
|
+
import { SearchQueryTooShort, UserNotFound } from "../domain/UserErrors.ts"
|
|
5
|
+
import { Authorization } from "./Authorization.ts"
|
|
6
|
+
|
|
7
|
+
export class UsersApiGroup extends HttpApiGroup.make("users")
|
|
8
|
+
.add(
|
|
9
|
+
HttpApiEndpoint.get("list", "/", {
|
|
10
|
+
query: {
|
|
11
|
+
search: Schema.optional(Schema.String)
|
|
12
|
+
},
|
|
13
|
+
// Use the `json` variant of the model for API responses. It shares the
|
|
14
|
+
// field declarations with the database variants, but can encode values
|
|
15
|
+
// differently where needed.
|
|
16
|
+
success: Schema.Array(User.json)
|
|
17
|
+
}),
|
|
18
|
+
HttpApiEndpoint.get("search", "/search", {
|
|
19
|
+
// For get requests, payload uses the query string
|
|
20
|
+
payload: {
|
|
21
|
+
search: Schema.String
|
|
22
|
+
},
|
|
23
|
+
success: [
|
|
24
|
+
Schema.Array(User.json),
|
|
25
|
+
Schema.String.pipe(HttpApiSchema.asText({
|
|
26
|
+
contentType: "text/csv"
|
|
27
|
+
}))
|
|
28
|
+
],
|
|
29
|
+
error: [
|
|
30
|
+
SearchQueryTooShort.pipe(
|
|
31
|
+
// If you want an error to return no content, you can use
|
|
32
|
+
// `HttpApiSchema.asNoContent` and provide a decoder that transforms the
|
|
33
|
+
// error into the appropriate type.
|
|
34
|
+
HttpApiSchema.asNoContent({
|
|
35
|
+
decode: () => new SearchQueryTooShort()
|
|
36
|
+
})
|
|
37
|
+
),
|
|
38
|
+
// You can also add some of the built in `HttpApiError`s to handle common
|
|
39
|
+
// error cases like bad requests, unauthorized, etc.
|
|
40
|
+
HttpApiError.RequestTimeoutNoContent
|
|
41
|
+
]
|
|
42
|
+
}),
|
|
43
|
+
HttpApiEndpoint.get("getById", "/:id", {
|
|
44
|
+
params: {
|
|
45
|
+
// Path parameter values are automatically coerced from their string
|
|
46
|
+
// form using `Schema.toCodecStringTree`, so schemas that decode from
|
|
47
|
+
// other types (like numbers) work here as well.
|
|
48
|
+
id: UserId
|
|
49
|
+
},
|
|
50
|
+
success: User.json,
|
|
51
|
+
error: UserNotFound.pipe(
|
|
52
|
+
// If you want an error to return no content, you can use
|
|
53
|
+
// `HttpApiSchema.asNoContent` and provide a decoder that transforms the
|
|
54
|
+
// error into the appropriate type.
|
|
55
|
+
HttpApiSchema.asNoContent({
|
|
56
|
+
decode: () => new UserNotFound()
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
}),
|
|
60
|
+
HttpApiEndpoint.post("create", "/", {
|
|
61
|
+
// For post requests, payload uses the request body. It defaults to JSON,
|
|
62
|
+
// but you can specify other content types as well using
|
|
63
|
+
// `HttpApiSchema.asText`, `HttpApiSchema.asMultipart`, etc.
|
|
64
|
+
//
|
|
65
|
+
// The `jsonCreate` variant only exposes the fields clients are allowed
|
|
66
|
+
// to provide, so the generated id and timestamps cannot be set here.
|
|
67
|
+
payload: User.jsonCreate,
|
|
68
|
+
success: User.json
|
|
69
|
+
}),
|
|
70
|
+
HttpApiEndpoint.patch("update", "/:id", {
|
|
71
|
+
params: {
|
|
72
|
+
id: UserId
|
|
73
|
+
},
|
|
74
|
+
// The `jsonUpdate` variant similarly excludes the id and the managed
|
|
75
|
+
// timestamps from the update payload.
|
|
76
|
+
payload: User.jsonUpdate,
|
|
77
|
+
success: User.json,
|
|
78
|
+
error: UserNotFound.pipe(
|
|
79
|
+
HttpApiSchema.asNoContent({
|
|
80
|
+
decode: () => new UserNotFound()
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
}),
|
|
84
|
+
HttpApiEndpoint.get("me", "/me", {
|
|
85
|
+
success: User.json,
|
|
86
|
+
error: UserNotFound.pipe(HttpApiSchema.status(404))
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
// You can apply middleware to entire groups, which is useful for things like
|
|
90
|
+
// authentication and authorization.
|
|
91
|
+
//
|
|
92
|
+
// You can also apply middleware to individual endpoints if you need more
|
|
93
|
+
// fine-grained control.
|
|
94
|
+
.middleware(Authorization)
|
|
95
|
+
// To add a common prefix to all endpoints in a group, you can use the `prefix`
|
|
96
|
+
// method. This is useful for grouping related endpoints together under a common
|
|
97
|
+
// path segment. In this case, all endpoints in the `UsersApiGroup` will be
|
|
98
|
+
// prefixed with `/users`.
|
|
99
|
+
.prefix("/users")
|
|
100
|
+
// You can add OpenAPI annotations to groups, endpoints, and even parameters and
|
|
101
|
+
// request bodies. These will be merged together to generate the final OpenAPI
|
|
102
|
+
// docs for the API
|
|
103
|
+
.annotateMerge(OpenApi.annotations({
|
|
104
|
+
title: "Users",
|
|
105
|
+
description: "User management endpoints"
|
|
106
|
+
}))
|
|
107
|
+
{}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
import { Model } from "effect/schema"
|
|
3
|
+
|
|
4
|
+
export const UserId = Schema.String.pipe(Schema.brand("UserId"))
|
|
5
|
+
export type UserId = typeof UserId.Type
|
|
6
|
+
|
|
7
|
+
// `Model.Class` derives variants for the database (`User`, `User.insert`,
|
|
8
|
+
// `User.update`) and the JSON API (`User.json`, `User.jsonCreate`,
|
|
9
|
+
// `User.jsonUpdate`) from a single field declaration.
|
|
10
|
+
export class User extends Model.Class<User>("User")({
|
|
11
|
+
// A UUID v4 primary key generated by the application on insert. It is
|
|
12
|
+
// excluded from the `jsonCreate` / `jsonUpdate` variants, so API clients can
|
|
13
|
+
// never set it.
|
|
14
|
+
id: Model.UuidV4Insert(UserId),
|
|
15
|
+
name: Schema.String,
|
|
16
|
+
email: Schema.String,
|
|
17
|
+
// Timestamps are managed by the model: set on insert, refreshed on update,
|
|
18
|
+
// and also excluded from the JSON create / update variants.
|
|
19
|
+
createdAt: Model.DateTimeInsert,
|
|
20
|
+
updatedAt: Model.DateTimeUpdate
|
|
21
|
+
}) {}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
export class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
4
|
+
"UserNotFound",
|
|
5
|
+
{},
|
|
6
|
+
// You can specify the status code for this error inline
|
|
7
|
+
{ httpApiStatus: 404 }
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
export class SearchQueryTooShort
|
|
11
|
+
extends Schema.TaggedError<SearchQueryTooShort>()("SearchQueryTooShort", {}, { httpApiStatus: 422 })
|
|
12
|
+
{
|
|
13
|
+
static readonly minimumLength = 2
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Create a wrapper error class for all errors in the Users API.
|
|
17
|
+
//
|
|
18
|
+
// This prevents adding too many error types to services / endpoint definitions.
|
|
19
|
+
//
|
|
20
|
+
export class UsersError extends Schema.TaggedError<UsersError>()("UsersError", {
|
|
21
|
+
reason: Schema.Union([UserNotFound, SearchQueryTooShort])
|
|
22
|
+
}) {}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DateTime, Effect, Layer, Redacted } from "effect"
|
|
2
|
+
import { Authorization, CurrentUser, Unauthorized } from "../api/Authorization.ts"
|
|
3
|
+
import { User, UserId } from "../domain/User.ts"
|
|
4
|
+
|
|
5
|
+
const fixedTimestamp = DateTime.makeUnsafe("2026-01-01T00:00:00Z")
|
|
6
|
+
const devUser = new User({
|
|
7
|
+
id: UserId.make("bf3dbe33-0ad2-4c9c-9c9e-733e57bdcbee"),
|
|
8
|
+
name: "Dev User",
|
|
9
|
+
email: "dev@acme.com",
|
|
10
|
+
createdAt: fixedTimestamp,
|
|
11
|
+
updatedAt: fixedTimestamp
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// The implementation of the Authorization middleware. It is seperate from the
|
|
15
|
+
// service definition to avoid leaking it into a client.
|
|
16
|
+
export const AuthorizationLayer = Layer.effect(
|
|
17
|
+
Authorization,
|
|
18
|
+
Effect.gen(function*() {
|
|
19
|
+
// Here you could access services required by the middleware, like a
|
|
20
|
+
// database or an external auth provider.
|
|
21
|
+
yield* Effect.logInfo("Starting Authorization middleware")
|
|
22
|
+
|
|
23
|
+
return Authorization.of({
|
|
24
|
+
bearer: Effect.fn(function*(httpEffect, { credential }) {
|
|
25
|
+
// Validate the token and return an Unauthorized error if it's invalid.
|
|
26
|
+
const token = Redacted.value(credential)
|
|
27
|
+
if (token !== "dev-token") {
|
|
28
|
+
return yield* new Unauthorized({ message: "Missing or invalid bearer token" })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Provide the current user to the rest of the stack. This will be
|
|
32
|
+
// available in any endpoint or middleware that runs after this one.
|
|
33
|
+
return yield* Effect.provideService(httpEffect, CurrentUser, devUser)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect"
|
|
2
|
+
import { HttpApiBuilder, HttpApiError } from "effect/http-api"
|
|
3
|
+
import { Api } from "../../api/Api.ts"
|
|
4
|
+
import { CurrentUser } from "../../api/Authorization.ts"
|
|
5
|
+
import { AuthorizationLayer } from "../Authorization.ts"
|
|
6
|
+
import { Users } from "../Users.ts"
|
|
7
|
+
|
|
8
|
+
// The handlers without their dependencies provided, so tests can supply an
|
|
9
|
+
// alternative `Users` implementation.
|
|
10
|
+
export const UsersApiHandlersNoDeps = HttpApiBuilder.group(
|
|
11
|
+
Api,
|
|
12
|
+
"users",
|
|
13
|
+
Effect.fn(function*(handlers) {
|
|
14
|
+
const users = yield* Users
|
|
15
|
+
|
|
16
|
+
return handlers.handleAll({
|
|
17
|
+
list: ({ query }) =>
|
|
18
|
+
users.list(query.search).pipe(
|
|
19
|
+
// The list endpoint expects no errors, so we convert any potential
|
|
20
|
+
// errors into a 500 Internal Server Error.
|
|
21
|
+
Effect.orDie
|
|
22
|
+
),
|
|
23
|
+
search: Effect.fn(function*({ payload }) {
|
|
24
|
+
if (payload.search === "bad-request") {
|
|
25
|
+
// You can use the built in error types like any other
|
|
26
|
+
// Schema.TaggedError
|
|
27
|
+
return yield* new HttpApiError.RequestTimeout()
|
|
28
|
+
}
|
|
29
|
+
return yield* users.list(payload.search).pipe(
|
|
30
|
+
Effect.catchReason(
|
|
31
|
+
"UsersError",
|
|
32
|
+
"SearchQueryTooShort",
|
|
33
|
+
// Re-fail the "SearchQueryTooShort" reason
|
|
34
|
+
Effect.fail,
|
|
35
|
+
// All other reasons are unexpected, so we convert them into a 500
|
|
36
|
+
// Internal Server Error.
|
|
37
|
+
Effect.die
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
}),
|
|
41
|
+
getById: ({ params }) =>
|
|
42
|
+
users.getById(params.id).pipe(
|
|
43
|
+
// You can also use Effect.catchReasons to handle multiple error
|
|
44
|
+
// reasons at once
|
|
45
|
+
Effect.catchReasons("UsersError", {
|
|
46
|
+
UserNotFound: (e) => Effect.fail(e)
|
|
47
|
+
}, Effect.die)
|
|
48
|
+
),
|
|
49
|
+
create: ({ payload }) =>
|
|
50
|
+
users.create(payload).pipe(
|
|
51
|
+
Effect.orDie
|
|
52
|
+
// You could alse use Effect.unwrapReason to moves rror reasons up to
|
|
53
|
+
// the top level, so you can handle them with Effect.catch or
|
|
54
|
+
// Effect.catchTag etc.
|
|
55
|
+
//
|
|
56
|
+
// Effect.unwrapReason("UsersError"),
|
|
57
|
+
// Effect.catchTags({
|
|
58
|
+
// UserNotFound: Effect.die,
|
|
59
|
+
// SearchQueryTooShort: Effect.die
|
|
60
|
+
// })
|
|
61
|
+
),
|
|
62
|
+
update: ({ params, payload }) =>
|
|
63
|
+
users.update(params.id, payload).pipe(
|
|
64
|
+
Effect.catchReasons("UsersError", {
|
|
65
|
+
UserNotFound: (e) => Effect.fail(e)
|
|
66
|
+
}, Effect.die)
|
|
67
|
+
),
|
|
68
|
+
me: () =>
|
|
69
|
+
// The Authorization middleware provides the CurrentUser service, so we
|
|
70
|
+
// can access it here.
|
|
71
|
+
CurrentUser
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// The handlers with all dependencies provided, ready to serve. The SQL-backed
|
|
77
|
+
// `Users.layer` keeps the database wiring out of the server entrypoint.
|
|
78
|
+
export const UsersApiHandlers = UsersApiHandlersNoDeps.pipe(
|
|
79
|
+
Layer.provide([Users.layer, AuthorizationLayer])
|
|
80
|
+
)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node"
|
|
2
|
+
import { Context, Effect, Layer, Schema } from "effect"
|
|
3
|
+
import { SqlClient, SqlModel, SqlSchema } from "effect/sql"
|
|
4
|
+
import { User } from "../domain/User.ts"
|
|
5
|
+
import type { UserId } from "../domain/User.ts"
|
|
6
|
+
import { SearchQueryTooShort, UserNotFound, UsersError } from "../domain/UserErrors.ts"
|
|
7
|
+
|
|
8
|
+
// The SqlClient layer determines which database the SQL implementation talks
|
|
9
|
+
// to. Swap it for another driver package to target a different database.
|
|
10
|
+
const SqlLayer = SqliteClient.layer({ filename: ":memory:" })
|
|
11
|
+
|
|
12
|
+
// Migrations are effects keyed by `<id>_<name>` that run once, in id order. A
|
|
13
|
+
// real application would keep each migration in its own file and load them
|
|
14
|
+
// with `SqliteMigrator.fromFileSystem` instead of an inline record.
|
|
15
|
+
const MigratorLayer = SqliteMigrator.layer({
|
|
16
|
+
loader: SqliteMigrator.fromRecord({
|
|
17
|
+
"0001_create_users": Effect.gen(function*() {
|
|
18
|
+
const sql = yield* SqlClient.SqlClient
|
|
19
|
+
yield* sql`
|
|
20
|
+
CREATE TABLE users (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
email TEXT NOT NULL,
|
|
24
|
+
createdAt TEXT NOT NULL,
|
|
25
|
+
updatedAt TEXT NOT NULL
|
|
26
|
+
)
|
|
27
|
+
`
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export class Users extends Context.Service<Users, {
|
|
33
|
+
list(search: string | undefined): Effect.Effect<Array<User>, UsersError>
|
|
34
|
+
getById(id: UserId): Effect.Effect<User, UsersError>
|
|
35
|
+
create(input: typeof User.jsonCreate.Type): Effect.Effect<User, UsersError>
|
|
36
|
+
update(id: UserId, input: typeof User.jsonUpdate.Type): Effect.Effect<User, UsersError>
|
|
37
|
+
}>()("acme/Users") {
|
|
38
|
+
// The SQL implementation only requires a `SqlClient`, so entrypoints and
|
|
39
|
+
// tests decide how the database is provided.
|
|
40
|
+
static readonly layerNoDeps = Layer.effect(
|
|
41
|
+
Users,
|
|
42
|
+
Effect.gen(function*() {
|
|
43
|
+
const sql = yield* SqlClient.SqlClient
|
|
44
|
+
|
|
45
|
+
// CRUD goes through a repository derived from the `User` model. Each
|
|
46
|
+
// operation uses the matching model variant to encode its input and
|
|
47
|
+
// decodes rows with the full model schema.
|
|
48
|
+
const repo = yield* SqlModel.makeRepository(User, {
|
|
49
|
+
tableName: "users",
|
|
50
|
+
spanPrefix: "Users",
|
|
51
|
+
idColumn: "id"
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Queries the repository does not cover are written with the `sql` tag
|
|
55
|
+
// and decoded with the model schema.
|
|
56
|
+
const listAll = SqlSchema.findAll({
|
|
57
|
+
Request: Schema.Void,
|
|
58
|
+
Result: User,
|
|
59
|
+
execute: () => sql`SELECT * FROM users ORDER BY createdAt`
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const searchUsers = SqlSchema.findAll({
|
|
63
|
+
Request: Schema.String,
|
|
64
|
+
Result: User,
|
|
65
|
+
execute: (search) => {
|
|
66
|
+
const pattern = `%${search}%`
|
|
67
|
+
return sql`SELECT * FROM users WHERE name LIKE ${pattern} OR email LIKE ${pattern}`
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const list = Effect.fn("Users.list")(function*(search: string | undefined) {
|
|
72
|
+
if (search === undefined || search.length === 0) {
|
|
73
|
+
return yield* Effect.orDie(listAll())
|
|
74
|
+
} else if (search.length < SearchQueryTooShort.minimumLength) {
|
|
75
|
+
return yield* new UsersError({
|
|
76
|
+
reason: new SearchQueryTooShort()
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
yield* Effect.annotateCurrentSpan({ search })
|
|
80
|
+
return yield* Effect.orDie(searchUsers(search))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const getById = Effect.fn("Users.getById")((id: UserId) =>
|
|
84
|
+
repo.findById(id).pipe(
|
|
85
|
+
Effect.catchTags({
|
|
86
|
+
NoSuchElementError: () => new UsersError({ reason: new UserNotFound() }),
|
|
87
|
+
// Database and encoding failures are unexpected, so treat them as
|
|
88
|
+
// defects to keep the service interface focused on domain errors.
|
|
89
|
+
SchemaError: Effect.die,
|
|
90
|
+
SqlError: Effect.die
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const create = Effect.fn("Users.create")((input: typeof User.jsonCreate.Type) =>
|
|
96
|
+
// `User.insert.makeEffect` fills in the generated id and timestamps
|
|
97
|
+
// using the Effect clock, so tests can control them with `TestClock`.
|
|
98
|
+
User.insert.makeEffect(input).pipe(
|
|
99
|
+
Effect.flatMap(repo.insert),
|
|
100
|
+
Effect.orDie
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const update = Effect.fn("Users.update")(function*(id: UserId, input: typeof User.jsonUpdate.Type) {
|
|
105
|
+
// Ensure the user exists first, so a missing id fails with the domain
|
|
106
|
+
// error instead of a defect.
|
|
107
|
+
yield* getById(id)
|
|
108
|
+
const update = yield* User.update.makeEffect({ id, ...input }).pipe(Effect.orDie)
|
|
109
|
+
return yield* repo.update(update).pipe(Effect.orDie)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return Users.of({ list, getById, create, update })
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// The fully provided SQL implementation: the database client and migrations
|
|
117
|
+
// are implementation details, so this layer requires nothing.
|
|
118
|
+
static readonly layer: Layer.Layer<Users> = this.layerNoDeps.pipe(
|
|
119
|
+
Layer.provide(MigratorLayer.pipe(Layer.provideMerge(SqlLayer))),
|
|
120
|
+
Layer.orDie
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// An in-memory implementation for tests, so the HTTP stack can be exercised
|
|
124
|
+
// without a database.
|
|
125
|
+
static readonly layerMemory = Layer.effect(
|
|
126
|
+
Users,
|
|
127
|
+
Effect.gen(function*() {
|
|
128
|
+
const users = new Map<UserId, User>()
|
|
129
|
+
|
|
130
|
+
const makeUser = (input: typeof User.jsonCreate.Type) =>
|
|
131
|
+
User.insert.makeEffect(input).pipe(
|
|
132
|
+
Effect.map((user) => new User(user)),
|
|
133
|
+
Effect.orDie
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const admin = yield* makeUser({ name: "Admin", email: "admin@acme.dev" })
|
|
137
|
+
users.set(admin.id, admin)
|
|
138
|
+
|
|
139
|
+
const list = Effect.fn("Users.list")(function*(search: string | undefined) {
|
|
140
|
+
const allUsers = Array.from(users.values())
|
|
141
|
+
if (search === undefined || search.length === 0) {
|
|
142
|
+
return allUsers
|
|
143
|
+
} else if (search.length < SearchQueryTooShort.minimumLength) {
|
|
144
|
+
return yield* new UsersError({
|
|
145
|
+
reason: new SearchQueryTooShort()
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
yield* Effect.annotateCurrentSpan({ search })
|
|
149
|
+
const normalized = search.toLowerCase()
|
|
150
|
+
return allUsers.filter((user) =>
|
|
151
|
+
user.name.toLowerCase().includes(normalized) || user.email.toLowerCase().includes(normalized)
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const getById = Effect.fn("Users.getById")(function*(id: UserId) {
|
|
156
|
+
yield* Effect.annotateCurrentSpan({ id })
|
|
157
|
+
const user = users.get(id)
|
|
158
|
+
if (user === undefined) {
|
|
159
|
+
return yield* new UsersError({
|
|
160
|
+
reason: new UserNotFound()
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
return user
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const create = Effect.fn("Users.create")(function*(input: typeof User.jsonCreate.Type) {
|
|
167
|
+
const user = yield* makeUser(input)
|
|
168
|
+
users.set(user.id, user)
|
|
169
|
+
return user
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const update = Effect.fn("Users.update")(function*(id: UserId, input: typeof User.jsonUpdate.Type) {
|
|
173
|
+
const existing = yield* getById(id)
|
|
174
|
+
const update = yield* User.update.makeEffect({ id, ...input }).pipe(Effect.orDie)
|
|
175
|
+
const updated = new User({ ...existing, ...update })
|
|
176
|
+
users.set(id, updated)
|
|
177
|
+
return updated
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return Users.of({ list, getById, create, update })
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
*/
|
|
7
|
+
import { Config, Context, Effect, Layer, Redacted, Schema, Stream } from "effect"
|
|
8
|
+
import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLMiddleware, GraphQLProtocol } from "effect/graphql"
|
|
9
|
+
import { 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.
|
|
29
|
+
export const PageInfo = Schema.Struct({
|
|
30
|
+
hasNextPage: Schema.Boolean,
|
|
31
|
+
endCursor: Schema.NullOr(Schema.String)
|
|
32
|
+
})
|
|
33
|
+
export interface PageInfo extends Schema.Struct.Type<typeof PageInfo.fields> {}
|
|
34
|
+
|
|
35
|
+
export const Issue = Schema.Struct({ number: Schema.Int, title: Schema.String })
|
|
36
|
+
export interface Issue extends Schema.Struct.Type<typeof Issue.fields> {}
|
|
37
|
+
|
|
38
|
+
export const IssueConnection = Schema.Struct({
|
|
39
|
+
pageInfo: PageInfo,
|
|
40
|
+
nodes: Schema.NullOr(Schema.Array(Schema.NullOr(Issue)))
|
|
41
|
+
})
|
|
42
|
+
export interface IssueConnection extends Schema.Struct.Type<typeof IssueConnection.fields> {}
|
|
43
|
+
|
|
44
|
+
export const Repository = Schema.Struct({ issues: IssueConnection })
|
|
45
|
+
export interface Repository extends Schema.Struct.Type<typeof Repository.fields> {}
|
|
46
|
+
|
|
47
|
+
export 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.
|
|
62
|
+
export 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.
|
|
79
|
+
export const GitHubGraphQLGroup = GraphQLGroup.make(RepoIssues).middleware(GitHubAuth)
|
|
80
|
+
|
|
81
|
+
// The client has one method per operation, named after it.
|
|
82
|
+
export 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
|
+
|
|
97
|
+
export 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))
|