@effect/platform 0.62.5 → 0.63.1
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/HttpApi/package.json +6 -0
- package/HttpApiBuilder/package.json +6 -0
- package/HttpApiClient/package.json +6 -0
- package/HttpApiEndpoint/package.json +6 -0
- package/HttpApiError/package.json +6 -0
- package/HttpApiGroup/package.json +6 -0
- package/HttpApiSchema/package.json +6 -0
- package/HttpApiSecurity/package.json +6 -0
- package/HttpApiSwagger/package.json +6 -0
- package/OpenApi/package.json +6 -0
- package/README.md +863 -302
- package/dist/cjs/HttpApi.js +168 -0
- package/dist/cjs/HttpApi.js.map +1 -0
- package/dist/cjs/HttpApiBuilder.js +471 -0
- package/dist/cjs/HttpApiBuilder.js.map +1 -0
- package/dist/cjs/HttpApiClient.js +134 -0
- package/dist/cjs/HttpApiClient.js.map +1 -0
- package/dist/cjs/HttpApiEndpoint.js +181 -0
- package/dist/cjs/HttpApiEndpoint.js.map +1 -0
- package/dist/cjs/HttpApiError.js +69 -0
- package/dist/cjs/HttpApiError.js.map +1 -0
- package/dist/cjs/HttpApiGroup.js +151 -0
- package/dist/cjs/HttpApiGroup.js.map +1 -0
- package/dist/cjs/HttpApiSchema.js +241 -0
- package/dist/cjs/HttpApiSchema.js.map +1 -0
- package/dist/cjs/HttpApiSecurity.js +83 -0
- package/dist/cjs/HttpApiSecurity.js.map +1 -0
- package/dist/cjs/HttpApiSwagger.js +50 -0
- package/dist/cjs/HttpApiSwagger.js.map +1 -0
- package/dist/cjs/HttpMethod.js +1 -1
- package/dist/cjs/HttpMethod.js.map +1 -1
- package/dist/cjs/HttpRouter.js +6 -1
- package/dist/cjs/HttpRouter.js.map +1 -1
- package/dist/cjs/OpenApi.js +317 -0
- package/dist/cjs/OpenApi.js.map +1 -0
- package/dist/cjs/index.js +21 -1
- package/dist/cjs/internal/apiSwagger.js +2 -0
- package/dist/cjs/internal/apiSwagger.js.map +1 -0
- package/dist/cjs/internal/httpRouter.js +6 -1
- package/dist/cjs/internal/httpRouter.js.map +1 -1
- package/dist/cjs/internal/multipart.js +5 -1
- package/dist/cjs/internal/multipart.js.map +1 -1
- package/dist/dts/HttpApi.d.ts +156 -0
- package/dist/dts/HttpApi.d.ts.map +1 -0
- package/dist/dts/HttpApiBuilder.d.ts +296 -0
- package/dist/dts/HttpApiBuilder.d.ts.map +1 -0
- package/dist/dts/HttpApiClient.d.ts +31 -0
- package/dist/dts/HttpApiClient.d.ts.map +1 -0
- package/dist/dts/HttpApiEndpoint.d.ts +350 -0
- package/dist/dts/HttpApiEndpoint.d.ts.map +1 -0
- package/dist/dts/HttpApiError.d.ts +70 -0
- package/dist/dts/HttpApiError.d.ts.map +1 -0
- package/dist/dts/HttpApiGroup.d.ts +196 -0
- package/dist/dts/HttpApiGroup.d.ts.map +1 -0
- package/dist/dts/HttpApiSchema.d.ts +223 -0
- package/dist/dts/HttpApiSchema.d.ts.map +1 -0
- package/dist/dts/HttpApiSecurity.d.ts +122 -0
- package/dist/dts/HttpApiSecurity.d.ts.map +1 -0
- package/dist/dts/HttpApiSwagger.d.ts +10 -0
- package/dist/dts/HttpApiSwagger.d.ts.map +1 -0
- package/dist/dts/HttpMethod.d.ts +16 -0
- package/dist/dts/HttpMethod.d.ts.map +1 -1
- package/dist/dts/HttpRouter.d.ts +8 -0
- package/dist/dts/HttpRouter.d.ts.map +1 -1
- package/dist/dts/HttpServerResponse.d.ts +3 -3
- package/dist/dts/HttpServerResponse.d.ts.map +1 -1
- package/dist/dts/OpenApi.d.ts +348 -0
- package/dist/dts/OpenApi.d.ts.map +1 -0
- package/dist/dts/index.d.ts +40 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/internal/apiSwagger.d.ts +2 -0
- package/dist/dts/internal/apiSwagger.d.ts.map +1 -0
- package/dist/dts/internal/httpRouter.d.ts.map +1 -1
- package/dist/esm/HttpApi.js +157 -0
- package/dist/esm/HttpApi.js.map +1 -0
- package/dist/esm/HttpApiBuilder.js +447 -0
- package/dist/esm/HttpApiBuilder.js.map +1 -0
- package/dist/esm/HttpApiClient.js +124 -0
- package/dist/esm/HttpApiClient.js.map +1 -0
- package/dist/esm/HttpApiEndpoint.js +169 -0
- package/dist/esm/HttpApiEndpoint.js.map +1 -0
- package/dist/esm/HttpApiError.js +59 -0
- package/dist/esm/HttpApiError.js.map +1 -0
- package/dist/esm/HttpApiGroup.js +140 -0
- package/dist/esm/HttpApiGroup.js.map +1 -0
- package/dist/esm/HttpApiSchema.js +217 -0
- package/dist/esm/HttpApiSchema.js.map +1 -0
- package/dist/esm/HttpApiSecurity.js +73 -0
- package/dist/esm/HttpApiSecurity.js.map +1 -0
- package/dist/esm/HttpApiSwagger.js +40 -0
- package/dist/esm/HttpApiSwagger.js.map +1 -0
- package/dist/esm/HttpMethod.js +1 -1
- package/dist/esm/HttpMethod.js.map +1 -1
- package/dist/esm/HttpRouter.js +5 -0
- package/dist/esm/HttpRouter.js.map +1 -1
- package/dist/esm/OpenApi.js +299 -0
- package/dist/esm/OpenApi.js.map +1 -0
- package/dist/esm/index.js +40 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/apiSwagger.js +2 -0
- package/dist/esm/internal/apiSwagger.js.map +1 -0
- package/dist/esm/internal/httpRouter.js +5 -0
- package/dist/esm/internal/httpRouter.js.map +1 -1
- package/dist/esm/internal/multipart.js +5 -1
- package/dist/esm/internal/multipart.js.map +1 -1
- package/package.json +83 -3
- package/src/HttpApi.ts +342 -0
- package/src/HttpApiBuilder.ts +869 -0
- package/src/HttpApiClient.ts +228 -0
- package/src/HttpApiEndpoint.ts +818 -0
- package/src/HttpApiError.ts +113 -0
- package/src/HttpApiGroup.ts +365 -0
- package/src/HttpApiSchema.ts +384 -0
- package/src/HttpApiSecurity.ts +169 -0
- package/src/HttpApiSwagger.ts +46 -0
- package/src/HttpMethod.ts +19 -1
- package/src/HttpRouter.ts +9 -0
- package/src/HttpServerResponse.ts +3 -3
- package/src/OpenApi.ts +665 -0
- package/src/index.ts +50 -0
- package/src/internal/apiSwagger.ts +7 -0
- package/src/internal/httpRouter.ts +9 -0
- package/src/internal/multipart.ts +5 -1
package/README.md
CHANGED
|
@@ -8,416 +8,573 @@ This package empowers you to perform various operations, such as:
|
|
|
8
8
|
|
|
9
9
|
| **Operation** | **Description** |
|
|
10
10
|
| -------------- | ------------------------------------------------------------------------------------------------ |
|
|
11
|
-
|
|
|
12
|
-
| Command | Creating and running a command with the specified process name and an optional list of arguments |
|
|
13
|
-
| FileSystem | Reading and writing from/to the file system |
|
|
11
|
+
| HTTP API | Declarative HTTP API servers & clients |
|
|
14
12
|
| HTTP Client | Sending HTTP requests and receiving responses |
|
|
15
13
|
| HTTP Server | Creating HTTP servers to handle incoming requests |
|
|
16
14
|
| HTTP Router | Routing HTTP requests to specific handlers |
|
|
15
|
+
| Terminal | Reading and writing from/to standard input/output |
|
|
16
|
+
| Command | Creating and running a command with the specified process name and an optional list of arguments |
|
|
17
|
+
| FileSystem | Reading and writing from/to the file system |
|
|
17
18
|
| KeyValueStore | Storing and retrieving key-value pairs |
|
|
18
19
|
| PlatformLogger | Creating a logger that writes to a specified file from another string logger |
|
|
19
20
|
|
|
20
21
|
By utilizing `@effect/platform`, you can write code that remains platform-agnostic, ensuring compatibility across different environments.
|
|
21
22
|
|
|
22
|
-
#
|
|
23
|
+
# HTTP API
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
## Overview
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
The `HttpApi` family of modules provide a declarative way to define HTTP APIs.
|
|
28
|
+
You can create an API by combining multiple endpoints, each with its own set of
|
|
29
|
+
schemas that define the request and response types.
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
|
|
31
|
-
import { Effect } from "effect"
|
|
31
|
+
After you have defined your API, you can use it to implement a server or derive
|
|
32
|
+
a client that can interact with the server.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
const displayMessage = Effect.gen(function* (_) {
|
|
35
|
-
const terminal = yield* _(Terminal.Terminal)
|
|
36
|
-
yield* _(terminal.display("a message\n"))
|
|
37
|
-
})
|
|
34
|
+
## Defining an API
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
To define an API, you need to create a set of endpoints. Each endpoint is
|
|
37
|
+
defined by a path, a method, and a set of schemas that define the request and
|
|
38
|
+
response types.
|
|
39
|
+
|
|
40
|
+
Each set of endpoints is added to an `HttpApiGroup`, which can be combined with
|
|
41
|
+
other groups to create a complete API.
|
|
42
|
+
|
|
43
|
+
### Your first `HttpApiGroup`
|
|
44
|
+
|
|
45
|
+
Let's define a simple CRUD API for managing users. First, we need to make an
|
|
46
|
+
`HttpApiGroup` that contains our endpoints.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
|
|
50
|
+
import { Schema } from "@effect/schema"
|
|
51
|
+
|
|
52
|
+
// Our domain "User" Schema
|
|
53
|
+
class User extends Schema.Class<User>("User")({
|
|
54
|
+
id: Schema.Number,
|
|
55
|
+
name: Schema.String,
|
|
56
|
+
createdAt: Schema.DateTimeUtc
|
|
57
|
+
}) {}
|
|
58
|
+
|
|
59
|
+
const usersApi = HttpApiGroup.make("users").pipe(
|
|
60
|
+
HttpApiGroup.add(
|
|
61
|
+
// each endpoint has a name and a path
|
|
62
|
+
HttpApiEndpoint.get("findById", "/users/:id").pipe(
|
|
63
|
+
// the endpoint can have a Schema for a successful response
|
|
64
|
+
HttpApiEndpoint.setSuccess(User),
|
|
65
|
+
// and here is a Schema for the path parameters
|
|
66
|
+
HttpApiEndpoint.setPath(
|
|
67
|
+
Schema.Struct({
|
|
68
|
+
id: Schema.NumberFromString
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
),
|
|
73
|
+
HttpApiGroup.add(
|
|
74
|
+
HttpApiEndpoint.post("create", "/users").pipe(
|
|
75
|
+
HttpApiEndpoint.setSuccess(User),
|
|
76
|
+
// and here is a Schema for the request payload / body
|
|
77
|
+
//
|
|
78
|
+
// this is a POST request, so the payload is in the body
|
|
79
|
+
// but for a GET request, the payload would be in the URL search params
|
|
80
|
+
HttpApiEndpoint.setPayload(
|
|
81
|
+
Schema.Struct({
|
|
82
|
+
name: Schema.String
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
),
|
|
87
|
+
// by default, the endpoint will respond with a 204 No Content
|
|
88
|
+
HttpApiGroup.add(HttpApiEndpoint.del("delete", "/users/:id")),
|
|
89
|
+
HttpApiGroup.add(
|
|
90
|
+
HttpApiEndpoint.patch("update", "/users/:id").pipe(
|
|
91
|
+
HttpApiEndpoint.setSuccess(User),
|
|
92
|
+
HttpApiEndpoint.setPayload(
|
|
93
|
+
Schema.Struct({
|
|
94
|
+
name: Schema.String
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
)
|
|
41
100
|
```
|
|
42
101
|
|
|
43
|
-
|
|
102
|
+
You can also extend the `HttpApiGroup` with a class to gain an opaque type.
|
|
103
|
+
We will use this API style in the following examples:
|
|
44
104
|
|
|
45
105
|
```ts
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
106
|
+
class UsersApi extends HttpApiGroup.make("users").pipe(
|
|
107
|
+
HttpApiGroup.add(
|
|
108
|
+
HttpApiEndpoint.get("findById", "/users/:id")
|
|
109
|
+
// ... same as above
|
|
110
|
+
)
|
|
111
|
+
) {}
|
|
112
|
+
```
|
|
49
113
|
|
|
50
|
-
|
|
51
|
-
const readLine = Effect.gen(function* (_) {
|
|
52
|
-
const terminal = yield* _(Terminal.Terminal)
|
|
53
|
-
const input = yield* _(terminal.readLine)
|
|
54
|
-
yield* _(Console.log(`input: ${input}`))
|
|
55
|
-
})
|
|
114
|
+
### Creating the top level `HttpApi`
|
|
56
115
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
116
|
+
Once you have defined your groups, you can combine them into a single `HttpApi`.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { HttpApi } from "@effect/platform"
|
|
120
|
+
|
|
121
|
+
class MyApi extends HttpApi.empty.pipe(HttpApi.addGroup(UsersApi)) {}
|
|
60
122
|
```
|
|
61
123
|
|
|
62
|
-
|
|
124
|
+
Or with the non-opaque style:
|
|
63
125
|
|
|
64
126
|
```ts
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
import { Effect, Option, Random } from "effect"
|
|
127
|
+
const api = HttpApi.empty.pipe(HttpApi.addGroup(usersApi))
|
|
128
|
+
```
|
|
68
129
|
|
|
69
|
-
|
|
130
|
+
### Adding OpenApi annotations
|
|
70
131
|
|
|
71
|
-
|
|
72
|
-
const n = parseInt(input, 10)
|
|
73
|
-
return isNaN(n) || n < 1 || n > 100 ? Option.none() : Option.some(n)
|
|
74
|
-
}
|
|
132
|
+
You can add OpenApi annotations to your API by using the `OpenApi` module.
|
|
75
133
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
134
|
+
Let's add a title to our `UsersApi` group:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { OpenApi } from "@effect/platform"
|
|
138
|
+
|
|
139
|
+
class UsersApi extends HttpApiGroup.make("users").pipe(
|
|
140
|
+
HttpApiGroup.add(
|
|
141
|
+
HttpApiEndpoint.get("findById", "/users/:id")
|
|
142
|
+
// ... same as above
|
|
143
|
+
),
|
|
144
|
+
// add an OpenApi title & description
|
|
145
|
+
OpenApi.annotate({
|
|
146
|
+
title: "Users API",
|
|
147
|
+
description: "API for managing users"
|
|
80
148
|
})
|
|
149
|
+
) {}
|
|
150
|
+
```
|
|
81
151
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
yield* _(terminal.display("Enter a guess: "))
|
|
85
|
-
return yield* _(terminal.readLine)
|
|
86
|
-
})
|
|
152
|
+
Now when you generate OpenApi documentation, the title and description will be
|
|
153
|
+
included.
|
|
87
154
|
|
|
88
|
-
|
|
89
|
-
number,
|
|
90
|
-
Terminal.QuitException | PlatformError,
|
|
91
|
-
Terminal.Terminal
|
|
92
|
-
> = Effect.gen(function* (_) {
|
|
93
|
-
const input = yield* _(prompt)
|
|
94
|
-
const guess = parseGuess(input)
|
|
95
|
-
if (Option.isNone(guess)) {
|
|
96
|
-
yield* _(display("You must enter an integer from 1 to 100"))
|
|
97
|
-
return yield* _(answer)
|
|
98
|
-
}
|
|
99
|
-
return guess.value
|
|
100
|
-
})
|
|
155
|
+
You can also add OpenApi annotations to the top-level `HttpApi`:
|
|
101
156
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Effect.gen(function* (_) {
|
|
109
|
-
if (guess > secret) {
|
|
110
|
-
yield* _(display("Too high"))
|
|
111
|
-
return yield* _(ko)
|
|
112
|
-
} else if (guess < secret) {
|
|
113
|
-
yield* _(display("Too low"))
|
|
114
|
-
return yield* _(ko)
|
|
115
|
-
} else {
|
|
116
|
-
return yield* _(ok)
|
|
117
|
-
}
|
|
157
|
+
```ts
|
|
158
|
+
class MyApi extends HttpApi.empty.pipe(
|
|
159
|
+
HttpApi.addGroup(UsersApi),
|
|
160
|
+
OpenApi.annotate({
|
|
161
|
+
title: "My API",
|
|
162
|
+
description: "My awesome API"
|
|
118
163
|
})
|
|
164
|
+
) {}
|
|
165
|
+
```
|
|
119
166
|
|
|
120
|
-
|
|
167
|
+
### Adding errors
|
|
121
168
|
|
|
122
|
-
|
|
123
|
-
secret: number
|
|
124
|
-
): Effect.Effect<
|
|
125
|
-
void,
|
|
126
|
-
Terminal.QuitException | PlatformError,
|
|
127
|
-
Terminal.Terminal
|
|
128
|
-
> =>
|
|
129
|
-
Effect.gen(function* (_) {
|
|
130
|
-
const guess = yield* _(answer)
|
|
131
|
-
return yield* _(
|
|
132
|
-
check(
|
|
133
|
-
secret,
|
|
134
|
-
guess,
|
|
135
|
-
end,
|
|
136
|
-
Effect.suspend(() => loop(secret))
|
|
137
|
-
)
|
|
138
|
-
)
|
|
139
|
-
})
|
|
169
|
+
You can add error responses to your endpoints using the following apis:
|
|
140
170
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"We have selected a random number between 1 and 100. See if you can guess it in 10 turns or fewer. We'll tell you if your guess was too high or too low."
|
|
145
|
-
)
|
|
146
|
-
)
|
|
147
|
-
yield* _(loop(yield* _(secret)))
|
|
148
|
-
})
|
|
149
|
-
```
|
|
171
|
+
- `HttpApiEndpoint.addError` - add an error response for a single endpoint
|
|
172
|
+
- `HttpApiGroup.addError` - add an error response for all endpoints in a group
|
|
173
|
+
- `HttpApi.addError` - add an error response for all endpoints in the api
|
|
150
174
|
|
|
151
|
-
|
|
175
|
+
The group & api level errors are useful for adding common error responses that
|
|
176
|
+
can be used in middleware.
|
|
177
|
+
|
|
178
|
+
Here is an example of adding a 404 error to the `UsersApi` group:
|
|
152
179
|
|
|
153
180
|
```ts
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
181
|
+
// define the error schemas
|
|
182
|
+
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
183
|
+
"UserNotFound",
|
|
184
|
+
{}
|
|
185
|
+
) {}
|
|
157
186
|
|
|
158
|
-
|
|
187
|
+
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
188
|
+
"Unauthorized",
|
|
189
|
+
{}
|
|
190
|
+
) {}
|
|
191
|
+
|
|
192
|
+
class UsersApi extends HttpApiGroup.make("users").pipe(
|
|
193
|
+
HttpApiGroup.add(
|
|
194
|
+
HttpApiEndpoint.get("findById", "/users/:id").pipe(
|
|
195
|
+
// here we are adding our error response
|
|
196
|
+
HttpApiEndpoint.addError(UserNotFound, { status: 404 }),
|
|
197
|
+
HttpApiEndpoint.setSuccess(User),
|
|
198
|
+
HttpApiEndpoint.setPath(Schema.Struct({ id: Schema.NumberFromString }))
|
|
199
|
+
)
|
|
200
|
+
),
|
|
201
|
+
// or we could add an error to the group
|
|
202
|
+
HttpApiGroup.addError(Unauthorized, { status: 401 })
|
|
203
|
+
) {}
|
|
159
204
|
```
|
|
160
205
|
|
|
161
|
-
|
|
206
|
+
It is worth noting that you can add multiple error responses to an endpoint,
|
|
207
|
+
just by calling `HttpApiEndpoint.addError` multiple times.
|
|
208
|
+
|
|
209
|
+
### Multipart requests
|
|
210
|
+
|
|
211
|
+
If you need to handle file uploads, you can use the `HttpApiSchema.Multipart`
|
|
212
|
+
api to flag a `HttpApiEndpoint` payload schema as a multipart request.
|
|
213
|
+
|
|
214
|
+
You can then use the schemas from the `Multipart` module to define the expected
|
|
215
|
+
shape of the multipart request.
|
|
162
216
|
|
|
163
217
|
```ts
|
|
164
|
-
import {
|
|
165
|
-
import * as Effect from "effect/Effect"
|
|
166
|
-
import { game } from "./game.js"
|
|
218
|
+
import { HttpApiSchema, Multipart } from "@effect/platform"
|
|
167
219
|
|
|
168
|
-
|
|
220
|
+
class UsersApi extends HttpApiGroup.make("users").pipe(
|
|
221
|
+
HttpApiGroup.add(
|
|
222
|
+
HttpApiEndpoint.post("upload", "/users/upload").pipe(
|
|
223
|
+
HttpApiEndpoint.setPayload(
|
|
224
|
+
HttpApiSchema.Multipart(
|
|
225
|
+
Schema.Struct({
|
|
226
|
+
// add a "files" field to the schema
|
|
227
|
+
files: Multipart.FilesSchema
|
|
228
|
+
})
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
) {}
|
|
169
234
|
```
|
|
170
235
|
|
|
171
|
-
|
|
236
|
+
### Adding security annotations
|
|
172
237
|
|
|
173
|
-
|
|
238
|
+
The `HttpApiSecurity` module provides a way to add security annotations to your
|
|
239
|
+
API.
|
|
174
240
|
|
|
175
|
-
|
|
176
|
-
import { Command, CommandExecutor } from "@effect/platform"
|
|
177
|
-
import {
|
|
178
|
-
NodeCommandExecutor,
|
|
179
|
-
NodeFileSystem,
|
|
180
|
-
NodeRuntime
|
|
181
|
-
} from "@effect/platform-node"
|
|
182
|
-
import { Effect } from "effect"
|
|
241
|
+
It offers the following authorization types:
|
|
183
242
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
243
|
+
- `HttpApiSecurity.apiKey` - API key authorization through headers, query
|
|
244
|
+
parameters, or cookies.
|
|
245
|
+
- `HttpApiSecurity.basicAuth` - HTTP Basic authentication.
|
|
246
|
+
- `HttpApiSecurity.bearerAuth` - Bearer token authentication.
|
|
187
247
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
console.log("Running tsc...")
|
|
248
|
+
You can annotate your API with these security types using the
|
|
249
|
+
`OpenApi.annotate` api as before.
|
|
191
250
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
251
|
+
```ts
|
|
252
|
+
import { HttpApiSecurity } from "@effect/platform"
|
|
253
|
+
|
|
254
|
+
const security = HttpApiSecurity.apiKey({
|
|
255
|
+
in: "cookie",
|
|
256
|
+
key: "token"
|
|
196
257
|
})
|
|
197
258
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
)
|
|
259
|
+
class UsersApi extends HttpApiGroup.make("users").pipe(
|
|
260
|
+
HttpApiGroup.add(
|
|
261
|
+
HttpApiEndpoint.get("findById", "/users/:id").pipe(
|
|
262
|
+
// add the security annotation to the endpoint
|
|
263
|
+
OpenApi.annotate({ security })
|
|
264
|
+
)
|
|
265
|
+
),
|
|
266
|
+
// or at the group level
|
|
267
|
+
OpenApi.annotate({ security }),
|
|
268
|
+
|
|
269
|
+
// or just for the endpoints above this line
|
|
270
|
+
HttpApiGroup.annotateEndpoints(OpenApi.Security, security),
|
|
271
|
+
// this endpoint will not have the security annotation
|
|
272
|
+
HttpApiGroup.add(HttpApiEndpoint.get("list", "/users"))
|
|
273
|
+
) {}
|
|
205
274
|
```
|
|
206
275
|
|
|
207
|
-
|
|
276
|
+
### Changing the response encoding
|
|
208
277
|
|
|
209
|
-
|
|
278
|
+
By default, the response is encoded as JSON. You can change the encoding using
|
|
279
|
+
the `HttpApiSchema.withEncoding` api.
|
|
280
|
+
|
|
281
|
+
Here is an example of changing the encoding to text/csv:
|
|
210
282
|
|
|
211
283
|
```ts
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
284
|
+
class UsersApi extends HttpApiGroup.make("users").pipe(
|
|
285
|
+
HttpApiGroup.add(
|
|
286
|
+
HttpApiEndpoint.get("csv", "/users/csv").pipe(
|
|
287
|
+
HttpApiEndpoint.setSuccess(
|
|
288
|
+
Schema.String.pipe(
|
|
289
|
+
HttpApiSchema.withEncoding({
|
|
290
|
+
kind: "Text",
|
|
291
|
+
contentType: "text/csv"
|
|
292
|
+
})
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
) {}
|
|
298
|
+
```
|
|
219
299
|
|
|
220
|
-
|
|
221
|
-
stream: Stream.Stream<Uint8Array, E, R>
|
|
222
|
-
): Effect.Effect<string, E, R> =>
|
|
223
|
-
stream.pipe(Stream.decodeText(), Stream.runFold(String.empty, String.concat))
|
|
300
|
+
## Implementing a server
|
|
224
301
|
|
|
225
|
-
|
|
226
|
-
|
|
302
|
+
Now that you have defined your API, you can implement a server that serves the
|
|
303
|
+
endpoints.
|
|
227
304
|
|
|
228
|
-
|
|
305
|
+
The `HttpApiBuilder` module provides all the apis you need to implement your
|
|
306
|
+
server.
|
|
229
307
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
308
|
+
### Implementing a `HttpApiGroup`
|
|
309
|
+
|
|
310
|
+
First up, let's implement an `UsersApi` group with a single `findById` endpoint.
|
|
311
|
+
|
|
312
|
+
The `HttpApiBuilder.group` api takes the `HttpApi` definition, the group name,
|
|
313
|
+
and a function that adds the handlers required for the group.
|
|
314
|
+
|
|
315
|
+
Each endpoint is implemented using the `HttpApiBuilder.handle` api.
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
import {
|
|
319
|
+
HttpApi,
|
|
320
|
+
HttpApiBuilder,
|
|
321
|
+
HttpApiEndpoint,
|
|
322
|
+
HttpApiGroup
|
|
323
|
+
} from "@effect/platform"
|
|
324
|
+
import { Schema } from "@effect/schema"
|
|
325
|
+
import { DateTime, Effect } from "effect"
|
|
326
|
+
|
|
327
|
+
// here is our api definition
|
|
328
|
+
class User extends Schema.Class<User>("User")({
|
|
329
|
+
id: Schema.Number,
|
|
330
|
+
name: Schema.String,
|
|
331
|
+
createdAt: Schema.DateTimeUtc
|
|
332
|
+
}) {}
|
|
333
|
+
|
|
334
|
+
class UsersApi extends HttpApiGroup.make("users").pipe(
|
|
335
|
+
HttpApiGroup.add(
|
|
336
|
+
HttpApiEndpoint.get("findById", "/users/:id").pipe(
|
|
337
|
+
HttpApiEndpoint.setSuccess(User),
|
|
338
|
+
HttpApiEndpoint.setPath(
|
|
339
|
+
Schema.Struct({
|
|
340
|
+
id: Schema.NumberFromString
|
|
341
|
+
})
|
|
244
342
|
)
|
|
245
343
|
)
|
|
246
344
|
)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
345
|
+
) {}
|
|
346
|
+
|
|
347
|
+
class MyApi extends HttpApi.empty.pipe(HttpApi.addGroup(UsersApi)) {}
|
|
348
|
+
|
|
349
|
+
// --------------------------------------------
|
|
350
|
+
// Implementation
|
|
351
|
+
// --------------------------------------------
|
|
352
|
+
|
|
353
|
+
// the `HttpApiBuilder.group` api returns a `Layer`
|
|
354
|
+
const UsersApiLive: Layer.Layer<HttpApiGroup.HttpApiGroup.Service<"users">> =
|
|
355
|
+
HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
356
|
+
handlers.pipe(
|
|
357
|
+
// the parameters & payload are passed to the handler function.
|
|
358
|
+
HttpApiBuilder.handle("findById", ({ path: { id } }) =>
|
|
359
|
+
Effect.succeed(
|
|
360
|
+
new User({
|
|
361
|
+
id,
|
|
362
|
+
name: "John Doe",
|
|
363
|
+
createdAt: DateTime.unsafeNow()
|
|
364
|
+
})
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
)
|
|
254
368
|
)
|
|
255
|
-
)
|
|
256
369
|
```
|
|
257
370
|
|
|
258
|
-
|
|
371
|
+
### Using services inside a `HttpApiGroup`
|
|
259
372
|
|
|
260
|
-
|
|
373
|
+
If you need to use services inside your handlers, you can return an
|
|
374
|
+
`Effect` from the `HttpApiBuilder.group` api.
|
|
261
375
|
|
|
262
376
|
```ts
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
377
|
+
class UsersRepository extends Context.Tag("UsersRepository")<
|
|
378
|
+
UsersRepository,
|
|
379
|
+
{
|
|
380
|
+
readonly findById: (id: number) => Effect.Effect<User>
|
|
381
|
+
}
|
|
382
|
+
>() {}
|
|
383
|
+
|
|
384
|
+
// the dependencies will show up in the resulting `Layer`
|
|
385
|
+
const UsersApiLive: Layer.Layer<
|
|
386
|
+
HttpApiGroup.HttpApiGroup.Service<"users">,
|
|
387
|
+
never,
|
|
388
|
+
UsersRepository
|
|
389
|
+
> = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
390
|
+
// we can return an Effect that creates our handlers
|
|
391
|
+
Effect.gen(function* () {
|
|
392
|
+
const repository = yield* UsersRepository
|
|
393
|
+
return handlers.pipe(
|
|
394
|
+
HttpApiBuilder.handle("findById", ({ path: { id } }) =>
|
|
395
|
+
repository.findById(id)
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
})
|
|
271
399
|
)
|
|
272
|
-
|
|
273
|
-
// Run the command using NodeRuntime with the NodeContext layer
|
|
274
|
-
NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))
|
|
275
400
|
```
|
|
276
401
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
The `@effect/platform/FileSystem` module provides a single `FileSystem` tag, which acts as the gateway for interacting with the filesystem.
|
|
280
|
-
|
|
281
|
-
Here's a list of operations that can be performed using the `FileSystem` tag:
|
|
402
|
+
### Implementing a `HttpApi`
|
|
282
403
|
|
|
283
|
-
|
|
284
|
-
| --------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
285
|
-
| **access** | `path: string`, `options?: AccessFileOptions` | `Effect<void, PlatformError>` | Check if a file can be accessed. You can optionally specify the level of access to check for. |
|
|
286
|
-
| **copy** | `fromPath: string`, `toPath: string`, `options?: CopyOptions` | `Effect<void, PlatformError>` | Copy a file or directory from `fromPath` to `toPath`. Equivalent to `cp -r`. |
|
|
287
|
-
| **copyFile** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Copy a file from `fromPath` to `toPath`. |
|
|
288
|
-
| **chmod** | `path: string`, `mode: number` | `Effect<void, PlatformError>` | Change the permissions of a file. |
|
|
289
|
-
| **chown** | `path: string`, `uid: number`, `gid: number` | `Effect<void, PlatformError>` | Change the owner and group of a file. |
|
|
290
|
-
| **exists** | `path: string` | `Effect<boolean, PlatformError>` | Check if a path exists. |
|
|
291
|
-
| **link** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Create a hard link from `fromPath` to `toPath`. |
|
|
292
|
-
| **makeDirectory** | `path: string`, `options?: MakeDirectoryOptions` | `Effect<void, PlatformError>` | Create a directory at `path`. You can optionally specify the mode and whether to recursively create nested directories. |
|
|
293
|
-
| **makeTempDirectory** | `options?: MakeTempDirectoryOptions` | `Effect<string, PlatformError>` | Create a temporary directory. By default, the directory will be created inside the system's default temporary directory. |
|
|
294
|
-
| **makeTempDirectoryScoped** | `options?: MakeTempDirectoryOptions` | `Effect<string, PlatformError, Scope>` | Create a temporary directory inside a scope. Functionally equivalent to `makeTempDirectory`, but the directory will be automatically deleted when the scope is closed. |
|
|
295
|
-
| **makeTempFile** | `options?: MakeTempFileOptions` | `Effect<string, PlatformError>` | Create a temporary file. The directory creation is functionally equivalent to `makeTempDirectory`. The file name will be a randomly generated string. |
|
|
296
|
-
| **makeTempFileScoped** | `options?: MakeTempFileOptions` | `Effect<string, PlatformError, Scope>` | Create a temporary file inside a scope. Functionally equivalent to `makeTempFile`, but the file will be automatically deleted when the scope is closed. |
|
|
297
|
-
| **open** | `path: string`, `options?: OpenFileOptions` | `Effect<File, PlatformError, Scope>` | Open a file at `path` with the specified `options`. The file handle will be automatically closed when the scope is closed. |
|
|
298
|
-
| **readDirectory** | `path: string`, `options?: ReadDirectoryOptions` | `Effect<Array<string>, PlatformError>` | List the contents of a directory. You can recursively list the contents of nested directories by setting the `recursive` option. |
|
|
299
|
-
| **readFile** | `path: string` | `Effect<Uint8Array, PlatformError>` | Read the contents of a file. |
|
|
300
|
-
| **readFileString** | `path: string`, `encoding?: string` | `Effect<string, PlatformError>` | Read the contents of a file as a string. |
|
|
301
|
-
| **readLink** | `path: string` | `Effect<string, PlatformError>` | Read the destination of a symbolic link. |
|
|
302
|
-
| **realPath** | `path: string` | `Effect<string, PlatformError>` | Resolve a path to its canonicalized absolute pathname. |
|
|
303
|
-
| **remove** | `path: string`, `options?: RemoveOptions` | `Effect<void, PlatformError>` | Remove a file or directory. By setting the `recursive` option to `true`, you can recursively remove nested directories. |
|
|
304
|
-
| **rename** | `oldPath: string`, `newPath: string` | `Effect<void, PlatformError>` | Rename a file or directory. |
|
|
305
|
-
| **sink** | `path: string`, `options?: SinkOptions` | `Sink<void, Uint8Array, never, PlatformError>` | Create a writable `Sink` for the specified `path`. |
|
|
306
|
-
| **stat** | `path: string` | `Effect<File.Info, PlatformError>` | Get information about a file at `path`. |
|
|
307
|
-
| **stream** | `path: string`, `options?: StreamOptions` | `Stream<Uint8Array, PlatformError>` | Create a readable `Stream` for the specified `path`. |
|
|
308
|
-
| **symlink** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Create a symbolic link from `fromPath` to `toPath`. |
|
|
309
|
-
| **truncate** | `path: string`, `length?: SizeInput` | `Effect<void, PlatformError>` | Truncate a file to a specified length. If the `length` is not specified, the file will be truncated to length `0`. |
|
|
310
|
-
| **utimes** | `path: string`, `atime: Date \| number`, `mtime: Date \| number` | `Effect<void, PlatformError>` | Change the file system timestamps of the file at `path`. |
|
|
311
|
-
| **watch** | `path: string` | `Stream<WatchEvent, PlatformError>` | Watch a directory or file for changes. |
|
|
404
|
+
Once all your groups are implemented, you can implement the top-level `HttpApi`.
|
|
312
405
|
|
|
313
|
-
|
|
406
|
+
This is done using the `HttpApiBuilder.api` api, and then using `Layer.provide`
|
|
407
|
+
to add all the group implementations.
|
|
314
408
|
|
|
315
409
|
```ts
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
410
|
+
const MyApiLive: Layer.Layer<HttpApi.HttpApi.Service> = HttpApiBuilder.api(
|
|
411
|
+
MyApi
|
|
412
|
+
).pipe(Layer.provide(UsersApiLive))
|
|
413
|
+
```
|
|
319
414
|
|
|
320
|
-
|
|
321
|
-
const readFileString = Effect.gen(function* (_) {
|
|
322
|
-
const fs = yield* _(FileSystem.FileSystem)
|
|
415
|
+
### Serving the API
|
|
323
416
|
|
|
324
|
-
|
|
325
|
-
const content = yield* _(fs.readFileString("./index.ts", "utf8"))
|
|
326
|
-
console.log(content)
|
|
327
|
-
})
|
|
417
|
+
Finally, you can serve the API using the `HttpApiBuilder.serve` api.
|
|
328
418
|
|
|
329
|
-
|
|
330
|
-
|
|
419
|
+
You can also add middleware to the server using the `HttpMiddleware` module, or
|
|
420
|
+
use some of the middleware Layer's from the `HttpApiBuilder` module.
|
|
331
421
|
|
|
332
|
-
|
|
422
|
+
```ts
|
|
423
|
+
import { HttpMiddleware, HttpServer } from "@effect/platform"
|
|
424
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
425
|
+
import { createServer } from "node:http"
|
|
333
426
|
|
|
334
|
-
|
|
427
|
+
// use the `HttpApiBuilder.serve` function to register our API with the HTTP
|
|
428
|
+
// server
|
|
429
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
430
|
+
// Add CORS middleware
|
|
431
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
432
|
+
// Provide the API implementation
|
|
433
|
+
Layer.provide(MyApiLive),
|
|
434
|
+
// Log the address the server is listening on
|
|
435
|
+
HttpServer.withLogAddress,
|
|
436
|
+
// Provide the HTTP server implementation
|
|
437
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
438
|
+
)
|
|
335
439
|
|
|
336
|
-
|
|
440
|
+
// run the server
|
|
441
|
+
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
|
|
442
|
+
```
|
|
337
443
|
|
|
338
|
-
##
|
|
444
|
+
## Implementing `HttpApiSecurity`
|
|
339
445
|
|
|
340
|
-
|
|
446
|
+
If you are using `HttpApiSecurity` in your API, you can use the security
|
|
447
|
+
definition to implement a middleware that will protect your endpoints.
|
|
341
448
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
- **remove**: Delete a key-value pair.
|
|
345
|
-
- **clear**: Remove all key-value pairs.
|
|
346
|
-
- **size**: Get the number of stored pairs.
|
|
347
|
-
- **modify**: Atomically modify a value.
|
|
348
|
-
- **has**: Check if a key exists.
|
|
349
|
-
- **isEmpty**: Check if the store is empty.
|
|
449
|
+
The `HttpApiBuilder.middlewareSecurity` api will assist you in creating this
|
|
450
|
+
middleware.
|
|
350
451
|
|
|
351
|
-
|
|
452
|
+
Here is an example:
|
|
352
453
|
|
|
353
454
|
```ts
|
|
354
|
-
|
|
355
|
-
|
|
455
|
+
// our cookie security definition
|
|
456
|
+
const security = HttpApiSecurity.apiKey({
|
|
457
|
+
in: "cookie",
|
|
458
|
+
key: "token"
|
|
459
|
+
})
|
|
356
460
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
461
|
+
// the user repository service
|
|
462
|
+
class UsersRepository extends Context.Tag("UsersRepository")<
|
|
463
|
+
UsersRepository,
|
|
464
|
+
{
|
|
465
|
+
readonly findByToken: (token: Redacted.Redacted) => Effect.Effect<User>
|
|
466
|
+
}
|
|
467
|
+
>() {}
|
|
468
|
+
|
|
469
|
+
// the security middleware will supply the current user to the handlers
|
|
470
|
+
class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
|
|
471
|
+
|
|
472
|
+
// implement the middleware
|
|
473
|
+
const makeSecurityMiddleware: Effect.Effect<
|
|
474
|
+
HttpApiBuilder.SecurityMiddleware<CurrentUser>,
|
|
475
|
+
never,
|
|
476
|
+
UsersRepository
|
|
477
|
+
> = Effect.gen(function* () {
|
|
478
|
+
const repository = yield* UsersRepository
|
|
479
|
+
return HttpApiBuilder.middlewareSecurity(
|
|
480
|
+
// the security definition
|
|
481
|
+
security,
|
|
482
|
+
// the Context.Tag this middleware will provide
|
|
483
|
+
CurrentUser,
|
|
484
|
+
// the function to get the user from the token
|
|
485
|
+
(token) => repository.findByToken(token)
|
|
486
|
+
)
|
|
487
|
+
})
|
|
360
488
|
|
|
361
|
-
|
|
362
|
-
|
|
489
|
+
// use the middleware
|
|
490
|
+
const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
491
|
+
Effect.gen(function* () {
|
|
492
|
+
// construct the security middleware
|
|
493
|
+
const securityMiddleware = yield* makeSecurityMiddleware
|
|
494
|
+
|
|
495
|
+
return handlers.pipe(
|
|
496
|
+
HttpApiBuilder.handle("findById", ({ path: { id } }) =>
|
|
497
|
+
Effect.succeed(
|
|
498
|
+
new User({ id, name: "John Doe", createdAt: DateTime.unsafeNow() })
|
|
499
|
+
)
|
|
500
|
+
),
|
|
501
|
+
// apply the middleware to the findById endpoint
|
|
502
|
+
securityMiddleware
|
|
503
|
+
// any endpoint after this will not be protected
|
|
504
|
+
)
|
|
505
|
+
})
|
|
506
|
+
)
|
|
507
|
+
```
|
|
363
508
|
|
|
364
|
-
|
|
365
|
-
|
|
509
|
+
If you need to set the security cookie from within a handler, you can use the
|
|
510
|
+
`HttpApiBuilder.securitySetCookie` api.
|
|
366
511
|
|
|
367
|
-
|
|
368
|
-
|
|
512
|
+
By default, the cookie will be set with the `HttpOnly` and `Secure` flags.
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
const security = HttpApiSecurity.apiKey({
|
|
516
|
+
in: "cookie",
|
|
517
|
+
key: "token"
|
|
369
518
|
})
|
|
370
519
|
|
|
371
|
-
|
|
520
|
+
const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
|
|
521
|
+
handlers.pipe(
|
|
522
|
+
HttpApiBuilder.handle("login", () =>
|
|
523
|
+
// set the security cookie
|
|
524
|
+
HttpApiBuilder.securitySetCookie(
|
|
525
|
+
security,
|
|
526
|
+
Redacted.make("keep me secret")
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
)
|
|
372
531
|
```
|
|
373
532
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
The module provides several built-in implementations to suit different needs:
|
|
533
|
+
### Serving Swagger documentation
|
|
377
534
|
|
|
378
|
-
|
|
379
|
-
- **File System Store**: `layerFileSystem` offers a file-based store for persistent storage needs.
|
|
380
|
-
- **Schema Store**: `layerSchema` enables schema-based validation for stored values, ensuring data integrity and type safety.
|
|
535
|
+
You can add Swagger documentation to your API using the `HttpApiSwagger` module.
|
|
381
536
|
|
|
382
|
-
|
|
537
|
+
You just need to provide the `HttpApiSwagger.layer` to your server
|
|
538
|
+
implementation:
|
|
383
539
|
|
|
384
|
-
|
|
540
|
+
```ts
|
|
541
|
+
import { HttpApiSwagger } from "@effect/platform"
|
|
385
542
|
|
|
386
|
-
|
|
543
|
+
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
|
|
544
|
+
// add the swagger documentation layer
|
|
545
|
+
Layer.provide(
|
|
546
|
+
HttpApiSwagger.layer({
|
|
547
|
+
// "/docs" is the default path for the swagger documentation
|
|
548
|
+
path: "/docs"
|
|
549
|
+
})
|
|
550
|
+
),
|
|
551
|
+
Layer.provide(HttpApiBuilder.middlewareCors()),
|
|
552
|
+
Layer.provide(MyApiLive),
|
|
553
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
554
|
+
)
|
|
555
|
+
```
|
|
387
556
|
|
|
388
|
-
|
|
389
|
-
import { KeyValueStore, layerMemory } from "@effect/platform/KeyValueStore"
|
|
390
|
-
import { Schema } from "@effect/schema"
|
|
391
|
-
import { Effect } from "effect"
|
|
557
|
+
## Deriving a client
|
|
392
558
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
name: Schema.String,
|
|
396
|
-
age: Schema.Number
|
|
397
|
-
})
|
|
559
|
+
Once you have defined your API, you can derive a client that can interact with
|
|
560
|
+
the server.
|
|
398
561
|
|
|
399
|
-
|
|
400
|
-
const store = (yield* KeyValueStore).forSchema(Person)
|
|
562
|
+
The `HttpApiClient` module provides all the apis you need to derive a client.
|
|
401
563
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
yield* store.set("user1", value)
|
|
405
|
-
console.log(yield* store.size) // Outputs: 1
|
|
564
|
+
```ts
|
|
565
|
+
import { HttpApiClient } from "@effect/platform"
|
|
406
566
|
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
567
|
+
Effect.gen(function* () {
|
|
568
|
+
const client = yield* HttpApiClient.make(MyApi, {
|
|
569
|
+
baseUrl: "http://localhost:3000"
|
|
570
|
+
// You can transform the HttpClient to add things like authentication
|
|
571
|
+
// transformClient: ....
|
|
572
|
+
})
|
|
573
|
+
const user = yield* client.users.findById({ path: { id: 1 } })
|
|
574
|
+
yield* Effect.log(user)
|
|
410
575
|
})
|
|
411
|
-
|
|
412
|
-
Effect.runPromise(program.pipe(Effect.provide(layerMemory)))
|
|
413
576
|
```
|
|
414
577
|
|
|
415
|
-
In this example:
|
|
416
|
-
|
|
417
|
-
- **Person**: Defines the structure for the values stored in the key-value store.
|
|
418
|
-
- **store.set**: Stores a value adhering to `Person`.
|
|
419
|
-
- **store.get**: Retrieves and validates the stored value against `Person`.
|
|
420
|
-
|
|
421
578
|
# HTTP Client
|
|
422
579
|
|
|
423
580
|
## Overview
|
|
@@ -500,7 +657,12 @@ const myClient = HttpClient.makeDefault((req) =>
|
|
|
500
657
|
req,
|
|
501
658
|
// Simulate a response from a server
|
|
502
659
|
new Response(
|
|
503
|
-
JSON.stringify({
|
|
660
|
+
JSON.stringify({
|
|
661
|
+
userId: 1,
|
|
662
|
+
id: 1,
|
|
663
|
+
title: "title...",
|
|
664
|
+
body: "body..."
|
|
665
|
+
})
|
|
504
666
|
)
|
|
505
667
|
)
|
|
506
668
|
)
|
|
@@ -2183,3 +2345,402 @@ const handler = HttpApp.toWebHandler(router)
|
|
|
2183
2345
|
const response = await handler(new Request("http://localhost:3000/foo"))
|
|
2184
2346
|
console.log(await response.text()) // Output: content 2
|
|
2185
2347
|
```
|
|
2348
|
+
|
|
2349
|
+
# Terminal
|
|
2350
|
+
|
|
2351
|
+
The `@effect/platform/Terminal` module exports a single `Terminal` tag, which serves as the entry point to reading from and writing to standard input and standard output.
|
|
2352
|
+
|
|
2353
|
+
## Writing to standard output
|
|
2354
|
+
|
|
2355
|
+
```ts
|
|
2356
|
+
import { Terminal } from "@effect/platform"
|
|
2357
|
+
import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
|
|
2358
|
+
import { Effect } from "effect"
|
|
2359
|
+
|
|
2360
|
+
// const displayMessage: Effect.Effect<void, PlatformError, Terminal.Terminal>
|
|
2361
|
+
const displayMessage = Effect.gen(function* (_) {
|
|
2362
|
+
const terminal = yield* _(Terminal.Terminal)
|
|
2363
|
+
yield* _(terminal.display("a message\n"))
|
|
2364
|
+
})
|
|
2365
|
+
|
|
2366
|
+
NodeRuntime.runMain(displayMessage.pipe(Effect.provide(NodeTerminal.layer)))
|
|
2367
|
+
// Output: "a message"
|
|
2368
|
+
```
|
|
2369
|
+
|
|
2370
|
+
## Reading from standard input
|
|
2371
|
+
|
|
2372
|
+
```ts
|
|
2373
|
+
import { Terminal } from "@effect/platform"
|
|
2374
|
+
import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
|
|
2375
|
+
import { Console, Effect } from "effect"
|
|
2376
|
+
|
|
2377
|
+
// const readLine: Effect.Effect<void, Terminal.QuitException, Terminal.Terminal>
|
|
2378
|
+
const readLine = Effect.gen(function* (_) {
|
|
2379
|
+
const terminal = yield* _(Terminal.Terminal)
|
|
2380
|
+
const input = yield* _(terminal.readLine)
|
|
2381
|
+
yield* _(Console.log(`input: ${input}`))
|
|
2382
|
+
})
|
|
2383
|
+
|
|
2384
|
+
NodeRuntime.runMain(readLine.pipe(Effect.provide(NodeTerminal.layer)))
|
|
2385
|
+
// Input: "hello"
|
|
2386
|
+
// Output: "input: hello"
|
|
2387
|
+
```
|
|
2388
|
+
|
|
2389
|
+
These simple examples illustrate how to utilize the `Terminal` module for handling standard input and output in your programs. Let's use this knowledge to build a number guessing game:
|
|
2390
|
+
|
|
2391
|
+
```ts
|
|
2392
|
+
import { Terminal } from "@effect/platform"
|
|
2393
|
+
import type { PlatformError } from "@effect/platform/Error"
|
|
2394
|
+
import { Effect, Option, Random } from "effect"
|
|
2395
|
+
|
|
2396
|
+
export const secret = Random.nextIntBetween(1, 100)
|
|
2397
|
+
|
|
2398
|
+
const parseGuess = (input: string) => {
|
|
2399
|
+
const n = parseInt(input, 10)
|
|
2400
|
+
return isNaN(n) || n < 1 || n > 100 ? Option.none() : Option.some(n)
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
const display = (message: string) =>
|
|
2404
|
+
Effect.gen(function* (_) {
|
|
2405
|
+
const terminal = yield* _(Terminal.Terminal)
|
|
2406
|
+
yield* _(terminal.display(`${message}\n`))
|
|
2407
|
+
})
|
|
2408
|
+
|
|
2409
|
+
const prompt = Effect.gen(function* (_) {
|
|
2410
|
+
const terminal = yield* _(Terminal.Terminal)
|
|
2411
|
+
yield* _(terminal.display("Enter a guess: "))
|
|
2412
|
+
return yield* _(terminal.readLine)
|
|
2413
|
+
})
|
|
2414
|
+
|
|
2415
|
+
const answer: Effect.Effect<
|
|
2416
|
+
number,
|
|
2417
|
+
Terminal.QuitException | PlatformError,
|
|
2418
|
+
Terminal.Terminal
|
|
2419
|
+
> = Effect.gen(function* (_) {
|
|
2420
|
+
const input = yield* _(prompt)
|
|
2421
|
+
const guess = parseGuess(input)
|
|
2422
|
+
if (Option.isNone(guess)) {
|
|
2423
|
+
yield* _(display("You must enter an integer from 1 to 100"))
|
|
2424
|
+
return yield* _(answer)
|
|
2425
|
+
}
|
|
2426
|
+
return guess.value
|
|
2427
|
+
})
|
|
2428
|
+
|
|
2429
|
+
const check = <A, E, R>(
|
|
2430
|
+
secret: number,
|
|
2431
|
+
guess: number,
|
|
2432
|
+
ok: Effect.Effect<A, E, R>,
|
|
2433
|
+
ko: Effect.Effect<A, E, R>
|
|
2434
|
+
): Effect.Effect<A, E | PlatformError, R | Terminal.Terminal> =>
|
|
2435
|
+
Effect.gen(function* (_) {
|
|
2436
|
+
if (guess > secret) {
|
|
2437
|
+
yield* _(display("Too high"))
|
|
2438
|
+
return yield* _(ko)
|
|
2439
|
+
} else if (guess < secret) {
|
|
2440
|
+
yield* _(display("Too low"))
|
|
2441
|
+
return yield* _(ko)
|
|
2442
|
+
} else {
|
|
2443
|
+
return yield* _(ok)
|
|
2444
|
+
}
|
|
2445
|
+
})
|
|
2446
|
+
|
|
2447
|
+
const end = display("You guessed it!")
|
|
2448
|
+
|
|
2449
|
+
const loop = (
|
|
2450
|
+
secret: number
|
|
2451
|
+
): Effect.Effect<
|
|
2452
|
+
void,
|
|
2453
|
+
Terminal.QuitException | PlatformError,
|
|
2454
|
+
Terminal.Terminal
|
|
2455
|
+
> =>
|
|
2456
|
+
Effect.gen(function* (_) {
|
|
2457
|
+
const guess = yield* _(answer)
|
|
2458
|
+
return yield* _(
|
|
2459
|
+
check(
|
|
2460
|
+
secret,
|
|
2461
|
+
guess,
|
|
2462
|
+
end,
|
|
2463
|
+
Effect.suspend(() => loop(secret))
|
|
2464
|
+
)
|
|
2465
|
+
)
|
|
2466
|
+
})
|
|
2467
|
+
|
|
2468
|
+
export const game = Effect.gen(function* (_) {
|
|
2469
|
+
yield* _(
|
|
2470
|
+
display(
|
|
2471
|
+
"We have selected a random number between 1 and 100. See if you can guess it in 10 turns or fewer. We'll tell you if your guess was too high or too low."
|
|
2472
|
+
)
|
|
2473
|
+
)
|
|
2474
|
+
yield* _(loop(yield* _(secret)))
|
|
2475
|
+
})
|
|
2476
|
+
```
|
|
2477
|
+
|
|
2478
|
+
Let's run the game in Node.js:
|
|
2479
|
+
|
|
2480
|
+
```ts
|
|
2481
|
+
import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
|
|
2482
|
+
import * as Effect from "effect/Effect"
|
|
2483
|
+
import { game } from "./game.js"
|
|
2484
|
+
|
|
2485
|
+
NodeRuntime.runMain(game.pipe(Effect.provide(NodeTerminal.layer)))
|
|
2486
|
+
```
|
|
2487
|
+
|
|
2488
|
+
Let's run the game in Bun:
|
|
2489
|
+
|
|
2490
|
+
```ts
|
|
2491
|
+
import { BunRuntime, BunTerminal } from "@effect/platform-bun"
|
|
2492
|
+
import * as Effect from "effect/Effect"
|
|
2493
|
+
import { game } from "./game.js"
|
|
2494
|
+
|
|
2495
|
+
BunRuntime.runMain(game.pipe(Effect.provide(BunTerminal.layer)))
|
|
2496
|
+
```
|
|
2497
|
+
|
|
2498
|
+
# Command
|
|
2499
|
+
|
|
2500
|
+
As an example of using the `@effect/platform/Command` module, let's see how to run the TypeScript compiler `tsc`:
|
|
2501
|
+
|
|
2502
|
+
```ts
|
|
2503
|
+
import { Command, CommandExecutor } from "@effect/platform"
|
|
2504
|
+
import {
|
|
2505
|
+
NodeCommandExecutor,
|
|
2506
|
+
NodeFileSystem,
|
|
2507
|
+
NodeRuntime
|
|
2508
|
+
} from "@effect/platform-node"
|
|
2509
|
+
import { Effect } from "effect"
|
|
2510
|
+
|
|
2511
|
+
// const program: Effect.Effect<string, PlatformError, CommandExecutor.CommandExecutor>
|
|
2512
|
+
const program = Effect.gen(function* (_) {
|
|
2513
|
+
const executor = yield* _(CommandExecutor.CommandExecutor)
|
|
2514
|
+
|
|
2515
|
+
// Creating a command to run the TypeScript compiler
|
|
2516
|
+
const command = Command.make("tsc", "--noEmit")
|
|
2517
|
+
console.log("Running tsc...")
|
|
2518
|
+
|
|
2519
|
+
// Executing the command and capturing the output
|
|
2520
|
+
const output = yield* _(executor.string(command))
|
|
2521
|
+
console.log(output)
|
|
2522
|
+
return output
|
|
2523
|
+
})
|
|
2524
|
+
|
|
2525
|
+
// Running the program with the necessary runtime and executor layers
|
|
2526
|
+
NodeRuntime.runMain(
|
|
2527
|
+
program.pipe(
|
|
2528
|
+
Effect.provide(NodeCommandExecutor.layer),
|
|
2529
|
+
Effect.provide(NodeFileSystem.layer)
|
|
2530
|
+
)
|
|
2531
|
+
)
|
|
2532
|
+
```
|
|
2533
|
+
|
|
2534
|
+
## Obtaining Information About the Running Process
|
|
2535
|
+
|
|
2536
|
+
Here, we'll explore how to retrieve information about a running process.
|
|
2537
|
+
|
|
2538
|
+
```ts
|
|
2539
|
+
import { Command, CommandExecutor } from "@effect/platform"
|
|
2540
|
+
import {
|
|
2541
|
+
NodeCommandExecutor,
|
|
2542
|
+
NodeFileSystem,
|
|
2543
|
+
NodeRuntime
|
|
2544
|
+
} from "@effect/platform-node"
|
|
2545
|
+
import { Effect, Stream, String } from "effect"
|
|
2546
|
+
|
|
2547
|
+
const runString = <E, R>(
|
|
2548
|
+
stream: Stream.Stream<Uint8Array, E, R>
|
|
2549
|
+
): Effect.Effect<string, E, R> =>
|
|
2550
|
+
stream.pipe(Stream.decodeText(), Stream.runFold(String.empty, String.concat))
|
|
2551
|
+
|
|
2552
|
+
const program = Effect.gen(function* (_) {
|
|
2553
|
+
const executor = yield* _(CommandExecutor.CommandExecutor)
|
|
2554
|
+
|
|
2555
|
+
const command = Command.make("ls")
|
|
2556
|
+
|
|
2557
|
+
const [exitCode, stdout, stderr] = yield* _(
|
|
2558
|
+
// Start running the command and return a handle to the running process.
|
|
2559
|
+
executor.start(command),
|
|
2560
|
+
Effect.flatMap((process) =>
|
|
2561
|
+
Effect.all(
|
|
2562
|
+
[
|
|
2563
|
+
// Waits for the process to exit and returns the ExitCode of the command that was run.
|
|
2564
|
+
process.exitCode,
|
|
2565
|
+
// The standard output stream of the process.
|
|
2566
|
+
runString(process.stdout),
|
|
2567
|
+
// The standard error stream of the process.
|
|
2568
|
+
runString(process.stderr)
|
|
2569
|
+
],
|
|
2570
|
+
{ concurrency: 3 }
|
|
2571
|
+
)
|
|
2572
|
+
)
|
|
2573
|
+
)
|
|
2574
|
+
console.log({ exitCode, stdout, stderr })
|
|
2575
|
+
})
|
|
2576
|
+
|
|
2577
|
+
NodeRuntime.runMain(
|
|
2578
|
+
Effect.scoped(program).pipe(
|
|
2579
|
+
Effect.provide(NodeCommandExecutor.layer),
|
|
2580
|
+
Effect.provide(NodeFileSystem.layer)
|
|
2581
|
+
)
|
|
2582
|
+
)
|
|
2583
|
+
```
|
|
2584
|
+
|
|
2585
|
+
## Running a Platform Command with stdout Streamed to process.stdout
|
|
2586
|
+
|
|
2587
|
+
To run a command (for example `cat`) and stream its `stdout` to `process.stdout` follow these steps:
|
|
2588
|
+
|
|
2589
|
+
```ts
|
|
2590
|
+
import { Command } from "@effect/platform"
|
|
2591
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node"
|
|
2592
|
+
import { Effect } from "effect"
|
|
2593
|
+
|
|
2594
|
+
// Create a command to run `cat` on a file and inherit stdout
|
|
2595
|
+
const program = Command.make("cat", "./some-file.txt").pipe(
|
|
2596
|
+
Command.stdout("inherit"),
|
|
2597
|
+
Command.exitCode
|
|
2598
|
+
)
|
|
2599
|
+
|
|
2600
|
+
// Run the command using NodeRuntime with the NodeContext layer
|
|
2601
|
+
NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))
|
|
2602
|
+
```
|
|
2603
|
+
|
|
2604
|
+
# FileSystem
|
|
2605
|
+
|
|
2606
|
+
The `@effect/platform/FileSystem` module provides a single `FileSystem` tag, which acts as the gateway for interacting with the filesystem.
|
|
2607
|
+
|
|
2608
|
+
Here's a list of operations that can be performed using the `FileSystem` tag:
|
|
2609
|
+
|
|
2610
|
+
| **Name** | **Arguments** | **Return** | **Description** |
|
|
2611
|
+
| --------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2612
|
+
| **access** | `path: string`, `options?: AccessFileOptions` | `Effect<void, PlatformError>` | Check if a file can be accessed. You can optionally specify the level of access to check for. |
|
|
2613
|
+
| **copy** | `fromPath: string`, `toPath: string`, `options?: CopyOptions` | `Effect<void, PlatformError>` | Copy a file or directory from `fromPath` to `toPath`. Equivalent to `cp -r`. |
|
|
2614
|
+
| **copyFile** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Copy a file from `fromPath` to `toPath`. |
|
|
2615
|
+
| **chmod** | `path: string`, `mode: number` | `Effect<void, PlatformError>` | Change the permissions of a file. |
|
|
2616
|
+
| **chown** | `path: string`, `uid: number`, `gid: number` | `Effect<void, PlatformError>` | Change the owner and group of a file. |
|
|
2617
|
+
| **exists** | `path: string` | `Effect<boolean, PlatformError>` | Check if a path exists. |
|
|
2618
|
+
| **link** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Create a hard link from `fromPath` to `toPath`. |
|
|
2619
|
+
| **makeDirectory** | `path: string`, `options?: MakeDirectoryOptions` | `Effect<void, PlatformError>` | Create a directory at `path`. You can optionally specify the mode and whether to recursively create nested directories. |
|
|
2620
|
+
| **makeTempDirectory** | `options?: MakeTempDirectoryOptions` | `Effect<string, PlatformError>` | Create a temporary directory. By default, the directory will be created inside the system's default temporary directory. |
|
|
2621
|
+
| **makeTempDirectoryScoped** | `options?: MakeTempDirectoryOptions` | `Effect<string, PlatformError, Scope>` | Create a temporary directory inside a scope. Functionally equivalent to `makeTempDirectory`, but the directory will be automatically deleted when the scope is closed. |
|
|
2622
|
+
| **makeTempFile** | `options?: MakeTempFileOptions` | `Effect<string, PlatformError>` | Create a temporary file. The directory creation is functionally equivalent to `makeTempDirectory`. The file name will be a randomly generated string. |
|
|
2623
|
+
| **makeTempFileScoped** | `options?: MakeTempFileOptions` | `Effect<string, PlatformError, Scope>` | Create a temporary file inside a scope. Functionally equivalent to `makeTempFile`, but the file will be automatically deleted when the scope is closed. |
|
|
2624
|
+
| **open** | `path: string`, `options?: OpenFileOptions` | `Effect<File, PlatformError, Scope>` | Open a file at `path` with the specified `options`. The file handle will be automatically closed when the scope is closed. |
|
|
2625
|
+
| **readDirectory** | `path: string`, `options?: ReadDirectoryOptions` | `Effect<Array<string>, PlatformError>` | List the contents of a directory. You can recursively list the contents of nested directories by setting the `recursive` option. |
|
|
2626
|
+
| **readFile** | `path: string` | `Effect<Uint8Array, PlatformError>` | Read the contents of a file. |
|
|
2627
|
+
| **readFileString** | `path: string`, `encoding?: string` | `Effect<string, PlatformError>` | Read the contents of a file as a string. |
|
|
2628
|
+
| **readLink** | `path: string` | `Effect<string, PlatformError>` | Read the destination of a symbolic link. |
|
|
2629
|
+
| **realPath** | `path: string` | `Effect<string, PlatformError>` | Resolve a path to its canonicalized absolute pathname. |
|
|
2630
|
+
| **remove** | `path: string`, `options?: RemoveOptions` | `Effect<void, PlatformError>` | Remove a file or directory. By setting the `recursive` option to `true`, you can recursively remove nested directories. |
|
|
2631
|
+
| **rename** | `oldPath: string`, `newPath: string` | `Effect<void, PlatformError>` | Rename a file or directory. |
|
|
2632
|
+
| **sink** | `path: string`, `options?: SinkOptions` | `Sink<void, Uint8Array, never, PlatformError>` | Create a writable `Sink` for the specified `path`. |
|
|
2633
|
+
| **stat** | `path: string` | `Effect<File.Info, PlatformError>` | Get information about a file at `path`. |
|
|
2634
|
+
| **stream** | `path: string`, `options?: StreamOptions` | `Stream<Uint8Array, PlatformError>` | Create a readable `Stream` for the specified `path`. |
|
|
2635
|
+
| **symlink** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Create a symbolic link from `fromPath` to `toPath`. |
|
|
2636
|
+
| **truncate** | `path: string`, `length?: SizeInput` | `Effect<void, PlatformError>` | Truncate a file to a specified length. If the `length` is not specified, the file will be truncated to length `0`. |
|
|
2637
|
+
| **utimes** | `path: string`, `atime: Date \| number`, `mtime: Date \| number` | `Effect<void, PlatformError>` | Change the file system timestamps of the file at `path`. |
|
|
2638
|
+
| **watch** | `path: string` | `Stream<WatchEvent, PlatformError>` | Watch a directory or file for changes. |
|
|
2639
|
+
|
|
2640
|
+
Let's explore a simple example using `readFileString`:
|
|
2641
|
+
|
|
2642
|
+
```ts
|
|
2643
|
+
import { FileSystem } from "@effect/platform"
|
|
2644
|
+
import { NodeFileSystem, NodeRuntime } from "@effect/platform-node"
|
|
2645
|
+
import { Effect } from "effect"
|
|
2646
|
+
|
|
2647
|
+
// const readFileString: Effect.Effect<void, PlatformError, FileSystem.FileSystem>
|
|
2648
|
+
const readFileString = Effect.gen(function* (_) {
|
|
2649
|
+
const fs = yield* _(FileSystem.FileSystem)
|
|
2650
|
+
|
|
2651
|
+
// Reading the content of the same file where this code is written
|
|
2652
|
+
const content = yield* _(fs.readFileString("./index.ts", "utf8"))
|
|
2653
|
+
console.log(content)
|
|
2654
|
+
})
|
|
2655
|
+
|
|
2656
|
+
NodeRuntime.runMain(readFileString.pipe(Effect.provide(NodeFileSystem.layer)))
|
|
2657
|
+
```
|
|
2658
|
+
|
|
2659
|
+
# KeyValueStore
|
|
2660
|
+
|
|
2661
|
+
## Overview
|
|
2662
|
+
|
|
2663
|
+
The `KeyValueStore` module provides a robust and effectful interface for managing key-value pairs. It supports asynchronous operations, ensuring data integrity and consistency, and includes built-in implementations for in-memory, file system-based, and schema-validated stores.
|
|
2664
|
+
|
|
2665
|
+
## Basic Usage
|
|
2666
|
+
|
|
2667
|
+
The `KeyValueStore` interface includes the following operations:
|
|
2668
|
+
|
|
2669
|
+
- **get**: Retrieve a value by key.
|
|
2670
|
+
- **set**: Store a key-value pair.
|
|
2671
|
+
- **remove**: Delete a key-value pair.
|
|
2672
|
+
- **clear**: Remove all key-value pairs.
|
|
2673
|
+
- **size**: Get the number of stored pairs.
|
|
2674
|
+
- **modify**: Atomically modify a value.
|
|
2675
|
+
- **has**: Check if a key exists.
|
|
2676
|
+
- **isEmpty**: Check if the store is empty.
|
|
2677
|
+
|
|
2678
|
+
**Example**
|
|
2679
|
+
|
|
2680
|
+
```ts
|
|
2681
|
+
import { KeyValueStore, layerMemory } from "@effect/platform/KeyValueStore"
|
|
2682
|
+
import { Effect } from "effect"
|
|
2683
|
+
|
|
2684
|
+
const program = Effect.gen(function* () {
|
|
2685
|
+
const store = yield* KeyValueStore
|
|
2686
|
+
console.log(yield* store.size) // Outputs: 0
|
|
2687
|
+
|
|
2688
|
+
yield* store.set("key", "value")
|
|
2689
|
+
console.log(yield* store.size) // Outputs: 1
|
|
2690
|
+
|
|
2691
|
+
const value = yield* store.get("key")
|
|
2692
|
+
console.log(value) // Outputs: { _id: 'Option', _tag: 'Some', value: 'value' }
|
|
2693
|
+
|
|
2694
|
+
yield* store.remove("key")
|
|
2695
|
+
console.log(yield* store.size) // Outputs: 0
|
|
2696
|
+
})
|
|
2697
|
+
|
|
2698
|
+
Effect.runPromise(program.pipe(Effect.provide(layerMemory)))
|
|
2699
|
+
```
|
|
2700
|
+
|
|
2701
|
+
## Built-in Implementations
|
|
2702
|
+
|
|
2703
|
+
The module provides several built-in implementations to suit different needs:
|
|
2704
|
+
|
|
2705
|
+
- **In-Memory Store**: `layerMemory` provides a simple, in-memory key-value store, ideal for lightweight or testing scenarios.
|
|
2706
|
+
- **File System Store**: `layerFileSystem` offers a file-based store for persistent storage needs.
|
|
2707
|
+
- **Schema Store**: `layerSchema` enables schema-based validation for stored values, ensuring data integrity and type safety.
|
|
2708
|
+
|
|
2709
|
+
## Schema Store
|
|
2710
|
+
|
|
2711
|
+
The `SchemaStore` implementation allows you to validate and parse values according to a defined schema. This ensures that all data stored in the key-value store adheres to the specified structure, enhancing data integrity and type safety.
|
|
2712
|
+
|
|
2713
|
+
**Example**
|
|
2714
|
+
|
|
2715
|
+
```ts
|
|
2716
|
+
import { KeyValueStore, layerMemory } from "@effect/platform/KeyValueStore"
|
|
2717
|
+
import { Schema } from "@effect/schema"
|
|
2718
|
+
import { Effect } from "effect"
|
|
2719
|
+
|
|
2720
|
+
// Define a schema for the values
|
|
2721
|
+
const Person = Schema.Struct({
|
|
2722
|
+
name: Schema.String,
|
|
2723
|
+
age: Schema.Number
|
|
2724
|
+
})
|
|
2725
|
+
|
|
2726
|
+
const program = Effect.gen(function* () {
|
|
2727
|
+
const store = (yield* KeyValueStore).forSchema(Person)
|
|
2728
|
+
|
|
2729
|
+
// Create a value that adheres to the schema
|
|
2730
|
+
const value = { name: "Alice", age: 30 }
|
|
2731
|
+
yield* store.set("user1", value)
|
|
2732
|
+
console.log(yield* store.size) // Outputs: 1
|
|
2733
|
+
|
|
2734
|
+
// Retrieve and validate the value
|
|
2735
|
+
const retrievedValue = yield* store.get("user1")
|
|
2736
|
+
console.log(retrievedValue) // Outputs: { _id: 'Option', _tag: 'Some', value: { name: 'Alice', age: 30 } }
|
|
2737
|
+
})
|
|
2738
|
+
|
|
2739
|
+
Effect.runPromise(program.pipe(Effect.provide(layerMemory)))
|
|
2740
|
+
```
|
|
2741
|
+
|
|
2742
|
+
In this example:
|
|
2743
|
+
|
|
2744
|
+
- **Person**: Defines the structure for the values stored in the key-value store.
|
|
2745
|
+
- **store.set**: Stores a value adhering to `Person`.
|
|
2746
|
+
- **store.get**: Retrieves and validates the stored value against `Person`.
|