Collectors

A collector resolves a handler parameter by its type from the invocation context. Operations declaring that parameter receive the value automatically.

import { Collector } from '@fougere/core';
import type { InvocationContext } from '@fougere/core';
import User from '../../user/entities/User.js';

/**
 * The auth middleware puts the session user on ctx.state.user —
 * this surfaces it to any handler that declares `user?: User`.
 */
export default class CurrentUserCollector extends Collector(User) {
  async collect(ctx: InvocationContext) {
    return ctx.state.user as User | undefined;
  }
}

From then on, in any handler of the Frond:

async mine(user?: User): Promise<Post[]> {}
async publish(id: string, user?: User): Promise<Post> {}

Resolution rules

  • The match is by checked type: Collector(User) resolves user?: User, user: User | undefined, and aliases with the same meaning. The class is registered as UserCollector by convention.
  • The target needs no schema. Nothing reads its fields, so an ordinary class works — Collector(Ability) for a value you build from state at every call.
  • Absence is undefined. Prefer the concise user?: User. The type checker also resolves an alias such as type CurrentUser = User | undefined.
  • Collectors are DI-injectable classes — declare constructor dependencies as usual.

State and the gradient

ctx.state is the request state built by the consuming application, for example from the session. During a remote call, this state is sent with the invocation, so the collector receives the same ctx.state.user as it would locally.

A collector does not cross a Frond boundary

Keep the collector in each Frond that consumes it. What happens otherwise deserves to be said precisely, because it is neither "later" nor "nothing":

  • it is not at the split. The binding is decided at boot, in a single process, from the collectors of that Frond itself;
  • the parameter is not empty. A type the Frond cannot resolve falls into the fourth binding rule, body. So the handler receives the request body where it expects a User.

Put plainly: user?: User declared in a Frond that has no UserCollector receives whatever the client sent. A handler judging on user.role judges a caller-supplied value.

An earlier version of this page said the collector was "lost after a process split". That was wrong twice, and the truth is less comfortable.

Next: Errors — typed errors across layers.

Built with Fougere — this site runs on the framework it documents.