breadfruit 2.2.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -21
- package/index.js +240 -81
- package/package.json +39 -9
- package/.coveralls.yml +0 -2
- package/.travis.yml +0 -4
- package/happy_breadfruit.png +0 -0
- package/test/index.js +0 -95
package/README.md
CHANGED
|
@@ -1,49 +1,258 @@
|
|
|
1
1
|
# breadfruit
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/breadfruit)
|
|
4
|
+
[](https://github.com/iceddev/breadfruit/actions/workflows/ci.yml)
|
|
5
|
+
[](https://codecov.io/gh/iceddev/breadfruit)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://www.npmjs.com/package/breadfruit)
|
|
8
|
+
[](https://github.com/iceddev/breadfruit/blob/main/LICENSE)
|
|
4
9
|
|
|
5
|
-
Not really bread. Not really fruit. Just like this package.
|
|
10
|
+
Not really bread. Not really fruit. Just like this package. Simple CRUD helpers on top of [knex](https://knexjs.org/).
|
|
6
11
|
|
|
7
12
|

|
|
8
13
|
|
|
9
|
-
##
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install breadfruit
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Node.js `>=22`.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
Breadfruit is an ES module with a default export.
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import breadfruit from 'breadfruit';
|
|
10
28
|
|
|
11
|
-
```javascript
|
|
12
29
|
const config = {
|
|
13
|
-
client: '
|
|
30
|
+
client: 'pg',
|
|
14
31
|
connection: 'postgres://postgres@localhost:5432/someDatabase',
|
|
15
|
-
pool: { min: 1, max: 7 }
|
|
32
|
+
pool: { min: 1, max: 7 },
|
|
16
33
|
};
|
|
17
34
|
|
|
18
|
-
const
|
|
35
|
+
const { browse, read, edit, add, del, raw } = breadfruit(config);
|
|
19
36
|
```
|
|
20
37
|
|
|
21
|
-
##
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
### `browse(table, fields, filter, options?)`
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
const {browse, read, edit, add, del, raw} = require('breadfruit')(config);
|
|
42
|
+
Returns an array of rows.
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
const users = await browse('users', ['username', 'user_id'], {active: true});
|
|
44
|
+
```js
|
|
45
|
+
const users = await browse('users', ['username', 'user_id'], { active: true });
|
|
46
|
+
```
|
|
28
47
|
|
|
48
|
+
Supported `options`:
|
|
49
|
+
- `limit` (default `1000`)
|
|
50
|
+
- `offset` (default `0`)
|
|
51
|
+
- `orderBy` — column name or array of column names
|
|
52
|
+
- `sortOrder` — `'ASC'` / `'DESC'` (default `'ASC'`), or an array matching `orderBy`
|
|
53
|
+
- `dateField` (default `'created_at'`)
|
|
54
|
+
- `search_start_date` / `search_end_date` — adds a `whereBetween` on `dateField`
|
|
55
|
+
- `dbApi` — override the internal knex instance (useful for transactions)
|
|
29
56
|
|
|
30
|
-
|
|
31
|
-
const user = await read('users', ['username', 'first_name'], {user_id: 1337});
|
|
57
|
+
### `read(table, fields, filter, options?)`
|
|
32
58
|
|
|
59
|
+
Returns a single row.
|
|
33
60
|
|
|
34
|
-
|
|
35
|
-
const
|
|
61
|
+
```js
|
|
62
|
+
const user = await read('users', ['username', 'first_name'], { user_id: 1337 });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `add(table, returnFields, data, options?)`
|
|
66
|
+
|
|
67
|
+
Inserts and returns the new row.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
const newUser = await add('users', ['user_id'], {
|
|
71
|
+
first_name: 'Howard',
|
|
72
|
+
username: 'howitzer',
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `edit(table, returnFields, data, filter, options?)`
|
|
77
|
+
|
|
78
|
+
Updates matching rows and returns the first updated row.
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const updated = await edit(
|
|
82
|
+
'users',
|
|
83
|
+
['username', 'first_name'],
|
|
84
|
+
{ first_name: 'Howard' },
|
|
85
|
+
{ user_id: 1337 },
|
|
86
|
+
);
|
|
87
|
+
```
|
|
36
88
|
|
|
89
|
+
### `del(table, filter, options?)`
|
|
37
90
|
|
|
38
|
-
|
|
39
|
-
const newUser = await add('users', ['user_id'], {first_name: 'Howard', username: 'howitzer'});
|
|
91
|
+
Deletes matching rows and returns the count.
|
|
40
92
|
|
|
93
|
+
```js
|
|
94
|
+
const count = await del('users', { user_id: 1337 });
|
|
95
|
+
```
|
|
41
96
|
|
|
42
|
-
|
|
43
|
-
const deleteCount = await del('users', {user_id: 1337});
|
|
97
|
+
### `raw(sql, options?)`
|
|
44
98
|
|
|
99
|
+
Runs a raw SQL statement and returns rows.
|
|
45
100
|
|
|
46
|
-
|
|
101
|
+
```js
|
|
47
102
|
const rows = await raw('select * from users');
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `count(table, filter, options?)`
|
|
48
106
|
|
|
107
|
+
Returns the count of matching rows as a number.
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
const activeUsers = await count('users', { active: true });
|
|
49
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
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
ISC
|
package/index.js
CHANGED
|
@@ -1,89 +1,248 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
function connect(settings) {
|
|
4
|
-
const knex = knexConstructor(settings);
|
|
5
|
-
const defaultOptions = {
|
|
6
|
-
dateField: 'created_at',
|
|
7
|
-
dbApi: knex,
|
|
8
|
-
limit: 1000,
|
|
9
|
-
offset: 0,
|
|
10
|
-
sortOrder: 'ASC',
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
browse(table, fields, filter, options = {}) {
|
|
15
|
-
const dbApi = options.dbApi || defaultOptions.dbApi || knex;
|
|
16
|
-
const limit = options.limit || defaultOptions.limit;
|
|
17
|
-
const offset = options.offset || defaultOptions.offset;
|
|
18
|
-
const dateField = options.dateField || defaultOptions.dateField;
|
|
19
|
-
const sortOrder = options.sortOrder || defaultOptions.sortOrder;
|
|
20
|
-
|
|
21
|
-
let query = dbApi(table)
|
|
22
|
-
.where(filter)
|
|
23
|
-
.select(fields)
|
|
24
|
-
.limit(limit)
|
|
25
|
-
.offset(offset);
|
|
26
|
-
|
|
27
|
-
if (options.search_start_date && options.search_end_date) {
|
|
28
|
-
query = query
|
|
29
|
-
.whereBetween(dateField, [options.search_start_date, options.search_end_date]);
|
|
30
|
-
}
|
|
1
|
+
import knexConstructor from 'knex';
|
|
31
2
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
3
|
+
const defaults = {
|
|
4
|
+
dateField: 'created_at',
|
|
5
|
+
limit: 1000,
|
|
6
|
+
offset: 0,
|
|
7
|
+
sortOrder: 'ASC',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const OPERATORS = {
|
|
11
|
+
eq: '=',
|
|
12
|
+
ne: '!=',
|
|
13
|
+
gt: '>',
|
|
14
|
+
gte: '>=',
|
|
15
|
+
lt: '<',
|
|
16
|
+
lte: '<=',
|
|
17
|
+
like: 'like',
|
|
18
|
+
ilike: 'ilike',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Apply a filter object to a Knex query. Supports:
|
|
22
|
+
// { col: value } -> WHERE col = value
|
|
23
|
+
// { col: [a, b, c] } -> WHERE col IN (...)
|
|
24
|
+
// { col: null } -> WHERE col IS NULL
|
|
25
|
+
// { col: { gt: x, lte: y } } -> WHERE col > x AND col <= y
|
|
26
|
+
// { col: { in: [a, b] } } -> WHERE col IN (...)
|
|
27
|
+
// { col: { notIn: [a, b] } } -> WHERE col NOT IN (...)
|
|
28
|
+
// { col: { between: [a, b] } } -> WHERE col BETWEEN a AND b
|
|
29
|
+
function applyFilter(query, filter) {
|
|
30
|
+
if (!filter) return query;
|
|
31
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
32
|
+
if (value === undefined) continue;
|
|
33
|
+
if (value === null) {
|
|
34
|
+
query = query.whereNull(key);
|
|
35
|
+
} else if (Array.isArray(value)) {
|
|
36
|
+
query = query.whereIn(key, value);
|
|
37
|
+
} else if (typeof value === 'object' && !(value instanceof Date) && !value.toSQL) {
|
|
38
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
39
|
+
if (operand === undefined) continue;
|
|
40
|
+
if (op === 'in') query = query.whereIn(key, operand);
|
|
41
|
+
else if (op === 'notIn') query = query.whereNotIn(key, operand);
|
|
42
|
+
else if (op === 'between') query = query.whereBetween(key, operand);
|
|
43
|
+
else if (op === 'notBetween') query = query.whereNotBetween(key, operand);
|
|
44
|
+
else if (op === 'null') {
|
|
45
|
+
query = operand ? query.whereNull(key) : query.whereNotNull(key);
|
|
46
|
+
} else if (OPERATORS[op]) {
|
|
47
|
+
query = query.where(key, OPERATORS[op], operand);
|
|
40
48
|
} else {
|
|
41
|
-
|
|
49
|
+
throw new Error(`Unknown filter operator: ${op}`);
|
|
42
50
|
}
|
|
43
51
|
}
|
|
52
|
+
} else {
|
|
53
|
+
query = query.where(key, value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return query;
|
|
57
|
+
}
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
59
|
+
// Resolve a soft-delete config to { column, value, undeletedValue }.
|
|
60
|
+
// Accepts:
|
|
61
|
+
// true -> { column: 'is_deleted', value: true, undeletedValue: false }
|
|
62
|
+
// { column, value, undeletedValue } -> used as-is
|
|
63
|
+
// value can be:
|
|
64
|
+
// - a literal (true, false, Date, etc.)
|
|
65
|
+
// - the string 'NOW' -> knex.fn.now()
|
|
66
|
+
// - a Knex raw expression (e.g. knex.fn.now())
|
|
67
|
+
// - a function -> called at delete time
|
|
68
|
+
function resolveSoftDelete(config, knex) {
|
|
69
|
+
if (!config) return null;
|
|
70
|
+
if (config === true) {
|
|
71
|
+
return { column: 'is_deleted', value: true, undeletedValue: false };
|
|
72
|
+
}
|
|
73
|
+
if (typeof config !== 'object') {
|
|
74
|
+
throw new Error('softDelete must be true or an object');
|
|
75
|
+
}
|
|
76
|
+
const column = config.column || 'is_deleted';
|
|
77
|
+
const undeletedValue = 'undeletedValue' in config ? config.undeletedValue : false;
|
|
78
|
+
let value = 'value' in config ? config.value : true;
|
|
79
|
+
if (value === 'NOW') value = knex.fn.now();
|
|
80
|
+
return { column, value, undeletedValue };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveValue(value) {
|
|
84
|
+
return typeof value === 'function' ? value() : value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default function connect(settings) {
|
|
88
|
+
// Allow passing an existing knex instance or a config
|
|
89
|
+
const knex = typeof settings === 'function' ? settings : knexConstructor(settings);
|
|
90
|
+
|
|
91
|
+
function browse(table, fields, filter, options = {}) {
|
|
92
|
+
const dbApi = options.dbApi || knex;
|
|
93
|
+
const limit = options.limit ?? defaults.limit;
|
|
94
|
+
const offset = options.offset ?? defaults.offset;
|
|
95
|
+
const dateField = options.dateField || defaults.dateField;
|
|
96
|
+
const sortOrder = options.sortOrder || defaults.sortOrder;
|
|
97
|
+
|
|
98
|
+
let query = dbApi(table).select(fields).limit(limit).offset(offset);
|
|
99
|
+
|
|
100
|
+
query = applyFilter(query, filter);
|
|
101
|
+
|
|
102
|
+
if (options.search_start_date && options.search_end_date) {
|
|
103
|
+
query = query.whereBetween(dateField, [
|
|
104
|
+
options.search_start_date,
|
|
105
|
+
options.search_end_date,
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.orderBy) {
|
|
110
|
+
if (Array.isArray(options.orderBy)) {
|
|
111
|
+
options.orderBy.forEach((orderBy, index) => {
|
|
112
|
+
const order = Array.isArray(sortOrder) ? sortOrder[index] : sortOrder;
|
|
113
|
+
query = query.orderBy(orderBy, order);
|
|
73
114
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return dbApi(table)
|
|
78
|
-
.where(filter)
|
|
79
|
-
.del();
|
|
80
|
-
},
|
|
81
|
-
raw(sql, options = {}) {
|
|
82
|
-
const dbApi = options.dbApi || knex;
|
|
83
|
-
return dbApi.raw(sql, options)
|
|
84
|
-
.then(res => res.rows || res);
|
|
115
|
+
} else {
|
|
116
|
+
query = query.orderBy(options.orderBy, sortOrder);
|
|
117
|
+
}
|
|
85
118
|
}
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
119
|
|
|
89
|
-
|
|
120
|
+
return query;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function read(table, fields, filter, options = {}) {
|
|
124
|
+
const dbApi = options.dbApi || knex;
|
|
125
|
+
let query = dbApi(table).select(fields);
|
|
126
|
+
query = applyFilter(query, filter);
|
|
127
|
+
const row = await query.first();
|
|
128
|
+
return row || null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function add(table, fields, data, options = {}) {
|
|
132
|
+
const dbApi = options.dbApi || knex;
|
|
133
|
+
const [row] = await dbApi(table).returning(fields).insert(data);
|
|
134
|
+
return row;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function edit(table, fields, data, filter, options = {}) {
|
|
138
|
+
const dbApi = options.dbApi || knex;
|
|
139
|
+
let query = dbApi(table).returning(fields).update(data);
|
|
140
|
+
query = applyFilter(query, filter);
|
|
141
|
+
const [row] = await query;
|
|
142
|
+
return row;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function del(table, filter, options = {}) {
|
|
146
|
+
const dbApi = options.dbApi || knex;
|
|
147
|
+
let query = dbApi(table).del();
|
|
148
|
+
query = applyFilter(query, filter);
|
|
149
|
+
return query;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function count(table, filter, options = {}) {
|
|
153
|
+
const dbApi = options.dbApi || knex;
|
|
154
|
+
let query = dbApi(table).count('* as total');
|
|
155
|
+
query = applyFilter(query, filter);
|
|
156
|
+
const [row] = await query;
|
|
157
|
+
return Number(row.total);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function upsert(table, fields, data, conflictColumns, options = {}) {
|
|
161
|
+
const dbApi = options.dbApi || knex;
|
|
162
|
+
const cols = Array.isArray(conflictColumns) ? conflictColumns : [conflictColumns];
|
|
163
|
+
const [row] = await dbApi(table)
|
|
164
|
+
.insert(data)
|
|
165
|
+
.onConflict(cols)
|
|
166
|
+
.merge()
|
|
167
|
+
.returning(fields);
|
|
168
|
+
return row;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function raw(sql, options = {}) {
|
|
172
|
+
const dbApi = options.dbApi || knex;
|
|
173
|
+
const res = await dbApi.raw(sql, options);
|
|
174
|
+
return res.rows || res;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function transaction(callback) {
|
|
178
|
+
return knex.transaction(callback);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Table-bound API with optional softDelete and viewName support.
|
|
182
|
+
// Returns an object with the same BREAD methods but bound to a specific table,
|
|
183
|
+
// with soft-delete filtering baked into browse/read/del, and optional
|
|
184
|
+
// separate readTable (view) for reads vs writes.
|
|
185
|
+
function forTable(tableName, tableOptions = {}) {
|
|
186
|
+
const softDelete = resolveSoftDelete(tableOptions.softDelete, knex);
|
|
187
|
+
const readTable = tableOptions.viewName || tableName;
|
|
188
|
+
const writeTable = tableName;
|
|
189
|
+
|
|
190
|
+
function addSoftDeleteFilter(filter = {}, options = {}) {
|
|
191
|
+
if (!softDelete) return filter;
|
|
192
|
+
if (options.withDeleted) return filter;
|
|
193
|
+
// Don't override if caller explicitly set the column
|
|
194
|
+
if (filter[softDelete.column] !== undefined) return filter;
|
|
195
|
+
return { ...filter, [softDelete.column]: softDelete.undeletedValue };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
browse(fields, filter, options = {}) {
|
|
200
|
+
return browse(readTable, fields, addSoftDeleteFilter(filter, options), options);
|
|
201
|
+
},
|
|
202
|
+
read(fields, filter, options = {}) {
|
|
203
|
+
return read(readTable, fields, addSoftDeleteFilter(filter, options), options);
|
|
204
|
+
},
|
|
205
|
+
add(fields, data, options = {}) {
|
|
206
|
+
return add(writeTable, fields, data, options);
|
|
207
|
+
},
|
|
208
|
+
edit(fields, data, filter, options = {}) {
|
|
209
|
+
return edit(writeTable, fields, data, addSoftDeleteFilter(filter, options), options);
|
|
210
|
+
},
|
|
211
|
+
del(filter, options = {}) {
|
|
212
|
+
if (softDelete) {
|
|
213
|
+
const dbApi = options.dbApi || knex;
|
|
214
|
+
let query = dbApi(writeTable).update({
|
|
215
|
+
[softDelete.column]: resolveValue(softDelete.value),
|
|
216
|
+
});
|
|
217
|
+
query = applyFilter(query, addSoftDeleteFilter(filter, options));
|
|
218
|
+
return query;
|
|
219
|
+
}
|
|
220
|
+
return del(writeTable, filter, options);
|
|
221
|
+
},
|
|
222
|
+
restore(filter, options = {}) {
|
|
223
|
+
if (!softDelete) {
|
|
224
|
+
throw new Error('restore() requires softDelete to be configured');
|
|
225
|
+
}
|
|
226
|
+
const dbApi = options.dbApi || knex;
|
|
227
|
+
let query = dbApi(writeTable).update({
|
|
228
|
+
[softDelete.column]: softDelete.undeletedValue,
|
|
229
|
+
});
|
|
230
|
+
// When restoring, look only at deleted rows
|
|
231
|
+
const restoreFilter = { ...filter, [softDelete.column]: { ne: softDelete.undeletedValue } };
|
|
232
|
+
query = applyFilter(query, restoreFilter);
|
|
233
|
+
return query;
|
|
234
|
+
},
|
|
235
|
+
count(filter, options = {}) {
|
|
236
|
+
return count(readTable, addSoftDeleteFilter(filter, options), options);
|
|
237
|
+
},
|
|
238
|
+
upsert(fields, data, conflictColumns, options = {}) {
|
|
239
|
+
return upsert(writeTable, fields, data, conflictColumns, options);
|
|
240
|
+
},
|
|
241
|
+
softDelete,
|
|
242
|
+
tableName: writeTable,
|
|
243
|
+
readTable,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { browse, read, add, edit, del, raw, count, upsert, transaction, forTable, knex };
|
|
248
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breadfruit",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Boilerplate
|
|
5
|
-
"
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "Boilerplate SQL query helpers for Node.js using Knex",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./index.js",
|
|
10
|
+
"default": "./index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
6
20
|
"scripts": {
|
|
7
|
-
"
|
|
21
|
+
"lint": "eslint .",
|
|
22
|
+
"lint:fix": "eslint . --fix",
|
|
23
|
+
"test": "node --test",
|
|
24
|
+
"test:coverage": "c8 --reporter=text --reporter=lcov node --test",
|
|
25
|
+
"prepublishOnly": "npm run lint && npm test"
|
|
8
26
|
},
|
|
9
27
|
"author": "Alyson Zepeda",
|
|
10
28
|
"license": "ISC",
|
|
@@ -14,14 +32,26 @@
|
|
|
14
32
|
"homepage": "https://github.com/iceddev/breadfruit#readme",
|
|
15
33
|
"repository": {
|
|
16
34
|
"type": "git",
|
|
17
|
-
"url": "git+
|
|
35
|
+
"url": "git+https://github.com/iceddev/breadfruit.git"
|
|
18
36
|
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"knex",
|
|
39
|
+
"sql",
|
|
40
|
+
"crud",
|
|
41
|
+
"postgres",
|
|
42
|
+
"mysql",
|
|
43
|
+
"database"
|
|
44
|
+
],
|
|
19
45
|
"dependencies": {
|
|
20
|
-
"knex": "^
|
|
46
|
+
"knex": "^3.2.0"
|
|
21
47
|
},
|
|
22
48
|
"devDependencies": {
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
49
|
+
"@eslint/js": "^10.0.1",
|
|
50
|
+
"c8": "^11.0.0",
|
|
51
|
+
"eslint": "^10.2.0",
|
|
52
|
+
"globals": "^17.5.0"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
26
56
|
}
|
|
27
57
|
}
|
package/.coveralls.yml
DELETED
package/.travis.yml
DELETED
package/happy_breadfruit.png
DELETED
|
Binary file
|
package/test/index.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chai = require('chai');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
var expect = chai.expect;
|
|
7
|
-
chai.should();
|
|
8
|
-
|
|
9
|
-
var breadfruit = require('../');
|
|
10
|
-
|
|
11
|
-
describe('breadfruit', function(){
|
|
12
|
-
|
|
13
|
-
it('be awesome', function(done){
|
|
14
|
-
done();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('should create an instance of the api', function(done) {
|
|
18
|
-
const api = breadfruit({client: 'pg'});
|
|
19
|
-
api.should.be.an('object');
|
|
20
|
-
done();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should fail to browse without a real connection', function(done) {
|
|
24
|
-
const api = breadfruit({client: 'pg'});
|
|
25
|
-
api.browse('tableName', [], {})
|
|
26
|
-
.catch((error) => {
|
|
27
|
-
done();
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should fail to browse without a real connection', function(done) {
|
|
32
|
-
const api = breadfruit({client: 'pg'});
|
|
33
|
-
api.browse('tableName', [], {}, {orderBy: 'someColumn'})
|
|
34
|
-
.catch((error) => {
|
|
35
|
-
done();
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should fail to browse without a real connection', function(done) {
|
|
40
|
-
const api = breadfruit({client: 'pg'});
|
|
41
|
-
api.browse('tableName', [], {}, {orderBy: ['someColumn', 'otherColumn']})
|
|
42
|
-
.catch((error) => {
|
|
43
|
-
done();
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should fail to browse without a real connection', function(done) {
|
|
48
|
-
const api = breadfruit({client: 'pg'});
|
|
49
|
-
api.browse('tableName', [], {}, {orderBy: ['someColumn', 'otherColumn'], sortOrder: ['asc', 'desc']})
|
|
50
|
-
.catch((error) => {
|
|
51
|
-
done();
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should fail to read without a real connection', function(done) {
|
|
56
|
-
const api = breadfruit({client: 'pg'});
|
|
57
|
-
api.read('tableName', [], {})
|
|
58
|
-
.catch((error) => {
|
|
59
|
-
done();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should fail to add without a real connection', function(done) {
|
|
64
|
-
const api = breadfruit({client: 'pg'});
|
|
65
|
-
api.add('tableName', [], {})
|
|
66
|
-
.catch((error) => {
|
|
67
|
-
done();
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should fail to edit without a real connection', function(done) {
|
|
72
|
-
const api = breadfruit({client: 'pg'});
|
|
73
|
-
api.edit('tableName', [], {})
|
|
74
|
-
.catch((error) => {
|
|
75
|
-
done();
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should fail to delete without a real connection', function(done) {
|
|
80
|
-
const api = breadfruit({client: 'pg'});
|
|
81
|
-
api.del('tableName', [], {})
|
|
82
|
-
.catch((error) => {
|
|
83
|
-
done();
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should fail to do raw query without a real connection', function(done) {
|
|
88
|
-
const api = breadfruit({client: 'pg'});
|
|
89
|
-
api.raw('select NOW()', {})
|
|
90
|
-
.catch((error) => {
|
|
91
|
-
done();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
});
|