
Security News
Risky Biz Podcast: Making Reachability Analysis Work in Real-World Codebases
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
iagate-querykit
Advanced tools
QueryKit: lightweight TypeScript query toolkit with models, views, triggers, events, scheduler and adapters (better-sqlite3).
Minimal, typed building blocks for SQL-centric data apps in TypeScript. Compose queries with QueryBuilder
, manage views and triggers, schedule jobs, run simulations, and route to one or multiple databases. Includes a better-sqlite3
executor.
npm install iagate-querykit
# optional peer
npm install better-sqlite3
QueryBuilder<T>(table)
: Build SQL with a fluent API and execute via a configured executor.ViewManager
: Create/drop views and schedule refreshes.TriggerManager
: Create/drop SQLite triggers.scheduler
: Simple interval scheduler with named tasks.simulationManager
: Dry-run mode that executes against an in-memory virtual state.MultiDatabaseManager
: Register and execute against multiple adapters.BetterSqlite3Executor
: DatabaseExecutor for better-sqlite3
.new QueryBuilder<T>(table)
initial(data?)
, tracking()
select(columns)
, selectRaw(sql)
, aggregatesSelect(columns)
, distinct()
insert(data)
, update(data)
, delete()
, updateOrInsert(attrs, values)
, increment(column, amount?)
, decrement(column, amount?)
where(column, op, value)
, orWhere(column, op, value)
, whereIf(condition, ...)
, whereAll(conditions)
whereIn
, orWhereIn
, whereNotIn
, orWhereNotIn
, whereNull
, orWhereNull
, whereNotNull
, orWhereNotNull
, whereBetween
, whereNotBetween
whereColumn(first, op, second)
whereRaw(sql, bindings?, logical?)
, whereRawSearch(searchTerm, columns)
, whereExists(query)
, whereNotExists(query)
whereLike
, orWhereLike
, whereContains
, whereStartsWith
, whereEndsWith
, whereILike
, whereContainsCI
, whereStartsWithCI
, whereEndsWithCI
, whereSearch
when(condition, cb)
, unless(condition, cb)
, clone()
innerJoin(table, on)
, leftJoin(table, on)
, rightJoin(table, on)
, innerJoinOn(left, right)
, leftJoinOn(left, right)
, rightJoinOn(left, right)
orderBy(column, dir?)
, orderByMany([{ column, direction? }])
, limit(n)
, offset(n)
, groupBy(columns)
, groupByOne(column)
, having(column, op, value)
, havingRaw(sql, bindings?, logical?)
, havingIf(condition, column, op, value)
count(column?, alias?)
, sum(column, alias?)
, avg(column, alias?)
, min(column, alias?)
, max(column, alias?)
, selectExpression(expr, alias?)
, selectCount
, selectSum
, selectAvg
, selectMin
, selectMax
, selectCaseSum(conditionSql, alias)
paginate(page?, perPage?)
, range(field, start?, end?)
, period(field, key?)
union(query)
, unionAll(query)
toSql(): { sql, bindings }
all<U= T>()
, exists()
, pluck(column)
run()
, allSync<U>()
, getSync<U>()
, firstSync<U>()
, pluckSync(column)
, scalarSync<U>(alias?)
get<U>()
, first<U>()
, find(id)
make()
createOrReplaceView(viewName, query)
scheduleViewRefresh(viewName, query, intervalMs)
unscheduleViewRefresh(viewName)
dropView(viewName)
listViews(): string[]
viewExists(viewName): boolean
view<T>(viewName): QueryBuilder<T>
createTrigger(name, table, timing: 'BEFORE'|'AFTER'|'INSTEAD OF', event: 'INSERT'|'UPDATE'|'DELETE', body)
dropTrigger(name)
listTriggers(): string[]
triggerExists(name): boolean
schedule(name, task, intervalMs)
unschedule(name)
parallel(...queries: QueryBuilder[]): Promise<any[]>
isActive()
start(initialState: Record<string, any[] | QueryBuilder<any>>)
stop()
getStateFor(tableName)
updateStateFor(tableName, data)
getInstance(config?)
initialize(createAdapter: (config) => BaseDatabaseAdapter)
getAdapter(name)
/ getDefaultAdapter()
executeOnMultiple(databaseNames, sql, params?) => Record<string, QueryResult>
QueryKitConfig
: { defaultExecutor?, eventBus?, simulation?, multiDb? }
setDefaultExecutor(executor)
, setEventBus(bus)
, setSimulationController(sim)
, setMultiDbRegistry(reg)
raw(sql)
table<T>(tableName)
→ QueryBuilder<T>
Model
query<T>()
fill(attrs)
, save()
import { setDefaultExecutor } from 'iagate-querykit'
import { BetterSqlite3Executor } from 'iagate-querykit/adapters/better-sqlite3'
setDefaultExecutor(new BetterSqlite3Executor('app.sqlite'))
import { QueryBuilder } from 'iagate-querykit'
// SELECT id, email FROM users WHERE active = 1 ORDER BY created_at DESC LIMIT 50
const users = await new QueryBuilder<{ id: number; email: string; active: number }>('users')
.select(['id', 'email'])
.where('active', '=', 1)
.orderBy('created_at', 'DESC')
.limit(50)
.all()
// Aggregates
const total = await new QueryBuilder('orders').count().scalarSync<number>('count')
// Insert / Update / Delete
await new QueryBuilder('users').insert({ email: 'a@b.com', active: 1 }).make()
await new QueryBuilder('users').where('id', '=', 1).update({ active: 0 }).make()
await new QueryBuilder('users').where('id', '=', 2).delete().make()
import { raw, QueryBuilder } from 'iagate-querykit'
const rows = await new QueryBuilder('users')
.select(['id'])
.whereIn('id', [1, 2, 3])
.whereNull('deleted_at')
.whereBetween('created_at', ['2025-01-01', '2025-12-31'])
.havingRaw('COUNT(id) > 0')
.selectRaw('strftime("%Y-%m", created_at) AS ym')
.all()
// EXISTS
const sub = new QueryBuilder('orders').select(['user_id']).where('amount', '>', 0)
const exists = await new QueryBuilder('users').whereExists(sub).limit(1).exists()
import { ViewManager, QueryBuilder } from 'iagate-querykit'
const views = new ViewManager()
views.createOrReplaceView(
'active_users',
new QueryBuilder('users').select(['id', 'email']).where('active', '=', 1),
)
// schedule refresh every 10 minutes
views.scheduleViewRefresh(
'active_users',
new QueryBuilder('users').select(['id', 'email']).where('active', '=', 1),
10 * 60 * 1000,
)
import { TriggerManager } from 'iagate-querykit'
const triggers = new TriggerManager()
triggers.createTrigger(
'users_update_timestamp',
'users',
'BEFORE',
'UPDATE',
`
SET NEW.updated_at = CURRENT_TIMESTAMP;
`,
)
import { scheduler } from 'iagate-querykit'
scheduler.schedule('nightly-maintenance', () => {
// rotate tokens, refresh views, etc.
}, 24 * 60 * 60 * 1000)
import { parallel, QueryBuilder } from 'iagate-querykit'
const [recentUsers, topOrders] = await parallel(
new QueryBuilder('users').select(['id']).orderBy('created_at', 'DESC').limit(10),
new QueryBuilder('orders').select(['id']).orderBy('amount', 'DESC').limit(5),
)
import { simulationManager, QueryBuilder } from 'iagate-querykit'
await simulationManager.start({
users: [
{ id: 1, email: 'a@b.com', active: 1 },
{ id: 2, email: 'c@d.com', active: 0 },
],
})
const q = new QueryBuilder('users')
.select(['id', 'email'])
.where('active', '=', 1)
.limit(1)
// dry run
const one = await q.all<{ id: number; email: string }>()
// track writes without touching DB
await new QueryBuilder('users').insert({ id: 3, email: 'x@y.com', active: 1 }).initial(one).tracking()
simulationManager.stop()
import { MultiDatabaseManager } from 'iagate-querykit'
const multi = MultiDatabaseManager.getInstance({
defaultDatabase: 'primary',
databases: {
primary: { name: 'primary', type: 'sqlite', filePath: 'db1.sqlite' },
analytics: { name: 'analytics', type: 'sqlite', filePath: 'db2.sqlite' },
},
})
await multi.initialize(({ filePath }) => new BetterSqlite3Executor(filePath!))
const results = await multi.executeOnMultiple(
['primary', 'analytics'],
'SELECT COUNT(*) as c FROM users',
)
Common tasks:
make build
make test
make publish
MIT
import { QueryBuilder, simulationManager } from 'iagate-querykit'
await simulationManager.start({
users: [
{ id: 1, email: 'a@b.com', active: 1 },
{ id: 2, email: 'c@d.com', active: 0 },
],
})
const q = new QueryBuilder<{ id: number; email: string; active: number }>('users')
.insert({ id: 3, email: 'x@y.com', active: 1 })
await q.initial().tracking()
// Expected virtual state for 'users' (no real DB touched):
// [
// { id: 1, email: 'a@b.com', active: 1 },
// { id: 2, email: 'c@d.com', active: 0 },
// { id: 3, email: 'x@y.com', active: 1 },
// ]
simulationManager.stop()
import { parallel, QueryBuilder } from 'iagate-querykit'
const results = await parallel(
new QueryBuilder('users').select(['id']).orderBy('id', 'DESC').limit(2),
new QueryBuilder('orders').insert({ id: 10, amount: 99 }),
)
// Expected:
// results[0] -> last 2 users rows
// results[1] -> { changes: 1, lastInsertRowid: 10 }
import { MultiDatabaseManager } from 'iagate-querykit'
const multi = MultiDatabaseManager.getInstance({
defaultDatabase: 'primary',
databases: {
primary: { name: 'primary', type: 'sqlite', filePath: 'db1.sqlite' },
analytics: { name: 'analytics', type: 'sqlite', filePath: 'db2.sqlite' },
},
})
await multi.initialize(({ filePath }) => new BetterSqlite3Executor(filePath!))
const out = await multi.executeOnMultiple([
'primary',
'analytics',
], 'SELECT COUNT(*) as c FROM users')
// Expected shape:
// {
// primary: { data: [{ c: 42 }] },
// analytics:{ data: [{ c: 42 }] },
// }
import { QueryBuilder } from 'iagate-querykit'
const a = new QueryBuilder('users').select(['id', 'email']).where('active', '=', 1)
const b = new QueryBuilder('users').select(['id', 'email']).where('active', '=', 0)
const unioned = a.unionAll(b).orderBy('id', 'DESC').paginate(2, 10)
const { sql, bindings } = unioned.toSql()
// Expected SQL:
// (SELECT id, email FROM users WHERE id = ? /*...*/ ) UNION ALL (SELECT ...) ORDER BY id DESC LIMIT ? OFFSET ?
// Expected bindings length: >= 3
import { QueryBuilder } from 'iagate-querykit'
const q = new QueryBuilder('users')
.selectCount('*', 'total')
.selectCaseSum('active = 1', 'active_count')
const { sql } = q.toSql()
// Expected SQL contains:
// SELECT COUNT(*) AS total, SUM(CASE WHEN active = 1 THEN 1 ELSE 0 END) AS active_count FROM users
import { QueryBuilder } from 'iagate-querykit'
const last7d = new QueryBuilder('logins')
.period('created_at', '7d')
.select(['id'])
const last24h = new QueryBuilder('logins')
.period('created_at', '24h')
.select(['id'])
// Expected: Both produce WHERE created_at >= ? ISO timestamp bounds
import { ViewManager, QueryBuilder } from 'iagate-querykit'
const views = new ViewManager()
const q = new QueryBuilder('events')
.select(['user_id'])
.whereRaw('timestamp >= datetime("now", "-1 day")')
views.createOrReplaceView('active_last_day', q)
views.scheduleViewRefresh('active_last_day', q, 15 * 60 * 1000)
// Expected: a SQLite view named active_last_day exists and will refresh every 15m
import { TriggerManager } from 'iagate-querykit'
const triggers = new TriggerManager()
triggers.createTrigger(
'orders_set_updated_at',
'orders',
'BEFORE',
'UPDATE',
`SET NEW.updated_at = CURRENT_TIMESTAMP;`
)
// Expected: a trigger exists in sqlite_master for 'orders_set_updated_at'
import { Model } from 'iagate-querykit'
class User extends Model {
protected static tableName = 'users'
protected fillable = ['email', 'active']
}
const u = new User()
u.fill({ id: 999, email: 'x@y.com', active: 1 })
await u.save()
// Expected: INSERT only email and active (id is guarded)
import { table } from 'iagate-querykit'
const Users = table<{ id: number; email: string }>('users')
const first = Users.orderBy('id', 'ASC').firstSync<{ id: number; email: string }>()
// Expected: { id, email } of the first row
FAQs
QueryKit: lightweight TypeScript query toolkit with models, views, triggers, events, scheduler and adapters (better-sqlite3).
The npm package iagate-querykit receives a total of 21 weekly downloads. As such, iagate-querykit popularity was classified as not popular.
We found that iagate-querykit 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.
Security News
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
Security News
/Research
Malicious Nx npm versions stole secrets and wallet info using AI CLI tools; Socket’s AI scanner detected the supply chain attack and flagged the malware.
Security News
CISA’s 2025 draft SBOM guidance adds new fields like hashes, licenses, and tool metadata to make software inventories more actionable.