@effect/sql-sqlite-wasm 4.0.0-beta.99 → 4.0.0-rc.108

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 (92) hide show
  1. package/AGENTS.md +381 -0
  2. package/CLAUDE.md +381 -0
  3. package/README.md +10 -3
  4. package/ai-docs/README.md +44 -0
  5. package/ai-docs/package.json +36 -0
  6. package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
  7. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +39 -0
  8. package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
  9. package/ai-docs/src/01_effect/01_basics/index.md +5 -0
  10. package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
  11. package/ai-docs/src/01_effect/02_schema/index.md +7 -0
  12. package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
  13. package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
  14. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
  15. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
  16. package/ai-docs/src/01_effect/03_services/index.md +5 -0
  17. package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
  18. package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
  19. package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
  20. package/ai-docs/src/01_effect/04_errors/index.md +1 -0
  21. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
  22. package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
  23. package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
  24. package/ai-docs/src/01_effect/05_resources/index.md +3 -0
  25. package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
  26. package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
  27. package/ai-docs/src/01_effect/06_running/index.md +1 -0
  28. package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
  29. package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
  30. package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
  31. package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
  32. package/ai-docs/src/03_stream/30_encoding.ts +165 -0
  33. package/ai-docs/src/03_stream/index.md +4 -0
  34. package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
  35. package/ai-docs/src/04_integration/index.md +5 -0
  36. package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
  37. package/ai-docs/src/05_batching/index.md +3 -0
  38. package/ai-docs/src/06_schedule/10_schedules.ts +110 -0
  39. package/ai-docs/src/06_schedule/index.md +3 -0
  40. package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
  41. package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
  42. package/ai-docs/src/07_datetime/index.md +5 -0
  43. package/ai-docs/src/08_observability/10_logging.ts +66 -0
  44. package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
  45. package/ai-docs/src/08_observability/index.md +7 -0
  46. package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
  47. package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
  48. package/ai-docs/src/09_testing/index.md +1 -0
  49. package/ai-docs/src/10_predicate/01_basics.ts +14 -0
  50. package/ai-docs/src/10_predicate/index.md +9 -0
  51. package/ai-docs/src/50_http-client/10_basics.ts +102 -0
  52. package/ai-docs/src/50_http-client/index.md +3 -0
  53. package/ai-docs/src/51_http-server/10_basics.ts +116 -0
  54. package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
  55. package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
  56. package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
  57. package/ai-docs/src/51_http-server/fixtures/api/Users.ts +91 -0
  58. package/ai-docs/src/51_http-server/fixtures/domain/User.ts +12 -0
  59. package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
  60. package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +36 -0
  61. package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +71 -0
  62. package/ai-docs/src/51_http-server/fixtures/server/Users.ts +62 -0
  63. package/ai-docs/src/51_http-server/index.md +3 -0
  64. package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
  65. package/ai-docs/src/60_child-process/index.md +3 -0
  66. package/ai-docs/src/70_cli/10_basics.ts +136 -0
  67. package/ai-docs/src/70_cli/index.md +5 -0
  68. package/ai-docs/src/71_ai/10_language-model.ts +156 -0
  69. package/ai-docs/src/71_ai/20_tools.ts +226 -0
  70. package/ai-docs/src/71_ai/30_chat.ts +158 -0
  71. package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
  72. package/ai-docs/src/71_ai/index.md +5 -0
  73. package/ai-docs/src/80_cluster/10_entities.ts +97 -0
  74. package/ai-docs/src/80_cluster/index.md +4 -0
  75. package/ai-docs/src/index.md +10 -0
  76. package/ai-docs/tsconfig.json +24 -0
  77. package/dist/OpfsWorker.d.ts +1 -1
  78. package/dist/OpfsWorker.d.ts.map +1 -1
  79. package/dist/OpfsWorker.js +2 -2
  80. package/dist/OpfsWorker.js.map +1 -1
  81. package/dist/SqliteClient.d.ts +2 -2
  82. package/dist/SqliteClient.d.ts.map +1 -1
  83. package/dist/SqliteClient.js +15 -6
  84. package/dist/SqliteClient.js.map +1 -1
  85. package/dist/SqliteMigrator.d.ts +2 -2
  86. package/dist/SqliteMigrator.js +2 -2
  87. package/dist/SqliteMigrator.js.map +1 -1
  88. package/dist/index.js.map +1 -1
  89. package/package.json +10 -8
  90. package/src/OpfsWorker.ts +5 -2
  91. package/src/SqliteClient.ts +16 -8
  92. package/src/SqliteMigrator.ts +2 -2
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @title Getting started with HttpClient
3
+ *
4
+ * Define a service that uses the HttpClient module to fetch data from an external API
5
+ */
6
+ import { Context, Effect, flow, Layer, Schedule, Schema } from "effect"
7
+ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
8
+
9
+ class Todo extends Schema.Class<Todo>("Todo")({
10
+ userId: Schema.Int,
11
+ id: Schema.Int,
12
+ title: Schema.String,
13
+ completed: Schema.Boolean
14
+ }) {}
15
+
16
+ export class JsonPlaceholder extends Context.Service<JsonPlaceholder, {
17
+ readonly allTodos: Effect.Effect<ReadonlyArray<Todo>, JsonPlaceholderError>
18
+ getTodo(id: number): Effect.Effect<Todo, JsonPlaceholderError>
19
+ createTodo(todo: Omit<Todo, "id">): Effect.Effect<Todo, JsonPlaceholderError>
20
+ }>()("app/JsonPlaceholder") {
21
+ static readonly layer = Layer.effect(
22
+ JsonPlaceholder,
23
+ Effect.gen(function*() {
24
+ // Access the HttpClient service, and apply some common middleware to all
25
+ // requests:
26
+ const client = (yield* HttpClient.HttpClient).pipe(
27
+ // Add a base URL to all requests made with this client, and set the
28
+ // Accept header to expect JSON responses
29
+ HttpClient.mapRequest(flow(
30
+ HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com"),
31
+ HttpClientRequest.acceptJson
32
+ )),
33
+ // Fail if the response status is not 2xx
34
+ HttpClient.filterStatusOk,
35
+ // Retry transient errors (network issues, 5xx responses) with an
36
+ // exponential backoff.
37
+ //
38
+ // See the schedule documentation for more complex retry strategies.
39
+ HttpClient.retryTransient({
40
+ schedule: Schedule.exponential(100),
41
+ times: 3
42
+ })
43
+ )
44
+
45
+ const allTodos = client.get("/todos").pipe(
46
+ Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Array(Todo))),
47
+ Effect.mapError((cause) => new JsonPlaceholderError({ cause })),
48
+ Effect.withSpan("JsonPlaceholder.allTodos")
49
+ )
50
+
51
+ // Use the HttpClient to fetch a todo item by id, and decode the response
52
+ // using the Todo schema.
53
+ const getTodo = Effect.fn("JsonPlaceholder.getTodo")(function*(id: number) {
54
+ // Annotate the current span with the id of the todo being fetched, so
55
+ // that it shows up in telemetry for this request.
56
+ yield* Effect.annotateCurrentSpan({ id })
57
+
58
+ const todo = yield* client.get(`/todos/${id}`, {
59
+ // You can pass additional options to individual requests.
60
+ // There are options for query parameters, request body, headers, and
61
+ // more.
62
+ urlParams: { format: "json" }
63
+ }).pipe(
64
+ Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)),
65
+ Effect.mapError((cause) => new JsonPlaceholderError({ cause }))
66
+ )
67
+
68
+ return todo
69
+ })
70
+
71
+ // You can use the HttpClientRequest module to build up more complex
72
+ // requests:
73
+ const createTodo = Effect.fn("JsonPlaceholder.createTodo")(function*(todo: Omit<Todo, "id">) {
74
+ yield* Effect.annotateCurrentSpan({ title: todo.title })
75
+
76
+ const createdTodo = yield* HttpClientRequest.post("/todos").pipe(
77
+ // The HttpClientRequest module has many helper functions for building requests.
78
+ HttpClientRequest.setUrlParams({ format: "json" }),
79
+ HttpClientRequest.bodyJsonUnsafe(todo),
80
+ client.execute,
81
+ Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)),
82
+ Effect.mapError((cause) => new JsonPlaceholderError({ cause }))
83
+ )
84
+
85
+ return createdTodo
86
+ })
87
+
88
+ return JsonPlaceholder.of({
89
+ allTodos,
90
+ getTodo,
91
+ createTodo
92
+ })
93
+ })
94
+ ).pipe(
95
+ // Provide the fetch-based HttpClient implementation
96
+ Layer.provide(FetchHttpClient.layer)
97
+ )
98
+ }
99
+
100
+ export class JsonPlaceholderError extends Schema.TaggedError<JsonPlaceholderError>()("JsonPlaceholderError", {
101
+ cause: Schema.Defect()
102
+ }) {}
@@ -0,0 +1,3 @@
1
+ ## Effect HttpClient
2
+
3
+ Build http clients with the `HttpClient` module.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @title Getting started with HttpApi
3
+ *
4
+ * Define a schema-first API, implement handlers, secure endpoints with
5
+ * middleware, serve it over HTTP, and call it using a generated typed client.
6
+ */
7
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
8
+ import { Context, Effect, flow, Layer, Schedule } from "effect"
9
+ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
10
+ import { HttpApiBuilder, HttpApiClient, HttpApiMiddleware, HttpApiScalar } from "effect/unstable/httpapi"
11
+ import { createServer } from "node:http"
12
+ // Api definitions should **always** be seperate from the server implementation,
13
+ // so that they can be shared between the server and client without leaking
14
+ // server code into clients.
15
+ // Ideally, the would use a seperate package in a monorepo.
16
+ import { Api } from "./fixtures/api/Api.ts"
17
+ import { Authorization } from "./fixtures/api/Authorization.ts"
18
+ import { UsersApiHandlers } from "./fixtures/server/Users/http.ts"
19
+
20
+ // This walkthrough focuses on runtime wiring and typed client usage.
21
+ // See the fixture files for the API schemas, endpoint definitions and handlers:
22
+
23
+ const SystemApiHandlers = HttpApiBuilder.group(
24
+ Api,
25
+ "system",
26
+ Effect.fn(function*(handlers) {
27
+ return handlers.handle("health", () => Effect.void)
28
+ })
29
+ )
30
+
31
+ const ApiRoutes = HttpApiBuilder.layer(Api, {
32
+ openapiPath: "/openapi.json"
33
+ }).pipe(
34
+ // Provide all the handler Layers for the API.
35
+ Layer.provide([UsersApiHandlers, SystemApiHandlers])
36
+ )
37
+
38
+ // Define a /docs route that serves scalar documentation
39
+ const DocsRoute = HttpApiScalar.layer(Api, {
40
+ path: "/docs"
41
+ })
42
+
43
+ // Merge all the http routes together
44
+ const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute)
45
+
46
+ // Create an HTTP server Layer that serves the API routes.
47
+ //
48
+ // Here we are using the NodeHttpServer, but you could also use the
49
+ // BunHttpServer
50
+ export const HttpServerLayer = HttpRouter.serve(AllRoutes).pipe(
51
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
52
+ )
53
+
54
+ // Then run the server using Layer.launch
55
+ Layer.launch(HttpServerLayer).pipe(
56
+ NodeRuntime.runMain
57
+ )
58
+
59
+ // Or create a web handler, which can be used in serverless environments
60
+ export const { handler, dispose } = HttpRouter.toWebHandler(AllRoutes.pipe(
61
+ Layer.provide(HttpServer.layerServices)
62
+ ))
63
+
64
+ // -----------------
65
+ // Client side setup
66
+ // -----------------
67
+
68
+ export const AuthorizationClient = HttpApiMiddleware.layerClient(
69
+ Authorization,
70
+ Effect.fn(function*({ next, request }) {
71
+ // Here you can modify the request and pass it down the middleware chain.
72
+ // This is where you would add authentication tokens, custom headers, etc.
73
+ // For this example, we just add a hardcoded bearer token to all requests.
74
+ return yield* next(HttpClientRequest.bearerToken(request, "dev-token"))
75
+ })
76
+ )
77
+
78
+ // Define the HttpApiClient service, which will be used to make requests to the
79
+ // API.
80
+ export class ApiClient extends Context.Service<ApiClient, HttpApiClient.ForApi<typeof Api>>()("acme/ApiClient") {
81
+ static readonly layer = Layer.effect(
82
+ ApiClient,
83
+ HttpApiClient.make(Api, {
84
+ // Use transformClient to apply middleware to the generated client. This
85
+ // is useful for settings the base url and applying retry policies.
86
+ transformClient: (client) =>
87
+ client.pipe(
88
+ HttpClient.mapRequest(flow(
89
+ HttpClientRequest.prependUrl("http://localhost:3000")
90
+ )),
91
+ HttpClient.retryTransient({
92
+ schedule: Schedule.exponential(100),
93
+ times: 3
94
+ })
95
+ )
96
+ })
97
+ ).pipe(
98
+ // Provide the client implementation of the Authorization middleware, which
99
+ // is required.
100
+ Layer.provide(AuthorizationClient),
101
+ // Supply a HttpClient implementation to use for making requests. Here we
102
+ // use the FetchHttpClient, but you could also use the NodeHttpClient or
103
+ // BunHttpClient.
104
+ Layer.provide(FetchHttpClient.layer)
105
+ )
106
+ }
107
+
108
+ // The generated client mirrors your API definition, so renames and schema
109
+ // changes are checked end-to-end at compile time.
110
+ export const callApi = Effect.gen(function*() {
111
+ const client = yield* ApiClient
112
+
113
+ yield* client.health()
114
+ }).pipe(
115
+ Effect.provide(ApiClient.layer)
116
+ )
@@ -0,0 +1,14 @@
1
+ import { HttpApi, OpenApi } from "effect/unstable/httpapi"
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/unstable/httpapi"
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/unstable/httpapi"
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,91 @@
1
+ import { Schema } from "effect"
2
+ import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
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
+ success: Schema.Array(User)
14
+ }),
15
+ HttpApiEndpoint.get("search", "/search", {
16
+ // For get requests, payload uses the query string
17
+ payload: {
18
+ search: Schema.String
19
+ },
20
+ success: [
21
+ Schema.Array(User),
22
+ Schema.String.pipe(HttpApiSchema.asText({
23
+ contentType: "text/csv"
24
+ }))
25
+ ],
26
+ error: [
27
+ SearchQueryTooShort.pipe(
28
+ // If you want an error to return no content, you can use
29
+ // `HttpApiSchema.asNoContent` and provide a decoder that transforms the
30
+ // error into the appropriate type.
31
+ HttpApiSchema.asNoContent({
32
+ decode: () => new SearchQueryTooShort()
33
+ })
34
+ ),
35
+ // You can also add some of the built in `HttpApiError`s to handle common
36
+ // error cases like bad requests, unauthorized, etc.
37
+ HttpApiError.RequestTimeoutNoContent
38
+ ]
39
+ }),
40
+ HttpApiEndpoint.get("getById", "/:id", {
41
+ params: {
42
+ // Path parameter schemas need to be able to decode from strings.
43
+ // Schema.decodeTo can be used to "bridge" between schemas
44
+ id: Schema.FiniteFromString.pipe(
45
+ Schema.decodeTo(UserId)
46
+ )
47
+ },
48
+ success: User,
49
+ error: UserNotFound.pipe(
50
+ // If you want an error to return no content, you can use
51
+ // `HttpApiSchema.asNoContent` and provide a decoder that transforms the
52
+ // error into the appropriate type.
53
+ HttpApiSchema.asNoContent({
54
+ decode: () => new UserNotFound()
55
+ })
56
+ )
57
+ }),
58
+ HttpApiEndpoint.post("create", "/", {
59
+ // For post requests, payload uses the request body. It defaults to JSON,
60
+ // but you can specify other content types as well using
61
+ // `HttpApiSchema.asText`, `HttpApiSchema.asMultipart`, etc.
62
+ payload: Schema.Struct({
63
+ name: Schema.String,
64
+ email: Schema.String
65
+ }),
66
+ success: User
67
+ }),
68
+ HttpApiEndpoint.get("me", "/me", {
69
+ success: User,
70
+ error: UserNotFound.pipe(HttpApiSchema.status(404))
71
+ })
72
+ )
73
+ // You can apply middleware to entire groups, which is useful for things like
74
+ // authentication and authorization.
75
+ //
76
+ // You can also apply middleware to individual endpoints if you need more
77
+ // fine-grained control.
78
+ .middleware(Authorization)
79
+ // To add a common prefix to all endpoints in a group, you can use the `prefix`
80
+ // method. This is useful for grouping related endpoints together under a common
81
+ // path segment. In this case, all endpoints in the `UsersApiGroup` will be
82
+ // prefixed with `/users`.
83
+ .prefix("/users")
84
+ // You can add OpenAPI annotations to groups, endpoints, and even parameters and
85
+ // request bodies. These will be merged together to generate the final OpenAPI
86
+ // docs for the API
87
+ .annotateMerge(OpenApi.annotations({
88
+ title: "Users",
89
+ description: "User management endpoints"
90
+ }))
91
+ {}
@@ -0,0 +1,12 @@
1
+ import { Schema } from "effect"
2
+
3
+ export const UserId = Schema.Int.pipe(
4
+ Schema.brand("UserId")
5
+ )
6
+ export type UserId = typeof UserId.Type
7
+
8
+ export class User extends Schema.Class<User>("User")({
9
+ id: UserId,
10
+ name: Schema.String,
11
+ email: Schema.String
12
+ }) {}
@@ -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,36 @@
1
+ import { Effect, Layer, Redacted } from "effect"
2
+ import { Authorization, CurrentUser, Unauthorized } from "../api/Authorization.ts"
3
+ import { User, UserId } from "../domain/User.ts"
4
+
5
+ // The implementation of the Authorization middleware. It is seperate from the
6
+ // service definition to avoid leaking it into a client.
7
+ export const AuthorizationLayer = Layer.effect(
8
+ Authorization,
9
+ Effect.gen(function*() {
10
+ // Here you could access services required by the middleware, like a
11
+ // database or an external auth provider.
12
+ yield* Effect.logInfo("Starting Authorization middleware")
13
+
14
+ return Authorization.of({
15
+ bearer: Effect.fn(function*(httpEffect, { credential }) {
16
+ // Validate the token and return an Unauthorized error if it's invalid.
17
+ const token = Redacted.value(credential)
18
+ if (token !== "dev-token") {
19
+ return yield* new Unauthorized({ message: "Missing or invalid bearer token" })
20
+ }
21
+
22
+ // Provide the current user to the rest of the stack. This will be
23
+ // available in any endpoint or middleware that runs after this one.
24
+ return yield* Effect.provideService(
25
+ httpEffect,
26
+ CurrentUser,
27
+ new User({
28
+ id: UserId.make(1),
29
+ name: "Dev User",
30
+ email: "dev@acme.com"
31
+ })
32
+ )
33
+ })
34
+ })
35
+ })
36
+ )
@@ -0,0 +1,71 @@
1
+ import { Effect, Layer } from "effect"
2
+ import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
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
+ export const UsersApiHandlers = HttpApiBuilder.group(
9
+ Api,
10
+ "users",
11
+ Effect.fn(function*(handlers) {
12
+ const users = yield* Users
13
+
14
+ return handlers
15
+ .handle("list", ({ query }) =>
16
+ users.list(query.search).pipe(
17
+ // The list endpoint expects no errors, so we convert any potential
18
+ // errors into a 500 Internal Server Error.
19
+ Effect.orDie
20
+ ))
21
+ .handle(
22
+ "search",
23
+ 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
+ )
42
+ .handle("getById", ({ params }) =>
43
+ users.getById(params.id).pipe(
44
+ // You can also use Effect.catchReasons to handle multiple error
45
+ // reasons at once
46
+ Effect.catchReasons("UsersError", {
47
+ UserNotFound: (e) => Effect.fail(e)
48
+ }, Effect.die)
49
+ ))
50
+ .handle("create", ({ payload }) =>
51
+ users.create(payload).pipe(
52
+ Effect.orDie
53
+ // You could alse use Effect.unwrapReason to moves rror reasons up to
54
+ // the top level, so you can handle them with Effect.catch or
55
+ // Effect.catchTag etc.
56
+ //
57
+ // Effect.unwrapReason("UsersError"),
58
+ // Effect.catchTags({
59
+ // UserNotFound: Effect.die,
60
+ // SearchQueryTooShort: Effect.die
61
+ // })
62
+ ))
63
+ .handle("me", () =>
64
+ // The Authorization middleware provides the CurrentUser service, so we
65
+ // can access it here.
66
+ CurrentUser)
67
+ })
68
+ ).pipe(
69
+ // Provide the dependencies for the handlers.
70
+ Layer.provide([Users.layer, AuthorizationLayer])
71
+ )
@@ -0,0 +1,62 @@
1
+ import { Context, Effect, Layer, Ref } from "effect"
2
+ import { User, UserId } from "../domain/User.ts"
3
+ import { SearchQueryTooShort, UserNotFound, UsersError } from "../domain/UserErrors.ts"
4
+
5
+ export class Users extends Context.Service<Users, {
6
+ list(search: string | undefined): Effect.Effect<Array<User>, UsersError>
7
+ getById(id: UserId): Effect.Effect<User, UsersError>
8
+ create(input: { readonly name: string; readonly email: string }): Effect.Effect<User, UsersError>
9
+ }>()("acme/Users") {
10
+ static readonly layer = Layer.effect(
11
+ Users,
12
+ Effect.gen(function*() {
13
+ const users = new Map<number, User>([
14
+ [
15
+ 1,
16
+ new User({
17
+ id: UserId.make(1),
18
+ name: "Admin",
19
+ email: "admin@acme.dev"
20
+ })
21
+ ]
22
+ ])
23
+ const nextId = yield* Ref.make(2)
24
+
25
+ const list = Effect.fn("UsersRepo.list")(function*(search: string | undefined) {
26
+ const allUsers = Array.from(users.values())
27
+ if (search === undefined || search.length === 0) {
28
+ return allUsers
29
+ } else if (search.length < SearchQueryTooShort.minimumLength) {
30
+ return yield* new UsersError({
31
+ reason: new SearchQueryTooShort()
32
+ })
33
+ }
34
+ yield* Effect.annotateCurrentSpan({ search })
35
+ const normalized = search.toLowerCase()
36
+ return allUsers.filter((user) =>
37
+ user.name.toLowerCase().includes(normalized) || user.email.toLowerCase().includes(normalized)
38
+ )
39
+ })
40
+
41
+ const getById = Effect.fn("UsersRepo.getById")(function*(id: UserId) {
42
+ yield* Effect.annotateCurrentSpan({ id })
43
+ const user = users.get(id)
44
+ if (user === undefined) {
45
+ return yield* new UsersError({
46
+ reason: new UserNotFound()
47
+ })
48
+ }
49
+ return user
50
+ })
51
+
52
+ const create = Effect.fn("UsersRepo.create")(function*(input: { readonly name: string; readonly email: string }) {
53
+ const id = yield* Ref.getAndUpdate(nextId, (current) => current + 1)
54
+ const user = new User({ id: UserId.make(id), ...input })
55
+ users.set(user.id, user)
56
+ return user
57
+ })
58
+
59
+ return Users.of({ list, getById, create })
60
+ })
61
+ )
62
+ }
@@ -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.