Skip to content

Commit

Permalink
feat: add detailed JSDoc comments for Input, Bot, Logger, Replica, an…
Browse files Browse the repository at this point in the history
…d ScriptRunner classes
  • Loading branch information
ptsgrn committed Jan 11, 2025
1 parent 9dcfc26 commit b734d9a
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 7 deletions.
4 changes: 4 additions & 0 deletions core/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export class ServiceBase {
public config = config;
}

/**
* Input service to prompt the user for input
* @extends ServiceBase
*/
export class Input extends ServiceBase {
/**
* Prompt the user for some input from choices
Expand Down
71 changes: 68 additions & 3 deletions core/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Replica } from '@core/replica';
import { Cron, type CronOptions } from 'croner';
import chalk from 'chalk';
import { ServiceBase, Input } from './base';
import { createId } from '@paralleldrive/cuid2';

export class Bot extends ServiceBase {
private _botOptions: MwnOptions = {
Expand All @@ -27,69 +28,133 @@ export class Bot extends ServiceBase {
},
}

/**
* Information about the bot
*/
public info: {
/**
* The ID of the bot. Must be unique.
*/
id: string;
/**
* The name of the script.
*/
name: string;
/**
* A brief description of the script.
*/
description: string;
/**
* The frequency at which the script should run.
*/
frequency?: string;
/**
* The source of the script. Used for tracking purposes.
*/
scriptSource?: string;
/**
* The run ID of the script. Automatically generated.
*/
rid?: string;
}

/**
* The cron job for the bot
* @type {Cron}
*/
public job?: Cron

public bot = new Mwn({
/**
* The bot instance
* @type {Mwn}
*/
public bot: Mwn = new Mwn({
...this._botOptions
});

/**
* The commander CLI instance
*/
public cli = new Command()

/**
* The replica instance
*/
public replica = new Replica()

/**
* The input service
*/
public input = new Input()

constructor() {
super()
this.info = {
id: "bot",
name: "Bot",
description: "A bot"
description: "A bot",
}
this.initBot()
}

/**
* The script description
*/
get scriptDescription() {
return `${chalk.bold(this.info.name)}\n${this.info.description}`
}

/**
* Initialize the bot
*/
private async initBot() {
this.bot = new Mwn(this.botOptions);
this.bot.initOAuth()
await this.bot.getTokensAndSiteInfo()
}

/**
* Set the MWN bot options
*/
set botOptions(options: MwnOptions) {
this._botOptions = options;
this.bot.setOptions(options);
}

get botOptions() {
/**
* Get the MWN bot options
* @returns {MwnOptions}
*/
get botOptions(): MwnOptions {
return this._botOptions;
}

/**
* Run before the main run method
*/
async beforeRun() { }

/**
* The main run method
*/
async run() {
console.error('Please implement run() method')
process.exit(1)
}

/**
* Cleanup after the main run method
*/
async afterRun() {
if (this.replica.conn) {
this.replica.end()
}
}

/**
* Start the bot and wait for scheduled time
* @param options The cron options
*/
async schedule(options: {
pattern?: string | Date;
options?: CronOptions;
Expand Down
13 changes: 13 additions & 0 deletions core/helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
/**
* Converts a human-readable file size string into bytes.
*
* @param size - The human-readable file size string (e.g., '5MB', '1.2GB').
* @returns The size in bytes.
* @throws Will throw an error if the input size format is invalid.
*
* @example
* ```typescript
* humanReadableToBytes('5MB'); // Returns 5242880
* humanReadableToBytes('1.2GB'); // Returns 1288490188.8
* ```
*/
export function humanReadableToBytes(size: string) {
const units = {
B: 1,
Expand Down
22 changes: 20 additions & 2 deletions core/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createLogger, transports, format } from 'winston';
import { createLogger, transports, format, Logger } from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
import { config } from '@core/config';
import chalk from 'chalk';
Expand All @@ -12,7 +12,25 @@ const loggerFormat = format.combine(
format.json(),
)

export const logger = createLogger({
/**
* Logger configuration using `winston` library with multiple transports.
*
* Transports:
* - `DailyRotateFile`: Rotates log files daily with a maximum size of 20MB and keeps logs for 7 days.
* - `File`: Logs error messages to a file with a specified maximum size.
* - `Console`: Logs messages to the console with colorized output.
*
* Exception Handlers:
* - `File`: Logs uncaught exceptions to a file with a specified maximum size.
*
* Rejection Handlers:
* - `File`: Logs unhandled promise rejections to a file with a specified maximum size.
*
* @constant
* @type {Logger}
* @default
*/
export const logger: Logger = createLogger({
level: config.logger.level,
format: loggerFormat,
transports: [
Expand Down
44 changes: 42 additions & 2 deletions core/replica.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { $ } from "bun"
import { ServiceBase } from '@core/base';
import { config } from '@core/config';

/**
* Generates the replica host URL for a given database name and cluster.
*
* @param dbname - The name of the database. If it ends with "_p", the suffix will be removed.
* @param cluster - The cluster name, either "web" or "analytics". Defaults to "web".
* @returns The replica host URL as a string.
* @throws Will throw an error if the cluster name is not "web" or "analytics".
*/
function getReplicaHost(dbname: string, cluster: string = "web") {
if (!['web', 'analytics'].includes(cluster)) {
throw new Error('Invalid cluster name')
Expand All @@ -18,7 +26,6 @@ function getReplicaHost(dbname: string, cluster: string = "web") {
return host
}


export class Replica extends ServiceBase {
private _replicaOptions: mysql.PoolOptions = {
user: this.config.replica.username,
Expand All @@ -27,8 +34,16 @@ export class Replica extends ServiceBase {
waitForConnections: true,
}

/**
* Represents the MySQL connection instance.
* It can be either a `mysql.Connection` object or `null` if the connection is not established.
*/
public conn: mysql.Connection | null = null

/**
* Initializes the replica connection.
* If the script is not running on Toolforge, it will log a warning message.
*/
public async init() {
this.log.debug('Initializing replica connection')
if (!this.isRunOnToolforge()) {
Expand Down Expand Up @@ -57,6 +72,12 @@ export class Replica extends ServiceBase {
}
}

/**
* Creates an SSH tunnel to the replica database via Toolforge.
* @param dbname Wiki's database name to connect to.
* @param cluster Database cluster of the wiki database. Defaults to "web".
* @param port Local port to forward the connection to. Defaults to 3306.
*/
public static async createReplicaTunnel(dbname: string, cluster: string = "web", port: number = 3306) {
if (!config.toolforge.login) {
throw new Error('Please fill in the Toolforge login in this.config.toml')
Expand All @@ -69,10 +90,20 @@ export class Replica extends ServiceBase {
await $`ssh -N ${config.toolforge.login} -L ${port}:${host}:3306`
}

/**
* Checks if the script is running on Toolforge.
* @returns True if the script is running on Toolforge, false otherwise.
*/
private isRunOnToolforge() {
return process.env.TOOLFORGE === "true"
}

/**
* Executes a SQL query on the replica database.
* @param sql SQL query to execute
* @param values Values to bind to the query
* @returns The result of the query
*/
public async query(sql: string, values: any[] = []) {
if (!this.conn) {
this.log.debug('Replica connection not initialized, initializing...')
Expand All @@ -86,7 +117,16 @@ export class Replica extends ServiceBase {
return this.conn?.end()
}


/**
* Sets the replica options and reinitializes the connection.
* @param options The replica options to set
* @example
* ```typescript
* replica.setReplicaOptions({
* host: 'enwiki.web.db.svc.wikimedia.cloud',
* });
* ```
*/
set replicaOptions(options: mysql.PoolOptions) {
this.log.debug('Setting replica options')
this._replicaOptions = {
Expand Down
5 changes: 5 additions & 0 deletions core/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import { ServiceBase } from './base'
import { readdir } from 'node:fs/promises'
import { join, resolve } from 'node:path'

/**
* The main script runner for the bot
* @extends ServiceBase
*/
export class ScriptRunner extends ServiceBase {
private cli = new Command()

public scheduled: Record<string, Bot> = {}

async scriptModule(scriptName: string) {
Expand Down

0 comments on commit b734d9a

Please sign in to comment.