Skip to content

Database Integration Pattern

When your bot grows, you will inevitably need to connect to a database to store user preferences, statistics, or application state. A common mistake is to import a global database singleton directly into your commands. This tightly couples your commands to the database implementation, making them hard to test and maintain.

Instead, we recommend using an Abstract Base Command that implements WhatsBotCord’s ICommand interface. This base class will act as a bridge, handling the database connection and injecting it into your specialized commands.

Here is a real-world example of an abstract CustomCommand that receives a Database (AppDB) and a File System (IFileSystem) via dependency injection.

It intercepts the run execution to fetch the user’s preferred language from the database, configures the IChatContext with translated messages, and finally delegates the execution to a custom RUN method that your actual commands will implement.

import type { AdditionalAPI, CommandArgs, IChatContext, ICommand } from "whatsbotcord";
export abstract class CustomCommand implements ICommand {
// Required fields by ICommand
abstract name: string;
// Custom fields for our app
abstract minimumPrivilege: string;
// Dependencies
protected DB: any;
protected FS: any;
constructor(databaseToUse: any, fileSystemToUse: any) {
this.DB = databaseToUse;
this.FS = fileSystemToUse;
}
// We override the original run() method provided by Whatsbotcord
public async run(ctx: IChatContext, api: AdditionalAPI, args: CommandArgs): Promise<void> {
// 1. Fetch user data from our injected Database
const foundUser = await this.DB.Players.FindUser(
args.participantIdLID,
args.participantIdPN,
args.chatId
);
// 2. Set context wait messages overriding defaults based on user's lang
if (foundUser) {
ctx.Config.cancelFeedbackMsg = `Cancelled in ${foundUser.PreferredLanguage}`;
ctx.Config.wrongTypeFeedbackMsg = `Wrong type in ${foundUser.PreferredLanguage}`;
}
// 3. Delegate to the concrete command implementation
await this.RUN(ctx, api, args);
}
// The method subclasses must implement
abstract RUN(ctx: IChatContext, api: AdditionalAPI, args: CommandArgs): Promise<void>;
}

Now, whenever you want to create a new command, you simply extend your base CustomCommand. The database is already available via this.DB!

export default class ProfileCommand extends CustomCommand {
name = "profile";
minimumPrivilege = "USER";
public async RUN(ctx: IChatContext, api: AdditionalAPI, args: CommandArgs): Promise<void> {
// We can safely use this.DB because it was injected!
const stats = await this.DB.Stats.GetByChatId(ctx.FixedChatId);
await ctx.SendText(`Your current stats: ${stats.score}`);
}
}

By abstracting the run method, you ensure that every command automatically executes the required pre-requisites (like fetching the user or verifying roles) without duplicating code across all your files.