Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/slimy-points-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@effect/platform": minor
---

Add `requires` support to `HttpApiMiddleware.Tag` to allow middlewares to depend on other middleware outputs.

Details

- New option: `requires?: Context.Tag | ReadonlyArray<Context.Tag>` when defining a middleware tag.

Example

```
export class AdminUser extends HttpApiMiddleware.Tag<AdminUser>()("Http/Admin", {
failure: HttpApiError.Forbidden,
requires: CurrentUser // or: [CurrentUser, Session]
}) {}

// Inside the middleware implementation you can now safely use the required services
yield* CurrentUser
```
39 changes: 28 additions & 11 deletions packages/platform/src/HttpApiBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,23 +708,40 @@ const applyMiddleware = <A extends Effect.Effect<any, any, any>>(
middleware: MiddlewareMap,
handler: A
) => {
// Build a wrapper that threads provided services to both subsequent middleware effects and the final handler
let wrap = <X extends Effect.Effect<any, any, any>>(inner: X): X => inner
for (const entry of middleware.values()) {
const effect = HttpApiMiddleware.SecurityTypeId in entry.tag ? makeSecurityMiddleware(entry as any) : entry.effect
const prevWrap = wrap
if (entry.tag.optional) {
const previous = handler
handler = Effect.matchEffect(effect, {
onFailure: () => previous,
onSuccess: entry.tag.provides !== undefined
? (value) => Effect.provideService(previous, entry.tag.provides as any, value)
: (_) => previous
}) as any
if (entry.tag.provides !== undefined) {
wrap = (inner: any) => {
const innerWrapped = prevWrap(inner)
const effectWrapped = prevWrap(effect)
return Effect.matchEffect(effectWrapped, {
onFailure: () => innerWrapped,
onSuccess: (value) => Effect.provideService(innerWrapped, entry.tag.provides as any, value)
}) as any
}
} else {
wrap = (inner: any) => {
const innerWrapped = prevWrap(inner)
const effectWrapped = prevWrap(effect)
return Effect.matchEffect(effectWrapped, {
onFailure: () => innerWrapped,
onSuccess: () => innerWrapped
}) as any
}
}
} else {
handler = entry.tag.provides !== undefined
? Effect.provideServiceEffect(handler, entry.tag.provides as any, effect) as any
: Effect.zipRight(effect, handler) as any
if (entry.tag.provides !== undefined) {
wrap = (inner: any) => Effect.provideServiceEffect(prevWrap(inner), entry.tag.provides as any, prevWrap(effect)) as any
} else {
wrap = (inner: any) => Effect.zipRight(prevWrap(effect), prevWrap(inner)) as any
}
}
}
return handler
return wrap(handler)
}

const securityMiddlewareCache = globalValue<WeakMap<any, Effect.Effect<any, any, any>>>(
Expand Down
44 changes: 39 additions & 5 deletions packages/platform/src/HttpApiMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,22 @@ export const isSecurity = (u: TagClassAny): u is TagClassSecurityAny => hasPrope
* @since 1.0.0
* @category models
*/
export interface HttpApiMiddleware<Provides, E> extends Effect.Effect<Provides, E, HttpRouter.HttpRouter.Provided> {}
export interface HttpApiMiddleware<Provides, E, R = never>
extends Effect.Effect<Provides, E, HttpRouter.HttpRouter.Provided | R> {}

/**
* @since 1.0.0
* @category models
*/
export type HttpApiMiddlewareSecurity<Security extends Record<string, HttpApiSecurity.HttpApiSecurity>, Provides, E> = {
export type HttpApiMiddlewareSecurity<
Security extends Record<string, HttpApiSecurity.HttpApiSecurity>,
Provides,
E,
R = never
> = {
readonly [K in keyof Security]: (
_: HttpApiSecurity.HttpApiSecurity.Type<Security[K]>
) => Effect.Effect<Provides, E, HttpRouter.HttpRouter.Provided>
) => Effect.Effect<Provides, E, HttpRouter.HttpRouter.Provided | R>
}

/**
Expand Down Expand Up @@ -102,6 +108,12 @@ export declare namespace HttpApiMiddleware {
*/
export type ErrorContext<A> = A extends { readonly [TypeId]: { readonly failureContext: infer R } } ? R : never

/**
* @since 1.0.0
* @category models
*/
export type Requires<A> = A extends { readonly [TypeId]: { readonly requires: infer Req } } ? Req : never

/**
* @since 1.0.0
* @category models
Expand Down Expand Up @@ -131,7 +143,8 @@ export type TagClass<
HttpApiMiddlewareSecurity<
Options["security"],
TagClass.Service<Options>,
TagClass.FailureService<Options>
TagClass.FailureService<Options>,
TagClass.Requires<Options>
>
>,
Options["security"]
Expand All @@ -142,7 +155,8 @@ export type TagClass<
Options,
HttpApiMiddleware<
TagClass.Service<Options>,
TagClass.FailureService<Options>
TagClass.FailureService<Options>,
TagClass.Requires<Options>
>
>

Expand All @@ -169,6 +183,16 @@ export declare namespace TagClass {
? Context.Tag.Service<Options["provides"]>
: void

/**
* @since 1.0.0
* @category models
*/
export type Requires<Options> = Options extends { readonly requires: Context.Tag<any, any> }
? Context.Tag.Identifier<Options["requires"]>
: Options extends { readonly requires: ReadonlyArray<Context.Tag<any, any>> }
? Context.Tag.Identifier<Options["requires"][number]>
: never

/**
* @since 1.0.0
* @category models
Expand Down Expand Up @@ -213,6 +237,7 @@ export declare namespace TagClass {
& {
readonly [TypeId]: {
readonly provides: Provides<Options>
readonly requires: Requires<Options>
readonly failure: Failure<Options>
readonly failureContext: FailureContext<Options>
}
Expand All @@ -222,6 +247,9 @@ export declare namespace TagClass {
readonly failure: FailureSchema<Options>
readonly provides: Options extends { readonly provides: Context.Tag<any, any> } ? Options["provides"]
: undefined
readonly requires: Options extends { readonly requires: Context.Tag<any, any> | ReadonlyArray<Context.Tag<any, any>> }
? Options["requires"]
: undefined
}

/**
Expand All @@ -248,6 +276,7 @@ export interface TagClassAny extends Context.Tag<any, HttpApiMiddleware.Any> {
readonly [TypeId]: TypeId
readonly optional: boolean
readonly provides?: Context.Tag<any, any>
readonly requires?: ReadonlyArray<Context.Tag<any, any>>
readonly failure: Schema.Schema.All
}

Expand All @@ -270,6 +299,7 @@ export const Tag = <Self>(): <
readonly optional?: boolean
readonly failure?: Schema.Schema.All
readonly provides?: Context.Tag<any, any>
readonly requires?: Context.Tag<any, any> | ReadonlyArray<Context.Tag<any, any>>
readonly security?: Record<string, HttpApiSecurity.HttpApiSecurity>
}
>(
Expand All @@ -283,6 +313,7 @@ export const Tag = <Self>(): <
readonly security?: Record<string, HttpApiSecurity.HttpApiSecurity>
readonly failure?: Schema.Schema.All
readonly provides?: Context.Tag<any, any>
readonly requires?: Context.Tag<any, any> | ReadonlyArray<Context.Tag<any, any>>
}
) => {
const Err = globalThis.Error as any
Expand All @@ -305,6 +336,9 @@ export const Tag = <Self>(): <
if (options?.provides) {
TagClass_.provides = options.provides
}
if (options?.requires) {
TagClass_.requires = Array.isArray(options.requires) ? options.requires : [options.requires]
}
TagClass_.optional = options?.optional ?? false
if (options?.security) {
if (Object.keys(options.security).length === 0) {
Expand Down