diff --git a/.eslintrc.json b/.eslintrc.json index 9645413..7ce6663 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,10 +3,7 @@ "es2021": true, "node": true }, - "extends": [ - "standard-with-typescript", - "prettier" - ], + "extends": ["standard-with-typescript", "prettier"], "overrides": [], "parser": "@typescript-eslint/parser", "parserOptions": { @@ -14,12 +11,11 @@ "sourceType": "module", "project": "./tsconfig.json" }, - "rules": {}, - "ignorePatterns": [ - "coverage", - "dist", - "__tests__/", - "jest.config.ts", - "*.yml" - ] -} \ No newline at end of file + "rules": { + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "off" + }, + "ignorePatterns": ["coverage", "dist", "__tests__/", "jest.config.ts", "*.yml"] +} diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b58c70e..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,6 +0,0 @@ -# [3.6.0](https://github.com/TogetherCrew/mongo-lib/compare/v3.5.0...v3.6.0) (2025-05-06) - - -### Features - -* implement repositories ([cfcd79d](https://github.com/TogetherCrew/mongo-lib/commit/cfcd79d8f22b2fcf7a5cacaf5dca0600c8f29df3)) diff --git a/package.json b/package.json index 46b23cf..4b5d649 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "test": "jest --detectOpenHandles", "format": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\" \"*.ts\" ", "prepublishOnly": "npm test", - "version": "npm run format && git add -A src" + "version": "npm run format && git add -A src", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" }, "repository": { "type": "git", diff --git a/src/connection.ts b/src/connection.ts index d2f4f7f..6a9b312 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1,58 +1,66 @@ -import mongoose, { Connection } from 'mongoose'; +import mongoose, { type Connection } from 'mongoose'; export default class MongoConnectionManager { - private static instance: MongoConnectionManager; + private static instance: MongoConnectionManager | undefined; + private mongoConnection: Connection | null = null; + private currentUri = ''; - // Private constructorto prevent direct instantiation private constructor() {} public static getInstance(): MongoConnectionManager { - if (typeof MongoConnectionManager.instance === 'undefined') { + if (MongoConnectionManager.instance === undefined) { MongoConnectionManager.instance = new MongoConnectionManager(); } return MongoConnectionManager.instance; } - public async connect(url: string): Promise { + public async connect(uri: string): Promise { + if (this.mongoConnection !== null && this.mongoConnection.readyState === 1) { + if (uri === this.currentUri) return; + throw new Error('MongoDB connection already exists with a different URI'); + } + try { - if (this.mongoConnection !== null) { - throw new Error('MongoDB connection already exists'); - } - await mongoose.connect(url); + await mongoose.connect(uri); this.mongoConnection = mongoose.connection; - console.log('Connected to MongoDB!'); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - console.error(`Failed to connect to MongoDB: ${errorMessage}`); - throw new Error(`Failed to connect to MongoDB: ${errorMessage}`); + this.currentUri = uri; + + console.log('Connected to MongoDB'); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'Unknown error'; + console.error(`Failed to connect to MongoDB: ${msg}`); + throw new Error(`Failed to connect to MongoDB: ${msg}`); } } public async disconnect(): Promise { + if (this.mongoConnection === null) { + console.warn('No active MongoDB connection to disconnect.'); + return; + } + try { - if (this.mongoConnection !== null) { - await mongoose.disconnect(); - this.mongoConnection = null; - console.log('Disconnected from MongoDB'); - } else { - console.warn('No active MongoDB connection to disconnect.'); - } - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - console.error(`Failed to disconnect from MongoDB: ${errorMessage}`); - throw new Error(`Failed to disconnect from MongoDB: ${errorMessage}`); + await mongoose.disconnect(); + this.mongoConnection = null; + this.currentUri = ''; + console.log('Disconnected from MongoDB'); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'Unknown error'; + console.error(`Failed to disconnect from MongoDB: ${msg}`); + throw new Error(`Failed to disconnect from MongoDB: ${msg}`); } } public ensureConnected(): void { if (this.mongoConnection === null) { - throw new Error('No active MongoDB connection. Please connect before performing database operations.'); + throw new Error('No active MongoDB connection. Please connect first.'); } } - public getConnection(): Connection | null { + public getConnection(): Connection { this.ensureConnected(); - return this.mongoConnection; + // Non‑null assertion is safe after explicit check above. + return this.mongoConnection as Connection; } } diff --git a/src/databaseManager.ts b/src/databaseManager.ts index 031eece..aab418b 100644 --- a/src/databaseManager.ts +++ b/src/databaseManager.ts @@ -1,7 +1,15 @@ -import { Snowflake } from 'discord.js'; -import mongoose, { Connection } from 'mongoose'; +/* eslint-disable no-console */ +import { type Snowflake } from 'discord.js'; +import mongoose, { type Connection } from 'mongoose'; -import { IChannel, IGuildMember, IHeatMap, IMemberActivity, IRawInfo, IRole } from './interfaces'; +import { + type IChannel, + type IGuildMember, + type IHeatMap, + type IMemberActivity, + type IRawInfo, + type IRole, +} from './interfaces'; import { channelSchema, guildMemberSchema, @@ -12,55 +20,62 @@ import { } from './models/schemas'; export default class DatabaseManager { - private static instance: DatabaseManager; - private modelCache: Record = {}; + private static instance: DatabaseManager | undefined; + + private readonly modelCache = new Map>(); public static getInstance(): DatabaseManager { - if (typeof DatabaseManager.instance === 'undefined') { + if (DatabaseManager.instance === undefined) { DatabaseManager.instance = new DatabaseManager(); } return DatabaseManager.instance; } public async getGuildDb(guildId: Snowflake): Promise { - const dbName = guildId; - const db = mongoose.connection.useDb(dbName, { useCache: true }); + const db = mongoose.connection.useDb(guildId, { useCache: true }); await this.setupModels(db, 'guild'); return db; } public async getPlatformDb(platformId: string): Promise { - const dbName = platformId; - const db = mongoose.connection.useDb(dbName, { useCache: true }); + const db = mongoose.connection.useDb(platformId, { useCache: true }); await this.setupModels(db, 'platform'); return db; } private async setupModels(db: Connection, dbType: 'guild' | 'platform'): Promise { - if (!this.modelCache[db.name]) { - try { - if (dbType === 'platform') { - db.model('HeatMap', heatMapSchema); - db.model('MemberActivity', MemberActivitySchema); - } else if (dbType === 'guild') { - db.model('RawInfo', rawInfoSchema); - db.model('GuildMember', guildMemberSchema); - db.model('Channel', channelSchema); - db.model('Role', roleSchema); + let compilePromise: Promise | undefined = this.modelCache.get(db.name); + + if (compilePromise === undefined) { + compilePromise = (async (): Promise => { + try { + if (dbType === 'platform') { + db.model('HeatMap', heatMapSchema); + db.model('MemberActivity', MemberActivitySchema); + } else { + db.model('RawInfo', rawInfoSchema); + db.model('GuildMember', guildMemberSchema); + db.model('Channel', channelSchema); + db.model('Role', roleSchema); + } + } catch (err) { + console.error(`Error setting up models for ${db.name}:`, err); } - this.modelCache[db.name] = true; - } catch (error) { - console.error(`Error setting up models for ${db.name}:`, error); - } + })(); + + this.modelCache.set(db.name, compilePromise); } + + await compilePromise; } public async deleteDatabase(db: Connection): Promise { - const dbName = db.name; try { await db.dropDatabase(); - } catch (error) { - console.error(`Error deleting database ${dbName}:`, error); + await db.close(); + this.modelCache.delete(db.name); + } catch (err) { + console.error(`Error deleting database ${db.name}:`, err); } } } diff --git a/src/interfaces/Module.interface.ts b/src/interfaces/Module.interface.ts index 1eb2efb..a79878b 100644 --- a/src/interfaces/Module.interface.ts +++ b/src/interfaces/Module.interface.ts @@ -1,6 +1,6 @@ -import { Model, Types } from 'mongoose'; +import { type Model, type Types } from 'mongoose'; -import { ModuleNames, PlatformNames } from '../config/enums'; +import { type ModuleNames, type PlatformNames } from '../config/enums'; export interface IModule { name: ModuleNames; diff --git a/src/repositories/announcement.repository.ts b/src/repositories/announcement.repository.ts index d8f56d9..92f2255 100644 --- a/src/repositories/announcement.repository.ts +++ b/src/repositories/announcement.repository.ts @@ -1,6 +1,6 @@ -import { Model } from 'mongoose'; +import { type Model } from 'mongoose'; import { BaseRepository } from './base.repository'; -import { IAnnouncement } from '../interfaces'; +import { type IAnnouncement } from '../interfaces'; import Announcement from '../models/Announcement.model'; export class AnnouncementRepository extends BaseRepository { @@ -10,4 +10,3 @@ export class AnnouncementRepository extends BaseRepository { } export const announcementRepository = new AnnouncementRepository(); -export default announcementRepository; diff --git a/src/repositories/base.repository.ts b/src/repositories/base.repository.ts index 8cb4c4e..c81a2bb 100644 --- a/src/repositories/base.repository.ts +++ b/src/repositories/base.repository.ts @@ -1,5 +1,13 @@ -// src/db/repositories/base.repository.ts -import { FilterQuery, LeanDocument, Model, PopulateOptions, ProjectionType, QueryOptions, UpdateQuery } from 'mongoose'; +import { + type FilterQuery, + type HydratedDocument, + type LeanDocument, + type Model, + type PopulateOptions, + type ProjectionType, + type QueryOptions, + type UpdateQuery, +} from 'mongoose'; export interface PaginateOptions { page?: number; @@ -8,51 +16,63 @@ export interface PaginateOptions { populate?: string | PopulateOptions | Array; } +interface PaginateResult { + results: Array>; + page: number; + limit: number; + totalPages: number; + totalResults: number; +} + +interface PaginateModel extends Model { + paginate: (filter: FilterQuery, options: PaginateOptions) => Promise>; +} + export class BaseRepository { constructor(private readonly model: Model) {} - async create(doc: Partial): Promise { + async create(doc: Partial): Promise> { return await this.model.create(doc); } - async createMany(docs: Array>): Promise { + async createMany(docs: Array>): Promise>> { return await this.model.insertMany(docs); } - async findById(id: string, projection?: ProjectionType, options?: QueryOptions): Promise { - return await this.model.findById(id, projection, options); + async findById(id: string, projection?: ProjectionType, options?: QueryOptions): Promise | null> { + return await this.model.findById(id, projection, options).lean(); } - async find(filter: FilterQuery, projection?: ProjectionType, options?: QueryOptions): Promise { - return await this.model.find(filter, projection, options); + async find( + filter: FilterQuery, + projection?: ProjectionType, + options?: QueryOptions, + ): Promise>> { + return await this.model.find(filter, projection, options).lean(); } - async updateOne( + async findOne( filter: FilterQuery, - update: UpdateQuery, + projection?: ProjectionType, options?: QueryOptions, - ): Promise<{ acknowledged: boolean; modifiedCount: number; upsertedId: unknown }> { + ): Promise | null> { + return await this.model.findOne(filter, projection, options).lean(); + } + + async updateOne(filter: FilterQuery, update: UpdateQuery, options?: QueryOptions): Promise { return await this.model.updateOne(filter, update, options); } - async deleteOne(filter: FilterQuery): Promise<{ deletedCount?: number }> { + async deleteOne(filter: FilterQuery): Promise { return await this.model.deleteOne(filter); } - async deleteMany(filter: FilterQuery): Promise<{ deletedCount?: number }> { + async deleteMany(filter: FilterQuery): Promise { return await this.model.deleteMany(filter); } - async paginate( - filter: FilterQuery, - options: PaginateOptions, - ): Promise<{ - results: Array>; - page: number; - limit: number; - totalPages: number; - totalResults: number; - }> { - return (this.model as any).paginate(filter, options); + async paginate(filter: FilterQuery, options: PaginateOptions): Promise> { + const modelWithPaginate = this.model as unknown as PaginateModel; + return await modelWithPaginate.paginate(filter, options); } } diff --git a/src/repositories/channel.repository.ts b/src/repositories/channel.repository.ts index 7605fb9..f7a9704 100644 --- a/src/repositories/channel.repository.ts +++ b/src/repositories/channel.repository.ts @@ -1,13 +1,12 @@ -import { Model } from 'mongoose'; +import { type Connection } from 'mongoose'; + +import { type IChannel } from '../interfaces'; import { BaseRepository } from './base.repository'; -import { IChannel } from '../interfaces'; -import Channel from '../models/Channel.model'; export class ChannelRepository extends BaseRepository { - constructor(model: Model = Channel) { - super(model); + constructor(connection: Connection) { + super(connection.model('Channel')); } } -export const channelRepository = new ChannelRepository(); -export default channelRepository; +export const makeChannelRepository = (connection: Connection) => new ChannelRepository(connection); diff --git a/src/repositories/community.repository.ts b/src/repositories/community.repository.ts index 32e0039..5ed350f 100644 --- a/src/repositories/community.repository.ts +++ b/src/repositories/community.repository.ts @@ -1,6 +1,6 @@ -import { Model } from 'mongoose'; +import { type Model } from 'mongoose'; import { BaseRepository } from './base.repository'; -import { ICommunity } from '../interfaces'; +import { type ICommunity } from '../interfaces'; import Community from '../models/Community.model'; export class CommunityRepository extends BaseRepository { @@ -10,4 +10,3 @@ export class CommunityRepository extends BaseRepository { } export const communityRepository = new CommunityRepository(); -export default communityRepository; diff --git a/src/repositories/guildMember.repository.ts b/src/repositories/guildMember.repository.ts index f2ab9ba..1f2badb 100644 --- a/src/repositories/guildMember.repository.ts +++ b/src/repositories/guildMember.repository.ts @@ -1,13 +1,12 @@ -import { Model } from 'mongoose'; +import { type Connection } from 'mongoose'; + +import { type IGuildMember } from '../interfaces'; import { BaseRepository } from './base.repository'; -import { IGuildMember } from '../interfaces'; -import GuildMember from '../models/GuildMember.model'; export class GuildMemberRepository extends BaseRepository { - constructor(model: Model = GuildMember) { - super(model); + constructor(connection: Connection) { + super(connection.model('GuildMember')); } } -export const guildMemberRepository = new GuildMemberRepository(); -export default guildMemberRepository; +export const makeGuildMemberRepository = (connection: Connection) => new GuildMemberRepository(connection); diff --git a/src/repositories/heatMap.repository.ts b/src/repositories/heatMap.repository.ts index a4f8702..7744760 100644 --- a/src/repositories/heatMap.repository.ts +++ b/src/repositories/heatMap.repository.ts @@ -1,13 +1,12 @@ -import { Model } from 'mongoose'; +import { type Connection } from 'mongoose'; + +import { type IHeatMap } from '../interfaces'; import { BaseRepository } from './base.repository'; -import { IHeatMap } from '../interfaces'; -import HeatMap from '../models/HeatMap.model'; export class HeatMapRepository extends BaseRepository { - constructor(model: Model = HeatMap) { - super(model); + constructor(connection: Connection) { + super(connection.model('HeatMap')); } } -export const heatMapRepository = new HeatMapRepository(); -export default heatMapRepository; +export const makeHeatMapRepository = (connection: Connection) => new HeatMapRepository(connection); diff --git a/src/repositories/index.ts b/src/repositories/index.ts index c35ab3d..7791569 100644 --- a/src/repositories/index.ts +++ b/src/repositories/index.ts @@ -1,27 +1,27 @@ import { BaseRepository } from './base.repository'; -import userRepository from './user.repository'; -import tokenRepository from './token.repository'; -import heatMapRepository from './heatMap.repository'; -import rawInfoRepository from './rawInfo.repository'; -import memberActivityRepository from './memberActivity.repository'; -import guildMemberRepository from './guildMember.repository'; -import channelRepository from './channel.repository'; -import roleRepository from './role.repository'; -import communityRepository from './community.repository'; -import platformRepository from './platform.repository'; -import announcementRepository from './announcement.repository'; -import moduleRepository from './module.repository'; +import { userRepository } from './user.repository'; +import { tokenRepository } from './token.repository'; +import { makeHeatMapRepository } from './heatMap.repository'; +import { makeRawInfoRepository } from './rawInfo.repository'; +import { makeMemberActivityRepository } from './memberActivity.repository'; +import { makeGuildMemberRepository } from './guildMember.repository'; +import { makeChannelRepository } from './channel.repository'; +import { makeRoleRepository } from './role.repository'; +import { communityRepository } from './community.repository'; +import { platformRepository } from './platform.repository'; +import { announcementRepository } from './announcement.repository'; +import { moduleRepository } from './module.repository'; export { BaseRepository, userRepository, tokenRepository, - heatMapRepository, - rawInfoRepository, - memberActivityRepository, - guildMemberRepository, - channelRepository, - roleRepository, + makeHeatMapRepository, + makeRawInfoRepository, + makeMemberActivityRepository, + makeGuildMemberRepository, + makeChannelRepository, + makeRoleRepository, communityRepository, platformRepository, announcementRepository, diff --git a/src/repositories/memberActivity.repository.ts b/src/repositories/memberActivity.repository.ts index 121ef93..baaeadf 100644 --- a/src/repositories/memberActivity.repository.ts +++ b/src/repositories/memberActivity.repository.ts @@ -1,13 +1,12 @@ -import { Model } from 'mongoose'; +import { type Connection } from 'mongoose'; + +import { type IMemberActivity } from '../interfaces'; import { BaseRepository } from './base.repository'; -import { IMemberActivity } from '../interfaces'; -import MemberActivity from '../models/memberActivity.model'; export class MemberActivityRepository extends BaseRepository { - constructor(model: Model = MemberActivity) { - super(model); + constructor(connection: Connection) { + super(connection.model('MemberActivity')); } } -export const memberActivityRepository = new MemberActivityRepository(); -export default memberActivityRepository; +export const makeMemberActivityRepository = (connection: Connection) => new MemberActivityRepository(connection); diff --git a/src/repositories/module.repository.ts b/src/repositories/module.repository.ts index 6674942..1f1afdd 100644 --- a/src/repositories/module.repository.ts +++ b/src/repositories/module.repository.ts @@ -1,6 +1,6 @@ -import { Model } from 'mongoose'; +import { type Model } from 'mongoose'; import { BaseRepository } from './base.repository'; -import { IModule } from '../interfaces'; +import { type IModule } from '../interfaces'; import Module from '../models/Module.model'; export class ModuleRepository extends BaseRepository { @@ -10,4 +10,3 @@ export class ModuleRepository extends BaseRepository { } export const moduleRepository = new ModuleRepository(); -export default moduleRepository; diff --git a/src/repositories/platform.repository.ts b/src/repositories/platform.repository.ts index 556f83a..e3f114a 100644 --- a/src/repositories/platform.repository.ts +++ b/src/repositories/platform.repository.ts @@ -1,6 +1,6 @@ -import { Model } from 'mongoose'; +import { type Model } from 'mongoose'; import { BaseRepository } from './base.repository'; -import { IPlatform } from '../interfaces'; +import { type IPlatform } from '../interfaces'; import Platform from '../models/Platfrom.model'; export class PlatformRepository extends BaseRepository { @@ -10,4 +10,3 @@ export class PlatformRepository extends BaseRepository { } export const platformRepository = new PlatformRepository(); -export default platformRepository; diff --git a/src/repositories/rawInfo.repository.ts b/src/repositories/rawInfo.repository.ts index be207fe..ef19fcd 100644 --- a/src/repositories/rawInfo.repository.ts +++ b/src/repositories/rawInfo.repository.ts @@ -1,13 +1,12 @@ -import { Model } from 'mongoose'; +import { type Connection } from 'mongoose'; + +import { type IRawInfo } from '../interfaces'; import { BaseRepository } from './base.repository'; -import { IRawInfo } from '../interfaces'; -import RawInfo from '../models/RawInfo.model'; export class RawInfoRepository extends BaseRepository { - constructor(model: Model = RawInfo) { - super(model); + constructor(connection: Connection) { + super(connection.model('RawInfo')); } } -export const rawInfoRepository = new RawInfoRepository(); -export default rawInfoRepository; +export const makeRawInfoRepository = (connection: Connection) => new RawInfoRepository(connection); diff --git a/src/repositories/role.repository.ts b/src/repositories/role.repository.ts index d40d963..82675b5 100644 --- a/src/repositories/role.repository.ts +++ b/src/repositories/role.repository.ts @@ -1,13 +1,12 @@ -import { Model } from 'mongoose'; +import { type Connection } from 'mongoose'; + +import { type IRole } from '../interfaces'; import { BaseRepository } from './base.repository'; -import { IRole } from '../interfaces'; -import Role from '../models/Role.model'; export class RoleRepository extends BaseRepository { - constructor(model: Model = Role) { - super(model); + constructor(connection: Connection) { + super(connection.model('Role')); } } -export const roleRepository = new RoleRepository(); -export default roleRepository; +export const makeRoleRepository = (connection: Connection) => new RoleRepository(connection); diff --git a/src/repositories/token.repository.ts b/src/repositories/token.repository.ts index e59491a..ad43a5d 100644 --- a/src/repositories/token.repository.ts +++ b/src/repositories/token.repository.ts @@ -1,6 +1,6 @@ -import { Model } from 'mongoose'; +import { type Model } from 'mongoose'; import { BaseRepository } from './base.repository'; -import { IToken } from '../interfaces'; +import { type IToken } from '../interfaces'; import Token from '../models/Token.model'; export class TokenRepository extends BaseRepository { @@ -10,4 +10,3 @@ export class TokenRepository extends BaseRepository { } export const tokenRepository = new TokenRepository(); -export default tokenRepository; diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index 5c2c2a7..1ee70e5 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -1,6 +1,6 @@ -import { Model } from 'mongoose'; +import { type Model } from 'mongoose'; import { BaseRepository } from './base.repository'; -import { IUser } from '../interfaces'; +import { type IUser } from '../interfaces'; import User from '../models/User.model'; export class UserRepository extends BaseRepository { @@ -10,4 +10,3 @@ export class UserRepository extends BaseRepository { } export const userRepository = new UserRepository(); -export default userRepository;