MiniORM Documentation

Installation

To install MiniORM, add it to your project using npm:

terminal
npm install miniorm
📌
MiniORM requires the database driver packages for the databases you intend to use.

Required Dependencies

terminal
# For MySQL
npm install mysql2

# For PostgreSQL
npm install pg

# For SQLite
npm install sqlite3

Connection method

The connection method establishes a database connection with the specified name and configuration. It supports MySQL, PostgreSQL, and SQLite databases.

Connection Examples

connection-examples.ts
import { MiniOrm } from 'miniorm';

// MySQL connection
MiniOrm.connection('mysql_db', {
    type: 'mysql',
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'my_database',
    port: 3306,
    ping: 30000 // Optional ping interval in ms
});

// PostgreSQL connection
MiniOrm.connection('pg_db', {
    type: 'postgresql',
    host: 'localhost',
    user: 'postgres',
    password: 'password',
    database: 'my_database',
    port: 5432,
    ssl: false
});

// SQLite connection
MiniOrm.connection('sqlite_db', {
    type: 'sqlite',
    path: './database.sqlite'
});
📌
The connection method automatically handles reconnection if the ping option is specified. Each connection is stored with a unique name that can be referenced in other operations.

runQuery method

The runQuery method executes SQL queries on a specified database connection. It automatically handles the query execution based on the database type.

runQuery Examples

query-examples.ts
import { MiniOrm } from 'miniorm';

// Setup connections
MiniOrm.connection('mysql_db', {
    type: 'mysql',
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'my_database'
});

// Basic SELECT query
const users = await MiniOrm.runQuery('mysql_db', 'SELECT * FROM users', []);

// Query with parameters
const user = await MiniOrm.runQuery(
    'mysql_db',
    'SELECT * FROM users WHERE id = ?',
    [5]
);

// INSERT query with parameters
await MiniOrm.runQuery(
    'mysql_db',
    'INSERT INTO users (name, email) VALUES (?, ?)',
    ['John Doe', 'john@example.com']
);

// Silent query execution (no logs)
await MiniOrm.runQuery('mysql_db false', 'SELECT 1', []);
📌
The runQuery method handles different database types automatically. You can append " false" to the database name to suppress logs for operations like health checks.

disconnect method

The disconnect method closes a database connection properly, ensuring resources are released.

disconnect Examples

disconnect-examples.ts
import { MiniOrm, EventManager } from 'miniorm';

// Setup connection
MiniOrm.connection('temp_db', {
    type: 'mysql',
    host: 'localhost',
    user: 'root',
    database: 'temp'
});

// Run some queries
await MiniOrm.runQuery('temp_db', 'SELECT * FROM logs', []);

// Close the connection when done
await MiniOrm.disconnect('temp_db');

// Listen for disconnect events
EventManager.on('disconnect-temp_db', () => {
    console.log('Database disconnected, cleaning up resources...');
});
📌
The disconnect method emits events that can be captured to perform cleanup operations. Add " false" to the database name to suppress disconnect logs.

reconnect method

The reconnect method re-establishes a connection to a database that was previously disconnected or failed.

reconnect Examples

reconnect-examples.ts
import { MiniOrm } from 'miniorm';

// Setup connection
MiniOrm.connection('analytics_db', {
    type: 'postgresql',
    host: 'localhost',
    database: 'analytics',
    ping: 60000 // Auto-ping every minute
});

// Manually reconnect after network issue
try {
    await MiniOrm.runQuery('analytics_db', 'SELECT * FROM events', []);
} catch (error) {
    console.log('Connection lost, attempting to reconnect...');
    await MiniOrm.reconnect('analytics_db');
}

// Implementing custom reconnection logic
async function ensureConnection(dbName) {
    try {
        const result = await MiniOrm.runQuery(dbName + ' false', 'SELECT 1', []);
        if (!result) await MiniOrm.reconnect(dbName);
    } catch (err) {
        await MiniOrm.reconnect(dbName);
    }
}
📌
The reconnect method is automatically used by the ping mechanism if enabled, but can also be called manually to recover from connection failures.

TableBuilder Classes

MiniOrm provides two table builder classes with different approaches to creating database tables: FluentTableBuilder and DeclarativeTableBuilder. Both support MySQL, PostgreSQL, and SQLite with automatic SQL syntax adaptation.

Constructor

tablebuilder-constructor.ts
import { DeclarativeTableBuilder, FluentTableBuilder } from 'miniorm';

// Create a new table builder for a specific table and database
const usersTable = new DeclarativeTableBuilder('users', 'main_db');
const productsTable = new FluentTableBuilder('products', 'inventory_db');
📌
Both builders require a table name and database name. The database must be previously connected using MiniOrm.connection().

FluentTableBuilder

The FluentTableBuilder provides a chainable API for defining tables with a more natural language-like syntax.

Column Methods

fluent-columns.ts
import { FluentTableBuilder } from 'miniorm';

const table = new FluentTableBuilder('users', 'app_db')
    // Auto-incrementing primary key
    .id()
    // String column with length
    .string('username', 100)
    .notNULL()
    .unique()
    // Integer column
    .integer('age', 3)
    .notNULL()
    // Boolean column
    .bool('active')
    .default(true)
    // Enum column
    .enum('role', ['user', 'admin', 'moderator'])
    .default('user')
    // Add created_at and updated_at columns
    .timestamps();
📌
The FluentTableBuilder provides specific methods for common column types and constraints. The id() method is a shortcut for creating an auto-incrementing primary key.

DeclarativeTableBuilder

The DeclarativeTableBuilder uses a declarative approach where you define columns with configuration objects.

Column Method

declarative-columns.ts
import { DeclarativeTableBuilder, DatabaseColumnType } from 'miniorm';

const table = new DeclarativeTableBuilder('users', 'app_db')
    .column({
        name: 'id',
        type: DatabaseColumnType.INTEGER,
        primaryKey: true,
        autoIncrement: true
    })
    .column({
        name: 'username',
        type: DatabaseColumnType.VARCHAR,
        length: 100,
        unique: true,
        notNull: true
    })
    .column({
        name: 'email',
        type: DatabaseColumnType.VARCHAR,
        length: 191,
        unique: true,
        notNull: true
    })
    .column({
        name: 'role',
        type: DatabaseColumnType.VARCHAR,
        enum: ['user', 'admin', 'moderator'],
        defaultValue: 'user'
    });
📌
The column method accepts a configuration object with properties like name, type, length, primaryKey, unique, notNull, and defaultValue.

set Method

Both builders support the set method to define multiple tables in a single chain.

FluentTableBuilder Example

fluent-set.ts
import { FluentTableBuilder } from 'miniorm';

const builder = new FluentTableBuilder('users', 'app_db')
    .id()
    .string('name', 100)
    .notNULL()
    // Switch to a new table in the same database
    .set({ table: 'posts' })
    .id()
    .string('title', 200)
    .notNULL()
    .integer('user_id', 11)
    // Switch to a new table in a different database
    .set({ table: 'logs', db: 'analytics_db' })
    .id()
    .string('event', 100);

DeclarativeTableBuilder Example

declarative-set.ts
import { DeclarativeTableBuilder, DatabaseColumnType } from 'miniorm';

const builder = new DeclarativeTableBuilder('users', 'app_db')
    .column({
        name: 'id',
        type: DatabaseColumnType.INTEGER,
        primaryKey: true
    })
    .column({
        name: 'name',
        type: DatabaseColumnType.VARCHAR,
        length: 100
    })
    // Switch to a new table
    .set('posts')
    .column({
        name: 'id',
        type: DatabaseColumnType.INTEGER,
        primaryKey: true
    })
    // Switch to a new database
    .set('logs', 'analytics_db')
    .column({
        name: 'id',
        type: DatabaseColumnType.INTEGER,
        primaryKey: true
    });

build Method

The build method executes the CREATE TABLE statements for all defined tables.

build-example.ts
import { MiniOrm, FluentTableBuilder } from 'miniorm';

// Setup database connections
MiniOrm.connection('app_db', {
    type: 'mysql',
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'app'
});

// Define tables
const builder = new FluentTableBuilder('users', 'app_db')
    .id()
    .string('email', 191)
    .unique()
    .notNULL()
    .timestamps();

// Create tables
async function setupDatabase() {
    await builder.build();
    console.log('Tables created successfully');
}

setupDatabase();
📌
The build method checks if tables exist before creating them, making it safe to run multiple times. It automatically adapts SQL syntax for different database types.

checkTableExists Method

Both builders provide a static method to check if a table exists in a database.

check-table-exists.ts
import { MiniOrm, FluentTableBuilder } from 'miniorm';

// Setup database connection
MiniOrm.connection('app_db', {
    type: 'mysql',
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'app'
});

async function checkAndCreateTables() {
    const db = MiniOrm.dbs['app_db'].db;
    const dbType = MiniOrm.dbs['app_db'].config.type;

    // Check if users table exists
    const usersExists = await FluentTableBuilder.checkTableExists(
        db,
        dbType,
        'users'
    );

    console.log('Users table exists:', usersExists);

    // Create table only if it doesn't exist
    if (!usersExists) {
        const builder = new FluentTableBuilder('users', 'app_db')
            .id()
            .string('name', 100)
            .notNULL();

        await builder.build();
    }
}

checkAndCreateTables();
📌
The checkTableExists method works with MySQL, PostgreSQL, and SQLite databases and is useful for conditional table creation or migrations.

QueryBuilder Class

The QueryBuilder class provides a fluent interface for building and executing SQL queries across different database types. It supports MySQL, PostgreSQL, and SQLite with automatic query adaptation and SQL injection protection.

Constructor

querybuilder-constructor.ts
import { QueryBuilder, BaseEntityClass } from 'miniorm';

// Create a query builder with table name
const usersQuery = new QueryBuilder('main_db', 'users');

// Create a query builder with entity class
class User extends BaseEntityClass {
    static $table = 'users'; // Optional custom table name
    id!: number;
    name!: string;
    email!: string;
}

const userQuery = new QueryBuilder('main_db', User);
📌
The QueryBuilder constructor requires a database name and either a table name string or an entity class. Entity classes can define a custom table name with the static $table property.

select method

The select method starts a SELECT query and optionally specifies which fields to retrieve.

Select Examples

select-examples.ts
import { QueryBuilder } from 'miniorm';

// Select all fields
const allUsers = new QueryBuilder('main_db', 'users')
    .select()
    .run();

// Select specific fields
const userNames = new QueryBuilder('main_db', 'users')
    .select(['id', 'name', 'email'])
    .run();

// Select with type safety using entity class
interface User {
    id: number;
    name: string;
    email: string;
    created_at: Date;
}

const activeUsers = new QueryBuilder<User>('main_db', 'users')
    .select(['id', 'name', 'email'])
    .where({ active: true })
    .run();
📌
The select method can be called without arguments to select all fields (*) or with an array of field names to select specific columns. Using generic types provides type safety for field names.

where method

The where method adds WHERE conditions to filter query results with protection against SQL injection.

Where Examples

where-examples.ts
import { QueryBuilder } from 'miniorm';

// Simple equality condition
const activeUsers = new QueryBuilder('main_db', 'users')
    .select()
    .where({ active: true })
    .run();

// Multiple conditions (AND)
const adminUsers = new QueryBuilder('main_db', 'users')
    .select()
    .where({ role: 'admin', active: true })
    .run();

// Using operators
const recentUsers = new QueryBuilder('main_db', 'users')
    .select()
    .where({ 'created_at >': '2023-01-01' })
    .run();

// OR conditions
const specialUsers = new QueryBuilder('main_db', 'users')
    .select()
    .where({ role: 'admin', email: 'test@example.com', _OR: true })
    .run();

// IN operator
const specificUsers = new QueryBuilder('main_db', 'users')
    .select()
    .where({ 'id IN ': [1, 2, 3, 4] })
    .run();

// LIKE operator
const searchUsers = new QueryBuilder('main_db', 'users')
    .select()
    .where({ 'name LIKE ': '%john%' })
    .run();

// IS NULL
const noEmail = new QueryBuilder('main_db', 'users')
    .select()
    .where({ 'email IS ': 'NULL' })
    .run();
📌
The where method supports various operators including =, >, <, >=, <=, !=, LIKE, IN, and IS NULL. Multiple conditions are combined with AND by default, but can be switched to OR using the _OR flag.

order, limit, and offset methods

These methods control the sorting and pagination of query results.

Examples

pagination-examples.ts
import { QueryBuilder } from 'miniorm';

// Order results
const oldestFirst = new QueryBuilder('main_db', 'users')
    .select()
    .order(['created_at'], 'ASC')
    .run();

const newestFirst = new QueryBuilder('main_db', 'users')
    .select()
    .order(['created_at'], 'DESC')
    .run();

// Multiple order fields
const sortedUsers = new QueryBuilder('main_db', 'users')
    .select()
    .order(['role', 'created_at'], 'DESC')
    .run();

// Limit results (pagination)
const firstPage = new QueryBuilder('main_db', 'users')
    .select()
    .limit(10)
    .run();

// Limit with offset (pagination)
const secondPage = new QueryBuilder('main_db', 'users')
    .select()
    .limit(10)
    .offset(10)
    .run();

// Combining all together
const paginatedSorted = new QueryBuilder('main_db', 'users')
    .select(['id', 'name', 'email'])
    .where({ active: true })
    .order(['created_at'], 'DESC')
    .limit(10)
    .offset(20)
    .run();
📌
These methods can be chained in any order and the QueryBuilder will construct a valid SQL query. The limit and offset methods are particularly useful for implementing pagination in your application.

Aggregation methods

The QueryBuilder provides methods for common SQL aggregation functions: count, max, min, avg, and sum.

Aggregation Examples

aggregation-examples.ts
import { QueryBuilder } from 'miniorm';

// Count all records
const totalUsers = await new QueryBuilder('main_db', 'users')
    .count();

// Count with conditions
const activeCount = await new QueryBuilder('main_db', 'users')
    .where({ active: true })
    .count();

// Count specific field (non-null values)
const emailCount = await new QueryBuilder('main_db', 'users')
    .count('email');

// Maximum value
const highestScore = await new QueryBuilder('main_db', 'scores')
    .max('points');

// Minimum value
const lowestScore = await new QueryBuilder('main_db', 'scores')
    .min('points');

// Average value
const averageScore = await new QueryBuilder('main_db', 'scores')
    .avg('points');

// Sum values
const totalPoints = await new QueryBuilder('main_db', 'scores')
    .sum('points');

// With conditions
const teamPoints = await new QueryBuilder('main_db', 'scores')
    .where({ team_id: 5 })
    .sum('points');
📌
Aggregation methods return a single numeric value and can be combined with where conditions. These methods execute the query immediately and return the result as a number.

insert, update, and delete methods

These methods handle data modification operations with SQL injection protection.

Data Modification Examples

modification-examples.ts
import { QueryBuilder } from 'miniorm';

// Insert a new record
const insertResult = await new QueryBuilder('main_db', 'users')
    .insert({
        name: 'John Doe',
        email: 'john@example.com',
        active: true,
        created_at: new Date()
    });

// Update records
const updateResult = await new QueryBuilder('main_db', 'users')
    .update({ active: false })
    .where({ 'last_login <': '2023-01-01' })
    .run();

// Delete records
const deleteResult = await new QueryBuilder('main_db', 'users')
    .delete()
    .where({ 'created_at <': '2022-01-01' })
    .run();

// Insert with default values (if table has defaults)
const defaultInsert = await new QueryBuilder('main_db', 'logs')
    .insert({});
📌
The insert method returns a boolean indicating success, while update and delete operations need to be completed with run(). All methods protect against SQL injection by using parameterized queries.

first, end, and exists methods

These methods provide shortcuts for common query patterns.

Examples

shortcut-examples.ts
import { QueryBuilder } from 'miniorm';

// Get first record
const firstUser = await new QueryBuilder('main_db', 'users')
    .select()
    .order(['id'], 'ASC')
    .first();

// Get last record
const lastUser = await new QueryBuilder('main_db', 'users')
    .select()
    .order(['id'], 'ASC')
    .end();

// Check if records exist
const hasAdmins = await new QueryBuilder('main_db', 'users')
    .where({ role: 'admin' })
    .exists();

// Combining with other conditions
const hasActiveAdmins = await new QueryBuilder('main_db', 'users')
    .where({ role: 'admin', active: true })
    .exists();
📌
The first() method returns the first record matching the query, end() returns the last record, and exists() returns a boolean indicating if any matching records exist. These methods execute the query immediately and return the result.

pluck and between methods

The pluck method extracts a single column from query results, while between creates a range condition.

Examples

pluck-between-examples.ts
import { QueryBuilder } from 'miniorm';

// Get all user IDs
const userIds = await new QueryBuilder('main_db', 'users')
    .pluck('id');

// Get emails of active users
const activeEmails = await new QueryBuilder('main_db', 'users')
    .where({ active: true })
    .pluck('email');

// Find users with scores in a range
const mediumScores = await new QueryBuilder('main_db', 'scores')
    .between('points', 50, 75);

// Combine with other methods
const recentUserIds = await new QueryBuilder('main_db', 'users')
    .where({ active: true })
    .order(['created_at'], 'DESC')
    .limit(10)
    .pluck('id');
📌
The pluck method returns an array containing only the values from the specified column. The between method is a shortcut for creating a range condition and returns the matching records.

debug and raw methods

The debug method helps with query troubleshooting, while raw allows executing custom SQL queries.

Examples

debug-raw-examples.ts
import { QueryBuilder } from 'miniorm';

// Debug current query
new QueryBuilder('main_db', 'users')
    .select(['id', 'name'])
    .where({ active: true })
    .order(['created_at'], 'DESC')
    .debug('now');

// Debug final query (at execution time)
const result = await new QueryBuilder('main_db', 'users')
    .select(['id', 'name'])
    .where({ active: true })
    .debug('final')
    .run();

// Custom debug handler
const customDebug = await new QueryBuilder('main_db', 'users')
    .select()
    .debug('final', (sql, data) => {
        console.log('SQL to execute:', sql);
        console.log('Parameters:', data);
        // Log to file, send to monitoring service, etc.
    })
    .run();

// Execute raw SQL
const rawResult = await new QueryBuilder('main_db', 'users')
    .raw('SELECT COUNT(*) FROM <table> WHERE created_at > ?', ['2023-01-01']);
📌
The debug method can show the query either immediately ('now') or at execution time ('final'). The raw method allows executing custom SQL with the placeholder replaced by the current table name.

clone method

The clone method creates a copy of the current query builder, optionally targeting a different database or table.

Clone Examples

clone-examples.ts
import { QueryBuilder, BaseEntityClass } from 'miniorm';

// Create a base query
const baseQuery = new QueryBuilder('main_db', 'users')
    .select(['id', 'name', 'email'])
    .where({ active: true });

// Clone for the same table
const adminQuery = baseQuery.clone()
    .where({ role: 'admin' });

// Clone for a different table
const archivedQuery = baseQuery.clone({ table: 'archived_users' });

// Clone for a different database
const analyticsQuery = baseQuery.clone({ db: 'analytics_db' });

// Clone with entity class
class User extends BaseEntityClass {
    id!: number;
    name!: string;
    email!: string;
}

class AdminUser extends BaseEntityClass {
    static $table = 'admin_users';
    id!: number;
    name!: string;
    permissions!: string[];
}

const userQuery = new QueryBuilder('main_db', User)
    .select(['id', 'name']);

// Clone with different entity
const adminUserQuery = userQuery.clone<AdminUser>({ table: AdminUser });
📌
The clone method is useful for creating variations of a base query without modifying the original. It can target different tables or databases while preserving the query structure.

Connection Management

The QueryBuilder provides methods for managing database connections.

Connection Examples

connection-examples.ts
import { MiniOrm, QueryBuilder } from 'miniorm';

// Setup database connection
MiniOrm.connection('main_db', {
    type: 'mysql',
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'main'
});

// Disconnect from database
const query = new QueryBuilder('main_db', 'users');
await query.disconnect();

// Reconnect to database
await query.reconnect();

// Run query after reconnection
const users = await query
    .select()
    .where({ active: true })
    .run();
📌
The disconnect and reconnect methods allow managing database connections directly from the QueryBuilder instance. This is useful for long-running applications that need to manage connection lifecycles.

toSQL method

The toSQL method returns the SQL query string and parameter values without executing the query.

toSQL Examples

tosql-examples.ts
import { QueryBuilder } from 'miniorm';

// Build a query
const query = new QueryBuilder('main_db', 'users')
    .select(['id', 'name', 'email'])
    .where({ active: true })
    .order(['created_at'], 'DESC')
    .limit(10);

// Get SQL and parameters
const [sql, params] = query.toSQL();

console.log('SQL Query:', sql);
console.log('Parameters:', params);

// Useful for logging or custom execution
// Example output:
// SQL Query: SELECT id,name,email FROM users WHERE active = ? ORDER BY created_at DESC LIMIT 10
// Parameters: [true]
📌
The toSQL method is useful for debugging, logging, or when you need to get the query without executing it. It returns a tuple with the SQL string and an array of parameter values.

SQL Injection Protection

The QueryBuilder includes robust protection against SQL injection attacks.

Security Features

security-examples.ts
import { QueryBuilder } from 'miniorm';

// All user inputs are automatically parameterized
const safeQuery = new QueryBuilder('main_db', 'users')
    .select()
    .where({
        username: userInput, // Safe: value will be parameterized
        'email LIKE': `%${userInput}%` // Safe: value will be parameterized
    });

// The isIgnoreField and isIgnoreValue methods check for SQL injection attempts
try {
    const riskyQuery = new QueryBuilder('main_db', 'users')
        .select()
        .where({
            username: "admin'; DROP TABLE users; --" // Will be caught
        });
} catch (error) {
    console.error('SQL injection attempt detected:', error.message);
}

// The QueryBuilder blocks common SQL injection patterns
// - SQL comments (-- or /* */)
// - Multiple statements (;)
// - SQL keywords (UNION, SELECT, DROP, etc.)
// - Hex encoded values (0x...)
// - Unicode control characters
// - And many more
📌
The QueryBuilder uses parameterized queries and input validation to protect against SQL injection attacks. It checks all field names and values against a blacklist of suspicious patterns and throws errors when potential attacks are detected.

BaseEntityClass

The BaseEntityClass provides a foundation for creating strongly-typed entity classes that work with QueryBuilder.

Entity Examples

entity-examples.ts
import { BaseEntityClass, QueryBuilder } from 'miniorm';

// Basic entity class
class User extends BaseEntityClass<User> {
    // Table name defaults to lowercase class name ('user')
    id!: number;
    username!: string;
    email!: string;
    active: boolean = true;
    created_at!: Date;
}

// Entity with custom table name
class AdminUser extends BaseEntityClass<AdminUser> {
    static $table = 'administrators'; // Custom table name

    id!: number;
    username!: string;
    permissions!: string[];
}

// Using entities with QueryBuilder
const userQuery = new QueryBuilder<User>('main_db', User);
const adminQuery = new QueryBuilder<AdminUser>('main_db', AdminUser);

// Type-safe queries
const activeUsers = await userQuery
    .select(['id', 'username', 'email']) // TypeScript checks field names
    .where({ active: true })
    .run();

// The result is typed as User[]
activeUsers.forEach(user => {
    console.log(user.username); // TypeScript knows these properties exist
});
📌
The BaseEntityClass provides a foundation for type-safe database operations. Entity classes can define a custom table name with the static $table property, otherwise the table name defaults to the lowercase class name.

Practical Examples

Here are some real-world examples showing how to use QueryBuilder for common database operations.

User Authentication

auth-example.ts
import { QueryBuilder } from 'miniorm';

async function authenticateUser(email: string, password: string) {
    const user = await new QueryBuilder('main_db', 'users')
        .select(['id', 'name', 'email', 'password_hash', 'role'])
        .where({ email: email })
        .first();

    if (!user) {
        return { success: false, message: 'User not found' };
    }

    // Verify password (using a hypothetical password verification function)
    const passwordValid = await verifyPassword(password, user.password_hash);

    if (!passwordValid) {
        return { success: false, message: 'Invalid password' };
    }

    // Update last login timestamp
    await new QueryBuilder('main_db', 'users')
        .update({ last_login: new Date() })
        .where({ id: user.id })
        .run();

    // Return user without password hash
    const { password_hash, ...safeUser } = user;
    return { success: true, user: safeUser };
}

Pagination

pagination-example.ts
import { QueryBuilder } from 'miniorm';

async function getPaginatedUsers(page: number, pageSize: number, filters: any = {}) {
    const offset = (page - 1) * pageSize;

    // Get total count for pagination metadata
    const totalCount = await new QueryBuilder('main_db', 'users')
        .where(filters)
        .count();

    // Get page of users
    const users = await new QueryBuilder('main_db', 'users')
        .select(['id', 'name', 'email', 'role', 'created_at'])
        .where(filters)
        .order(['created_at'], 'DESC')
        .limit(pageSize)
        .offset(offset)
        .run();

    return {
        data: users,
        pagination: {
            page,
            pageSize,
            totalCount,
            totalPages: Math.ceil(totalCount / pageSize),
            hasNextPage: page * pageSize < totalCount,
            hasPrevPage: page > 1
        }
    };
}

Complex Reporting

reporting-example.ts
import { QueryBuilder } from 'miniorm';

async function getMonthlyStats(year: number, month: number) {
    // Format date for SQL comparison
    const startDate = `${year}-${month.toString().padStart(2, '0')}-01`;
    const endDate = month === 12
        ? `${year + 1}-01-01`
        : `${year}-${(month + 1).toString().padStart(2, '0')}-01`;

    // Get new user count
    const newUsers = await new QueryBuilder('main_db', 'users')
        .where({ 'created_at >=': startDate, 'created_at <': endDate })
        .count();

    // Get active user count
    const activeUsers = await new QueryBuilder('main_db', 'user_logins')
        .where({ 'login_time >=': startDate, 'login_time <': endDate })
        .pluck('user_id')
        .then(ids => new Set(ids).size);

    // Get revenue stats
    const totalRevenue = await new QueryBuilder('main_db', 'orders')
        .where({ 'created_at >=': startDate, 'created_at <': endDate, status: 'completed' })
        .sum('amount');

    // Get top products
    const topProducts = await new QueryBuilder('main_db', 'order_items')
        .raw(`
            SELECT p.name, p.id, SUM(oi.quantity) as total_sold
            FROM <table> oi
            JOIN products p ON oi.product_id = p.id
            JOIN orders o ON oi.order_id = o.id
            WHERE o.created_at >= ? AND o.created_at < ? AND o.status = ?
            GROUP BY p.id
            ORDER BY total_sold DESC
            LIMIT 5
        `, [startDate, endDate, 'completed']);

    return {
        period: { year, month },
        newUsers,
        activeUsers,
        totalRevenue,
        topProducts
    };
}
📌
These examples demonstrate how QueryBuilder can be used to implement common application features with clean, maintainable code that's protected against SQL injection.

Model Class

The Model class provides a higher-level abstraction over QueryBuilder, offering a simpler API for common database operations.

Basic Usage

model-basic.ts
import { Model, BaseEntityClass } from 'miniorm';

// Define an entity class
class User extends BaseEntityClass<User> {
    id!: number;
    name!: string;
    email!: string;
    active: boolean = true;
}

// Create a model instance
const userModel = new Model('main_db', User);

// Or with a string table name
const postsModel = new Model('main_db', 'posts');
📌
The Model class wraps a QueryBuilder instance and provides simplified methods for common database operations. It can be initialized with either an entity class or a table name string.

Find Methods

The Model class provides several methods for retrieving data with a simplified API.

Find Examples

model-find.ts
import { Model, BaseEntityClass } from 'miniorm';

class User extends BaseEntityClass<User> {
    id!: number;
    name!: string;
    email!: string;
    active: boolean = true;
}

const userModel = new Model<User>('main_db', 'user');

// Find all users
const allUsers = await userModel.findAll();

// Find all users with specific fields
const userNames = await userModel.findAll(['id', 'name']);

// Find with conditions
const activeUsers = await userModel.find({
    fields: ['id', 'name', 'email'],
    where: [{ active: true }],
    order: ['created_at', 'DESC'],
    limit: 10,
    offset: 0
});

// Find a single user
const firstUser = await userModel.findOne({
    where: [{ active: true }],
    order: ['id', 'ASC']
});

// Shorthand for findOne
const user = await userModel.first({
    where: [{ email: 'test@example.com' }]
});
📌
The find methods accept an options object that can include fields, where conditions, ordering, limit, and offset. This provides a more structured approach compared to the chained methods of QueryBuilder.

CRUD Operations

The Model class simplifies create, update, and delete operations.

CRUD Examples

model-crud.ts
import { Model, BaseEntityClass } from 'miniorm';

class User extends BaseEntityClass<User> {
    id!: number;
    name!: string;
    email!: string;
    active: boolean = true;
}

const userModel = new Model<User>('main_db', User);

// Create a new user
const created = await userModel.create({
    name: 'John Doe',
    email: 'john@example.com',
    active: true
});

// Update users
const updated = await userModel.update(
    { active: false },
    { 'last_login <': '2023-01-01' }
);

// Delete users
const deleted = await userModel.delete(
    { 'created_at <': '2022-01-01' }
);
📌
The CRUD methods provide a simpler interface than the QueryBuilder equivalents, while still offering the same functionality. The update and delete methods accept where conditions directly as arguments.

Aggregation Methods

The Model class provides direct access to common SQL aggregation functions.

Aggregation Examples

model-aggregation.ts
import { Model } from 'miniorm';

const userModel = new Model('main_db', 'users');
const scoreModel = new Model('main_db', 'scores');

// Count all records
const totalUsers = await userModel.count();

// Count specific field (non-null values)
const emailCount = await userModel.count('email');

// Maximum value
const highestScore = await scoreModel.max('points');

// Minimum value
const lowestScore = await scoreModel.min('points');

// Average value
const averageScore = await scoreModel.avg('points');

// Sum values
const totalPoints = await scoreModel.sum('points');
📌
The aggregation methods provide direct access to SQL's COUNT, MAX, MIN, AVG, and SUM functions. These methods execute the query immediately and return the numeric result.

Utility Methods

The Model class includes utility methods for common database operations.

Utility Examples

model-utility.ts
import { Model } from 'miniorm';

const userModel = new Model('main_db', 'users');

// Check if records exist
const hasAdmins = await userModel.exists({ role: 'admin' });

// Get values from a single column
const emails = await userModel.pluck('email');

// Execute raw SQL
const result = await userModel.raw(
    'SELECT COUNT(*) FROM <table> WHERE created_at > ?',
    ['2023-01-01']
);

// Access the underlying query builder for complex queries
const complexQuery = userModel.query()
    .select(['id', 'name'])
    .where({ active: true })
    .order(['created_at'], 'DESC')
    .limit(10);

const results = await complexQuery.run();
📌
The utility methods provide shortcuts for common operations like checking existence and extracting column values. The query() method gives access to the full QueryBuilder API for more complex queries.

Connection Management

The Model class includes methods for managing database connections.

Connection Examples

model-connection.ts
import { Model, MiniOrm } from 'miniorm';

// Setup database connection
MiniOrm.connection('main_db', {
    type: 'mysql',
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'app'
});

// Create model
const userModel = new Model('main_db', 'users');

// Disconnect from database
await userModel.disconnect();

// Reconnect to database
await userModel.reconnect();

// Run query after reconnection
const users = await userModel.find({
    where: [{ active: true }]
});
📌
The disconnect and reconnect methods allow managing database connections directly from the Model instance. This is useful for long-running applications that need to manage connection lifecycles.

Debugging

The Model class provides a debug method for troubleshooting queries.

Debug Example

model-debug.ts
import { Model } from 'miniorm';

const userModel = new Model('main_db', 'users');

// Debug the current query
userModel.debug('now').find({
    fields: ['id', 'name'],
    where: [{ active: true }],
    order: ['created_at', 'DESC'],
    limit: 10
});

// Debug the final query at execution time
const result = await userModel.debug('final').find({
    where: [{ role: 'admin' }]
});

// Custom debug handler
const customDebug = await userModel.debug('final', (sql, data) => {
    console.log('SQL to execute:', sql);
    console.log('Parameters:', data);
    // Log to file, send to monitoring service, etc.
}).findAll();
📌
The debug method works the same way as in QueryBuilder, showing the query either immediately ('now') or at execution time ('final'). It can be used with any of the Model's query methods.

Complete Example

This example demonstrates a complete workflow using the Model class.

Repository Pattern Example

model-repository.ts
import { MiniOrm, Model, BaseEntityClass } from 'miniorm';

// 1. Define entity class
class User extends BaseEntityClass<User> {
    id!: number;
    username!: string;
    email!: string;
    active: boolean = true;
    created_at!: Date;
}

// 2. Create a repository class
class UserRepository {
    private model: Model<User>;

    constructor() {
        this.model = new Model<User>('app_db', User);
    }

    async findById(id: number): Promise<User | undefined> {
        return await this.model.findOne({
            where: [{ id }]
        });
    }

    async findByEmail(email: string): Promise<User | undefined> {
        return await this.model.findOne({
            where: [{ email }]
        });
    }

    async getActiveUsers(page: number = 1, pageSize: number = 10): Promise<User[]> {
        return await this.model.find({
            where: [{ active: true }],
            order: ['created_at', 'DESC'],
            limit: pageSize,
            offset: (page - 1) * pageSize
        });
    }

    async createUser(userData: Omit<User, 'id' | 'created_at'>): Promise<boolean> {
        return await this.model.create(userData);
    }

    async updateUser(id: number, userData: Partial<User>): Promise<any> {
        return await this.model.update(userData, { id });
    }

    async deactivateUser(id: number): Promise<any> {
        return await this.model.update({ active: false }, { id });
    }

    async deleteUser(id: number): Promise<any> {
        return await this.model.delete({ id });
    }

    async getUserCount(onlyActive: boolean = false): Promise<number> {
        if (onlyActive) {
            return await this.model.query()
                .where({ active: true })
                .count();
        }
        return await this.model.count();
    }
}

// 3. Application usage
async function main() {
    // Setup database connection
    MiniOrm.connection('app_db', {
        type: 'mysql',
        host: 'localhost',
        user: 'app_user',
        password: 'secure_password',
        database: 'application'
    });

    const userRepo = new UserRepository();

    // Create a user
    await userRepo.createUser({
        username: 'john_doe',
        email: 'john@example.com',
        active: true
    });

    // Find user by email
    const user = await userRepo.findByEmail('john@example.com');

    if (user) {
        console.log(`Found user: ${user.username}`);

        // Update user
        await userRepo.updateUser(user.id, {
            username: 'john_doe_updated'
        });

        // Get active users
        const activeUsers = await userRepo.getActiveUsers(1, 10);
        console.log(`Active users: ${activeUsers.length}`);

        // Get user count
        const totalUsers = await userRepo.getUserCount();
        const activeCount = await userRepo.getUserCount(true);
        console.log(`Total users: ${totalUsers}, Active: ${activeCount}`);
    }

    // Clean up
    await userRepo.model.disconnect();
}

main();
📌
The Model class works well with the repository pattern, providing a clean abstraction for database operations. This approach separates database access logic from business logic, making the code more maintainable.

Best Practices

Here are some recommended practices when using the QueryBuilder:

  • Use entity classes for type safety and better code organization
  • Chain methods for readable query construction
  • Use debug() during development to inspect generated queries
  • Leverage aggregation methods for efficient data analysis
  • Use parameterized values instead of string concatenation for dynamic queries
  • Implement pagination for large result sets
  • Use transactions for operations that need to be atomic
  • Handle errors appropriately in production code
📌
The QueryBuilder is designed to be both powerful and safe. By following these best practices, you can write efficient, maintainable database code that's protected against common security issues.

Have suggestions or feature requests? Join our community and help shape the future of MiniORM.