diff --git a/.changeset/slimy-points-relate.md b/.changeset/slimy-points-relate.md new file mode 100644 index 00000000000..e26394abc79 --- /dev/null +++ b/.changeset/slimy-points-relate.md @@ -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` when defining a middleware tag. + +Example + +``` +export class AdminUser extends HttpApiMiddleware.Tag()("Http/Admin", { + failure: HttpApiError.Forbidden, + requires: CurrentUser // or: [CurrentUser, Session] +}) {} + +// Inside the middleware implementation you can now safely use the required services +yield* CurrentUser +``` diff --git a/packages/platform/src/HttpApiBuilder.ts b/packages/platform/src/HttpApiBuilder.ts index f00b7e931c2..451e95c70e9 100644 --- a/packages/platform/src/HttpApiBuilder.ts +++ b/packages/platform/src/HttpApiBuilder.ts @@ -708,23 +708,40 @@ const applyMiddleware = >( middleware: MiddlewareMap, handler: A ) => { + // Build a wrapper that threads provided services to both subsequent middleware effects and the final handler + let wrap = >(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>>( diff --git a/packages/platform/src/HttpApiMiddleware.ts b/packages/platform/src/HttpApiMiddleware.ts index 4804f41dd5c..0c0a5d41337 100644 --- a/packages/platform/src/HttpApiMiddleware.ts +++ b/packages/platform/src/HttpApiMiddleware.ts @@ -43,16 +43,22 @@ export const isSecurity = (u: TagClassAny): u is TagClassSecurityAny => hasPrope * @since 1.0.0 * @category models */ -export interface HttpApiMiddleware extends Effect.Effect {} +export interface HttpApiMiddleware + extends Effect.Effect {} /** * @since 1.0.0 * @category models */ -export type HttpApiMiddlewareSecurity, Provides, E> = { +export type HttpApiMiddlewareSecurity< + Security extends Record, + Provides, + E, + R = never +> = { readonly [K in keyof Security]: ( _: HttpApiSecurity.HttpApiSecurity.Type - ) => Effect.Effect + ) => Effect.Effect } /** @@ -102,6 +108,12 @@ export declare namespace HttpApiMiddleware { */ export type ErrorContext = A extends { readonly [TypeId]: { readonly failureContext: infer R } } ? R : never + /** + * @since 1.0.0 + * @category models + */ + export type Requires = A extends { readonly [TypeId]: { readonly requires: infer Req } } ? Req : never + /** * @since 1.0.0 * @category models @@ -131,7 +143,8 @@ export type TagClass< HttpApiMiddlewareSecurity< Options["security"], TagClass.Service, - TagClass.FailureService + TagClass.FailureService, + TagClass.Requires > >, Options["security"] @@ -142,7 +155,8 @@ export type TagClass< Options, HttpApiMiddleware< TagClass.Service, - TagClass.FailureService + TagClass.FailureService, + TagClass.Requires > > @@ -169,6 +183,16 @@ export declare namespace TagClass { ? Context.Tag.Service : void + /** + * @since 1.0.0 + * @category models + */ + export type Requires = Options extends { readonly requires: Context.Tag } + ? Context.Tag.Identifier + : Options extends { readonly requires: ReadonlyArray> } + ? Context.Tag.Identifier + : never + /** * @since 1.0.0 * @category models @@ -213,6 +237,7 @@ export declare namespace TagClass { & { readonly [TypeId]: { readonly provides: Provides + readonly requires: Requires readonly failure: Failure readonly failureContext: FailureContext } @@ -222,6 +247,9 @@ export declare namespace TagClass { readonly failure: FailureSchema readonly provides: Options extends { readonly provides: Context.Tag } ? Options["provides"] : undefined + readonly requires: Options extends { readonly requires: Context.Tag | ReadonlyArray> } + ? Options["requires"] + : undefined } /** @@ -248,6 +276,7 @@ export interface TagClassAny extends Context.Tag { readonly [TypeId]: TypeId readonly optional: boolean readonly provides?: Context.Tag + readonly requires?: ReadonlyArray> readonly failure: Schema.Schema.All } @@ -270,6 +299,7 @@ export const Tag = (): < readonly optional?: boolean readonly failure?: Schema.Schema.All readonly provides?: Context.Tag + readonly requires?: Context.Tag | ReadonlyArray> readonly security?: Record } >( @@ -283,6 +313,7 @@ export const Tag = (): < readonly security?: Record readonly failure?: Schema.Schema.All readonly provides?: Context.Tag + readonly requires?: Context.Tag | ReadonlyArray> } ) => { const Err = globalThis.Error as any @@ -305,6 +336,9 @@ export const Tag = (): < 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) {