Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .coveralls.yml

This file was deleted.

31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [20, 22, 24]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run test:coverage
- name: Upload coverage
if: matrix.node-version == 22
uses: codecov/codecov-action@v5
with:
files: ./coverage/lcov.info
fail_ci_if_error: false
10 changes: 2 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
# Dependency directory
# Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules

# Other
.DS_Store
coverage
.nyc_output
package-lock.json

.DS_Store
*.log
4 changes: 0 additions & 4 deletions .travis.yml

This file was deleted.

102 changes: 80 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,107 @@
# breadfruit

[![NPM](https://nodei.co/npm/breadfruit.png?compact=true)](https://nodei.co/npm/breadfruit/)
[![npm version](https://img.shields.io/npm/v/breadfruit.svg)](https://www.npmjs.com/package/breadfruit)
[![CI](https://github.com/iceddev/breadfruit/actions/workflows/ci.yml/badge.svg)](https://github.com/iceddev/breadfruit/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/iceddev/breadfruit/branch/main/graph/badge.svg)](https://codecov.io/gh/iceddev/breadfruit)
[![node](https://img.shields.io/node/v/breadfruit.svg)](https://nodejs.org)
[![npm downloads](https://img.shields.io/npm/dm/breadfruit.svg)](https://www.npmjs.com/package/breadfruit)
[![license](https://img.shields.io/npm/l/breadfruit.svg)](https://github.com/iceddev/breadfruit/blob/main/LICENSE)

Not really bread. Not really fruit. Just like this package. Some simple helpers on top of knex.
Not really bread. Not really fruit. Just like this package. Simple CRUD helpers on top of [knex](https://knexjs.org/).

![breadfruit](happy_breadfruit.png)

## create an instance of breadfruit
## Install

```sh
npm install breadfruit
```

Requires Node.js `>=20`.

## Usage

Breadfruit is an ES module with a default export.

```js
import breadfruit from 'breadfruit';

```javascript
const config = {
client: 'postgresql',
client: 'pg',
connection: 'postgres://postgres@localhost:5432/someDatabase',
pool: { min: 1, max: 7 }
pool: { min: 1, max: 7 },
};

const bread = require('breadfruit')(config);
const { browse, read, edit, add, del, raw } = breadfruit(config);
```

## Browse, Read, Edit, Add, Delete, Raw
## API

```javascript
const {browse, read, edit, add, del, raw} = require('breadfruit')(config);
### `browse(table, fields, filter, options?)`

//get an array of users, by table, columns, and a filter
const users = await browse('users', ['username', 'user_id'], {active: true});
Returns an array of rows.

```js
const users = await browse('users', ['username', 'user_id'], { active: true });
```

//get a single user by table, columns, and a filter
const user = await read('users', ['username', 'first_name'], {user_id: 1337});
Supported `options`:
- `limit` (default `1000`)
- `offset` (default `0`)
- `orderBy` — column name or array of column names
- `sortOrder` — `'ASC'` / `'DESC'` (default `'ASC'`), or an array matching `orderBy`
- `dateField` (default `'created_at'`)
- `search_start_date` / `search_end_date` — adds a `whereBetween` on `dateField`
- `dbApi` — override the internal knex instance (useful for transactions)

### `read(table, fields, filter, options?)`

//edit a user by table, returned columns, updated values, and a filter
const updatedUser = await edit('users', ['username', 'first_name'], {first_name: 'Howard'}, {user_id: 1337});
Returns a single row.

```js
const user = await read('users', ['username', 'first_name'], { user_id: 1337 });
```

//add a new user by table, returned columns, and user data
const newUser = await add('users', ['user_id'], {first_name: 'Howard', username: 'howitzer'});
### `add(table, returnFields, data, options?)`

Inserts and returns the new row.

//delete a user by table and a filter
const deleteCount = await del('users', {user_id: 1337});
```js
const newUser = await add('users', ['user_id'], {
first_name: 'Howard',
username: 'howitzer',
});
```

### `edit(table, returnFields, data, filter, options?)`

//perform a raw query
const rows = await raw('select * from users');
Updates matching rows and returns the first updated row.

```js
const updated = await edit(
'users',
['username', 'first_name'],
{ first_name: 'Howard' },
{ user_id: 1337 },
);
```

### `del(table, filter, options?)`

Deletes matching rows and returns the count.

```js
const count = await del('users', { user_id: 1337 });
```

### `raw(sql, options?)`

Runs a raw SQL statement and returns rows.

```js
const rows = await raw('select * from users');
```

## License

ISC
19 changes: 19 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import js from '@eslint/js';
import globals from 'globals';

export default [
js.configs.recommended,
{
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: { ...globals.node },
},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'prefer-const': 'error',
'no-var': 'error',
eqeqeq: ['error', 'always', { null: 'ignore' }],
},
},
];
140 changes: 65 additions & 75 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,89 +1,79 @@
const knexConstructor = require('knex');
import knexConstructor from 'knex';

function connect(settings) {
export default function connect(settings) {
const knex = knexConstructor(settings);
const defaultOptions = {
const defaults = {
dateField: 'created_at',
dbApi: knex,
limit: 1000,
offset: 0,
sortOrder: 'ASC',
};

return {
browse(table, fields, filter, options = {}) {
const dbApi = options.dbApi || defaultOptions.dbApi || knex;
const limit = options.limit || defaultOptions.limit;
const offset = options.offset || defaultOptions.offset;
const dateField = options.dateField || defaultOptions.dateField;
const sortOrder = options.sortOrder || defaultOptions.sortOrder;
function browse(table, fields, filter, options = {}) {
const dbApi = options.dbApi || knex;
const limit = options.limit ?? defaults.limit;
const offset = options.offset ?? defaults.offset;
const dateField = options.dateField || defaults.dateField;
const sortOrder = options.sortOrder || defaults.sortOrder;

let query = dbApi(table)
.where(filter)
.select(fields)
.limit(limit)
.offset(offset);
let query = dbApi(table)
.where(filter)
.select(fields)
.limit(limit)
.offset(offset);

if (options.search_start_date && options.search_end_date) {
query = query
.whereBetween(dateField, [options.search_start_date, options.search_end_date]);
}

if (options.orderBy) {
if(Array.isArray(options.orderBy)) {
options.orderBy.forEach((orderBy, index) => {
if(Array.isArray(sortOrder)) {
return query = query.orderBy(orderBy, sortOrder[index]);
}
query = query.orderBy(orderBy, sortOrder);
});
} else {
query = query.orderBy(options.orderBy, sortOrder);
}
}
if (options.search_start_date && options.search_end_date) {
query = query.whereBetween(dateField, [
options.search_start_date,
options.search_end_date,
]);
}

return query;
},
read(table, fields, filter, options = {}) {
const dbApi = options.dbApi || knex;
return dbApi(table)
.where(filter)
.select(fields)
.then(([row]) => {
return row;
if (options.orderBy) {
if (Array.isArray(options.orderBy)) {
options.orderBy.forEach((orderBy, index) => {
const order = Array.isArray(sortOrder) ? sortOrder[index] : sortOrder;
query = query.orderBy(orderBy, order);
});
},
add(table, fields, data, options = {}) {
const dbApi = options.dbApi || knex;
return dbApi(table)
.returning(fields)
.insert(data)
.then(([row]) => {
return row;
});
},
edit(table, fields, data, filter, options = {}) {
const dbApi = options.dbApi || knex;
return dbApi(table)
.where(filter)
.returning(fields)
.update(data)
.then(([row]) => {
return row;
});
},
del(table, filter, options = {}) {
const dbApi = options.dbApi || knex;
return dbApi(table)
.where(filter)
.del();
},
raw(sql, options = {}) {
const dbApi = options.dbApi || knex;
return dbApi.raw(sql, options)
.then(res => res.rows || res);
} else {
query = query.orderBy(options.orderBy, sortOrder);
}
}
};
}

module.exports = connect;
return query;
}

async function read(table, fields, filter, options = {}) {
const dbApi = options.dbApi || knex;
const [row] = await dbApi(table).where(filter).select(fields);
return row;
}

async function add(table, fields, data, options = {}) {
const dbApi = options.dbApi || knex;
const [row] = await dbApi(table).returning(fields).insert(data);
return row;
}

async function edit(table, fields, data, filter, options = {}) {
const dbApi = options.dbApi || knex;
const [row] = await dbApi(table)
.where(filter)
.returning(fields)
.update(data);
return row;
}

function del(table, filter, options = {}) {
const dbApi = options.dbApi || knex;
return dbApi(table).where(filter).del();
}

async function raw(sql, options = {}) {
const dbApi = options.dbApi || knex;
const res = await dbApi.raw(sql, options);
return res.rows || res;
}

return { browse, read, add, edit, del, raw, knex };
}
Loading
Loading