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 🙏

© 2026 – Pkg Stats / Ryan Hefner

arn-db

v0.3.0

Published

knex-style query builder for a Go Postgres query API over HTTP (/v2/raw)

Readme

arn-db

A knex-style query builder for Node.js. It talks to a Go query API (/v2/raw) over HTTP, and that API talks to Postgres.

Node app (arn-db) ──HTTP/JSON──▶ your-api.example.com/v2/raw (Go) ──▶ Postgres

Server-side only: every request is a POST with an apikey header. The API refuses browser requests (any request with an Origin header) and sends no CORS headers. Each key may be limited to a list of tables.

New here? Read GUIDE.md: every method, with recipes for nested AND/OR/NOT, relations, jsonb search and changes, counts, upsert, transactions and raw SQL.

Install

npm install arn-db

Set the API address and the app's key (in .env or the container env):

ARN_DB_URL=https://your-api.example.com/v2/raw
ARN_DB_KEY=<your API key>
import { db } from 'arn-db'          // or: const { db } = require('arn-db')

const { data, error } = await db('users').where('id', 1).single()

Importing arn-db loads ./.env into process.env (Node 20.12+; variables that are already set win), so your files need no dotenv import. db reads the two variables on its first query. ESM, CommonJS and TypeScript types are included.

For another key or server, make a client: createClient(url, key, { readFrom: 'replica' }).

Quick tour

// read
await db('users').select('id', 'name', 'city:meta->>city').where('active', true).orderBy('name').limit(20)
await db('users').where('votes', '>', 100).orWhere({ role: 'admin' })
await db('users').whereIn('id', [1, 2, 3]).whereNotNull('verified_at')
await db('users').whereBetween('age', [18, 30]).whereILike('email', '%@example.com')
await db('accounts').whereColumn('used', '<', 'limit')
await db('accounts').where(db.coalesce('last_read_at', 'created_at'), '<', db.now().minus('1 day'))

// nested groups: (id = 1 or id > 10) or (name = 'Tester')
await db('users').where((q) => q.where('id', 1).orWhere('id', '>', 10)).orWhere({ name: 'Tester' })

// one row, or null
const { data: user } = await db('users').where('email', email).single()

// count
const { data: n } = await db('users').where('active', true).count()             // a number

// write
await db('users').insert({ name, email }).returning('id')                         // [{ id }]
await db('users').where('id', id).update({ name, last_seen: db.now() })
await db('accounts').where('id', id).increment('total_used').updateJsonb('tags', 'addUnique', 'vip')
await db('posts').where('user_id', id).delete()

// upsert, find-or-create
await db('users').insert({ email, name }).onConflict('email').merge()
const { data } = await db('users').insert({ email, name }).onConflict('email').ignore().returning('id')

Every call resolves to { data, error, count, status } and never throws. A mistake the client can see (an unknown operator, a binding mismatch, an update without where) comes back as error.code === 'BAD_QUERY' and nothing is sent.

const { data, error } = await db('users').insert({ email })
if (error) {
  if (error.code === '23505') { /* email already used */ }
}

Where

| Method family (each has where/orWhere/whereNot/orWhereNot forms) | Example | |---|---| | where | .where('status', 1) · .where('votes', '>', 100) · .where({ a: 1, b: 2 }) · .where('deleted_at', null) | | whereIn | .whereIn('id', [1, 2]) · .whereNotIn('email', list) | | whereNull | .whereNull('deleted_at') · .whereNotNull('verified_at') | | whereBetween | .whereBetween('age', [18, 30]) | | whereLike / whereILike | .whereILike('email', '%@example.com') | | whereColumn | .whereColumn('updated_at', '>', 'created_at') | | whereExists | .whereExists('posts!user_id', (q) => q.where('published', true)) | | whereJsonb | .whereJsonb('tags', 'has', 'vip') | | whereRaw | .whereRaw('lower(email) = ?', [email]) |

Operators: = != <> > >= < <= like ilike not like not ilike in not in is is not. Anything else is a BAD_QUERY error, so a typo can never become SQL.

A function makes one set of parentheses, to any depth. The knex function () { this.where(...) } form works too.

jsonb

Search with whereJsonb(column, op, value):

| op | Matches when | |---|---| | has | the array contains the value | | hasAll | it contains every value | | hasAny | it contains at least one of the values (any element type) | | contains | the object (or array) includes these pairs / items, at any depth | | containedBy | it has nothing outside the list | | hasKey / hasAnyKey / hasAllKeys | the key(s) exist | | length | .whereJsonb('tags', 'length', '>', 2): the array size compared | | empty | [], {} or null | | equals | exactly this value (same order) |

Change with updateJsonb(column, op, value): append, appendOne, prepend, addUnique, remove, removeAt, setAt, set, merge, removeKey, increment. Use -> paths to work inside an object ('meta->labels'). It can be chained with update() and increment(), and the whole lot is one atomic statement.

Relations

await db('posts').select('title, author:users!user_id(name), comments(body), labels(name)')
await db('users').select('name, posts!user_id!inner(title)').where('posts.published', false)

// write related rows in the same statement (all or nothing)
await db('posts').insert({
  title: 'Hello',
  user_id: userId,
  comments: db.rel.create([{ body: 'first', author_id: userId }]),
  labels: db.rel.connect([labelId]),
}).returning('*')

db.rel.create, upsert, connectOrCreate, connect, disconnect, set, update and delete work for one-to-many, many-to-many and many-to-one links. See the table in GUIDE.md.

Transactions

The callback returns the steps without awaiting them. They run in order, and if any step fails, nothing is saved.

const { data, error } = await db.transaction((tx) => ({
  user: tx('users').insert({ email, name }).onConflict('email').ignore().returning('id'),
  post: tx('posts').insert({ user_id: tx.ref('user.id'), title }).returning('id'),
  n:    tx('posts').where('user_id', tx.ref('user.id')).count(),
}))
data.user[0].id
data.n             // a number

Raw SQL

db.raw(sql, bindings) is one SQL fragment, never a whole statement. It takes the knex binding forms: ? (a value), ?? (a column name), :name and :name:.

await db('users').whereRaw('lower(trim(email)) = ?', [email])
await db('users').select('id', db.raw('char_length(name)').as('name_len'))
await db('users').orderByRaw('length(name) desc, id')
await db('accounts').where('id', id).update({ score: db.raw('score * ?', [2]) })

Values are always sent as query parameters. The API key needs raw permission.

Read replicas

await db('posts').where('published', true).replica()
const reads = createClient(url, key, { readFrom: 'replica' })
await reads('users').where('email', email).primary()

Writes and transactions always use the primary.

Upgrading from 0.2

0.3 replaces the Supabase-style methods with knex-style ones. See GUIDE.md: Upgrading from 0.2 for the full table.

| 0.2 | 0.3 | |---|---| | db.from('t').select() | db('t') | | .eq('a', 1) · .lt('a', 1) · .in('a', [..]) | .where('a', 1) · .where('a', '<', 1) · .whereIn('a', [..]) | | .or((q) => q.eq(..).eq(..)) | .where((q) => q.where(..).orWhere(..)) | | .order('a', { ascending: false }) | .orderBy('a', 'desc') | | .upsert(row, { onConflict: 'email' }) | .insert(row).onConflict('email').merge() | | { n: db.ops.increment(1) } | .increment('n', 1) | | { tags: db.ops.append('x') } | .updateJsonb('tags', 'append', 'x') | | .overlaps('tags', [..]) / .contains(..) | .whereJsonb('tags', 'hasAny', [..]) / 'hasAll' | | db.col('b') | db.ref('b') or whereColumn |