SlintORM
Docsnpm

Plugin System

SlintORM's plugin system lets you hook into the ORM lifecycle — before and after every query, insert, update, delete, and migration. Plugins are registered with orm.use() and receive an event context they can inspect or mutate.

Plugin interface

A plugin is an object with a name, an install() method, an optional on() handler, and an optional priority.

// Plugin interface

interface SlintORMPlugin {
  name: string;
  install(orm: OrmInstance): void | Promise<void>;
  on?(event: string, ctx: EventContext): void | Promise<void>;
  priority?: number;  // lower runs first, default 100
}

// Event types:
//   beforeQuery    afterQuery
//   beforeInsert   afterInsert
//   beforeUpdate   afterUpdate
//   beforeDelete   afterDelete
//   beforeMigrate  afterMigrate

// EventContext:
interface EventContext {
  model?: ModelDefinition;
  table?: string;
  data?: Record<string, unknown>;
  filter?: Record<string, unknown>;
  query?: string;
  params?: unknown[];
  duration?: number;   // populated for after* events
}

Registering plugins

Call orm.use(plugin) to register a plugin. Plugins with lower priority values run first.

import { Orm } from 'slintorm';

const orm = new Orm({ /* config */ });

// Register a plugin
orm.use({
  name: 'audit-log',
  priority: 10,
  on(event, ctx) {
    if (event.startsWith('after')) {
      console.log(`[${event}] ${ctx.table}`, ctx.data ?? ctx.filter);
    }
  },
});

// Plugins run in priority order (lower number first)
// orm.use() can be called before or after defining models

Example: audit logging

Log every insert, update, and delete to an audit table.

// Example — audit logging plugin

const auditPlugin = {
  name: 'audit-log',
  priority: 20,

  install(orm) {
    // One-time setup — create audit table, register listeners, etc.
    console.log('Audit plugin installed');
  },

  async on(event, ctx) {
    // Only log mutating events
    const mutating = ['afterInsert', 'afterUpdate', 'afterDelete'];
    if (!mutating.includes(event)) return;

    await AuditLog.insert({
      table: ctx.table,
      event,
      modelName: ctx.model?.name ?? 'unknown',
      data: JSON.stringify(ctx.data ?? ctx.filter),
      timestamp: new Date().toISOString(),
    });
  },
};

orm.use(auditPlugin);

Example: query timing

Measure query duration and emit metrics to your monitoring system.

// Example — query timing plugin

const timingPlugin = {
  name: 'query-timing',
  priority: 5,  // run early for before*, late for after*

  on(event, ctx) {
    if (event === 'beforeQuery') {
      // Store start time on context
      ctx.__start = Date.now();
    }

    if (event === 'afterQuery') {
      const elapsed = Date.now() - (ctx.__start ?? Date.now());
      console.log(`[TIMING] ${ctx.table} — ${elapsed}ms`);

      // Emit metric to monitoring system
      metrics.timing('slintorm.query', elapsed, {
        table: ctx.table,
        operation: ctx.query?.split(' ')[0],  // SELECT, INSERT, etc.
      });
    }
  },
};

orm.use(timingPlugin);

Example: data transformation

Automatically set tenant IDs, trim strings, or enforce field conventions before writes and queries.

// Example — data transformation plugin

const transformPlugin = {
  name: 'data-transform',
  priority: 15,

  on(event, ctx) {
    if (event === 'beforeInsert' || event === 'beforeUpdate') {
      // Auto-set tenant ID on every mutation
      if (ctx.data && !ctx.data.tenantId) {
        ctx.data.tenantId = getCurrentTenant();
      }

      // Trim string fields
      if (ctx.data) {
        for (const [key, val] of Object.entries(ctx.data)) {
          if (typeof val === 'string') {
            ctx.data[key] = val.trim();
          }
        }
      }
    }

    if (event === 'beforeQuery') {
      // Auto-filter by tenant for multi-tenant apps
      if (ctx.filter && !ctx.filter.tenantId) {
        ctx.filter.tenantId = getCurrentTenant();
      }
    }
  },
};

orm.use(transformPlugin);

Lifecycle and cleanup

install() runs once at registration. on() runs for every matching event. Before-events can mutate context to affect the operation; after-events are read-only.

// Plugin lifecycle

// install() — called once when orm.use(plugin) is registered
//   Use for: creating tables, setting up connections, validation

// on(event, ctx) — called for every matching event
//   before* events: mutate ctx.data / ctx.filter to affect the operation
//   after* events: read-only observation (ctx.duration available)

// Cleanup — remove a plugin (not yet implemented, track via issue)
//   Future: orm.unuse('plugin-name') or plugin.uninstall()

// Plugins are called synchronously in priority order.
// If on() returns a Promise, the ORM awaits it before proceeding.
// A rejected promise in a before* event will abort the operation.
PreviousStreamingNextContext & Prepared Statements