Skip to content

Commit 16ad4b9

Browse files
committed
tests
1 parent 86b4cf8 commit 16ad4b9

6 files changed

Lines changed: 558 additions & 488 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,40 @@ name: CI
22

33
on:
44
push:
5-
branches: [main, master]
5+
branches: [main, master, orm_features]
66
pull_request:
77
branches: [main, master]
88

99
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: 22
17+
cache: npm
18+
- run: npm ci
19+
- run: npm run lint
20+
21+
audit:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
- uses: actions/setup-node@v4
26+
with:
27+
node-version: 22
28+
cache: npm
29+
- run: npm ci
30+
- name: Audit (fail on critical)
31+
run: npm audit --audit-level=critical
32+
1033
test:
1134
runs-on: ubuntu-latest
1235
strategy:
1336
fail-fast: false
1437
matrix:
15-
node-version: [20, 22, 24]
38+
node-version: [22, 24]
1639
steps:
1740
- uses: actions/checkout@v4
1841
- name: Use Node.js ${{ matrix.node-version }}
@@ -21,7 +44,6 @@ jobs:
2144
node-version: ${{ matrix.node-version }}
2245
cache: npm
2346
- run: npm ci
24-
- run: npm run lint
2547
- run: npm run test:coverage
2648
- name: Upload coverage
2749
if: matrix.node-version == 22

‎README.md‎

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Not really bread. Not really fruit. Just like this package. Simple CRUD helpers
1717
npm 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.
102102
const 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

107258
ISC

0 commit comments

Comments
 (0)