Spraxium logoSpraxium

Opciones y guards integrados

Haz los guards configurables con GuardOption y el helper withOptions para parámetros tipados. Explora los diez guards integrados disponibles en el paquete core y cómo usar cada uno.

Hacer configurables los guards con @GuardOption

Algunos guards necesitan parámetros para funcionar. Un role guard necesita saber qué roles revisar, un cooldown guard necesita la ventana de tiempo y un permission guard necesita qué permisos exigir. En lugar de hardcodear estos valores dentro de la clase guard, declaras propiedades configurables con el decorador @GuardOption y pasas valores cuando adjuntas el guard.

El decorador @GuardOption acepta un objeto de configuración con dos campos opcionales. El campo required vuelve obligatoria la opción, lo que significa que el framework lanza un error de configuración en runtime si no se proporciona. El campo default define un valor de respaldo que se aplica cuando no se pasa uno explícitamente. Si no defines ni required ni default, la opción simplemente es opcional y será undefined si no se proporciona.

min-roles.guard.ts
import { Guard, GuardOption } from '@spraxium/common';
import type { ExecutionContext, SpraxiumGuard } from '@spraxium/common';

@Guard()
export class MinRolesGuard implements SpraxiumGuard {
  @GuardOption({ default: 1 })
  minRoles!: number;

  canActivate(ctx: ExecutionContext): boolean {
    const member = ctx.getMember();
    if (!member || !('roles' in member)) return false;
    return member.roles.cache.size >= this.minRoles;
  }
}

En el ejemplo anterior, la propiedad minRoles tiene valor por defecto 1, así que si adjuntas este guard sin especificar valor, exigirá al menos un rol. Si quieres un umbral distinto, lo pasas mediante withOptions al adjuntar el guard.

El helper withOptions

La función withOptions es la forma de pasar opciones tipadas a un guard al adjuntarlo con @UseGuards. Recibe la clase guard como primer argumento y un objeto de opciones como segundo. El sistema de tipos de TypeScript infiere la forma de las opciones a partir de las propiedades @GuardOption del guard, así que obtienes autocompletado y errores de tipo si pasas una forma incorrecta.

moderation.handler.ts
import { UseGuards, withOptions } from '@spraxium/common';
import { PermissionGuard, RoleGuard } from '@spraxium/core';
import { MinRolesGuard } from '../guards/min-roles.guard';

@UseGuards(
  withOptions(RoleGuard, { roles: ['Moderator'] }),
  withOptions(PermissionGuard, { permissions: ['BanMembers'] }),
  withOptions(MinRolesGuard, { minRoles: 3 }),
)
export class ModerationHandler {
  async handle() {
    // all three guards must pass before this runs
  }
}

Si no necesitas pasar opciones porque el guard no tiene opciones obligatorias o sus valores por defecto son suficientes, puedes pasar la clase guard directamente sin withOptions. Ambos estilos funcionan dentro de la misma llamada a @UseGuards, así que puedes mezclar guards que requieren opciones con guards que no.

mixed-guards.handler.ts
import { UseGuards, withOptions } from '@spraxium/common';
import { GuildOnly, RoleGuard } from '@spraxium/core';

@UseGuards(GuildOnly, withOptions(RoleGuard, { roles: ['Admin'] }))
export class MixedHandler {
  async handle() {
    // GuildOnly needs no options, RoleGuard needs roles
  }
}

Guards integrados

Spraxium incluye diez guards integrados en el paquete core que cubren los patrones de acceso más comunes en Discord. Cada guard ya está decorado con @Guard() y listo para usar con @UseGuards o useGlobalGuards. El bloque siguiente muestra cada guard integrado, qué comprueba y qué opciones espera.

Uso de guards integrados

Los guards integrados sin opciones pueden pasarse directamente a @UseGuards o useGlobalGuards. Los guards con opciones obligatorias deben usar withOptions o el segundo parámetro de useGlobalGuards. Aquí tienes ejemplos prácticos con los patrones más comunes.

import { SpraxiumFactory } from '@spraxium/core';
import { NoBotGuard, OwnerOnly, CooldownGuard } from '@spraxium/core';

const app = await SpraxiumFactory.create({ token: process.env.DISCORD_TOKEN! });

app.useGlobalGuards(NoBotGuard);
app.useGlobalGuards(OwnerOnly, { ownerIds: ['123456789'] });
app.useGlobalGuards(CooldownGuard, { cooldown: 3000 });

await app.listen();

El RoleGuard comprueba si el miembro tiene al menos uno de los roles listados. Puedes pasar nombres de rol como "Moderator" o IDs snowflake de rol como "1234567890". El PermissionGuard comprueba si el miembro tiene todos los permisos listados, usando cadenas de permisos de Discord.js como "BanMembers", "ManageGuild" o "Administrator".

Crear un guard personalizado con opciones

Cuando los guards integrados no cubren tu caso de uso, creas un guard personalizado que declara propiedades configurables mediante @GuardOption. La clase guard define qué parámetros necesita, y cada lugar donde se usa aporta valores concretos mediante withOptions. Este patrón es cómo funcionan internamente guards integrados como RoleGuard y CooldownGuard, y sigues exactamente el mismo enfoque para tus propios guards.

Define la clase guard con @Guard

Empieza con una clase decorada con @Guard() que implemente SpraxiumGuard. El método canActivate contiene la lógica de comprobación, y cualquier parámetro configurable recibe el decorador @GuardOption en su propiedad. Marca opciones como required cuando el guard no puede funcionar sin ellas, o define un default cuando exista un valor de respaldo razonable. El framework lee estos decoradores en el momento de instanciación y rellena los valores antes de que canActivate llegue a ejecutarse.

Pasa opciones con withOptions en cada punto de uso

Al adjuntar el guard con @UseGuards, envuélvelo en withOptions y pasa los valores concretos que espera tu guard. TypeScript infiere la forma de las opciones a partir de las propiedades @GuardOption, así que obtienes autocompletado y errores en compilación si pasas un tipo incorrecto. Si olvidas una opción required, el framework lanza un error de configuración claro durante el bootstrap.

Reutiliza el mismo guard con valores diferentes

El poder de los guards configurables es que una sola clase sirve para muchos escenarios. Un MinAccountAgeGuard con minDays: 7 en un comando general y minDays: 90 en un comando de giveaway usa la misma clase con umbrales distintos. No necesitas clases guard separadas para cada umbral.

Aquí tienes un ejemplo completo de un guard personalizado que comprueba una edad mínima de cuenta y soporta múltiples opciones configurables. El guard tiene una opción obligatoria y otra opcional con valor por defecto.

import { Guard, GuardOption } from '@spraxium/common';
import type { ExecutionContext, SpraxiumGuard } from '@spraxium/common';

@Guard()
export class MinAccountAgeGuard implements SpraxiumGuard {
  @GuardOption({ required: true })
  minDays!: number;

  @GuardOption({ default: false })
  allowBots!: boolean;

  canActivate(ctx: ExecutionContext): boolean {
    const user = ctx.getUser();
    if (user.bot && !this.allowBots) return false;

    const createdAt = user.createdAt.getTime();
    const ageInDays = (Date.now() - createdAt) / (1000 * 60 * 60 * 24);
    return ageInDays >= this.minDays;
  }
}

Observa que el guard tiene dos opciones: minDays es obligatoria porque el guard no puede funcionar sin conocer el umbral, y allowBots tiene valor por defecto false porque la mayoría de los puntos de uso quieren bloquear bots. La tercera pestaña muestra cómo la misma clase guard acepta una configuración completamente distinta en otro punto de uso sin duplicar código.

Empieza por los integrados

Antes de crear un guard personalizado, revisa si uno de los diez guards integrados ya cubre tu caso de uso. Los guards personalizados son potentes, pero innecesarios cuando un integrado ya hace exactamente lo que necesitas.