@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.
Files changed (123) hide show
  1. package/HttpApi/package.json +6 -0
  2. package/HttpApiBuilder/package.json +6 -0
  3. package/HttpApiClient/package.json +6 -0
  4. package/HttpApiEndpoint/package.json +6 -0
  5. package/HttpApiError/package.json +6 -0
  6. package/HttpApiGroup/package.json +6 -0
  7. package/HttpApiSchema/package.json +6 -0
  8. package/HttpApiSecurity/package.json +6 -0
  9. package/HttpApiSwagger/package.json +6 -0
  10. package/OpenApi/package.json +6 -0
  11. package/README.md +863 -302
  12. package/dist/cjs/HttpApi.js +168 -0
  13. package/dist/cjs/HttpApi.js.map +1 -0
  14. package/dist/cjs/HttpApiBuilder.js +471 -0
  15. package/dist/cjs/HttpApiBuilder.js.map +1 -0
  16. package/dist/cjs/HttpApiClient.js +134 -0
  17. package/dist/cjs/HttpApiClient.js.map +1 -0
  18. package/dist/cjs/HttpApiEndpoint.js +181 -0
  19. package/dist/cjs/HttpApiEndpoint.js.map +1 -0
  20. package/dist/cjs/HttpApiError.js +69 -0
  21. package/dist/cjs/HttpApiError.js.map +1 -0
  22. package/dist/cjs/HttpApiGroup.js +151 -0
  23. package/dist/cjs/HttpApiGroup.js.map +1 -0
  24. package/dist/cjs/HttpApiSchema.js +241 -0
  25. package/dist/cjs/HttpApiSchema.js.map +1 -0
  26. package/dist/cjs/HttpApiSecurity.js +83 -0
  27. package/dist/cjs/HttpApiSecurity.js.map +1 -0
  28. package/dist/cjs/HttpApiSwagger.js +50 -0
  29. package/dist/cjs/HttpApiSwagger.js.map +1 -0
  30. package/dist/cjs/HttpMethod.js +1 -1
  31. package/dist/cjs/HttpMethod.js.map +1 -1
  32. package/dist/cjs/HttpRouter.js +6 -1
  33. package/dist/cjs/HttpRouter.js.map +1 -1
  34. package/dist/cjs/OpenApi.js +317 -0
  35. package/dist/cjs/OpenApi.js.map +1 -0
  36. package/dist/cjs/index.js +21 -1
  37. package/dist/cjs/internal/apiSwagger.js +2 -0
  38. package/dist/cjs/internal/apiSwagger.js.map +1 -0
  39. package/dist/cjs/internal/httpRouter.js +6 -1
  40. package/dist/cjs/internal/httpRouter.js.map +1 -1
  41. package/dist/cjs/internal/multipart.js +5 -1
  42. package/dist/cjs/internal/multipart.js.map +1 -1
  43. package/dist/dts/HttpApi.d.ts +156 -0
  44. package/dist/dts/HttpApi.d.ts.map +1 -0
  45. package/dist/dts/HttpApiBuilder.d.ts +296 -0
  46. package/dist/dts/HttpApiBuilder.d.ts.map +1 -0
  47. package/dist/dts/HttpApiClient.d.ts +31 -0
  48. package/dist/dts/HttpApiClient.d.ts.map +1 -0
  49. package/dist/dts/HttpApiEndpoint.d.ts +350 -0
  50. package/dist/dts/HttpApiEndpoint.d.ts.map +1 -0
  51. package/dist/dts/HttpApiError.d.ts +70 -0
  52. package/dist/dts/HttpApiError.d.ts.map +1 -0
  53. package/dist/dts/HttpApiGroup.d.ts +196 -0
  54. package/dist/dts/HttpApiGroup.d.ts.map +1 -0
  55. package/dist/dts/HttpApiSchema.d.ts +223 -0
  56. package/dist/dts/HttpApiSchema.d.ts.map +1 -0
  57. package/dist/dts/HttpApiSecurity.d.ts +122 -0
  58. package/dist/dts/HttpApiSecurity.d.ts.map +1 -0
  59. package/dist/dts/HttpApiSwagger.d.ts +10 -0
  60. package/dist/dts/HttpApiSwagger.d.ts.map +1 -0
  61. package/dist/dts/HttpMethod.d.ts +16 -0
  62. package/dist/dts/HttpMethod.d.ts.map +1 -1
  63. package/dist/dts/HttpRouter.d.ts +8 -0
  64. package/dist/dts/HttpRouter.d.ts.map +1 -1
  65. package/dist/dts/HttpServerResponse.d.ts +3 -3
  66. package/dist/dts/HttpServerResponse.d.ts.map +1 -1
  67. package/dist/dts/OpenApi.d.ts +348 -0
  68. package/dist/dts/OpenApi.d.ts.map +1 -0
  69. package/dist/dts/index.d.ts +40 -0
  70. package/dist/dts/index.d.ts.map +1 -1
  71. package/dist/dts/internal/apiSwagger.d.ts +2 -0
  72. package/dist/dts/internal/apiSwagger.d.ts.map +1 -0
  73. package/dist/dts/internal/httpRouter.d.ts.map +1 -1
  74. package/dist/esm/HttpApi.js +157 -0
  75. package/dist/esm/HttpApi.js.map +1 -0
  76. package/dist/esm/HttpApiBuilder.js +447 -0
  77. package/dist/esm/HttpApiBuilder.js.map +1 -0
  78. package/dist/esm/HttpApiClient.js +124 -0
  79. package/dist/esm/HttpApiClient.js.map +1 -0
  80. package/dist/esm/HttpApiEndpoint.js +169 -0
  81. package/dist/esm/HttpApiEndpoint.js.map +1 -0
  82. package/dist/esm/HttpApiError.js +59 -0
  83. package/dist/esm/HttpApiError.js.map +1 -0
  84. package/dist/esm/HttpApiGroup.js +140 -0
  85. package/dist/esm/HttpApiGroup.js.map +1 -0
  86. package/dist/esm/HttpApiSchema.js +217 -0
  87. package/dist/esm/HttpApiSchema.js.map +1 -0
  88. package/dist/esm/HttpApiSecurity.js +73 -0
  89. package/dist/esm/HttpApiSecurity.js.map +1 -0
  90. package/dist/esm/HttpApiSwagger.js +40 -0
  91. package/dist/esm/HttpApiSwagger.js.map +1 -0
  92. package/dist/esm/HttpMethod.js +1 -1
  93. package/dist/esm/HttpMethod.js.map +1 -1
  94. package/dist/esm/HttpRouter.js +5 -0
  95. package/dist/esm/HttpRouter.js.map +1 -1
  96. package/dist/esm/OpenApi.js +299 -0
  97. package/dist/esm/OpenApi.js.map +1 -0
  98. package/dist/esm/index.js +40 -0
  99. package/dist/esm/index.js.map +1 -1
  100. package/dist/esm/internal/apiSwagger.js +2 -0
  101. package/dist/esm/internal/apiSwagger.js.map +1 -0
  102. package/dist/esm/internal/httpRouter.js +5 -0
  103. package/dist/esm/internal/httpRouter.js.map +1 -1
  104. package/dist/esm/internal/multipart.js +5 -1
  105. package/dist/esm/internal/multipart.js.map +1 -1
  106. package/package.json +83 -3
  107. package/src/HttpApi.ts +342 -0
  108. package/src/HttpApiBuilder.ts +869 -0
  109. package/src/HttpApiClient.ts +228 -0
  110. package/src/HttpApiEndpoint.ts +818 -0
  111. package/src/HttpApiError.ts +113 -0
  112. package/src/HttpApiGroup.ts +365 -0
  113. package/src/HttpApiSchema.ts +384 -0
  114. package/src/HttpApiSecurity.ts +169 -0
  115. package/src/HttpApiSwagger.ts +46 -0
  116. package/src/HttpMethod.ts +19 -1
  117. package/src/HttpRouter.ts +9 -0
  118. package/src/HttpServerResponse.ts +3 -3
  119. package/src/OpenApi.ts +665 -0
  120. package/src/index.ts +50 -0
  121. package/src/internal/apiSwagger.ts +7 -0
  122. package/src/internal/httpRouter.ts +9 -0
  123. 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
+ }