Skip to content

Mongoose services

Write a service class for each MongoDB collection with the mongoose plugin. Covers its schema and key, the model name, typing the services map, and reading a service from a 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();
    }
}

declare module '@seedcord/plugin-mongoose' {
    interface MongooseServices {
        users: Users;
    }
}

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.
  • 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. A handler calls that method and never writes the query itself.
  • The declare module block adds users to the keys the decorator accepts. Typing the registry explains why you write it.

You never construct a service

The constructor takes the plugin, the core, and a model. The plugin only has all three once it has connected, so it builds your services itself during startup. It imports every .ts and .js file under the dir you gave the mongoose plugin, including subfolders. 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. services.users then reads undefined even though TypeScript types it as Users.

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.

declare module '@seedcord/plugin-mongoose' {
    interface MongooseServices {
        users: Users;
    }
}

@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. The decorator only accepts 'users' because the declare module block adds it to MongooseServices. modelName takes any string, so your editor doesn't suggest one and TypeScript can't catch a misspelled model name.

Typing the registry

MongooseServices starts empty, so the decorator doesn't accept any key until you declare one. The declare module block in src/services/Users.ts above adds users. The decorator then takes 'users' and checks that the class matches Users, the type you declared for that key. If you pass 'guilds' to the decorator before adding a guilds entry, TypeScript reports an error.

declare module '@seedcord/plugin-mongoose' {
    interface MongooseServices {
        users: Users;
        guilds: Guilds;
    }
}

RegisterMongooseService('
  • users
  • guilds

The decorator offers the two keys that declare module block adds to MongooseServices, users and guilds.

You add each entry by hand when you write a new service, since codegen doesn't write this block. It only writes the Core block from Typing a plugin.

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.MongooseServices.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 PluginMongooseServicesNotReady. Your handlers never read it that early, though, because they only start receiving interactions once init() has finished.