
Research
/Security News
Laravel Lang Compromised with RCE Backdoor Across 700+ Versions
Laravel Lang packages were compromised with an RCE backdoor across hundreds of versions, exposing cloud, CI/CD, and developer secrets.
@kysera/executor
Advanced tools
Unified Execution Layer for Kysera - plugin-aware Kysely wrapper that enables plugins to work seamlessly with both Repository and DAL patterns.
@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:
pnpm add @kysera/executor kysely
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
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')
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
})
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 instanceplugins - 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:
plugins = [] or enabled = falseinterceptQuery hookcreateExecutorSync(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.
destroyExecutor(executor)Destroy an executor and call the onDestroy hook for all registered plugins.
async function destroyExecutor<DB>(executor: KyseraExecutor<DB>): Promise<void>
Parameters:
executor - KyseraExecutor instance to destroyUse Case: Cleanup when shutting down application or when executor is no longer needed.
Example:
const executor = await createExecutor(db, [
{
name: 'connection-pool',
version: '1.0.0',
async onInit(db) {
console.log('Initializing connection pool')
},
async onDestroy(db) {
console.log('Closing connection pool')
// Close connections, release resources, etc.
}
}
])
// Later...
await destroyExecutor(executor)
// Logs: "Closing connection pool"
Important: After calling destroyExecutor, the executor should not be used for further queries.
wrapTransaction(trx, plugins)Wrap a Kysely transaction with plugins.
function wrapTransaction<DB>(
trx: Transaction<DB>,
plugins: readonly Plugin[]
): KyseraTransaction<DB>
Parameters:
trx - Kysely transaction instanceplugins - Plugins to apply to the transactionReturns: 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 instanceplugins - Plugins to applycontext - 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:
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:
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)]
PluginPlugin 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
/**
* Lifecycle: Called when executor is destroyed
* Use for cleanup, releasing resources, closing connections
*/
onDestroy?<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)onDestroy - Plugin cleanup/teardown (async)interceptQuery - Query interception (most common)extendRepository - Repository pattern onlyQueryBuilderContextContext 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.
ExecutorConfigConfiguration 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 })
PluginValidationErrorError 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[]
}
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
}
}
}
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
}
}
}
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
}
}
}
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
}
}
}
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()
}
}
}
}
}
The executor is designed for production workloads with minimal overhead.
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% |
interceptQuery, the executor takes a fast path with zero overheadcreateExecutorSync when plugins don't need initializationgetRawDb for internal queries that don't need interceptioncreateExecutor(db, plugins, { enabled: false })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
})
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.
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' }
Reserve priority ranges for different concerns:
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
}
Use conflictsWith to prevent incompatible plugins:
{
name: '@myorg/hard-delete',
version: '1.0.0',
conflictsWith: ['@kysera/soft-delete']
}
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
}
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)
})
The executor intercepts the following Kysely query builder methods to apply plugins:
selectFrom - SELECT queriesinsertInto - INSERT queriesupdateTable - UPDATE queriesdeleteFrom - DELETE queriesreplaceInto - REPLACE queries (MySQL)mergeInto - MERGE queries (SQL Server, Oracle)Example:
// All of these are intercepted and have plugins applied:
await executor.selectFrom('users').selectAll().execute()
await executor.insertInto('users').values(data).execute()
await executor.updateTable('users').set(data).execute()
await executor.deleteFrom('users').where('id', '=', 1).execute()
The withSchema() method now maintains the plugin proxy with caching for optimal performance:
// Schema switching preserves plugins
const publicUsers = await executor
.withSchema('public')
.selectFrom('users')
.selectAll()
.execute()
// soft-delete filter still applied!
const archiveUsers = await executor
.withSchema('archive')
.selectFrom('users')
.selectAll()
.execute()
// soft-delete filter still applied!
Performance Note: Schema proxies are cached, so repeated calls to withSchema('public') return the same proxy instance.
The Kysely sql template tag bypasses plugin interception:
import { sql } from 'kysely'
// ❌ This bypasses plugins (no soft-delete filter!)
const users = await executor
.selectFrom(sql`users`.as('users'))
.selectAll()
.execute()
// ✅ Workaround: Use normal query builders
const users = await executor
.selectFrom('users')
.selectAll()
.execute()
// Plugins applied correctly
Why this happens: The sql template tag creates raw SQL fragments that bypass the query builder chain where plugins are applied.
When you need raw SQL:
// For complex WHERE clauses, use sql inside the query builder:
const users = await executor
.selectFrom('users')
.where(sql`jsonb_array_length(metadata->'tags') > 5`)
.selectAll()
.execute()
// Soft-delete filter still applied to the FROM clause!
Common Table Expressions (CTEs) created with with() or withRecursive() have limited plugin support:
// ⚠️ Plugins only apply to the main query, not the CTE
const result = await executor
.with('active_users', db =>
db.selectFrom('users').selectAll() // ❌ Plugins NOT applied here
)
.selectFrom('active_users')
.selectAll()
.execute() // ✅ Plugins applied to outer query
// ✅ Workaround: Apply filters manually in CTE
const result = await executor
.with('active_users', db =>
db
.selectFrom('users')
.where('deleted_at', 'is', null) // Manual soft-delete filter
.selectAll()
)
.selectFrom('active_users')
.selectAll()
.execute()
Why this limitation exists: CTEs are defined before the main query executes, and the CTE callback receives the raw db instance, not the plugin-wrapped executor.
Future improvement: This may be addressed in a future version by wrapping the callback parameter.
The executor implementation uses several type assertions (as unknown as) due to Kysely's complex type system. These are intentional and documented inline in the source code. Here's why they're necessary:
The Plugin.interceptQuery method has an unconstrained generic parameter QB:
interface Plugin {
interceptQuery?<QB>(qb: QB, context: QueryBuilderContext): QB
}
Why it's unconstrained:
Kysely's query builders (SelectQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder) don't share a common base interface that includes query modification methods like where(), and(), etc. Each builder has a unique interface with different generic parameters.
How plugins handle this:
Plugins must handle type safety internally by:
Example:
interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
if (context.operation === 'select') {
// Cast to SelectQueryBuilder for type-safe WHERE clause
type GenericSelect = SelectQueryBuilder<Record<string, unknown>, string, Record<string, unknown>>;
return (qb as unknown as GenericSelect)
.where('deleted_at', 'is', null) as QB;
}
return qb;
}
Transaction wrapping requires explicit type casts because:
Transaction<DB> extends Kysely<DB>, but TypeScript requires explicit casts for the proxy systemKysely<DB>, not Transaction<DB>Transaction<DB> for user callbacksExample:
// Type assertions explained:
const wrappedTrx = createProxy(
trx as unknown as Kysely<DB>, // Transaction -> Kysely (structurally compatible)
interceptors,
allPlugins
)
return fn(wrappedTrx as unknown as Transaction<DB>) // Proxy -> Transaction
Methods like selectFrom, insertInto, etc. must be accessed dynamically:
const originalMethod = (db as unknown as Record<string, (t: string) => unknown>)[method]
Why: TypeScript doesn't allow dynamic property access on Kysely<DB> without an index signature. The cast is safe because we validate the method exists in INTERCEPTED_METHODS.
Object.assign returns intersection types that must be cast to union types:
return Object.assign(db, {
__kysera: true as const,
__plugins: plugins,
__rawDb: db
}) as KyseraExecutor<DB>
Why: KyseraExecutor<DB> = Kysely<DB> & KyseraExecutorMarker<DB>, and we're adding exactly those marker properties.
The wrapped transaction only exposes the .execute() method, not .setIsolationLevel() or other transaction builder methods.
Why this limitation exists:
Escape Hatch: Using __rawDb
You can access the raw database instance via executor.__rawDb or ctx.db.__rawDb to bypass plugins when needed:
import { withTransaction } from '@kysera/dal'
// Within a transaction, access raw db for special cases:
await withTransaction(executor, async ctx => {
// Regular query with plugins
const users = await ctx.db.selectFrom('users').selectAll().execute()
// Access raw db to bypass plugins
if ('__rawDb' in ctx.db) {
const rawDb = ctx.db.__rawDb
// This bypasses all plugins including soft-delete
const deletedUsers = await rawDb
.selectFrom('users')
.where('deleted_at', 'is not', null)
.selectAll()
.execute()
}
return users
})
Workaround for advanced use cases:
// Use raw database to set isolation level
await executor.__rawDb
.transaction()
.setIsolationLevel('serializable')
.execute(async trx => {
// Transaction runs with custom isolation level
// but without plugin interception
})
Alternative pattern:
// For plugins + custom isolation, wrap manually:
await executor.__rawDb
.transaction()
.setIsolationLevel('serializable')
.execute(async trx => {
const wrappedTrx = wrapTransaction(trx, getPlugins(executor))
// Now plugins are active in transaction with custom isolation
})
The executor prioritizes:
These architectural decisions allow the executor to provide a seamless plugin system while maintaining full type safety and Kysely compatibility.
Problem: Plugin's interceptQuery is not being called.
Solutions:
interceptQuery hook definedcreateExecutor(db, plugins, { enabled: true })dbProblem: PluginValidationError: Circular dependency: a -> b -> a
Solution: Review plugin dependencies and remove circular references.
Problem: Queries are slower with plugins.
Solutions:
interceptQuery)getRawDb for internal queriesProblem: 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;
}
MIT
FAQs
Unified Execution Layer for Kysera - Plugin-aware Kysely wrapper
The npm package @kysera/executor receives a total of 88 weekly downloads. As such, @kysera/executor popularity was classified as not popular.
We found that @kysera/executor demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Laravel Lang packages were compromised with an RCE backdoor across hundreds of versions, exposing cloud, CI/CD, and developer secrets.

Security News
Socket found a malicious postinstall hook across 700+ GitHub repos, including PHP packages on Packagist and Node.js project repositories.

Security News
Vibe coding at scale is reshaping how packages are created, contributed, and selected across the software supply chain