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.
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.
IUserlists the fields a user document has. It extendsMongooseDocument, which declares_id: string, the one field every document has.@RegisterMongooseService('users')gives the class its key. Your handlers read the service fromservicesunder that key, typed asUsersonce codegen has run.MongooseService<IUser>is the base class. PassingIUsertypesthis.modelasModel<IUser>, so TypeScript checks the fields in every query on that model againstusernameandstreak. Without the type argument, the model's type only has_id.schemais the Mongoose schema the plugin builds the model from. Make sure it'sstatic, because the decorator fails to compile without it.findByNamekeeps 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.
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.