@@ -17,7 +17,7 @@ Not really bread. Not really fruit. Just like this package. Simple CRUD helpers
1717npm install breadfruit
1818```
1919
20- Requires Node.js ` >=20 ` .
20+ Requires Node.js ` >=22 ` .
2121
2222## Usage
2323
@@ -102,6 +102,157 @@ Runs a raw SQL statement and returns rows.
102102const rows = await raw (' select * from users' );
103103```
104104
105+ ### ` count(table, filter, options?) `
106+
107+ Returns the count of matching rows as a number.
108+
109+ ``` js
110+ const activeUsers = await count (' users' , { active: true });
111+ ```
112+
113+ ### ` upsert(table, returnFields, data, conflictColumns, options?) `
114+
115+ Inserts a row, or updates on conflict. ` conflictColumns ` can be a string or array.
116+
117+ ``` js
118+ const row = await upsert (
119+ ' users' ,
120+ ' *' ,
121+ { email: ' luis@example.com' , name: ' Luis' },
122+ ' email' ,
123+ );
124+ ```
125+
126+ ### ` transaction(callback) `
127+
128+ Wraps ` knex.transaction() ` . Pass the ` trx ` object as ` dbApi ` in your method calls.
129+
130+ ``` js
131+ await transaction (async (trx ) => {
132+ await add (' users' , [' id' ], { name: ' a' }, { dbApi: trx });
133+ await add (' users' , [' id' ], { name: ' b' }, { dbApi: trx });
134+ });
135+ ```
136+
137+ ## Advanced
138+
139+ ### Passing an existing Knex instance
140+
141+ 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.
142+
143+ ``` js
144+ import knex from ' ./db.js' ;
145+ import breadfruit from ' breadfruit' ;
146+
147+ const bf = breadfruit (knex);
148+ ```
149+
150+ ### Composite filters
151+
152+ Filter values accept operators beyond simple equality.
153+
154+ | Shape | SQL |
155+ | ---| ---|
156+ | ` { col: value } ` | ` col = value ` |
157+ | ` { col: [a, b, c] } ` | ` col IN (a, b, c) ` |
158+ | ` { col: null } ` | ` col IS NULL ` |
159+ | ` { col: { eq: x } } ` | ` col = x ` |
160+ | ` { col: { ne: x } } ` | ` col != x ` |
161+ | ` { col: { gt: x } } ` | ` col > x ` |
162+ | ` { col: { gte: x } } ` | ` col >= x ` |
163+ | ` { col: { lt: x } } ` | ` col < x ` |
164+ | ` { col: { lte: x } } ` | ` col <= x ` |
165+ | ` { col: { like: 'x%' } } ` | ` col LIKE 'x%' ` |
166+ | ` { col: { ilike: 'x%' } } ` | ` col ILIKE 'x%' ` |
167+ | ` { col: { in: [a, b] } } ` | ` col IN (a, b) ` |
168+ | ` { col: { notIn: [a, b] } } ` | ` col NOT IN (a, b) ` |
169+ | ` { col: { between: [a, b] } } ` | ` col BETWEEN a AND b ` |
170+ | ` { col: { notBetween: [a, b] } } ` | ` col NOT BETWEEN a AND b ` |
171+ | ` { col: { null: true } } ` | ` col IS NULL ` |
172+ | ` { col: { null: false } } ` | ` col IS NOT NULL ` |
173+
174+ Multiple operators on the same column AND together:
175+
176+ ``` js
177+ await browse (' events' , ' *' , {
178+ count: { gt: 1 , lte: 100 },
179+ created_at: { gte: ' 2026-01-01' },
180+ });
181+ ```
182+
183+ ### ` forTable(tableName, options?) ` — table-bound helpers
184+
185+ Returns an object with the same BREAD methods but bound to a specific table, with optional ** soft delete** and ** view-for-reads** behavior.
186+
187+ ``` js
188+ const users = bf .forTable (' users' , {
189+ softDelete: true ,
190+ viewName: ' users_v' ,
191+ });
192+
193+ await users .browse (' *' , { active: true }); // reads from users_v
194+ await users .del ({ id: 42 }); // soft-deletes in users
195+ await users .restore ({ id: 42 }); // un-soft-deletes
196+ const total = await users .count ({}); // respects soft delete
197+ ```
198+
199+ #### Soft delete
200+
201+ Three options for the ` softDelete ` config:
202+
203+ ``` js
204+ // 1. Boolean shorthand — uses is_deleted column, true/false
205+ softDelete: true
206+
207+ // 2. Full config
208+ softDelete: {
209+ column: ' is_deleted' ,
210+ value: true , // set on delete
211+ undeletedValue: false , // the "active" value for filtering
212+ }
213+
214+ // 3. Timestamp style — deleted_at IS NULL means active
215+ softDelete: {
216+ column: ' deleted_at' ,
217+ value: ' NOW' , // special string -> knex.fn.now()
218+ undeletedValue: null ,
219+ }
220+ ```
221+
222+ The ` value ` field accepts:
223+ - a literal (` true ` , ` false ` , ` Date ` , etc.)
224+ - the string ` 'NOW' ` — becomes ` knex.fn.now() ` so the DB generates the timestamp
225+ - a Knex raw expression like ` knex.fn.now() ` or ` knex.raw('...') `
226+ - a function — called at delete time (runs in JS, not DB)
227+
228+ #### Reads from a view, writes to the table
229+
230+ Pass ` viewName ` to read from a view while writing to the underlying table. Great for denormalized read paths.
231+
232+ ``` js
233+ bf .forTable (' users' , { viewName: ' user_groups_v' });
234+ ```
235+
236+ #### ` withDeleted `
237+
238+ Bypass the soft-delete filter for admin or audit views:
239+
240+ ``` js
241+ const allUsers = await users .browse (' *' , {}, { withDeleted: true });
242+ const count = await users .count ({}, { withDeleted: true });
243+ ```
244+
245+ ### Transactions with ` forTable `
246+
247+ Pass ` dbApi: trx ` through just like the top-level API:
248+
249+ ``` js
250+ await bf .transaction (async (trx ) => {
251+ await users .add (' *' , { email: ' a@b.c' }, { dbApi: trx });
252+ await users .edit (' *' , { active: true }, { email: ' a@b.c' }, { dbApi: trx });
253+ });
254+ ```
255+
105256## License
106257
107258ISC
0 commit comments