@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
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 1.0.0
|
|
3
|
+
*/
|
|
4
|
+
import * as AST from "@effect/schema/AST"
|
|
5
|
+
import * as ParseResult from "@effect/schema/ParseResult"
|
|
6
|
+
import * as Schema from "@effect/schema/Schema"
|
|
7
|
+
import * as Chunk from "effect/Chunk"
|
|
8
|
+
import * as Context from "effect/Context"
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import * as Encoding from "effect/Encoding"
|
|
11
|
+
import * as FiberRef from "effect/FiberRef"
|
|
12
|
+
import { identity } from "effect/Function"
|
|
13
|
+
import { globalValue } from "effect/GlobalValue"
|
|
14
|
+
import * as Layer from "effect/Layer"
|
|
15
|
+
import * as Option from "effect/Option"
|
|
16
|
+
import { type Pipeable, pipeArguments } from "effect/Pipeable"
|
|
17
|
+
import type { ReadonlyRecord } from "effect/Record"
|
|
18
|
+
import * as Redacted from "effect/Redacted"
|
|
19
|
+
import type { Scope } from "effect/Scope"
|
|
20
|
+
import type { Covariant, Mutable, NoInfer } from "effect/Types"
|
|
21
|
+
import { unify } from "effect/Unify"
|
|
22
|
+
import type { Cookie } from "./Cookies.js"
|
|
23
|
+
import type { FileSystem } from "./FileSystem.js"
|
|
24
|
+
import * as HttpApi from "./HttpApi.js"
|
|
25
|
+
import * as HttpApiEndpoint from "./HttpApiEndpoint.js"
|
|
26
|
+
import { HttpApiDecodeError } from "./HttpApiError.js"
|
|
27
|
+
import type * as HttpApiGroup from "./HttpApiGroup.js"
|
|
28
|
+
import * as HttpApiSchema from "./HttpApiSchema.js"
|
|
29
|
+
import type * as HttpApiSecurity from "./HttpApiSecurity.js"
|
|
30
|
+
import * as HttpApp from "./HttpApp.js"
|
|
31
|
+
import * as HttpMethod from "./HttpMethod.js"
|
|
32
|
+
import * as HttpMiddleware from "./HttpMiddleware.js"
|
|
33
|
+
import * as HttpRouter from "./HttpRouter.js"
|
|
34
|
+
import * as HttpServer from "./HttpServer.js"
|
|
35
|
+
import * as HttpServerRequest from "./HttpServerRequest.js"
|
|
36
|
+
import * as HttpServerResponse from "./HttpServerResponse.js"
|
|
37
|
+
import * as OpenApi from "./OpenApi.js"
|
|
38
|
+
import type { Path } from "./Path.js"
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The router that the API endpoints are attached to.
|
|
42
|
+
*
|
|
43
|
+
* @since 1.0.0
|
|
44
|
+
* @category router
|
|
45
|
+
*/
|
|
46
|
+
export class Router extends HttpRouter.Tag("@effect/platform/HttpApiBuilder/Router")<Router>() {}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build an `HttpApp` from an `HttpApi` instance, and serve it using an
|
|
50
|
+
* `HttpServer`.
|
|
51
|
+
*
|
|
52
|
+
* Optionally, you can provide a middleware function that will be applied to
|
|
53
|
+
* the `HttpApp` before serving.
|
|
54
|
+
*
|
|
55
|
+
* @since 1.0.0
|
|
56
|
+
* @category constructors
|
|
57
|
+
*/
|
|
58
|
+
export const serve: {
|
|
59
|
+
(): Layer.Layer<never, never, HttpServer.HttpServer | HttpApi.HttpApi.Service | HttpRouter.HttpRouter.DefaultServices>
|
|
60
|
+
<R>(
|
|
61
|
+
middleware: (httpApp: HttpApp.Default) => HttpApp.Default<never, R>
|
|
62
|
+
): Layer.Layer<
|
|
63
|
+
never,
|
|
64
|
+
never,
|
|
65
|
+
| HttpServer.HttpServer
|
|
66
|
+
| HttpRouter.HttpRouter.DefaultServices
|
|
67
|
+
| Exclude<R, Scope | HttpServerRequest.HttpServerRequest>
|
|
68
|
+
| HttpApi.HttpApi.Service
|
|
69
|
+
>
|
|
70
|
+
} = (middleware?: HttpMiddleware.HttpMiddleware.Applied<any, never, any>): Layer.Layer<
|
|
71
|
+
never,
|
|
72
|
+
never,
|
|
73
|
+
any
|
|
74
|
+
> =>
|
|
75
|
+
httpApp.pipe(
|
|
76
|
+
Effect.map(HttpServer.serve(middleware!)),
|
|
77
|
+
Layer.unwrapEffect,
|
|
78
|
+
Layer.provide(Router.Live)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Construct an `HttpApp` from an `HttpApi` instance.
|
|
83
|
+
*
|
|
84
|
+
* @since 1.0.0
|
|
85
|
+
* @category constructors
|
|
86
|
+
*/
|
|
87
|
+
export const httpApp: Effect.Effect<
|
|
88
|
+
HttpApp.Default,
|
|
89
|
+
never,
|
|
90
|
+
Router | HttpApi.HttpApi.Service | HttpRouter.HttpRouter.DefaultServices
|
|
91
|
+
> = Effect.gen(function*() {
|
|
92
|
+
const api = yield* HttpApi.HttpApi
|
|
93
|
+
const router = yield* Router.router
|
|
94
|
+
const apiMiddleware = yield* Effect.serviceOption(Middleware)
|
|
95
|
+
const errorSchema = makeErrorSchema(api as any)
|
|
96
|
+
const encodeError = Schema.encodeUnknown(errorSchema)
|
|
97
|
+
return router.pipe(
|
|
98
|
+
apiMiddleware._tag === "Some" ? apiMiddleware.value : identity,
|
|
99
|
+
Effect.catchAll((error) =>
|
|
100
|
+
Effect.matchEffect(encodeError(error), {
|
|
101
|
+
onFailure: () => Effect.die(error),
|
|
102
|
+
onSuccess: Effect.succeed
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a root level `Layer` from an `HttpApi` instance.
|
|
110
|
+
*
|
|
111
|
+
* The `Layer` will provide the `HttpApi` service, and will require the
|
|
112
|
+
* implementation for all the `HttpApiGroup`'s contained in the `HttpApi`.
|
|
113
|
+
*
|
|
114
|
+
* The resulting `Layer` can be provided to the `HttpApiBuilder.serve` layer.
|
|
115
|
+
*
|
|
116
|
+
* @since 1.0.0
|
|
117
|
+
* @category constructors
|
|
118
|
+
*/
|
|
119
|
+
export const api = <Groups extends HttpApiGroup.HttpApiGroup.Any, Error, ErrorR>(
|
|
120
|
+
self: HttpApi.HttpApi<Groups, Error, ErrorR>
|
|
121
|
+
): Layer.Layer<HttpApi.HttpApi.Service, never, HttpApiGroup.HttpApiGroup.ToService<Groups> | ErrorR> =>
|
|
122
|
+
Layer.succeed(HttpApi.HttpApi, self) as any
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @since 1.0.0
|
|
126
|
+
* @category handlers
|
|
127
|
+
*/
|
|
128
|
+
export const HandlersTypeId: unique symbol = Symbol.for("@effect/platform/HttpApiBuilder/Handlers")
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @since 1.0.0
|
|
132
|
+
* @category handlers
|
|
133
|
+
*/
|
|
134
|
+
export type HandlersTypeId = typeof HandlersTypeId
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Represents a handled, or partially handled, `HttpApiGroup`.
|
|
138
|
+
*
|
|
139
|
+
* @since 1.0.0
|
|
140
|
+
* @category handlers
|
|
141
|
+
*/
|
|
142
|
+
export interface Handlers<
|
|
143
|
+
E,
|
|
144
|
+
R,
|
|
145
|
+
Endpoints extends HttpApiEndpoint.HttpApiEndpoint.All = never
|
|
146
|
+
> extends Pipeable {
|
|
147
|
+
readonly [HandlersTypeId]: {
|
|
148
|
+
_Endpoints: Covariant<Endpoints>
|
|
149
|
+
}
|
|
150
|
+
readonly group: HttpApiGroup.HttpApiGroup<any, HttpApiEndpoint.HttpApiEndpoint.All, any, R>
|
|
151
|
+
readonly handlers: Chunk.Chunk<Handlers.Item<E, R>>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @since 1.0.0
|
|
156
|
+
* @category handlers
|
|
157
|
+
*/
|
|
158
|
+
export declare namespace Handlers {
|
|
159
|
+
/**
|
|
160
|
+
* @since 1.0.0
|
|
161
|
+
* @category handlers
|
|
162
|
+
*/
|
|
163
|
+
export type Middleware<E, R, E1, R1> = (self: HttpRouter.Route.Middleware<E, R>) => HttpApp.Default<E1, R1>
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @since 1.0.0
|
|
167
|
+
* @category handlers
|
|
168
|
+
*/
|
|
169
|
+
export type Item<E, R> = {
|
|
170
|
+
readonly _tag: "Handler"
|
|
171
|
+
readonly endpoint: HttpApiEndpoint.HttpApiEndpoint.Any
|
|
172
|
+
readonly handler: HttpApiEndpoint.HttpApiEndpoint.Handler<any, E, R>
|
|
173
|
+
readonly withFullResponse: boolean
|
|
174
|
+
} | {
|
|
175
|
+
readonly _tag: "Middleware"
|
|
176
|
+
readonly middleware: Middleware<any, any, E, R>
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const HandlersProto = {
|
|
181
|
+
[HandlersTypeId]: {
|
|
182
|
+
_Endpoints: identity
|
|
183
|
+
},
|
|
184
|
+
pipe() {
|
|
185
|
+
return pipeArguments(this, arguments)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const makeHandlers = <E, R, Endpoints extends HttpApiEndpoint.HttpApiEndpoint.All>(
|
|
190
|
+
options: {
|
|
191
|
+
readonly group: HttpApiGroup.HttpApiGroup<any, HttpApiEndpoint.HttpApiEndpoint.All, any, R>
|
|
192
|
+
readonly handlers: Chunk.Chunk<Handlers.Item<E, R>>
|
|
193
|
+
}
|
|
194
|
+
): Handlers<E, R, Endpoints> => {
|
|
195
|
+
const self = Object.create(HandlersProto)
|
|
196
|
+
self.group = options.group
|
|
197
|
+
self.handlers = options.handlers
|
|
198
|
+
return self
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Create a `Layer` that will implement all the endpoints in an `HttpApiGroup`.
|
|
203
|
+
*
|
|
204
|
+
* An unimplemented `Handlers` instance is passed to the `build` function, which
|
|
205
|
+
* you can use to add handlers to the group.
|
|
206
|
+
*
|
|
207
|
+
* You can implement endpoints using the `HttpApiBuilder.handle` api.
|
|
208
|
+
*
|
|
209
|
+
* @since 1.0.0
|
|
210
|
+
* @category handlers
|
|
211
|
+
*/
|
|
212
|
+
export const group = <
|
|
213
|
+
Groups extends HttpApiGroup.HttpApiGroup.Any,
|
|
214
|
+
ApiError,
|
|
215
|
+
ApiErrorR,
|
|
216
|
+
const Name extends Groups["identifier"],
|
|
217
|
+
RH,
|
|
218
|
+
EX = never,
|
|
219
|
+
RX = never
|
|
220
|
+
>(
|
|
221
|
+
api: HttpApi.HttpApi<Groups, ApiError, ApiErrorR>,
|
|
222
|
+
groupName: Name,
|
|
223
|
+
build: (
|
|
224
|
+
handlers: Handlers<never, never, HttpApiGroup.HttpApiGroup.EndpointsWithName<Groups, Name>>
|
|
225
|
+
) =>
|
|
226
|
+
| Handlers<NoInfer<ApiError> | HttpApiGroup.HttpApiGroup.ErrorWithName<Groups, Name>, RH>
|
|
227
|
+
| Effect.Effect<Handlers<NoInfer<ApiError> | HttpApiGroup.HttpApiGroup.ErrorWithName<Groups, Name>, RH>, EX, RX>
|
|
228
|
+
): Layer.Layer<
|
|
229
|
+
HttpApiGroup.HttpApiGroup.Service<Name>,
|
|
230
|
+
EX,
|
|
231
|
+
RX | RH | HttpApiGroup.HttpApiGroup.ContextWithName<Groups, Name> | ApiErrorR
|
|
232
|
+
> =>
|
|
233
|
+
Router.use((router) =>
|
|
234
|
+
Effect.gen(function*() {
|
|
235
|
+
const context = yield* Effect.context<any>()
|
|
236
|
+
const group = Chunk.findFirst(api.groups, (group) => group.identifier === groupName)
|
|
237
|
+
if (group._tag === "None") {
|
|
238
|
+
throw new Error(`Group "${groupName}" not found in API`)
|
|
239
|
+
}
|
|
240
|
+
const result = build(makeHandlers({ group: group.value as any, handlers: Chunk.empty() }))
|
|
241
|
+
const handlers = Effect.isEffect(result) ? (yield* result) : result
|
|
242
|
+
const routes: Array<HttpRouter.Route<any, any>> = []
|
|
243
|
+
for (const item of handlers.handlers) {
|
|
244
|
+
if (item._tag === "Middleware") {
|
|
245
|
+
for (const route of routes) {
|
|
246
|
+
;(route as Mutable<HttpRouter.Route<any, any>>).handler = item.middleware(route.handler as any)
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
routes.push(handlerToRoute(
|
|
250
|
+
item.endpoint,
|
|
251
|
+
function(request) {
|
|
252
|
+
return Effect.mapInputContext(
|
|
253
|
+
item.handler(request),
|
|
254
|
+
(input) => Context.merge(context, input)
|
|
255
|
+
)
|
|
256
|
+
},
|
|
257
|
+
item.withFullResponse
|
|
258
|
+
))
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
yield* router.concat(HttpRouter.fromIterable(routes))
|
|
262
|
+
})
|
|
263
|
+
) as any
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Add the implementation for an `HttpApiEndpoint` to a `Handlers` group.
|
|
267
|
+
*
|
|
268
|
+
* @since 1.0.0
|
|
269
|
+
* @category handlers
|
|
270
|
+
*/
|
|
271
|
+
export const handle: {
|
|
272
|
+
<Endpoints extends HttpApiEndpoint.HttpApiEndpoint.All, const Name extends Endpoints["name"], E, R>(
|
|
273
|
+
name: Name,
|
|
274
|
+
handler: HttpApiEndpoint.HttpApiEndpoint.HandlerWithName<Endpoints, Name, E, R>
|
|
275
|
+
): <EG, RG>(self: Handlers<EG, RG, Endpoints>) => Handlers<
|
|
276
|
+
EG | Exclude<E, HttpApiEndpoint.HttpApiEndpoint.ErrorWithName<Endpoints, Name>> | HttpApiDecodeError,
|
|
277
|
+
RG | HttpApiEndpoint.HttpApiEndpoint.ExcludeProvided<R>,
|
|
278
|
+
HttpApiEndpoint.HttpApiEndpoint.ExcludeName<Endpoints, Name>
|
|
279
|
+
>
|
|
280
|
+
<Endpoints extends HttpApiEndpoint.HttpApiEndpoint.All, const Name extends Endpoints["name"], E, R>(
|
|
281
|
+
name: Name,
|
|
282
|
+
handler: HttpApiEndpoint.HttpApiEndpoint.HandlerResponseWithName<Endpoints, Name, E, R>,
|
|
283
|
+
options: {
|
|
284
|
+
readonly withFullResponse: true
|
|
285
|
+
}
|
|
286
|
+
): <EG, RG>(self: Handlers<EG, RG, Endpoints>) => Handlers<
|
|
287
|
+
EG | Exclude<E, HttpApiEndpoint.HttpApiEndpoint.ErrorWithName<Endpoints, Name>> | HttpApiDecodeError,
|
|
288
|
+
RG | HttpApiEndpoint.HttpApiEndpoint.ExcludeProvided<R>,
|
|
289
|
+
HttpApiEndpoint.HttpApiEndpoint.ExcludeName<Endpoints, Name>
|
|
290
|
+
>
|
|
291
|
+
} = <Endpoints extends HttpApiEndpoint.HttpApiEndpoint.All, const Name extends Endpoints["name"], E, R>(
|
|
292
|
+
name: Name,
|
|
293
|
+
handler: HttpApiEndpoint.HttpApiEndpoint.HandlerWithName<Endpoints, Name, E, R>,
|
|
294
|
+
options?: {
|
|
295
|
+
readonly withFullResponse: true
|
|
296
|
+
}
|
|
297
|
+
) =>
|
|
298
|
+
<EG, RG>(
|
|
299
|
+
self: Handlers<EG, RG, Endpoints>
|
|
300
|
+
): Handlers<
|
|
301
|
+
EG | Exclude<E, HttpApiEndpoint.HttpApiEndpoint.ErrorWithName<Endpoints, Name>> | HttpApiDecodeError,
|
|
302
|
+
RG | HttpApiEndpoint.HttpApiEndpoint.ExcludeProvided<R>,
|
|
303
|
+
HttpApiEndpoint.HttpApiEndpoint.ExcludeName<Endpoints, Name>
|
|
304
|
+
> => {
|
|
305
|
+
const o = Chunk.findFirst(self.group.endpoints, (endpoint) => endpoint.name === name)
|
|
306
|
+
if (o._tag === "None") {
|
|
307
|
+
throw new Error(`Endpoint "${name}" not found in group "${self.group.identifier}"`)
|
|
308
|
+
}
|
|
309
|
+
const endpoint = o.value
|
|
310
|
+
return makeHandlers({
|
|
311
|
+
group: self.group,
|
|
312
|
+
handlers: Chunk.append(self.handlers, {
|
|
313
|
+
_tag: "Handler",
|
|
314
|
+
endpoint,
|
|
315
|
+
handler,
|
|
316
|
+
withFullResponse: options?.withFullResponse === true
|
|
317
|
+
}) as any
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Add `HttpMiddleware` to a `Handlers` group.
|
|
323
|
+
*
|
|
324
|
+
* Any errors are required to have a corresponding schema in the API.
|
|
325
|
+
* You can add middleware errors to an `HttpApiGroup` using the `HttpApiGroup.addError`
|
|
326
|
+
* api.
|
|
327
|
+
*
|
|
328
|
+
* @since 1.0.0
|
|
329
|
+
* @category middleware
|
|
330
|
+
*/
|
|
331
|
+
export const middleware =
|
|
332
|
+
<E, R, E1, R1>(middleware: Handlers.Middleware<E, R, E1, R1>) =>
|
|
333
|
+
<Endpoints extends HttpApiEndpoint.HttpApiEndpoint.All>(
|
|
334
|
+
self: Handlers<E, R, Endpoints>
|
|
335
|
+
): Handlers<E1, HttpApiEndpoint.HttpApiEndpoint.ExcludeProvided<R1>, Endpoints> =>
|
|
336
|
+
makeHandlers<E1, HttpApiEndpoint.HttpApiEndpoint.ExcludeProvided<R1>, Endpoints>({
|
|
337
|
+
...self as any,
|
|
338
|
+
handlers: Chunk.append(self.handlers, {
|
|
339
|
+
_tag: "Middleware",
|
|
340
|
+
middleware
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @since 1.0.0
|
|
346
|
+
* @category middleware
|
|
347
|
+
*/
|
|
348
|
+
export class Middleware extends Context.Tag("@effect/platform/HttpApiBuilder/Middleware")<
|
|
349
|
+
Middleware,
|
|
350
|
+
HttpMiddleware.HttpMiddleware
|
|
351
|
+
>() {}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @since 1.0.0
|
|
355
|
+
* @category middleware
|
|
356
|
+
*/
|
|
357
|
+
export declare namespace ApiMiddleware {
|
|
358
|
+
/**
|
|
359
|
+
* @since 1.0.0
|
|
360
|
+
* @category middleware
|
|
361
|
+
*/
|
|
362
|
+
export type Fn<Error, R = HttpRouter.HttpRouter.Provided> = (
|
|
363
|
+
httpApp: HttpApp.Default
|
|
364
|
+
) => HttpApp.Default<Error, R>
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const middlewareAdd = (middleware: HttpMiddleware.HttpMiddleware): Effect.Effect<HttpMiddleware.HttpMiddleware> =>
|
|
368
|
+
Effect.map(
|
|
369
|
+
Effect.context<never>(),
|
|
370
|
+
(context) => {
|
|
371
|
+
const current = Context.getOption(context, Middleware)
|
|
372
|
+
const withContext: HttpMiddleware.HttpMiddleware = (httpApp) =>
|
|
373
|
+
Effect.mapInputContext(middleware(httpApp), (input) => Context.merge(context, input))
|
|
374
|
+
return current._tag === "None" ? withContext : (httpApp) => withContext(current.value(httpApp))
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
const middlewareAddNoContext = (
|
|
379
|
+
middleware: HttpMiddleware.HttpMiddleware
|
|
380
|
+
): Effect.Effect<HttpMiddleware.HttpMiddleware> =>
|
|
381
|
+
Effect.map(
|
|
382
|
+
Effect.serviceOption(Middleware),
|
|
383
|
+
(current): HttpMiddleware.HttpMiddleware => {
|
|
384
|
+
return current._tag === "None" ? middleware : (httpApp) => middleware(current.value(httpApp))
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Create an `HttpApi` level middleware `Layer`.
|
|
390
|
+
*
|
|
391
|
+
* @since 1.0.0
|
|
392
|
+
* @category middleware
|
|
393
|
+
*/
|
|
394
|
+
export const middlewareLayer: {
|
|
395
|
+
<EX = never, RX = never>(
|
|
396
|
+
middleware: ApiMiddleware.Fn<never> | Effect.Effect<ApiMiddleware.Fn<never>, EX, RX>,
|
|
397
|
+
options?: {
|
|
398
|
+
readonly withContext?: false | undefined
|
|
399
|
+
}
|
|
400
|
+
): Layer.Layer<never, EX, RX>
|
|
401
|
+
<R, EX = never, RX = never>(
|
|
402
|
+
middleware: ApiMiddleware.Fn<never, R> | Effect.Effect<ApiMiddleware.Fn<never, R>, EX, RX>,
|
|
403
|
+
options: {
|
|
404
|
+
readonly withContext: true
|
|
405
|
+
}
|
|
406
|
+
): Layer.Layer<never, EX, HttpRouter.HttpRouter.ExcludeProvided<R> | RX>
|
|
407
|
+
<Groups extends HttpApiGroup.HttpApiGroup.Any, Error, ErrorR, EX = never, RX = never>(
|
|
408
|
+
api: HttpApi.HttpApi<Groups, Error, ErrorR>,
|
|
409
|
+
middleware: ApiMiddleware.Fn<NoInfer<Error>> | Effect.Effect<ApiMiddleware.Fn<NoInfer<Error>>, EX, RX>,
|
|
410
|
+
options?: {
|
|
411
|
+
readonly withContext?: false | undefined
|
|
412
|
+
}
|
|
413
|
+
): Layer.Layer<never, EX, RX>
|
|
414
|
+
<Groups extends HttpApiGroup.HttpApiGroup.Any, Error, ErrorR, R, EX = never, RX = never>(
|
|
415
|
+
api: HttpApi.HttpApi<Groups, Error, ErrorR>,
|
|
416
|
+
middleware: ApiMiddleware.Fn<NoInfer<Error>, R> | Effect.Effect<ApiMiddleware.Fn<NoInfer<Error>, R>, EX, RX>,
|
|
417
|
+
options: {
|
|
418
|
+
readonly withContext: true
|
|
419
|
+
}
|
|
420
|
+
): Layer.Layer<never, EX, HttpRouter.HttpRouter.ExcludeProvided<R> | RX>
|
|
421
|
+
} = (
|
|
422
|
+
...args: [
|
|
423
|
+
middleware: ApiMiddleware.Fn<any, any> | Effect.Effect<ApiMiddleware.Fn<any, any>, any, any>,
|
|
424
|
+
options?: {
|
|
425
|
+
readonly withContext?: boolean | undefined
|
|
426
|
+
} | undefined
|
|
427
|
+
] | [
|
|
428
|
+
api: HttpApi.HttpApi.Any,
|
|
429
|
+
middleware: ApiMiddleware.Fn<any, any> | Effect.Effect<ApiMiddleware.Fn<any, any>, any, any>,
|
|
430
|
+
options?: {
|
|
431
|
+
readonly withContext?: boolean | undefined
|
|
432
|
+
} | undefined
|
|
433
|
+
]
|
|
434
|
+
): any => {
|
|
435
|
+
const apiFirst = HttpApi.isHttpApi(args[0])
|
|
436
|
+
const withContext = apiFirst ? args[2]?.withContext === true : (args as any)[1]?.withContext === true
|
|
437
|
+
const add = withContext ? middlewareAdd : middlewareAddNoContext
|
|
438
|
+
const middleware = apiFirst ? args[1] : args[0]
|
|
439
|
+
return Effect.isEffect(middleware)
|
|
440
|
+
? Layer.effect(Middleware, Effect.flatMap(middleware as any, add))
|
|
441
|
+
: Layer.effect(Middleware, add(middleware as any))
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Create an `HttpApi` level middleware `Layer`, that has a `Scope` provided to
|
|
446
|
+
* the constructor.
|
|
447
|
+
*
|
|
448
|
+
* @since 1.0.0
|
|
449
|
+
* @category middleware
|
|
450
|
+
*/
|
|
451
|
+
export const middlewareLayerScoped: {
|
|
452
|
+
<EX, RX>(
|
|
453
|
+
middleware: Effect.Effect<ApiMiddleware.Fn<never>, EX, RX>,
|
|
454
|
+
options?: {
|
|
455
|
+
readonly withContext?: false | undefined
|
|
456
|
+
}
|
|
457
|
+
): Layer.Layer<never, EX, Exclude<RX, Scope>>
|
|
458
|
+
<R, EX, RX>(
|
|
459
|
+
middleware: Effect.Effect<ApiMiddleware.Fn<never, R>, EX, RX>,
|
|
460
|
+
options: {
|
|
461
|
+
readonly withContext: true
|
|
462
|
+
}
|
|
463
|
+
): Layer.Layer<never, EX, HttpRouter.HttpRouter.ExcludeProvided<R> | Exclude<RX, Scope>>
|
|
464
|
+
<Groups extends HttpApiGroup.HttpApiGroup.Any, Error, ErrorR, EX, RX>(
|
|
465
|
+
api: HttpApi.HttpApi<Groups, Error, ErrorR>,
|
|
466
|
+
middleware: Effect.Effect<ApiMiddleware.Fn<NoInfer<Error>>, EX, RX>,
|
|
467
|
+
options?: {
|
|
468
|
+
readonly withContext?: false | undefined
|
|
469
|
+
}
|
|
470
|
+
): Layer.Layer<never, EX, Exclude<RX, Scope>>
|
|
471
|
+
<Groups extends HttpApiGroup.HttpApiGroup.Any, Error, ErrorR, R, EX, RX>(
|
|
472
|
+
api: HttpApi.HttpApi<Groups, Error, ErrorR>,
|
|
473
|
+
middleware: Effect.Effect<ApiMiddleware.Fn<NoInfer<Error>, R>, EX, RX>,
|
|
474
|
+
options: {
|
|
475
|
+
readonly withContext: true
|
|
476
|
+
}
|
|
477
|
+
): Layer.Layer<never, EX, HttpRouter.HttpRouter.ExcludeProvided<R> | Exclude<RX, Scope>>
|
|
478
|
+
} = (
|
|
479
|
+
...args: [
|
|
480
|
+
middleware: ApiMiddleware.Fn<any, any> | Effect.Effect<ApiMiddleware.Fn<any, any>, any, any>,
|
|
481
|
+
options?: {
|
|
482
|
+
readonly withContext?: boolean | undefined
|
|
483
|
+
} | undefined
|
|
484
|
+
] | [
|
|
485
|
+
api: HttpApi.HttpApi.Any,
|
|
486
|
+
middleware: ApiMiddleware.Fn<any, any> | Effect.Effect<ApiMiddleware.Fn<any, any>, any, any>,
|
|
487
|
+
options?: {
|
|
488
|
+
readonly withContext?: boolean | undefined
|
|
489
|
+
} | undefined
|
|
490
|
+
]
|
|
491
|
+
): any => {
|
|
492
|
+
const apiFirst = HttpApi.isHttpApi(args[0])
|
|
493
|
+
const withContext = apiFirst ? args[2]?.withContext === true : (args as any)[1]?.withContext === true
|
|
494
|
+
const add = withContext ? middlewareAdd : middlewareAddNoContext
|
|
495
|
+
const middleware = apiFirst ? args[1] : args[0]
|
|
496
|
+
return Layer.scoped(Middleware, Effect.flatMap(middleware as any, add))
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* A CORS middleware layer that can be provided to the `HttpApiBuilder.serve` layer.
|
|
501
|
+
*
|
|
502
|
+
* @since 1.0.0
|
|
503
|
+
* @category middleware
|
|
504
|
+
*/
|
|
505
|
+
export const middlewareCors = (
|
|
506
|
+
options?: {
|
|
507
|
+
readonly allowedOrigins?: ReadonlyArray<string> | undefined
|
|
508
|
+
readonly allowedMethods?: ReadonlyArray<string> | undefined
|
|
509
|
+
readonly allowedHeaders?: ReadonlyArray<string> | undefined
|
|
510
|
+
readonly exposedHeaders?: ReadonlyArray<string> | undefined
|
|
511
|
+
readonly maxAge?: number | undefined
|
|
512
|
+
readonly credentials?: boolean | undefined
|
|
513
|
+
} | undefined
|
|
514
|
+
): Layer.Layer<never> => middlewareLayer(HttpMiddleware.cors(options))
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* A middleware that adds an openapi.json endpoint to the API.
|
|
518
|
+
*
|
|
519
|
+
* @since 1.0.0
|
|
520
|
+
* @category middleware
|
|
521
|
+
*/
|
|
522
|
+
export const middlewareOpenApi = (
|
|
523
|
+
options?: {
|
|
524
|
+
readonly path?: HttpRouter.PathInput | undefined
|
|
525
|
+
} | undefined
|
|
526
|
+
): Layer.Layer<never, never, HttpApi.HttpApi.Service> =>
|
|
527
|
+
Router.use((router) =>
|
|
528
|
+
Effect.gen(function*() {
|
|
529
|
+
const api = yield* HttpApi.HttpApi
|
|
530
|
+
const spec = OpenApi.fromApi(api)
|
|
531
|
+
const response = yield* HttpServerResponse.json(spec).pipe(
|
|
532
|
+
Effect.orDie
|
|
533
|
+
)
|
|
534
|
+
yield* router.get(options?.path ?? "/openapi.json", Effect.succeed(response))
|
|
535
|
+
})
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* @since 1.0.0
|
|
540
|
+
* @category middleware
|
|
541
|
+
*/
|
|
542
|
+
export interface SecurityMiddleware<I, EM = never, RM = never> {
|
|
543
|
+
<Endpoints extends HttpApiEndpoint.HttpApiEndpoint.All, E, R>(
|
|
544
|
+
self: Handlers<E, R, Endpoints>
|
|
545
|
+
): Handlers<E | EM, Exclude<R, I> | HttpApiEndpoint.HttpApiEndpoint.ExcludeProvided<RM>, Endpoints>
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const bearerLen = `Bearer `.length
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* @since 1.0.0
|
|
552
|
+
* @category middleware
|
|
553
|
+
*/
|
|
554
|
+
export const securityDecode = <Security extends HttpApiSecurity.HttpApiSecurity>(
|
|
555
|
+
self: Security
|
|
556
|
+
): Effect.Effect<
|
|
557
|
+
HttpApiSecurity.HttpApiSecurity.Type<Security>,
|
|
558
|
+
never,
|
|
559
|
+
HttpServerRequest.HttpServerRequest | HttpServerRequest.ParsedSearchParams
|
|
560
|
+
> => {
|
|
561
|
+
switch (self._tag) {
|
|
562
|
+
case "Bearer": {
|
|
563
|
+
return Effect.map(
|
|
564
|
+
HttpServerRequest.HttpServerRequest,
|
|
565
|
+
(request) => Redacted.make((request.headers.authorization ?? "").slice(bearerLen)) as any
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
case "ApiKey": {
|
|
569
|
+
const schema = Schema.Struct({
|
|
570
|
+
[self.key]: Schema.String
|
|
571
|
+
})
|
|
572
|
+
const decode = unify(
|
|
573
|
+
self.in === "query"
|
|
574
|
+
? HttpServerRequest.schemaSearchParams(schema)
|
|
575
|
+
: self.in === "cookie"
|
|
576
|
+
? HttpServerRequest.schemaCookies(schema)
|
|
577
|
+
: HttpServerRequest.schemaHeaders(schema)
|
|
578
|
+
)
|
|
579
|
+
return Effect.match(decode, {
|
|
580
|
+
onFailure: () => Redacted.make("") as any,
|
|
581
|
+
onSuccess: (match) => Redacted.make(match[self.key])
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
case "Basic": {
|
|
585
|
+
const empty: HttpApiSecurity.HttpApiSecurity.Type<Security> = {
|
|
586
|
+
username: "",
|
|
587
|
+
password: Redacted.make("")
|
|
588
|
+
} as any
|
|
589
|
+
return HttpServerRequest.HttpServerRequest.pipe(
|
|
590
|
+
Effect.flatMap((request) => Encoding.decodeBase64String(request.headers.authorization ?? "")),
|
|
591
|
+
Effect.match({
|
|
592
|
+
onFailure: () => empty,
|
|
593
|
+
onSuccess: (header) => {
|
|
594
|
+
const parts = header.split(":")
|
|
595
|
+
if (parts.length !== 2) {
|
|
596
|
+
return empty
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
username: parts[0],
|
|
600
|
+
password: Redacted.make(parts[1])
|
|
601
|
+
} as any
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Set a cookie from an `HttpApiSecurity.HttpApiKey` instance.
|
|
611
|
+
*
|
|
612
|
+
* You can use this api before returning a response from an endpoint handler.
|
|
613
|
+
*
|
|
614
|
+
* ```ts
|
|
615
|
+
* ApiBuilder.handle(
|
|
616
|
+
* "authenticate",
|
|
617
|
+
* (_) => ApiBuilder.securitySetCookie(security, "secret123")
|
|
618
|
+
* )
|
|
619
|
+
* ```
|
|
620
|
+
*
|
|
621
|
+
* @since 1.0.0
|
|
622
|
+
* @category middleware
|
|
623
|
+
*/
|
|
624
|
+
export const securitySetCookie = (
|
|
625
|
+
self: HttpApiSecurity.ApiKey,
|
|
626
|
+
value: string | Redacted.Redacted,
|
|
627
|
+
options?: Cookie["options"]
|
|
628
|
+
): Effect.Effect<void> => {
|
|
629
|
+
const stringValue = typeof value === "string" ? value : Redacted.value(value)
|
|
630
|
+
return HttpApp.appendPreResponseHandler((_req, response) =>
|
|
631
|
+
Effect.orDie(
|
|
632
|
+
HttpServerResponse.setCookie(response, self.key, stringValue, {
|
|
633
|
+
secure: true,
|
|
634
|
+
httpOnly: true,
|
|
635
|
+
...options
|
|
636
|
+
})
|
|
637
|
+
)
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Make a middleware from an `HttpApiSecurity` instance, that can be used when
|
|
643
|
+
* constructing a `Handlers` group.
|
|
644
|
+
*
|
|
645
|
+
* @since 1.0.0
|
|
646
|
+
* @category middleware
|
|
647
|
+
* @example
|
|
648
|
+
* import { HttpApiBuilder, HttpApiSecurity } from "@effect/platform"
|
|
649
|
+
* import { Schema } from "@effect/schema"
|
|
650
|
+
* import { Context, Effect, Redacted } from "effect"
|
|
651
|
+
*
|
|
652
|
+
* class User extends Schema.Class<User>("User")({
|
|
653
|
+
* id: Schema.Number
|
|
654
|
+
* }) {}
|
|
655
|
+
*
|
|
656
|
+
* class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
|
|
657
|
+
*
|
|
658
|
+
* class Accounts extends Context.Tag("Accounts")<Accounts, {
|
|
659
|
+
* readonly findUserByAccessToken: (accessToken: string) => Effect.Effect<User>
|
|
660
|
+
* }>() {}
|
|
661
|
+
*
|
|
662
|
+
* const securityMiddleware = Effect.gen(function*() {
|
|
663
|
+
* const accounts = yield* Accounts
|
|
664
|
+
* return HttpApiBuilder.middlewareSecurity(
|
|
665
|
+
* HttpApiSecurity.bearer,
|
|
666
|
+
* CurrentUser,
|
|
667
|
+
* (token) => accounts.findUserByAccessToken(Redacted.value(token))
|
|
668
|
+
* )
|
|
669
|
+
* })
|
|
670
|
+
*/
|
|
671
|
+
export const middlewareSecurity = <Security extends HttpApiSecurity.HttpApiSecurity, I, S, EM, RM>(
|
|
672
|
+
self: Security,
|
|
673
|
+
tag: Context.Tag<I, S>,
|
|
674
|
+
f: (
|
|
675
|
+
credentials: HttpApiSecurity.HttpApiSecurity.Type<Security>
|
|
676
|
+
) => Effect.Effect<S, EM, RM>
|
|
677
|
+
): SecurityMiddleware<I, EM, RM> =>
|
|
678
|
+
middleware(Effect.provideServiceEffect(
|
|
679
|
+
tag,
|
|
680
|
+
Effect.flatMap(securityDecode(self), f)
|
|
681
|
+
)) as SecurityMiddleware<I, EM, RM>
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Make a middleware from an `HttpApiSecurity` instance, that can be used when
|
|
685
|
+
* constructing a `Handlers` group.
|
|
686
|
+
*
|
|
687
|
+
* This version does not supply any context to the handlers.
|
|
688
|
+
*
|
|
689
|
+
* @since 1.0.0
|
|
690
|
+
* @category middleware
|
|
691
|
+
*/
|
|
692
|
+
export const middlewareSecurityVoid = <Security extends HttpApiSecurity.HttpApiSecurity, X, EM, RM>(
|
|
693
|
+
self: Security,
|
|
694
|
+
f: (
|
|
695
|
+
credentials: HttpApiSecurity.HttpApiSecurity.Type<Security>
|
|
696
|
+
) => Effect.Effect<X, EM, RM>
|
|
697
|
+
): SecurityMiddleware<never, EM, RM> =>
|
|
698
|
+
middleware((httpApp) =>
|
|
699
|
+
securityDecode(self).pipe(
|
|
700
|
+
Effect.flatMap(f),
|
|
701
|
+
Effect.zipRight(httpApp)
|
|
702
|
+
)
|
|
703
|
+
) as SecurityMiddleware<never, EM, RM>
|
|
704
|
+
|
|
705
|
+
// internal
|
|
706
|
+
|
|
707
|
+
const requestPayload = (
|
|
708
|
+
request: HttpServerRequest.HttpServerRequest,
|
|
709
|
+
urlParams: ReadonlyRecord<string, string | Array<string>>,
|
|
710
|
+
isMultipart: boolean
|
|
711
|
+
): Effect.Effect<
|
|
712
|
+
unknown,
|
|
713
|
+
never,
|
|
714
|
+
| FileSystem
|
|
715
|
+
| Path
|
|
716
|
+
| Scope
|
|
717
|
+
> =>
|
|
718
|
+
HttpMethod.hasBody(request.method)
|
|
719
|
+
? isMultipart
|
|
720
|
+
? Effect.orDie(request.multipart)
|
|
721
|
+
: Effect.orDie(request.json)
|
|
722
|
+
: Effect.succeed(urlParams)
|
|
723
|
+
|
|
724
|
+
const handlerToRoute = (
|
|
725
|
+
endpoint: HttpApiEndpoint.HttpApiEndpoint.Any,
|
|
726
|
+
handler: HttpApiEndpoint.HttpApiEndpoint.Handler<any, any, any>,
|
|
727
|
+
isFullResponse: boolean
|
|
728
|
+
): HttpRouter.Route<any, any> => {
|
|
729
|
+
const decodePath = Option.map(endpoint.pathSchema, Schema.decodeUnknown)
|
|
730
|
+
const isMultipart = endpoint.payloadSchema.pipe(
|
|
731
|
+
Option.map((schema) => HttpApiSchema.getMultipart(schema.ast)),
|
|
732
|
+
Option.getOrElse(() => false)
|
|
733
|
+
)
|
|
734
|
+
const decodePayload = Option.map(endpoint.payloadSchema, Schema.decodeUnknown)
|
|
735
|
+
const decodeHeaders = Option.map(endpoint.headersSchema, Schema.decodeUnknown)
|
|
736
|
+
const encoding = HttpApiSchema.getEncoding(endpoint.successSchema.ast)
|
|
737
|
+
const successStatus = HttpApiSchema.getStatusSuccess(endpoint.successSchema)
|
|
738
|
+
const encodeSuccess = Option.map(HttpApiEndpoint.schemaSuccess(endpoint), (schema) => {
|
|
739
|
+
const encode = Schema.encodeUnknown(schema)
|
|
740
|
+
switch (encoding.kind) {
|
|
741
|
+
case "Json": {
|
|
742
|
+
return (body: unknown) =>
|
|
743
|
+
Effect.orDie(
|
|
744
|
+
Effect.flatMap(encode(body), (json) =>
|
|
745
|
+
HttpServerResponse.json(json, {
|
|
746
|
+
status: successStatus,
|
|
747
|
+
contentType: encoding.contentType
|
|
748
|
+
}))
|
|
749
|
+
)
|
|
750
|
+
}
|
|
751
|
+
case "Text": {
|
|
752
|
+
return (body: unknown) =>
|
|
753
|
+
Effect.map(Effect.orDie(encode(body)), (text) =>
|
|
754
|
+
HttpServerResponse.text(text as any, {
|
|
755
|
+
status: successStatus,
|
|
756
|
+
contentType: encoding.contentType
|
|
757
|
+
}))
|
|
758
|
+
}
|
|
759
|
+
case "Uint8Array": {
|
|
760
|
+
return (body: unknown) =>
|
|
761
|
+
Effect.map(Effect.orDie(encode(body)), (data) =>
|
|
762
|
+
HttpServerResponse.uint8Array(data as any, {
|
|
763
|
+
status: successStatus,
|
|
764
|
+
contentType: encoding.contentType
|
|
765
|
+
}))
|
|
766
|
+
}
|
|
767
|
+
case "UrlParams": {
|
|
768
|
+
return (body: unknown) =>
|
|
769
|
+
Effect.map(Effect.orDie(encode(body)), (params) =>
|
|
770
|
+
HttpServerResponse.urlParams(params as any, {
|
|
771
|
+
status: successStatus,
|
|
772
|
+
contentType: encoding.contentType
|
|
773
|
+
}))
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
})
|
|
777
|
+
return HttpRouter.makeRoute(
|
|
778
|
+
endpoint.method,
|
|
779
|
+
endpoint.path,
|
|
780
|
+
Effect.withFiberRuntime((fiber) => {
|
|
781
|
+
const context = fiber.getFiberRef(FiberRef.currentContext)
|
|
782
|
+
const request = Context.unsafeGet(context, HttpServerRequest.HttpServerRequest)
|
|
783
|
+
const routeContext = Context.unsafeGet(context, HttpRouter.RouteContext)
|
|
784
|
+
const urlParams = Context.unsafeGet(context, HttpServerRequest.ParsedSearchParams)
|
|
785
|
+
return (decodePath._tag === "Some"
|
|
786
|
+
? Effect.catchAll(decodePath.value(routeContext.params), HttpApiDecodeError.refailParseError)
|
|
787
|
+
: Effect.succeed(routeContext.params)).pipe(
|
|
788
|
+
Effect.bindTo("pathParams"),
|
|
789
|
+
decodePayload._tag === "Some"
|
|
790
|
+
? Effect.bind("payload", (_) =>
|
|
791
|
+
requestPayload(request, urlParams, isMultipart).pipe(
|
|
792
|
+
Effect.orDie,
|
|
793
|
+
Effect.flatMap((raw) => Effect.catchAll(decodePayload.value(raw), HttpApiDecodeError.refailParseError))
|
|
794
|
+
))
|
|
795
|
+
: identity,
|
|
796
|
+
decodeHeaders._tag === "Some"
|
|
797
|
+
? Effect.bind("headers", (_) => Effect.orDie(decodeHeaders.value(request.headers)))
|
|
798
|
+
: identity,
|
|
799
|
+
Effect.flatMap((input) => {
|
|
800
|
+
const request: any = { path: input.pathParams }
|
|
801
|
+
if ("payload" in input) {
|
|
802
|
+
request.payload = input.payload
|
|
803
|
+
}
|
|
804
|
+
if ("headers" in input) {
|
|
805
|
+
request.headers = input.headers
|
|
806
|
+
}
|
|
807
|
+
return handler(request)
|
|
808
|
+
}),
|
|
809
|
+
isFullResponse ?
|
|
810
|
+
identity as (_: any) => Effect.Effect<HttpServerResponse.HttpServerResponse> :
|
|
811
|
+
encodeSuccess._tag === "Some"
|
|
812
|
+
? Effect.flatMap(encodeSuccess.value)
|
|
813
|
+
: Effect.as(HttpServerResponse.empty({ status: successStatus }))
|
|
814
|
+
)
|
|
815
|
+
})
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const astCache = globalValue(
|
|
820
|
+
"@effect/platform/HttpApiBuilder/astCache",
|
|
821
|
+
() => new WeakMap<AST.AST, Schema.Schema.Any>()
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
const makeErrorSchema = (
|
|
825
|
+
api: HttpApi.HttpApi<HttpApiGroup.HttpApiGroup<string, HttpApiEndpoint.HttpApiEndpoint.Any>, any, any>
|
|
826
|
+
): Schema.Schema<unknown, HttpServerResponse.HttpServerResponse> => {
|
|
827
|
+
const schemas = new Set<Schema.Schema.Any>()
|
|
828
|
+
function processSchema(schema: Schema.Schema.Any): void {
|
|
829
|
+
if (astCache.has(schema.ast)) {
|
|
830
|
+
schemas.add(astCache.get(schema.ast)!)
|
|
831
|
+
return
|
|
832
|
+
}
|
|
833
|
+
const ast = schema.ast
|
|
834
|
+
if (ast._tag === "Union") {
|
|
835
|
+
for (const astType of ast.types) {
|
|
836
|
+
const errorSchema = Schema.make(astType).annotations({
|
|
837
|
+
...ast.annotations,
|
|
838
|
+
...astType.annotations
|
|
839
|
+
})
|
|
840
|
+
astCache.set(astType, errorSchema)
|
|
841
|
+
schemas.add(errorSchema)
|
|
842
|
+
}
|
|
843
|
+
} else {
|
|
844
|
+
astCache.set(ast, schema)
|
|
845
|
+
schemas.add(schema)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
processSchema(api.errorSchema)
|
|
849
|
+
for (const group of api.groups) {
|
|
850
|
+
for (const endpoint of group.endpoints) {
|
|
851
|
+
processSchema(endpoint.errorSchema)
|
|
852
|
+
}
|
|
853
|
+
processSchema(group.errorSchema)
|
|
854
|
+
}
|
|
855
|
+
return Schema.Union(...[...schemas].map((schema) => {
|
|
856
|
+
const status = HttpApiSchema.getStatusError(schema)
|
|
857
|
+
const encoded = AST.encodedAST(schema.ast)
|
|
858
|
+
const isEmpty = encoded._tag === "VoidKeyword"
|
|
859
|
+
return Schema.transformOrFail(Schema.Any, schema, {
|
|
860
|
+
decode: (_, __, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, _, "Encode only schema")),
|
|
861
|
+
encode: (error, _, ast) =>
|
|
862
|
+
isEmpty ?
|
|
863
|
+
HttpServerResponse.empty({ status }) :
|
|
864
|
+
HttpServerResponse.json(error, { status }).pipe(
|
|
865
|
+
Effect.mapError((error) => new ParseResult.Type(ast, error, "Could not encode to JSON"))
|
|
866
|
+
)
|
|
867
|
+
})
|
|
868
|
+
})) as any
|
|
869
|
+
}
|