npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@kysera/executor

v0.7.1

Published

Unified Execution Layer for Kysera - Plugin-aware Kysely wrapper

Readme

@kysera/executor

Unified Execution Layer for Kysera - plugin-aware Kysely wrapper that enables plugins to work seamlessly with both Repository and DAL patterns.

Overview

@kysera/executor is the foundation package for Kysera's plugin system. It provides transparent query interception that allows plugins to modify queries before execution without changing your code. This enables features like soft deletes, row-level security, audit logging, and more.

Key Features:

  • Zero Overhead Path - No performance penalty when no plugins or interceptors are used
  • Minimal Overhead - <15% overhead with 1-3 interceptor plugins in production workloads
  • Type Safe - Full TypeScript support with all Kysely types preserved
  • Transaction Support - Plugins automatically propagate through transactions
  • Plugin Validation - Detects conflicts, missing dependencies, and circular dependencies
  • Cross-Pattern - Works with both Repository and DAL patterns

Installation

pnpm add @kysera/executor kysely

Quick Start

Basic Usage

import { createExecutor } from '@kysera/executor';
import { softDeletePlugin } from '@kysera/soft-delete';
import { Kysely, PostgresDialect } from 'kysely';

const db = new Kysely<Database>({
  dialect: new PostgresDialect({ /* config */ })
});

// Create executor with plugins
const executor = await createExecutor(db, [
  softDeletePlugin()
]);

// All queries now have soft-delete filter applied automatically
const users = await executor
  .selectFrom('users')
  .selectAll()
  .execute();
// Returns only non-deleted users

With DAL Pattern

import { createExecutor } from '@kysera/executor';
import { createContext, createQuery, withTransaction } from '@kysera/dal';
import { softDeletePlugin } from '@kysera/soft-delete';

// Create executor with plugins
const executor = await createExecutor(db, [
  softDeletePlugin()
]);

// Create DAL context with executor
const ctx = createContext(executor);

// Define queries - plugins apply automatically
const getUsers = createQuery((ctx) =>
  ctx.db.selectFrom('users').selectAll().execute()
);

const getUser = createQuery((ctx, id: string) =>
  ctx.db
    .selectFrom('users')
    .where('id', '=', id)
    .selectAll()
    .executeTakeFirst()
);

// Execute - soft-delete filter automatically applied
const users = await getUsers(ctx);
const user = await getUser(ctx, 'user-123');

With Transactions

import { withTransaction } from '@kysera/dal';

// Plugins automatically propagate through transactions
await withTransaction(executor, async (ctx) => {
  const user = await ctx.db
    .insertInto('users')
    .values({ name: 'Alice' })
    .returningAll()
    .executeTakeFirst();

  const post = await ctx.db
    .insertInto('posts')
    .values({ user_id: user.id, title: 'Hello' })
    .returningAll()
    .executeTakeFirst();

  // Both queries respect all registered plugins
});

API Reference

Core Functions

createExecutor(db, plugins?, config?)

Creates a plugin-aware executor with async plugin initialization.

async function createExecutor<DB>(
  db: Kysely<DB>,
  plugins?: readonly Plugin[],
  config?: ExecutorConfig
): Promise<KyseraExecutor<DB>>

Parameters:

  • db - Kysely database instance
  • plugins - Array of plugins to apply (default: [])
  • config.enabled - Enable/disable plugin interception (default: true)

Returns: Plugin-aware executor

Example:

const executor = await createExecutor(db, [
  softDeletePlugin(),
  auditPlugin()
]);

Performance:

  • Zero overhead when plugins = [] or enabled = false
  • Zero overhead when plugins have no interceptQuery hook
  • Minimal overhead with interceptor plugins (<15% for 1-3 plugins)

createExecutorSync(db, plugins?, config?)

Synchronous version of createExecutor that skips async plugin initialization.

function createExecutorSync<DB>(
  db: Kysely<DB>,
  plugins?: readonly Plugin[],
  config?: ExecutorConfig
): KyseraExecutor<DB>

Use Case: When you need synchronous executor creation or plugins don't require onInit.

Example:

const executor = createExecutorSync(db, [softDeletePlugin()]);

isKyseraExecutor(value)

Type guard to check if a value is a KyseraExecutor.

function isKyseraExecutor<DB>(
  value: Kysely<DB> | KyseraExecutor<DB>
): value is KyseraExecutor<DB>

Example:

if (isKyseraExecutor(db)) {
  const plugins = getPlugins(db);
  console.log(`${plugins.length} plugins registered`);
}

getPlugins(executor)

Get the array of registered plugins from an executor.

function getPlugins<DB>(
  executor: KyseraExecutor<DB>
): readonly Plugin[]

Returns: Plugins in execution order (sorted by priority and dependencies)

Example:

const plugins = getPlugins(executor);
console.log(plugins.map(p => `${p.name}@${p.version}`));

getRawDb(executor)

Get the raw Kysely instance, bypassing all plugin interceptors.

function getRawDb<DB>(
  executor: Kysely<DB>
): Kysely<DB>

Use Case: For internal plugin operations that should not trigger other plugins.

Example:

// Inside a soft-delete plugin's restore method:
const rawDb = getRawDb(executor);

// This query bypasses soft-delete filter to find deleted records
const deletedUser = await rawDb
  .selectFrom('users')
  .where('id', '=', userId)
  .where('deleted_at', 'is not', null)
  .selectAll()
  .executeTakeFirst();

Important: Use with caution. Bypassing plugins can lead to inconsistent behavior.


wrapTransaction(trx, plugins)

Wrap a Kysely transaction with plugins.

function wrapTransaction<DB>(
  trx: Transaction<DB>,
  plugins: readonly Plugin[]
): KyseraTransaction<DB>

Parameters:

  • trx - Kysely transaction instance
  • plugins - Plugins to apply to the transaction

Returns: Plugin-aware transaction

Use Case: Manual transaction wrapping when not using withTransaction.

Example:

await db.transaction().execute(async (trx) => {
  const wrappedTrx = wrapTransaction(trx, [softDeletePlugin()]);

  // Plugins now apply within transaction
  const users = await wrappedTrx
    .selectFrom('users')
    .selectAll()
    .execute();
});

applyPlugins(qb, plugins, context)

Manually apply plugins to a query builder.

function applyPlugins<QB>(
  qb: QB,
  plugins: readonly Plugin[],
  context: QueryBuilderContext
): QB

Parameters:

  • qb - Query builder instance
  • plugins - Plugins to apply
  • context - Query context (operation, table, metadata)

Returns: Modified query builder

Use Case: Complex queries that bypass normal interception or custom plugin composition.

Example:

const qb = db.selectFrom('users').selectAll();
const context: QueryBuilderContext = {
  operation: 'select',
  table: 'users',
  metadata: {}
};

const modifiedQb = applyPlugins(qb, [softDeletePlugin()], context);
const users = await modifiedQb.execute();

validatePlugins(plugins)

Validate plugins for conflicts, duplicates, missing dependencies, and circular dependencies.

function validatePlugins(
  plugins: readonly Plugin[]
): void

Throws: PluginValidationError if validation fails

Validation checks:

  • Duplicate plugin names
  • Missing dependencies
  • Conflicting plugins
  • Circular dependencies

Example:

try {
  validatePlugins([
    { name: 'a', version: '1.0.0', dependencies: ['b'] },
    { name: 'b', version: '1.0.0', dependencies: ['a'] }
  ]);
} catch (error) {
  if (error instanceof PluginValidationError) {
    console.error(error.type, error.details);
    // type: 'CIRCULAR_DEPENDENCY'
    // details: { pluginName: 'a', cycle: ['a', 'b', 'a'] }
  }
}

resolvePluginOrder(plugins)

Resolve plugin execution order using topological sort with priority.

function resolvePluginOrder(
  plugins: readonly Plugin[]
): Plugin[]

Returns: Sorted plugins in execution order

Sorting rules:

  1. Dependencies must run before dependents
  2. Higher priority runs first (default priority: 0)
  3. Alphabetical by name when priority is equal

Example:

const plugins = [
  { name: 'audit', version: '1.0.0', priority: 50 },
  { name: 'soft-delete', version: '1.0.0', priority: 100 },
  { name: 'rls', version: '1.0.0', priority: 90 }
];

const sorted = resolvePluginOrder(plugins);
// Result: [soft-delete (100), rls (90), audit (50)]

Types

Plugin

Plugin interface - unified for both Repository and DAL patterns.

interface Plugin {
  /** Unique plugin name (e.g., '@kysera/soft-delete') */
  readonly name: string;

  /** Plugin version (semver) */
  readonly version: string;

  /** Plugin dependencies (must be loaded first) */
  readonly dependencies?: readonly string[];

  /** Higher priority = runs first (default: 0) */
  readonly priority?: number;

  /** Plugins that conflict with this one */
  readonly conflictsWith?: readonly string[];

  /**
   * Lifecycle: Called once when plugin is initialized
   * Use for setup, validation, or resource allocation
   */
  onInit?<DB>(executor: Kysely<DB>): Promise<void> | void;

  /**
   * Query interception: Modify query builder before execution
   * This is where most plugin logic lives
   * Works in both Repository and DAL patterns
   */
  interceptQuery?<QB>(qb: QB, context: QueryBuilderContext): QB;

  /**
   * Repository extensions: Add methods to repositories
   * Only used in Repository pattern, ignored in DAL
   */
  extendRepository?<T extends object>(repo: T): T;
}

Available Hooks:

  • onInit - Plugin initialization (async)
  • interceptQuery - Query interception (most common)
  • extendRepository - Repository pattern only

QueryBuilderContext

Context passed to interceptQuery hook.

interface QueryBuilderContext {
  /** Type of operation: 'select' | 'insert' | 'update' | 'delete' */
  readonly operation: 'select' | 'insert' | 'update' | 'delete';

  /** Table name being queried */
  readonly table: string;

  /** Additional metadata (extensible) */
  readonly metadata: Record<string, unknown>;
}

Example:

const plugin: Plugin = {
  name: 'my-plugin',
  version: '1.0.0',
  interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
    console.log(`${context.operation} on ${context.table}`);
    // Output: "select on users"
    return qb;
  }
};

KyseraExecutor<DB>

Plugin-aware Kysely wrapper type.

type KyseraExecutor<DB> = Kysely<DB> & {
  readonly __kysera: true;
  readonly __plugins: readonly Plugin[];
  readonly __rawDb: Kysely<DB>;
}

Usage: Use KyseraExecutor<DB> instead of Kysely<DB> when you need to ensure plugins are available.


KyseraTransaction<DB>

Plugin-aware Transaction wrapper type.

type KyseraTransaction<DB> = Transaction<DB> & {
  readonly __kysera: true;
  readonly __plugins: readonly Plugin[];
  readonly __rawDb: Kysely<DB>;
}

Usage: Returned by wrapTransaction and used internally by the executor's transaction handling.


ExecutorConfig

Configuration options for executor creation.

interface ExecutorConfig {
  /** Enable/disable plugin interception at runtime (default: true) */
  readonly enabled?: boolean;
}

Example:

// Disable plugins for testing or debugging
const executor = await createExecutor(db, plugins, { enabled: false });

PluginValidationError

Error thrown when plugin validation fails.

class PluginValidationError extends Error {
  readonly type: PluginValidationErrorType;
  readonly details: PluginValidationDetails;
}

type PluginValidationErrorType =
  | 'DUPLICATE_NAME'
  | 'MISSING_DEPENDENCY'
  | 'CONFLICT'
  | 'CIRCULAR_DEPENDENCY';

interface PluginValidationDetails {
  readonly pluginName: string;
  readonly missingDependency?: string;
  readonly conflictingPlugin?: string;
  readonly cycle?: readonly string[];
}

Creating Custom Plugins

Basic Plugin

import type { Plugin, QueryBuilderContext } from '@kysera/executor';

export function myPlugin(): Plugin {
  return {
    name: '@myorg/my-plugin',
    version: '1.0.0',
    priority: 50,

    interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
      // Add your logic here
      if (context.operation === 'select' && context.table === 'users') {
        return (qb as any).where('active', '=', true);
      }
      return qb;
    }
  };
}

Plugin with Initialization

export function cachePlugin(redisClient: Redis): Plugin {
  return {
    name: '@myorg/cache',
    version: '1.0.0',

    async onInit(db) {
      // Initialize cache, warm up, etc.
      await redisClient.ping();
      console.log('Cache plugin initialized');
    },

    interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
      // Add cache hints to metadata
      context.metadata.cacheKey = `${context.table}:${context.operation}`;
      return qb;
    }
  };
}

Plugin with Dependencies

export function auditPlugin(): Plugin {
  return {
    name: '@kysera/audit',
    version: '1.0.0',
    priority: 40,

    // Requires soft-delete to be loaded first
    dependencies: ['@kysera/soft-delete'],

    interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
      // Audit logic here
      return qb;
    }
  };
}

Plugin with Conflicts

export function hardDeletePlugin(): Plugin {
  return {
    name: '@myorg/hard-delete',
    version: '1.0.0',

    // Cannot coexist with soft-delete
    conflictsWith: ['@kysera/soft-delete'],

    interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
      // Hard delete logic
      return qb;
    }
  };
}

Accessing Raw Database in Plugins

import { getRawDb } from '@kysera/executor';

export function softDeletePlugin(): Plugin {
  return {
    name: '@kysera/soft-delete',
    version: '1.0.0',

    interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
      if (context.operation === 'select') {
        return (qb as any).where('deleted_at', 'is', null);
      }
      return qb;
    },

    extendRepository<T extends { executor: any }>(repo: T): T {
      return {
        ...repo,
        async restore(id: string) {
          // Use raw DB to bypass soft-delete filter
          const rawDb = getRawDb(repo.executor);

          return rawDb
            .updateTable('users')
            .where('id', '=', id)
            .set({ deleted_at: null })
            .returningAll()
            .executeTakeFirst();
        }
      };
    }
  };
}

Performance

The executor is designed for production workloads with minimal overhead.

Benchmark Results

Based on benchmark tests with SQLite (in-memory):

| Configuration | Queries/sec | Overhead vs Pure Kysely | |---------------|-------------|-------------------------| | Pure Kysely (baseline) | ~100,000 | 0% | | Executor (no plugins) | ~95,000 | <5% (zero overhead path) | | Executor (1 plugin) | ~90,000 | <15% | | Executor (3 plugins) | ~80,000 | <25% | | Executor (5 plugins) | ~70,000 | <35% |

Optimization Strategies

  1. Zero Overhead Path: When no plugins have interceptQuery, the executor takes a fast path with zero overhead
  2. Method Caching: Intercepted methods are cached to avoid repeated creation
  3. Set-based Lookups: O(1) lookups instead of array iterations
  4. Lazy Proxy Creation: Proxies are only created when needed

Performance Tips

  • Only enable plugins you need
  • Use createExecutorSync when plugins don't need initialization
  • Consider plugin priority - critical filters should run first
  • Use getRawDb for internal queries that don't need interception
  • Disable executor in development: createExecutor(db, plugins, { enabled: false })

Integration with DAL

The executor seamlessly integrates with @kysera/dal for functional query composition:

import { createExecutor } from '@kysera/executor';
import { createContext, createQuery, withTransaction } from '@kysera/dal';

// Create executor
const executor = await createExecutor(db, [
  softDeletePlugin(),
  rlsPlugin({ tenantIdColumn: 'tenant_id' })
]);

// Create context
const ctx = createContext(executor);

// Define queries
const getUser = createQuery((ctx, id: string) =>
  ctx.db
    .selectFrom('users')
    .where('id', '=', id)
    .selectAll()
    .executeTakeFirst()
);

const updateUser = createQuery((ctx, id: string, data: Partial<User>) =>
  ctx.db
    .updateTable('users')
    .where('id', '=', id)
    .set(data)
    .returningAll()
    .executeTakeFirst()
);

// Execute with automatic plugin application
const user = await getUser(ctx, 'user-123');

// Transactions preserve plugins
await withTransaction(executor, async (txCtx) => {
  await updateUser(txCtx, 'user-123', { name: 'Updated' });
  // Plugins still apply within transaction
});

Integration with Repository

The executor also powers the Repository pattern via @kysera/repository:

import { createORM } from '@kysera/repository';
import { softDeletePlugin } from '@kysera/soft-delete';
import { auditPlugin } from '@kysera/audit';

const orm = await createORM(db, [
  softDeletePlugin(),
  auditPlugin()
]);

const userRepo = orm.createRepository(createUserRepository);

// Plugins automatically applied to repository operations
const users = await userRepo.findAll(); // Soft-delete filter applied
const user = await userRepo.create({ name: 'Alice' }); // Audit log created

The repository internally uses createExecutor to power plugin functionality.

Best Practices

1. Plugin Naming

Use namespaced names to avoid conflicts:

// Good
{ name: '@kysera/soft-delete', version: '1.0.0' }
{ name: '@myorg/custom-plugin', version: '1.0.0' }

// Bad
{ name: 'soft-delete', version: '1.0.0' }
{ name: 'plugin', version: '1.0.0' }

2. Plugin Priority

Reserve priority ranges for different concerns:

  • 100-199: Core data filters (soft-delete, RLS)
  • 50-99: Middleware (audit, logging)
  • 0-49: Post-processing (caching, enrichment)

3. Use Dependencies

Declare dependencies explicitly to ensure correct load order:

{
  name: '@kysera/audit',
  version: '1.0.0',
  dependencies: ['@kysera/soft-delete'], // Audit needs soft-delete
  priority: 40
}

4. Avoid Conflicts

Use conflictsWith to prevent incompatible plugins:

{
  name: '@myorg/hard-delete',
  version: '1.0.0',
  conflictsWith: ['@kysera/soft-delete']
}

5. Type Safety

Always maintain type safety when modifying query builders:

interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
  // Cast to any only when necessary
  if (context.operation === 'select') {
    return (qb as any).where('deleted_at', 'is', null) as QB;
  }
  return qb; // Return unmodified if no changes
}

6. Testing Plugins

Test plugins in isolation before composing them:

import { applyPlugins } from '@kysera/executor';

it('should filter deleted records', async () => {
  const qb = db.selectFrom('users').selectAll();
  const context = { operation: 'select', table: 'users', metadata: {} };

  const filtered = applyPlugins(qb, [softDeletePlugin()], context);
  const users = await filtered.execute();

  expect(users.every(u => u.deleted_at === null)).toBe(true);
});

Troubleshooting

Plugin Not Applied

Problem: Plugin's interceptQuery is not being called.

Solutions:

  1. Verify plugin has interceptQuery hook defined
  2. Check if executor is disabled: createExecutor(db, plugins, { enabled: true })
  3. Ensure you're using the executor, not raw db

Circular Dependency Error

Problem: PluginValidationError: Circular dependency: a -> b -> a

Solution: Review plugin dependencies and remove circular references.

Performance Degradation

Problem: Queries are slower with plugins.

Solutions:

  1. Profile plugins individually to find bottlenecks
  2. Reduce number of plugins
  3. Optimize plugin logic (avoid expensive operations in interceptQuery)
  4. Consider using getRawDb for internal queries

Type Errors with Query Builders

Problem: TypeScript errors when modifying query builders in plugins.

Solution: Use type assertions carefully:

interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
  // Cast to any, apply method, cast back to QB
  return (qb as any).where('column', '=', value) as QB;
}

License

MIT

Related Packages