Not really bread. Not really fruit. Just like this package. Simple CRUD helpers on top of knex.
npm install breadfruitRequires Node.js >=22.
Breadfruit is an ES module with a default export.
importbreadfruitfrom'breadfruit';constconfig={client: 'pg',connection: 'postgres://postgres@localhost:5432/someDatabase',pool: {min: 1,max: 7},};const{ browse, read, edit, add, del, raw }=breadfruit(config);Returns an array of rows.
constusers=awaitbrowse('users',['username','user_id'],{active: true});Supported options:
limit(default1000)offset(default0)orderBy— column name or array of column namessortOrder—'ASC'/'DESC'(default'ASC'), or an array matchingorderBydateField(default'created_at')search_start_date/search_end_date— adds awhereBetweenondateFielddbApi— override the internal knex instance (useful for transactions)
Returns a single row.
constuser=awaitread('users',['username','first_name'],{user_id: 1337});Inserts and returns the new row.
constnewUser=awaitadd('users',['user_id'],{first_name: 'Howard',username: 'howitzer',});Updates matching rows and returns the first updated row.
constupdated=awaitedit('users',['username','first_name'],{first_name: 'Howard'},{user_id: 1337},);Deletes matching rows and returns the count.
constcount=awaitdel('users',{user_id: 1337});Runs a raw SQL statement and returns rows.
constrows=awaitraw('select * from users');Returns the count of matching rows as a number.
constactiveUsers=awaitcount('users',{active: true});Inserts a row, or updates on conflict. conflictColumns can be a string or array.
constrow=awaitupsert('users','*',{email: 'luis@example.com',name: 'Luis'},'email',);Wraps knex.transaction(). Pass the trx object as dbApi in your method calls.
awaittransaction(async(trx)=>{awaitadd('users',['id'],{name: 'a'},{dbApi: trx});awaitadd('users',['id'],{name: 'b'},{dbApi: trx});});Instead of a config object, you can pass a Knex instance. Useful when you already have a Knex connection in your app and want breadfruit to use it rather than open a second pool.
importknexfrom'./db.js';importbreadfruitfrom'breadfruit';constbf=breadfruit(knex);Filter values accept operators beyond simple equality.
| Shape | SQL |
|---|---|
{ col: value } | col = value |
{ col: [a, b, c] } | col IN (a, b, c) |
{ col: null } | col IS NULL |
{ col: { eq: x } } | col = x |
{ col: { ne: x } } | col != x |
{ col: { gt: x } } | col > x |
{ col: { gte: x } } | col >= x |
{ col: { lt: x } } | col < x |
{ col: { lte: x } } | col <= x |
{ col: { like: 'x%' } } | col LIKE 'x%' |
{ col: { ilike: 'x%' } } | col ILIKE 'x%' |
{ col: { in: [a, b] } } | col IN (a, b) |
{ col: { notIn: [a, b] } } | col NOT IN (a, b) |
{ col: { between: [a, b] } } | col BETWEEN a AND b |
{ col: { notBetween: [a, b] } } | col NOT BETWEEN a AND b |
{ col: { null: true } } | col IS NULL |
{ col: { null: false } } | col IS NOT NULL |
Multiple operators on the same column AND together:
awaitbrowse('events','*',{count: {gt: 1,lte: 100},created_at: {gte: '2026-01-01'},});Returns an object with the same BREAD methods but bound to a specific table, with optional soft delete and view-for-reads behavior.
constusers=bf.forTable('users',{softDelete: true,viewName: 'users_v',});awaitusers.browse('*',{active: true});// reads from users_vawaitusers.del({id: 42});// soft-deletes in usersawaitusers.restore({id: 42});// un-soft-deletesconsttotal=awaitusers.count({});// respects soft deleteThree options for the softDelete config:
// 1. Boolean shorthand — uses is_deleted column, true/false
softDelete: true// 2. Full config
softDelete: {column: 'is_deleted',value: true,// set on deleteundeletedValue: false,// the "active" value for filtering}// 3. Timestamp style — deleted_at IS NULL means active
softDelete: {column: 'deleted_at',value: 'NOW',// special string -> knex.fn.now()undeletedValue: null,}The value field accepts:
- a literal (
true,false,Date, etc.) - the string
'NOW'— becomesknex.fn.now()so the DB generates the timestamp - a Knex raw expression like
knex.fn.now()orknex.raw('...') - a function — called at delete time (runs in JS, not DB)
Pass viewName to read from a view while writing to the underlying table. Great for denormalized read paths.
bf.forTable('users',{viewName: 'user_groups_v'});Bypass the soft-delete filter for admin or audit views:
constallUsers=awaitusers.browse('*',{},{withDeleted: true});constcount=awaitusers.count({},{withDeleted: true});Pass dbApi: trx through just like the top-level API:
awaitbf.transaction(async(trx)=>{awaitusers.add('*',{email: 'a@b.c'},{dbApi: trx});awaitusers.edit('*',{active: true},{email: 'a@b.c'},{dbApi: trx});});ISC
