@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.
Files changed (237) hide show
  1. package/AGENTS.md +434 -0
  2. package/CLAUDE.md +434 -0
  3. package/LICENSE +21 -0
  4. package/README.md +506 -0
  5. package/ai-docs/README.md +44 -0
  6. package/ai-docs/package.json +36 -0
  7. package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
  8. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +52 -0
  9. package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
  10. package/ai-docs/src/01_effect/01_basics/index.md +7 -0
  11. package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
  12. package/ai-docs/src/01_effect/02_schema/index.md +7 -0
  13. package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
  14. package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
  15. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
  16. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
  17. package/ai-docs/src/01_effect/03_services/index.md +5 -0
  18. package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
  19. package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
  20. package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
  21. package/ai-docs/src/01_effect/04_errors/index.md +1 -0
  22. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
  23. package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
  24. package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
  25. package/ai-docs/src/01_effect/05_resources/index.md +3 -0
  26. package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
  27. package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
  28. package/ai-docs/src/01_effect/06_running/index.md +1 -0
  29. package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
  30. package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
  31. package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
  32. package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
  33. package/ai-docs/src/03_stream/30_encoding.ts +163 -0
  34. package/ai-docs/src/03_stream/index.md +4 -0
  35. package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
  36. package/ai-docs/src/04_integration/index.md +5 -0
  37. package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
  38. package/ai-docs/src/05_batching/index.md +3 -0
  39. package/ai-docs/src/06_schedule/10_schedules.ts +111 -0
  40. package/ai-docs/src/06_schedule/index.md +3 -0
  41. package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
  42. package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
  43. package/ai-docs/src/07_datetime/index.md +5 -0
  44. package/ai-docs/src/08_observability/10_logging.ts +66 -0
  45. package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
  46. package/ai-docs/src/08_observability/index.md +7 -0
  47. package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
  48. package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
  49. package/ai-docs/src/09_testing/index.md +1 -0
  50. package/ai-docs/src/10_predicate/01_basics.ts +14 -0
  51. package/ai-docs/src/10_predicate/index.md +9 -0
  52. package/ai-docs/src/40_sql/10_basics.ts +175 -0
  53. package/ai-docs/src/40_sql/index.md +6 -0
  54. package/ai-docs/src/50_http-client/10_basics.ts +102 -0
  55. package/ai-docs/src/50_http-client/index.md +3 -0
  56. package/ai-docs/src/51_http-server/10_basics.ts +118 -0
  57. package/ai-docs/src/51_http-server/20_testing.ts +100 -0
  58. package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
  59. package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
  60. package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
  61. package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
  62. package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
  63. package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
  64. package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
  65. package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
  66. package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
  67. package/ai-docs/src/51_http-server/index.md +3 -0
  68. package/ai-docs/src/52_graphql/10_basics.ts +117 -0
  69. package/ai-docs/src/52_graphql/20_testing.ts +45 -0
  70. package/ai-docs/src/52_graphql/30_batching.ts +118 -0
  71. package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
  72. package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
  73. package/ai-docs/src/52_graphql/index.md +6 -0
  74. package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
  75. package/ai-docs/src/60_child-process/index.md +3 -0
  76. package/ai-docs/src/70_cli/10_basics.ts +162 -0
  77. package/ai-docs/src/70_cli/index.md +5 -0
  78. package/ai-docs/src/71_ai/10_language-model.ts +156 -0
  79. package/ai-docs/src/71_ai/20_tools.ts +226 -0
  80. package/ai-docs/src/71_ai/30_chat.ts +158 -0
  81. package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
  82. package/ai-docs/src/71_ai/index.md +5 -0
  83. package/ai-docs/src/80_cluster/10_entities.ts +97 -0
  84. package/ai-docs/src/80_cluster/index.md +4 -0
  85. package/ai-docs/src/index.md +10 -0
  86. package/ai-docs/tsconfig.json +28 -0
  87. package/dist/AlchemyCloudflareCluster.d.ts +134 -0
  88. package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
  89. package/dist/AlchemyCloudflareCluster.js +111 -0
  90. package/dist/AlchemyCloudflareCluster.js.map +1 -0
  91. package/dist/CloudflareCluster.d.ts +196 -0
  92. package/dist/CloudflareCluster.d.ts.map +1 -0
  93. package/dist/CloudflareCluster.js +533 -0
  94. package/dist/CloudflareCluster.js.map +1 -0
  95. package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
  96. package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
  97. package/dist/CloudflareDurableObjectPrograms.js +295 -0
  98. package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
  99. package/dist/CloudflareDurableObjects.d.ts +149 -0
  100. package/dist/CloudflareDurableObjects.d.ts.map +1 -0
  101. package/dist/CloudflareDurableObjects.js +337 -0
  102. package/dist/CloudflareDurableObjects.js.map +1 -0
  103. package/dist/CloudflarePersistedQueue.d.ts +36 -0
  104. package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
  105. package/dist/CloudflarePersistedQueue.js +122 -0
  106. package/dist/CloudflarePersistedQueue.js.map +1 -0
  107. package/dist/CloudflareWorkflowEngine.d.ts +72 -0
  108. package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
  109. package/dist/CloudflareWorkflowEngine.js +187 -0
  110. package/dist/CloudflareWorkflowEngine.js.map +1 -0
  111. package/dist/index.d.ts +29 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +33 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/internal/alchemyCluster.d.ts +2 -0
  116. package/dist/internal/alchemyCluster.d.ts.map +1 -0
  117. package/dist/internal/alchemyCluster.js +75 -0
  118. package/dist/internal/alchemyCluster.js.map +1 -0
  119. package/dist/internal/boundedMap.d.ts +3 -0
  120. package/dist/internal/boundedMap.d.ts.map +1 -0
  121. package/dist/internal/boundedMap.js +15 -0
  122. package/dist/internal/boundedMap.js.map +1 -0
  123. package/dist/internal/cluster.d.ts +57 -0
  124. package/dist/internal/cluster.d.ts.map +1 -0
  125. package/dist/internal/cluster.js +2 -0
  126. package/dist/internal/cluster.js.map +1 -0
  127. package/dist/internal/clusterName.d.ts +2 -0
  128. package/dist/internal/clusterName.d.ts.map +1 -0
  129. package/dist/internal/clusterName.js +16 -0
  130. package/dist/internal/clusterName.js.map +1 -0
  131. package/dist/internal/entityKeepAlive.d.ts +2 -0
  132. package/dist/internal/entityKeepAlive.d.ts.map +1 -0
  133. package/dist/internal/entityKeepAlive.js +73 -0
  134. package/dist/internal/entityKeepAlive.js.map +1 -0
  135. package/dist/internal/entityMailbox.d.ts +2 -0
  136. package/dist/internal/entityMailbox.d.ts.map +1 -0
  137. package/dist/internal/entityMailbox.js +160 -0
  138. package/dist/internal/entityMailbox.js.map +1 -0
  139. package/dist/internal/entityRegistry.d.ts +16 -0
  140. package/dist/internal/entityRegistry.d.ts.map +1 -0
  141. package/dist/internal/entityRegistry.js +22 -0
  142. package/dist/internal/entityRegistry.js.map +1 -0
  143. package/dist/internal/entityReply.d.ts +2 -0
  144. package/dist/internal/entityReply.d.ts.map +1 -0
  145. package/dist/internal/entityReply.js +35 -0
  146. package/dist/internal/entityReply.js.map +1 -0
  147. package/dist/internal/entityRuntime.d.ts +2 -0
  148. package/dist/internal/entityRuntime.d.ts.map +1 -0
  149. package/dist/internal/entityRuntime.js +823 -0
  150. package/dist/internal/entityRuntime.js.map +1 -0
  151. package/dist/internal/entitySql.d.ts +2 -0
  152. package/dist/internal/entitySql.d.ts.map +1 -0
  153. package/dist/internal/entitySql.js +29 -0
  154. package/dist/internal/entitySql.js.map +1 -0
  155. package/dist/internal/entityStorage.d.ts +2 -0
  156. package/dist/internal/entityStorage.d.ts.map +1 -0
  157. package/dist/internal/entityStorage.js +98 -0
  158. package/dist/internal/entityStorage.js.map +1 -0
  159. package/dist/internal/entityWire.d.ts +2 -0
  160. package/dist/internal/entityWire.d.ts.map +1 -0
  161. package/dist/internal/entityWire.js +146 -0
  162. package/dist/internal/entityWire.js.map +1 -0
  163. package/dist/internal/queueRuntime.d.ts +2 -0
  164. package/dist/internal/queueRuntime.d.ts.map +1 -0
  165. package/dist/internal/queueRuntime.js +88 -0
  166. package/dist/internal/queueRuntime.js.map +1 -0
  167. package/dist/internal/queueStorage.d.ts +2 -0
  168. package/dist/internal/queueStorage.d.ts.map +1 -0
  169. package/dist/internal/queueStorage.js +129 -0
  170. package/dist/internal/queueStorage.js.map +1 -0
  171. package/dist/internal/registry.d.ts +10 -0
  172. package/dist/internal/registry.d.ts.map +1 -0
  173. package/dist/internal/registry.js +24 -0
  174. package/dist/internal/registry.js.map +1 -0
  175. package/dist/internal/singletonRegistry.d.ts +2 -0
  176. package/dist/internal/singletonRegistry.d.ts.map +1 -0
  177. package/dist/internal/singletonRegistry.js +9 -0
  178. package/dist/internal/singletonRegistry.js.map +1 -0
  179. package/dist/internal/singletonRuntime.d.ts +2 -0
  180. package/dist/internal/singletonRuntime.d.ts.map +1 -0
  181. package/dist/internal/singletonRuntime.js +42 -0
  182. package/dist/internal/singletonRuntime.js.map +1 -0
  183. package/dist/internal/singletonStorage.d.ts +2 -0
  184. package/dist/internal/singletonStorage.d.ts.map +1 -0
  185. package/dist/internal/singletonStorage.js +34 -0
  186. package/dist/internal/singletonStorage.js.map +1 -0
  187. package/dist/internal/transport.d.ts +2 -0
  188. package/dist/internal/transport.d.ts.map +1 -0
  189. package/dist/internal/transport.js +49 -0
  190. package/dist/internal/transport.js.map +1 -0
  191. package/dist/internal/workflowRegistry.d.ts +2 -0
  192. package/dist/internal/workflowRegistry.d.ts.map +1 -0
  193. package/dist/internal/workflowRegistry.js +29 -0
  194. package/dist/internal/workflowRegistry.js.map +1 -0
  195. package/dist/internal/workflowRuntime.d.ts +2 -0
  196. package/dist/internal/workflowRuntime.d.ts.map +1 -0
  197. package/dist/internal/workflowRuntime.js +322 -0
  198. package/dist/internal/workflowRuntime.js.map +1 -0
  199. package/dist/internal/workflowStorage.d.ts +2 -0
  200. package/dist/internal/workflowStorage.d.ts.map +1 -0
  201. package/dist/internal/workflowStorage.js +113 -0
  202. package/dist/internal/workflowStorage.js.map +1 -0
  203. package/dist/internal/workflowWire.d.ts +2 -0
  204. package/dist/internal/workflowWire.d.ts.map +1 -0
  205. package/dist/internal/workflowWire.js +52 -0
  206. package/dist/internal/workflowWire.js.map +1 -0
  207. package/package.json +77 -1
  208. package/src/AlchemyCloudflareCluster.ts +200 -0
  209. package/src/CloudflareCluster.ts +704 -0
  210. package/src/CloudflareDurableObjectPrograms.ts +543 -0
  211. package/src/CloudflareDurableObjects.ts +430 -0
  212. package/src/CloudflarePersistedQueue.ts +189 -0
  213. package/src/CloudflareWorkflowEngine.ts +220 -0
  214. package/src/index.ts +38 -0
  215. package/src/internal/alchemyCluster.ts +120 -0
  216. package/src/internal/boundedMap.ts +15 -0
  217. package/src/internal/cluster.ts +57 -0
  218. package/src/internal/clusterName.ts +19 -0
  219. package/src/internal/entityKeepAlive.ts +121 -0
  220. package/src/internal/entityMailbox.ts +324 -0
  221. package/src/internal/entityRegistry.ts +46 -0
  222. package/src/internal/entityReply.ts +54 -0
  223. package/src/internal/entityRuntime.ts +1318 -0
  224. package/src/internal/entitySql.ts +60 -0
  225. package/src/internal/entityStorage.ts +168 -0
  226. package/src/internal/entityWire.ts +204 -0
  227. package/src/internal/queueRuntime.ts +174 -0
  228. package/src/internal/queueStorage.ts +255 -0
  229. package/src/internal/registry.ts +31 -0
  230. package/src/internal/singletonRegistry.ts +21 -0
  231. package/src/internal/singletonRuntime.ts +88 -0
  232. package/src/internal/singletonStorage.ts +64 -0
  233. package/src/internal/transport.ts +67 -0
  234. package/src/internal/workflowRegistry.ts +78 -0
  235. package/src/internal/workflowRuntime.ts +443 -0
  236. package/src/internal/workflowStorage.ts +234 -0
  237. 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,3 @@
1
+ ## Building HttpApi servers
2
+
3
+ `HttpApi` gives you schema-first, type-safe HTTP APIs with runtime validation, typed clients, and OpenAPI docs from one definition.
@@ -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))