Skip to content

Mongoose services

Write a service class for each MongoDB collection with the mongoose plugin, and read those services from any handler.

Without services, every handler that looks up a user imports the model and writes its own findOne call. You copy the same query into several handlers, so changing how you look a user up means finding every copy.

A service keeps a collection's queries in one class. Extend MongooseService, declare the schema as a static, and decorate the class with a key. Handlers then call its methods through that key.

src/services/Users.ts
import {
    MongooseService,
    RegisterMongooseService
} from '@seedcord/plugin-mongoose';
import mongoose from 'mongoose';

import type { MongooseDocument } from '@seedcord/plugin-mongoose';

interface IUser extends MongooseDocument {
    username: string;
    streak: number;
}

@RegisterMongooseService('users')
export class Users extends MongooseService<IUser> {
    public static schema = new mongoose.Schema<IUser>({
        username: { type: String, required: true, unique: true },
        streak: { type: Number, default: 0 }
    });

    public findByName(username: string): Promise<IUser | null> {
        return this.model.findOne({ username }).lean().exec();
    }
}

Users.ts does several things at once, so let's go through it one part at a time.

  • IUser lists the fields a user document has. It extends MongooseDocument, which declares _id: string, the one field every document has.
  • @RegisterMongooseService('users') gives the class its key. Your handlers read the service from services under that key, typed as Users once codegen has run.
  • MongooseService<IUser> is the base class. Passing IUser types this.model as Model<IUser>, so TypeScript checks the fields in every query on that model against username and streak. Without the type argument, the model's type only has _id.
  • schema is the Mongoose schema the plugin builds the model from. Make sure it's static, because the decorator fails to compile without it.
  • findByName keeps the lookup inside the class. Your handlers call that method, so none of them writes the query.

You never construct a service

The constructor takes the plugin, the core, and a model, so the plugin builds your services itself, right after it connects. It imports every code file under the dir you gave the mongoose plugin, including subfolders, in order of path. That covers .ts, .tsx, .mts, .js, .jsx, and .mjs, and skips type declarations, dotfiles, dot-folders, and node_modules. For each exported class that extends MongooseService and carries the decorator, it builds the model from schema and constructs the class. That constructor call adds the instance to the plugin's services map under its key.

Warning

If a class isn't exported or has no @RegisterMongooseService, the plugin skips it without throwing or logging anything, and services.users reads undefined. Codegen skips the class too, so once it has run, services.users fails to compile.

The model name sets the collection

The plugin uses your key as the Mongoose model name. Mongoose pluralizes that name to pick the collection, so a key of user reads and writes the users collection. That's fine for a new database. If your collection already exists under a different name, pass modelName so the model name can differ from the key.

@RegisterMongooseService('users', { modelName: 'app_user' })
export class Users extends MongooseService<IUser> {
    public static schema = new mongoose.Schema<IUser>({
        username: { type: String, required: true }
    });
}

With modelName: 'app_user', mongoose reads the app_users collection, while your handlers still use the class as services.users. modelName takes any non-empty string, so your editor doesn't suggest one and TypeScript can't catch a misspelled model name.

Typing the registry

The plugin finds your services by importing dir when the bot starts, but TypeScript only reads your source. Typing each key by hand means a declare module entry per service, which you keep in step with the decorator yourself.

seedcord codegen imports dir the same way and writes each key it finds into src/seedcord-gen.ts. After that, services.users is typed as your Users service, and the decorator offers the keys codegen found. With a users and a guilds service, it offers both:

RegisterMongooseService('
  • users
  • guilds

Until codegen has run once, the decorator accepts any key, and TypeScript types every entry on services as object.

Warning

If you add or rename a service, its decorator fails to compile until codegen runs again, since the generated file only lists the keys from the last run. Run seedcord codegen after each one.

Reading a service

A handler gets a service from the plugin's services map, under the key you declared.

hover for typestap for types, arrow keys walk the tokens
const const found: IUser | nullfound =
    await this.BaseHandler<ChatInputCommandInteraction<"cached">, Core>.core: Corecore.Core.db: Mongoosedb.Mongoose.services: MongooseServicesservices.users: Usersusers.Users.findByName(username: string): Promise<IUser | null>findByName('ada');

await this.
RepliableHandler<ChatInputCommandInteraction<"cached">, Core, SentMessage, BufferResolvable | Stream | JSONEncodable<...> | Attachment | AttachmentBuilder | AttachmentPayload, ReplySender>.reply(
    response:
        | string
        | ReplyResponse<
              | BufferResolvable
              | Stream
              | JSONEncodable<APIAttachment>
              | Attachment
              | AttachmentBuilder
              | AttachmentPayload
          >,
    opts?: SendOpts
): Promise<SentMessage>
reply
(
const found: IUser | nullfound ? `Found ${const found: IUserfound.IUser.username: stringusername}.` : 'No record.' );

findByName resolves to an IUser or null, so the handler checks found before it reads username.

If you read services before the plugin's init() has finished, it throws PluginServicesNotReady. Your handlers never read it that early, though, because they only start receiving interactions once init() has finished.