Spraxium logoSpraxium

Prefixes API via Services

Manage guild prefixes at runtime by injecting PrefixDispatcher into services and lifecycle hooks. Learn each API method with isolated examples for read, write, remove, and reload operations.

Why keep runtime logic in services

PrefixConfig handles global policy and boot hydration, but domain rules for reading, writing, and auditing guild prefixes belong in dedicated services. Mixing runtime logic into config creates a maintenance bottleneck that grows harder to test as the system scales. Keeping them separate also makes it straightforward to swap storage backends or add audit trails without touching the configuration layer.

The entry point for all runtime prefix operations is PrefixDispatcher, which exposes guildPrefixes as a PrefixGuildManager instance. Injecting PrefixDispatcher into a service gives you full control over per-guild state without any direct dependency on config internals. Everything below operates on that single injection point.

  • PrefixConfig handles global prefix policy, feature flags, and the boot-time provider. Keep queries, authorization logic, and runtime mutations out of config.
  • Services own per-guild CRUD operations, persistence coordination, and audit logic. Avoid re-implementing bootstrap or parser behavior inside a service.
  • Repositories read and write prefix state in storage. Keep Discord response logic and handler orchestration separate from the repository layer.

getGuildPrefix

getGuildPrefix returns the array of active prefix strings for a given guild, or undefined if no override exists. This is the primary read method you use before displaying the current prefix to a user or building a help embed. Because the result comes from in-memory state, the call is synchronous and has no I/O cost.

Use this in read-only service methods and embed builders, not directly in handlers. Encapsulating the read in a service method lets you add fallback logic, caching wrappers, or telemetry without changing call sites. The pattern below returns an empty array rather than undefined to simplify downstream consumers.

src/modules/prefix/services/guild-prefix.service.ts
import { Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';

@Injectable()
export class GuildPrefixService {
  constructor(private readonly dispatcher: PrefixDispatcher) {}

  getForGuild(guildId: string): string[] {
    return this.dispatcher.guildPrefixes.getGuildPrefix(guildId) ?? [];
  }
}

hasGuildPrefix

hasGuildPrefix returns a boolean indicating whether a guild has an active prefix override in memory. Use this method when you only need to branch on existence, not inspect the prefix value itself. It is cheaper than calling getGuildPrefix and checking for undefined because the intent is explicit and can be optimized independently.

A common use case is in guards that should apply different behavior for custom-prefix guilds versus guilds using the global default. The guard can inject the service, call hasGuildPrefix, and short-circuit accordingly. Avoid using this method as a workaround for skipping normal CRUD operations.

src/modules/prefix/guards/custom-prefix.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';

@Injectable()
export class CustomPrefixGuard implements CanActivate {
  constructor(private readonly dispatcher: PrefixDispatcher) {}

  canActivate(context: ExecutionContext): boolean {
    const guildId = context.getMessage().guildId;
    if (!guildId) return false;
    return this.dispatcher.guildPrefixes.hasGuildPrefix(guildId);
  }
}

setGuildPrefix

setGuildPrefix creates or fully replaces the prefix override for a guild. The third argument accepts an optional metadata object for storing audit fields such as the actor ID and timestamp. Call this when a user or administrator updates the prefix through a command or API endpoint.

Always persist the new value to your database before calling setGuildPrefix in memory. If the database write fails after the memory mutation, the in-memory state diverges from durable state and synchronization is lost until the next reload. The persist-first pattern below keeps both layers consistent under partial failures.

src/modules/prefix/services/guild-prefix.service.ts
import { Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
import { GuildPrefixRepository } from '../repositories/guild-prefix.repository';

@Injectable()
export class GuildPrefixService {
  constructor(
    private readonly dispatcher: PrefixDispatcher,
    private readonly repository: GuildPrefixRepository,
  ) {}

  async setForGuild(
    guildId: string,
    prefix: string,
    actorId: string,
  ): Promise<void> {
    await this.repository.upsert(guildId, [prefix], actorId);

    this.dispatcher.guildPrefixes.setGuildPrefix(guildId, prefix, {
      updatedBy: actorId,
      updatedAt: new Date().toISOString(),
    });
  }
}

updateGuildPrefix

updateGuildPrefix patches the existing entry with a partial merge instead of a full replacement. Use it when you want to change only the prefix value and keep existing metadata fields intact, or when you want to update metadata without changing the prefix. It behaves identically to setGuildPrefix if no entry exists for the guild.

This method is useful for scenarios like an admin panel that pushes field-level updates rather than resubmitting the entire record. If your storage layer also supports partial updates, pair both calls to keep the database and in-memory state consistent with the same semantics.

src/modules/prefix/services/guild-prefix.service.ts
import { Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
import { GuildPrefixRepository } from '../repositories/guild-prefix.repository';

@Injectable()
export class GuildPrefixService {
  constructor(
    private readonly dispatcher: PrefixDispatcher,
    private readonly repository: GuildPrefixRepository,
  ) {}

  async updateForGuild(
    guildId: string,
    prefix: string,
    actorId: string,
  ): Promise<void> {
    await this.repository.patch(guildId, {
      prefixes: [prefix],
      updatedBy: actorId,
    });

    this.dispatcher.guildPrefixes.updateGuildPrefix(guildId, prefix, {
      updatedBy: actorId,
      updatedAt: new Date().toISOString(),
    });
  }
}

removeGuildPrefix

removeGuildPrefix deletes the in-memory override for a guild and returns true if an entry existed or false if the guild had no custom prefix. After removal, the parser falls back to the global prefix defined in PrefixConfig for that guild. Use this when a guild administrator resets their server to default behavior.

Follow the persist-first pattern here as well: remove from the database before calling removeGuildPrefix in memory. This ensures that a process restart will not re-hydrate the deleted entry from the provider. The example below returns a boolean to the caller so the handler can confirm whether a deletion actually happened.

src/modules/prefix/services/guild-prefix.service.ts
import { Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
import { GuildPrefixRepository } from '../repositories/guild-prefix.repository';

@Injectable()
export class GuildPrefixService {
  constructor(
    private readonly dispatcher: PrefixDispatcher,
    private readonly repository: GuildPrefixRepository,
  ) {}

  async removeForGuild(guildId: string): Promise<boolean> {
    await this.repository.remove(guildId);
    return this.dispatcher.guildPrefixes.removeGuildPrefix(guildId);
  }
}

getAllGuildIds

getAllGuildIds returns an array of every guild ID that currently has a prefix override in memory. Use it in administrative dashboards, synchronization jobs, or diagnostic endpoints that need to enumerate or iterate over configured guilds. The result reflects the live in-memory state, so it may differ from the database during a partially completed reload.

A common pattern is to call getAllGuildIds in a scheduled job that cross-checks memory against database records and repairs any drift. You can also use it to build a paginated list for a management API without issuing broad database scans. Keep the call inside the service layer so consumers do not depend directly on the dispatcher.

src/modules/prefix/services/guild-prefix.service.ts
import { Injectable } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';

@Injectable()
export class GuildPrefixService {
  constructor(private readonly dispatcher: PrefixDispatcher) {}

  listConfiguredGuilds(): string[] {
    return this.dispatcher.guildPrefixes.getAllGuildIds();
  }
}

loadFromProvider: onReady lifecycle hook

loadFromProvider accepts an async provider function, awaits it, and replaces all in-memory guild prefix entries with the result. Because it performs bulk I/O and is inherently tied to data availability, it belongs in a lifecycle hook rather than a general-purpose service method. Running it from a service would either require a caller to know when to invoke it or risk multiple concurrent full reloads.

The onReady hook of SpraxiumOnReady runs once after the Discord client logs in and is ready, which is exactly when initial data should be loaded into memory. Implement the hook in a dedicated service class, inject PrefixDispatcher alongside your repository, and call loadFromProvider there. You can also call loadFromProvider from a scheduled job when you need a controlled re-sync after bulk database changes.

src/modules/prefix/prefix-prefetch.service.ts
import { Injectable } from '@spraxium/common';
import type { SpraxiumOnReady } from '@spraxium/common';
import { PrefixDispatcher } from '@spraxium/core';
import type { Client } from 'discord.js';
import { GuildPrefixRepository } from './repositories/guild-prefix.repository';

@Injectable()
export class PrefixPrefetchService implements SpraxiumOnReady {
  constructor(
    private readonly dispatcher: PrefixDispatcher,
    private readonly repository: GuildPrefixRepository,
  ) {}

  async onReady(_client: Client<true>): Promise<void> {
    await this.dispatcher.guildPrefixes.loadFromProvider(async () => {
      const rows = await this.repository.findAll();
      return rows.map((row) => ({
        guildId: row.guildId,
        prefix: row.prefixes,
        metadata: {
          updatedBy: row.updatedBy,
          updatedAt: row.updatedAt,
        },
      }));
    });
  }
}

Register PrefixPrefetchService in a module so SpraxiumOnReady is called automatically. The framework invokes onReady on every injectable that implements it, so no extra wiring is required beyond the module declaration.

Production checklist

A robust implementation must keep in-memory and persisted state aligned under failure conditions. It also needs enough audit context for support and moderation workflows. This checklist helps validate those guarantees before release.

  • Persist before mutating memory: write to your database before calling any in-memory mutation method to prevent state divergence under partial failures.
  • Wrap dispatcher writes in a service: centralize business rules and validation in a single service class to simplify testing and future changes.
  • Store audit metadata on every write: record the actor ID and timestamp so moderation and support workflows can trace when a prefix changed.
  • Use onReady for initial load: call loadFromProvider from a lifecycle hook after the Discord client is ready to guarantee data is available before commands execute.
  • Schedule safe reload routines: reconcile in-memory state with the database after migrations or bulk imports to close any drift window.