Plugin API Reference
Plugin API Reference
Complete reference for the @ohcnetwork/leaderboard-api package.
Plugin Interface
interface Plugin {
name: string;
version: string;
setup?: (ctx: PluginContext) => Promise<void>;
scrape: (ctx: PluginContext) => Promise<void>;
aggregate?: (ctx: PluginContext) => Promise<void>;
}Properties
name (required)
- Type:
string - Description: Unique identifier for the plugin
- Example:
'github-scraper'
version (required)
- Type:
string - Description: Semantic version of the plugin
- Example:
'1.0.0'
Methods
setup(ctx) (optional)
Initialize the plugin and define activity types.
Parameters:
ctx(PluginContext): Plugin context
Returns: Promise<void>
Example:
async setup(ctx) {
await ctx.db.execute(`
INSERT OR IGNORE INTO activity_definition
(slug, name, description, points, icon)
VALUES (?, ?, ?, ?, ?)
`, ['pr_merged', 'PR Merged', 'Pull request merged', 10, 'git-merge']);
}scrape(ctx) (required)
Fetch data and store activities.
Parameters:
ctx(PluginContext): Plugin context
Returns: Promise<void>
Example:
async scrape(ctx) {
const data = await fetchFromAPI(ctx.config.apiKey);
for (const item of data) {
await ctx.db.execute(`
INSERT OR IGNORE INTO activity (...)
VALUES (...)
`, [...]);
}
}aggregate(ctx) (optional)
Compute plugin-specific aggregates. Called after all plugins have scraped and the main leaderboard aggregation has completed, so plugins can build on top of standard aggregates.
Parameters:
ctx(PluginContext): Plugin context
Returns: Promise<void>
Example:
async aggregate(ctx) {
const contributors = await contributorQueries.getAll(ctx.db);
for (const contributor of contributors) {
const activities = await activityQueries.getByContributor(ctx.db, contributor.username);
const mergedPRs = activities.filter(a => a.activity_definition === 'pr_merged');
await contributorAggregateQueries.upsert(ctx.db, {
aggregate: 'pr_merged_count',
contributor: contributor.username,
value: { type: 'number', value: mergedPRs.length, format: 'integer' },
meta: { calculated_at: new Date().toISOString() },
});
}
}PluginContext
Context object passed to plugin methods.
interface PluginContext {
db: Database;
config: PluginConfig;
orgConfig: OrgConfig;
logger: Logger;
}ctx.db
Database instance for storing data.
Type: Database
Methods:
execute(sql, params)- Execute single SQL statementbatch(statements)- Execute multiple statements in transactionclose()- Close database connection
ctx.config
Plugin-specific configuration from config.yaml.
Type: Record<string, unknown>
Example:
plugins:
github:
config:
token: xxx
org: myorgconst { token, org } = ctx.config;ctx.orgConfig
Organization information.
Type: OrgConfig
Properties:
name- Organization namedescription- Organization descriptionurl- Organization websitelogo_url- Logo URLsocials- Social media links
ctx.logger
Structured logger instance.
Type: Logger
Methods:
debug(message, meta?)- Debug logsinfo(message, meta?)- Info logswarn(message, meta?)- Warning logserror(message, error?, meta?)- Error logs
Database
Database interface for executing SQL queries.
interface Database {
execute(sql: string, params?: unknown[]): Promise<ExecuteResult>;
batch(statements: BatchStatement[]): Promise<BatchResult[]>;
close(): Promise<void>;
}execute(sql, params?)
Execute a single SQL statement.
Parameters:
sql(string): SQL queryparams(unknown[], optional): Query parameters
Returns: Promise<ExecuteResult>
Example:
const result = await ctx.db.execute(
"SELECT * FROM contributor WHERE username = ?",
["alice"],
);
console.log(result.rows); // Array of rows
console.log(result.rowsAffected); // Number of affected rowsbatch(statements)
Execute multiple SQL statements in a transaction.
Parameters:
statements(BatchStatement[]): Array of SQL statements
Returns: Promise<BatchResult[]>
Example:
await ctx.db.batch([
{
sql: 'INSERT INTO activity (...) VALUES (...)',
params: [...]
},
{
sql: 'INSERT INTO activity (...) VALUES (...)',
params: [...]
}
]);Logger
Structured logging interface.
interface Logger {
debug(message: string, meta?: Record<string, unknown>): void;
info(message: string, meta?: Record<string, unknown>): void;
warn(message: string, meta?: Record<string, unknown>): void;
error(message: string, error?: Error, meta?: Record<string, unknown>): void;
}Methods
debug(message, meta?)
Log debug information.
Example:
ctx.logger.debug("Processing item", { id: item.id, type: item.type });info(message, meta?)
Log informational messages.
Example:
ctx.logger.info("Fetched data", { count: items.length });warn(message, meta?)
Log warnings.
Example:
ctx.logger.warn("API rate limit approaching", { remaining: 10 });error(message, error?, meta?)
Log errors.
Example:
try {
await fetchData();
} catch (error) {
ctx.logger.error("Failed to fetch data", error, { retries: 3 });
}Database Schema
contributor
Contributor profiles.
CREATE TABLE contributor (
username VARCHAR PRIMARY KEY,
name VARCHAR,
role VARCHAR,
title VARCHAR,
avatar_url VARCHAR,
bio TEXT,
social_profiles JSON,
joining_date DATE,
meta JSON
);activity_definition
Activity types defined by plugins.
CREATE TABLE activity_definition (
slug VARCHAR PRIMARY KEY,
name VARCHAR NOT NULL,
description TEXT NOT NULL,
points SMALLINT,
icon VARCHAR
);activity
Individual activity records.
CREATE TABLE activity (
slug VARCHAR PRIMARY KEY,
contributor VARCHAR REFERENCES contributor(username) NOT NULL,
activity_definition VARCHAR REFERENCES activity_definition(slug) NOT NULL,
title VARCHAR,
occurred_at TIMESTAMP NOT NULL,
link VARCHAR,
text TEXT,
points SMALLINT,
meta JSON
);Indexes:
idx_activity_occurred_atonoccurred_atidx_activity_contributoroncontributoridx_activity_definitiononactivity_definition
Type Definitions
OrgConfig
interface OrgConfig {
name: string;
description: string;
url: string;
logo_url: string;
start_date?: string;
socials?: {
github?: string;
slack?: string;
linkedin?: string;
youtube?: string;
email?: string;
[key: string]: string | undefined;
};
}Contributor
interface Contributor {
username: string;
name: string | null;
role: string;
title: string | null;
avatar_url: string | null;
bio: string | null;
social_profiles: Record<string, string> | null;
joining_date: string | null;
meta: Record<string, unknown> | null;
}ActivityDefinition
interface ActivityDefinition {
slug: string;
name: string;
description: string;
points: number | null;
icon: string | null;
}Activity
interface Activity {
slug: string;
contributor: string;
activity_definition: string;
title: string | null;
occurred_at: string;
link: string | null;
text: string | null;
points: number | null;
meta: Record<string, unknown> | null;
}Error Handling
Plugins should throw errors for unrecoverable failures:
async scrape(ctx) {
if (!ctx.config.apiKey) {
throw new Error('apiKey is required');
}
try {
await fetchData(ctx);
} catch (error) {
ctx.logger.error('Scrape failed', error);
throw error; // Re-throw to fail the plugin run
}
}The plugin runner will catch and log the error, then exit with a non-zero code.
Best Practices
- Always use parameterized queries to prevent SQL injection
- Use
INSERT OR IGNOREto prevent duplicate activities - Log progress to help with debugging
- Validate configuration before making API calls
- Handle rate limits appropriately
- Use batch inserts for better performance
- Store unique slugs to identify activities
Examples
See the Creating Plugins guide for complete examples.