Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,19 @@
"es2021": true,
"node": true
},
"extends": [
"standard-with-typescript",
"prettier"
],
"extends": ["standard-with-typescript", "prettier"],
"overrides": [],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {},
"ignorePatterns": [
"coverage",
"dist",
"__tests__/",
"jest.config.ts",
"*.yml"
]
}
"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"]
}
6 changes: 0 additions & 6 deletions CHANGELOG.md

This file was deleted.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
64 changes: 36 additions & 28 deletions src/connection.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
public async connect(uri: string): Promise<void> {
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<void> {
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;
}
}
69 changes: 42 additions & 27 deletions src/databaseManager.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -12,55 +20,62 @@ import {
} from './models/schemas';

export default class DatabaseManager {
private static instance: DatabaseManager;
private modelCache: Record<string, boolean> = {};
private static instance: DatabaseManager | undefined;

private readonly modelCache = new Map<string, Promise<void>>();

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<Connection> {
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<Connection> {
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<void> {
if (!this.modelCache[db.name]) {
try {
if (dbType === 'platform') {
db.model<IHeatMap>('HeatMap', heatMapSchema);
db.model<IMemberActivity>('MemberActivity', MemberActivitySchema);
} else if (dbType === 'guild') {
db.model<IRawInfo>('RawInfo', rawInfoSchema);
db.model<IGuildMember>('GuildMember', guildMemberSchema);
db.model<IChannel>('Channel', channelSchema);
db.model<IRole>('Role', roleSchema);
let compilePromise: Promise<void> | undefined = this.modelCache.get(db.name);

if (compilePromise === undefined) {
compilePromise = (async (): Promise<void> => {
try {
if (dbType === 'platform') {
db.model<IHeatMap>('HeatMap', heatMapSchema);
db.model<IMemberActivity>('MemberActivity', MemberActivitySchema);
} else {
db.model<IRawInfo>('RawInfo', rawInfoSchema);
db.model<IGuildMember>('GuildMember', guildMemberSchema);
db.model<IChannel>('Channel', channelSchema);
db.model<IRole>('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<void> {
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);
}
}
}
4 changes: 2 additions & 2 deletions src/interfaces/Module.interface.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
5 changes: 2 additions & 3 deletions src/repositories/announcement.repository.ts
Original file line number Diff line number Diff line change
@@ -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<IAnnouncement> {
Expand All @@ -10,4 +10,3 @@ export class AnnouncementRepository extends BaseRepository<IAnnouncement> {
}

export const announcementRepository = new AnnouncementRepository();
export default announcementRepository;
68 changes: 44 additions & 24 deletions src/repositories/base.repository.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -8,51 +16,63 @@ export interface PaginateOptions {
populate?: string | PopulateOptions | Array<string | PopulateOptions>;
}

interface PaginateResult<T> {
results: Array<LeanDocument<T>>;
page: number;
limit: number;
totalPages: number;
totalResults: number;
}

interface PaginateModel<T> extends Model<T> {
paginate: (filter: FilterQuery<T>, options: PaginateOptions) => Promise<PaginateResult<T>>;
}

export class BaseRepository<T> {
constructor(private readonly model: Model<T>) {}

async create(doc: Partial<T>): Promise<T> {
async create(doc: Partial<T>): Promise<HydratedDocument<T>> {
return await this.model.create(doc);
}

async createMany(docs: Array<Partial<T>>): Promise<T[]> {
async createMany(docs: Array<Partial<T>>): Promise<Array<HydratedDocument<T>>> {
return await this.model.insertMany(docs);
}

async findById(id: string, projection?: ProjectionType<T>, options?: QueryOptions): Promise<T | null> {
return await this.model.findById(id, projection, options);
async findById(id: string, projection?: ProjectionType<T>, options?: QueryOptions): Promise<LeanDocument<T> | null> {
return await this.model.findById(id, projection, options).lean();
}

async find(filter: FilterQuery<T>, projection?: ProjectionType<T>, options?: QueryOptions): Promise<T[]> {
return await this.model.find(filter, projection, options);
async find(
filter: FilterQuery<T>,
projection?: ProjectionType<T>,
options?: QueryOptions,
): Promise<Array<LeanDocument<T>>> {
return await this.model.find(filter, projection, options).lean();
}

async updateOne(
async findOne(
filter: FilterQuery<T>,
update: UpdateQuery<T>,
projection?: ProjectionType<T>,
options?: QueryOptions,
): Promise<{ acknowledged: boolean; modifiedCount: number; upsertedId: unknown }> {
): Promise<LeanDocument<T> | null> {
return await this.model.findOne(filter, projection, options).lean();
}

async updateOne(filter: FilterQuery<T>, update: UpdateQuery<T>, options?: QueryOptions): Promise<any> {
return await this.model.updateOne(filter, update, options);
}

async deleteOne(filter: FilterQuery<T>): Promise<{ deletedCount?: number }> {
async deleteOne(filter: FilterQuery<T>): Promise<any> {
return await this.model.deleteOne(filter);
}

async deleteMany(filter: FilterQuery<T>): Promise<{ deletedCount?: number }> {
async deleteMany(filter: FilterQuery<T>): Promise<any> {
return await this.model.deleteMany(filter);
}

async paginate(
filter: FilterQuery<T>,
options: PaginateOptions,
): Promise<{
results: Array<LeanDocument<T>>;
page: number;
limit: number;
totalPages: number;
totalResults: number;
}> {
return (this.model as any).paginate(filter, options);
async paginate(filter: FilterQuery<T>, options: PaginateOptions): Promise<PaginateResult<T>> {
const modelWithPaginate = this.model as unknown as PaginateModel<T>;
return await modelWithPaginate.paginate(filter, options);
}
}
13 changes: 6 additions & 7 deletions src/repositories/channel.repository.ts
Original file line number Diff line number Diff line change
@@ -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<IChannel> {
constructor(model: Model<IChannel> = Channel) {
super(model);
constructor(connection: Connection) {
super(connection.model<IChannel>('Channel'));
}
}

export const channelRepository = new ChannelRepository();
export default channelRepository;
export const makeChannelRepository = (connection: Connection) => new ChannelRepository(connection);
Loading