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.
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.
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.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. A handler calls that method and never writes the query itself.- The
declare moduleblock addsusersto 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.
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.