Passing a new parameter to commands

Heyo! I am converting my bots to Sapphire, and on one of them, I pass the entity manager of my ORM to each command as I call them so I don't create too many (or else it creates a memory leak). So basically, I create an entity manager every time there's an event, instead of every time I need one. That worked fine before Sapphire, because I was the one calling my commands. But now, I am not anymore! So I thought about a way to do it. I could extend the container and add an "em" key to it. Then, iirc, there's an event Sapphire emits before an application command is called. I could plug into that event to initialize the entity manager in the container, and destroy it after the command is done (if there's an event for that too). Do you think that's a good approach? If not, what would you suggest? Thank you!
66 Replies
Lily Wonhalf
Lily Wonhalf11mo ago
@Helpers uwu
Lioness100
Lioness10011mo ago
Is this mikro-orm?
Lily Wonhalf
Lily Wonhalf11mo ago
ye
vladdy
vladdy11mo ago
Just add it to the container before calling login and you wouldn't need to pass it down to commands all pieces have this.container and container can also just be imported
Lily Wonhalf
Lily Wonhalf11mo ago
I used to have only one em
vladdy
vladdy11mo ago
And you still can
Lily Wonhalf
Lily Wonhalf11mo ago
And when I did, queries would collide
vladdy
vladdy11mo ago
oh
Lily Wonhalf
Lily Wonhalf11mo ago
Vladdy lemme talk >:C
vladdy
vladdy11mo ago
👀 No :3
Lily Wonhalf
Lily Wonhalf11mo ago
You can't cut me off in text chat >:C
vladdy
vladdy11mo ago
WATCH VladdyHeart
kyra
kyra11mo ago
He can cut you off in other ways tho
Lioness100
Lioness10011mo ago
I attached the db to container.db, and then used this decorator for every method that needed to fork the em
import { UseRequestContext as OriginalUseRequestContext } from '@mikro-orm/core';
import { container } from '@sapphire/framework';

/**
* This decorator wraps the code of the method it decorates in a new RequestContext.
* @see https://mikro-orm.io/docs/identity-map
*/
export const UseRequestContext = OriginalUseRequestContext.bind(null, container.db.orm);
import { UseRequestContext as OriginalUseRequestContext } from '@mikro-orm/core';
import { container } from '@sapphire/framework';

/**
* This decorator wraps the code of the method it decorates in a new RequestContext.
* @see https://mikro-orm.io/docs/identity-map
*/
export const UseRequestContext = OriginalUseRequestContext.bind(null, container.db.orm);
Example:
@ApplyOptions<ArimaCommand.Options>({ runIn: [CommandOptionsRunTypeEnum.GuildText] })
export class StatsCommand extends ArimaCommand {
@UseRequestContext()
public override async chatInputRun(interaction: ArimaCommand.Interaction<'cached'>) {
const user = interaction.options.getUser('player') ?? interaction.user;
const player = await this.container.db.members.findOne({ userId: user.id, guildId: interaction.guild.id });
// ....
@ApplyOptions<ArimaCommand.Options>({ runIn: [CommandOptionsRunTypeEnum.GuildText] })
export class StatsCommand extends ArimaCommand {
@UseRequestContext()
public override async chatInputRun(interaction: ArimaCommand.Interaction<'cached'>) {
const user = interaction.options.getUser('player') ?? interaction.user;
const player = await this.container.db.members.findOne({ userId: user.id, guildId: interaction.guild.id });
// ....
Lily Wonhalf
Lily Wonhalf11mo ago
AYOOO
Lioness100
Lioness10011mo ago
Docs on the original decorator: https://mikro-orm.io/docs/identity-map
kyra
kyra11mo ago
And yeah, what Lioness said
Lioness100
Lioness10011mo ago
while yall are goofing about 😒
kyra
kyra11mo ago
My original suggestion would have been to extend Args but... that wouldn't work outside message commands lmao There is a context object in the third arg tho
Lily Wonhalf
Lily Wonhalf11mo ago
Aight what Lioness is suggesting is very interesting, lemme look into it I'm still very foreign to decorators but they look sexy (kyra shut up)
kyra
kyra11mo ago
And if this happens synchronously, you can attach events to augment said context object
Lioness100
Lioness10011mo ago
cool beans if you want to see my full mikro orm setup for ideas, the repo I'm citing is this one: https://github.com/arimajs/Arima it may be out of date now though
Lily Wonhalf
Lily Wonhalf11mo ago
Won't that fork the em every time I use the decorator though?
Lioness100
Lioness10011mo ago
Yes, but I think that's what you want
Lily Wonhalf
Lily Wonhalf11mo ago
It's not lilypensive when I did that, it would have memory leaks I need it to be super optimized
Lioness100
Lioness10011mo ago
Why?
Lily Wonhalf
Lily Wonhalf11mo ago
Idk, I just observed that :p
Lioness100
Lioness10011mo ago
I'd ask in the mikro-orm slack so an expert can help, but I'm pretty sure one of the points of request context is to prevent memory leaks.
Lily Wonhalf
Lily Wonhalf11mo ago
I asked them and they were like "Ye, NodeJS takes up all the memory it can that's how it is" lol Yknow what I'll try again shrug Worst case scenario the valorant server doesn't have voicechats anymore :]
kyra
kyra11mo ago
Does Mikro not do pooling at all? o.o
Lioness100
Lioness10011mo ago
It does I'm confused about your original question then. You want to instantiate a new entity manager on demand instead of reusing the same one to save memory?
Lily Wonhalf
Lily Wonhalf11mo ago
When I was using the same one for everything queries would collide
Lioness100
Lioness10011mo ago
No description
Lioness100
Lioness10011mo ago
Yeah, that's why you would fork it
Lily Wonhalf
Lily Wonhalf11mo ago
Yeah, so I started forking it every time I would need one And memory leak Ok your approach in your repo seems much better than mine lol I don't understand one thing though
export const UseRequestContext = OriginalUseRequestContext.bind(null, container.db.orm);
like container.db.orm
Lioness100
Lioness10011mo ago
depends on how your code looks like, if you get the EM fork once and work with that, its ok, but you dont want multiple forks in one context = in one event handler, which is the same as one request
I think what he meant is that if you fork it once per command run, you'll be fine. But, if you access the getter multiple times and fork it multiple times in the same scope, you won't?
Lily Wonhalf
Lily Wonhalf11mo ago
What's that
Lioness100
Lioness10011mo ago
check index.ts
Lily Wonhalf
Lily Wonhalf11mo ago
container.db is your databasemanager but your databasemanager doesn't have an orm field So you're just hot-creating the field
Lioness100
Lioness10011mo ago
It does
Lily Wonhalf
Lily Wonhalf11mo ago
Huh Why not declaring it as a property?
Lioness100
Lioness10011mo ago
If you put modifiers on params in the constructor, typescript automatically places it in a property of the same name
No description
Lioness100
Lioness10011mo ago
(public readonly are the modifiers)
Lily Wonhalf
Lily Wonhalf11mo ago
Ooooooooooooh Just like in PHP 8 Ok sorry for comparing TS to PHP I will repent
Lioness100
Lioness10011mo ago
blobpensivepray
Lily Wonhalf
Lily Wonhalf11mo ago
ok now I understand Thank youuuuuuuu @kyra 🩵🩷🤍🩷🩵 >:c I watch u
Lioness100
Lioness10011mo ago
no problem
kyra
kyra11mo ago
I can make you scream in DMs
Lioness100
Lioness10011mo ago
what does kyra mean by that
Lily Wonhalf
Lily Wonhalf11mo ago
NOTHING
Lioness100
Lioness10011mo ago
9susLuz
Lily Wonhalf
Lily Wonhalf11mo ago
@lioness100 Let's say I have multiple methods in one class that use this.container.db Should I add the decorator to all of those methods?
Lioness100
Lioness10011mo ago
Yeah
Lily Wonhalf
Lily Wonhalf11mo ago
ok ty Okay so I'm putting that everywhere it's needed, and I am now in a file where I added the decoration but the file does not have the container key because it's not extending anything from Sapphire What's the best way to bring the container there?
Lioness100
Lioness10011mo ago
You can import container from @sapphire/framework
Lily Wonhalf
Lily Wonhalf11mo ago
Ah! ty :3
Lioness100
Lioness10011mo ago
👍
Lily Wonhalf
Lily Wonhalf11mo ago
I was scared I would have to create a store for my file ;-;
Lioness100
Lioness10011mo ago
But, also, if you use the decorator in the message I'm replying to, you probably won't have that problem, because you can bind the orm to the decorator and never have to input it again
Lily Wonhalf
Lily Wonhalf11mo ago
I don't understand, sorry
Lioness100
Lioness10011mo ago
If you import the decorator from mikro-orm, you'd have to do:
import { UseRequestContext } from '@mikro-orm/core';
import { container } from '@sapphire/framework';

class Something {
@UseRequestContext(container.db.orm)
public method1() {
// ...
}

@UseRequestContext(container.db.orm)
public method2() {
// ...
}
import { UseRequestContext } from '@mikro-orm/core';
import { container } from '@sapphire/framework';

class Something {
@UseRequestContext(container.db.orm)
public method1() {
// ...
}

@UseRequestContext(container.db.orm)
public method2() {
// ...
}
And you'd have to input container.db.orm in every usage of the decorator for every class. However, if you create utils/decorators.ts or similar and paste:
import { UseRequestContext as OriginalUseRequestContext } from '@mikro-orm/core';
import { container } from '@sapphire/framework';

/**
* This decorator wraps the code of the method it decorates in a new RequestContext.
* @see https://mikro-orm.io/docs/identity-map
*/
export const UseRequestContext = OriginalUseRequestContext.bind(null, container.db.orm);
import { UseRequestContext as OriginalUseRequestContext } from '@mikro-orm/core';
import { container } from '@sapphire/framework';

/**
* This decorator wraps the code of the method it decorates in a new RequestContext.
* @see https://mikro-orm.io/docs/identity-map
*/
export const UseRequestContext = OriginalUseRequestContext.bind(null, container.db.orm);
You can then import UseRequestContext from that file instead, which is already bound to container.db.orm, so you can do:
import { UseRequestContext } from '@mikro-orm/core';
- import { container } from '@sapphire/framework';

class Something {
- @UseRequestContext(container.db.orm)
+ @UseRequestContext()
public method1() {
// ...
}

- @UseRequestContext(container.db.orm)
+ @UseRequestContext()
public method2() {
// ...
}
import { UseRequestContext } from '@mikro-orm/core';
- import { container } from '@sapphire/framework';

class Something {
- @UseRequestContext(container.db.orm)
+ @UseRequestContext()
public method1() {
// ...
}

- @UseRequestContext(container.db.orm)
+ @UseRequestContext()
public method2() {
// ...
}
Fewer imports, less repetition
Lily Wonhalf
Lily Wonhalf11mo ago
And then use "container" directly inside?
Lioness100
Lioness10011mo ago
Sorry, I don't understand
Lily Wonhalf
Lily Wonhalf11mo ago
Like in the code of the methods I have to use container.database but if I don't import container, I can't, right?
@UseRequestContext()
public async run(client: Client) {
const { channelRepository } = container.database;
@UseRequestContext()
public async run(client: Client) {
const { channelRepository } = container.database;
Lioness100
Lioness10011mo ago
oh, I misunderstood yeah you'd still need to import it
Lily Wonhalf
Lily Wonhalf11mo ago
Ok Ok so now this is the error I have:
Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>
Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>
import { UseRequestContext as OriginalUseRequestContext } from '@mikro-orm/core';
import { container } from '@sapphire/framework';

export const UseRequestContext = OriginalUseRequestContext.bind(null, container.database.orm);
import { UseRequestContext as OriginalUseRequestContext } from '@mikro-orm/core';
import { container } from '@sapphire/framework';

export const UseRequestContext = OriginalUseRequestContext.bind(null, container.database.orm);
import { TsMorphMetadataProvider } from '@mikro-orm/reflection';
import { EntityManager, MikroORM } from '@mikro-orm/core';
import type { MySqlDriver } from '@mikro-orm/mysql';
import Logger from '@lilywonhalf/pretty-logger';
import AllowedUserRepository from '#structures/repositories/AllowedUserRepository';
import BannedUserRepository from '#structures/repositories/BannedUserRepository';
import ChannelRepository from '#structures/repositories/ChannelRepository';
import MutedUserRepository from '#structures/repositories/MutedUserRepository';
import SettingsRepository from '#structures/repositories/SettingsRepository';
import { AllowedUser } from '#structures/entities/AllowedUser';
import { BannedUser } from '#structures/entities/BannedUser';
import { Channel } from '#structures/entities/Channel';
import { MutedUser } from '#structures/entities/MutedUser';
import { Settings } from '#structures/entities/Settings';
import { MigrationGenerator } from '#structures/MigrationGenerator';

export default class Database {
private static instance: Database;

private _orm!: MikroORM<MySqlDriver>;
private _em!: EntityManager;
private _allowedUserRepository!: AllowedUserRepository;
private _bannedUserRepository!: BannedUserRepository;
private _channelRepository!: ChannelRepository;
private _mutedUserRepository!: MutedUserRepository;
private _settingsRepository!: SettingsRepository;

public constructor() {
if (Database.instance) {
return Database.instance;
}

Database.instance = this;
}

public async initialize() {
this._orm = await MikroORM.init<MySqlDriver>({
metadataProvider: TsMorphMetadataProvider,
entities: ['./dist/structures/entities'],
entitiesTs: ['./src/structures/entities'],
logger: (message: string) => Logger.info(message),
debug: true,
type: 'mysql',
dbName: process.env.DBNAME,
user: process.env.DBUSER,
host: process.env.DBHOST,
password: process.env.DBPASSWORD,
port: Number(process.env.DBPORT),
driverOptions: {
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
},
migrations: {
generator: MigrationGenerator,
path: './dist/migrations',
pathTs: './src/migrations',
},
});

this._em = this.orm.em;

this._allowedUserRepository = this.em.getRepository(AllowedUser);
this._bannedUserRepository = this.em.getRepository(BannedUser);
this._channelRepository = this.em.getRepository(Channel);
this._mutedUserRepository = this.em.getRepository(MutedUser);
this._settingsRepository = this.em.getRepository(Settings);
}

public get orm() {
return this._orm;
}

public get em() {
return this._em;
}

public get allowedUserRepository() {
return this._allowedUserRepository;
}

public get bannedUserRepository() {
return this._bannedUserRepository;
}

public get channelRepository() {
return this._channelRepository;
}

public get mutedUserRepository() {
return this._mutedUserRepository;
}

public get settingsRepository() {
return this._settingsRepository;
}
}
import { TsMorphMetadataProvider } from '@mikro-orm/reflection';
import { EntityManager, MikroORM } from '@mikro-orm/core';
import type { MySqlDriver } from '@mikro-orm/mysql';
import Logger from '@lilywonhalf/pretty-logger';
import AllowedUserRepository from '#structures/repositories/AllowedUserRepository';
import BannedUserRepository from '#structures/repositories/BannedUserRepository';
import ChannelRepository from '#structures/repositories/ChannelRepository';
import MutedUserRepository from '#structures/repositories/MutedUserRepository';
import SettingsRepository from '#structures/repositories/SettingsRepository';
import { AllowedUser } from '#structures/entities/AllowedUser';
import { BannedUser } from '#structures/entities/BannedUser';
import { Channel } from '#structures/entities/Channel';
import { MutedUser } from '#structures/entities/MutedUser';
import { Settings } from '#structures/entities/Settings';
import { MigrationGenerator } from '#structures/MigrationGenerator';

export default class Database {
private static instance: Database;

private _orm!: MikroORM<MySqlDriver>;
private _em!: EntityManager;
private _allowedUserRepository!: AllowedUserRepository;
private _bannedUserRepository!: BannedUserRepository;
private _channelRepository!: ChannelRepository;
private _mutedUserRepository!: MutedUserRepository;
private _settingsRepository!: SettingsRepository;

public constructor() {
if (Database.instance) {
return Database.instance;
}

Database.instance = this;
}

public async initialize() {
this._orm = await MikroORM.init<MySqlDriver>({
metadataProvider: TsMorphMetadataProvider,
entities: ['./dist/structures/entities'],
entitiesTs: ['./src/structures/entities'],
logger: (message: string) => Logger.info(message),
debug: true,
type: 'mysql',
dbName: process.env.DBNAME,
user: process.env.DBUSER,
host: process.env.DBHOST,
password: process.env.DBPASSWORD,
port: Number(process.env.DBPORT),
driverOptions: {
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
},
migrations: {
generator: MigrationGenerator,
path: './dist/migrations',
pathTs: './src/migrations',
},
});

this._em = this.orm.em;

this._allowedUserRepository = this.em.getRepository(AllowedUser);
this._bannedUserRepository = this.em.getRepository(BannedUser);
this._channelRepository = this.em.getRepository(Channel);
this._mutedUserRepository = this.em.getRepository(MutedUser);
this._settingsRepository = this.em.getRepository(Settings);
}

public get orm() {
return this._orm;
}

public get em() {
return this._em;
}

public get allowedUserRepository() {
return this._allowedUserRepository;
}

public get bannedUserRepository() {
return this._bannedUserRepository;
}

public get channelRepository() {
return this._channelRepository;
}

public get mutedUserRepository() {
return this._mutedUserRepository;
}

public get settingsRepository() {
return this._settingsRepository;
}
}
And before calling login:
container.database = new Database();

await container.database.initialize();
container.database = new Database();

await container.database.initialize();
I just removed the "solved" tag because it's not working quite yet Ok that error is fixed, it was just because I use the decorator on getters. But now I have another error 👀 . I'll try to fix it a bit before reporting here Aaaaand I fixed the rest myself, thank you again everyone!