biab
v1.0.77
Published
A TypeScript/Node.js library that provides a complete, opinionated backend foundation: database queries and mutations via [Orma](https://github.com/mendeljacks/orma), JWT authentication, Google OAuth, Apple Sign-In, and role-based access control — all wir
Downloads
3,755
Readme
biab – Backend in a Box
A TypeScript/Node.js library that provides a complete, opinionated backend foundation: database queries and mutations via Orma, JWT authentication, Google OAuth, Apple Sign-In, and role-based access control — all wired together and ready to drop into any Express-style server.
Table of Contents
Features
- Orma-powered queries & mutations – type-safe, relational database access over PostgreSQL
- JWT authentication – issue and verify tokens with a single secret
- Google OAuth – redirect-based and headless (mobile) flows
- Apple Sign-In – headless token verification and callback handler
- Role-based access control – declarative per-table CRUD permissions
- Ownership enforcement – pluggable hook to restrict rows by owner
- Database migrations – reset and re-apply via
db-migrate - Prepopulate – idempotently seed a database from plain JavaScript objects
- Extra macros – automatic
resource_idgeneration withcuid
Installation
npm install biabFor local development with hot-reload:
npm install -g nodemon
npm install
npm run devQuick Start
import express from 'express'
import { query, mutate } from 'biab/src/api/controllers'
import { orma_schema } from './orma_schema' // generated by introspect()
import { connection_edges } from './edges'
import { role_has_perms } from './perms'
import { byo_query_fn, pool, trans } from './db'
import { ensure_ownership } from './ownership'
import { add_resource_ids } from 'biab/src/config/extra_macros'
const app = express()
app.use(express.json())
const JWT_SECRET = process.env.JWT_SECRET!
app.post('/query', async (req, res) => {
try {
const result = await query(
req, JWT_SECRET, pool, connection_edges,
role_has_perms, orma_schema, ensure_ownership, byo_query_fn
)
res.status(200).json(result)
} catch (err) {
res.status(400).json(err)
}
})
app.post('/mutate', async (req, res) => {
try {
const result = await mutate(
req, JWT_SECRET, pool, connection_edges,
role_has_perms, orma_schema, ensure_ownership,
byo_query_fn, trans, add_resource_ids
)
res.status(200).json(result)
} catch (err) {
res.status(400).json(err)
}
})
app.listen(3000)Core Concepts
Query
import { query } from 'biab/src/api/controllers'
const result = await query(
req, // Express request – body contains the Orma query, headers contain the JWT
jwt_secret,
pool, // pg Pool
connection_edges, // ConnectionEdges from Orma
role_has_perms, // RoleHasPerms – see Role-Based Permissions
orma_schema, // OrmaSchema – generated by introspect()
ensure_ownership, // EnsureOwnershipFn – your custom ownership check
byo_query_fn // (sqls, pool) => Promise<results[]> – your query executor
)The handler authenticates the request, validates the Orma query against the schema, enforces permissions and ownership, then executes the query.
Mutate
import { mutate } from 'biab/src/api/controllers'
const result = await mutate(
req,
jwt_secret,
pool,
connection_edges,
role_has_perms,
orma_schema,
ensure_ownership,
byo_query_fn,
trans, // (fn, pool) => Promise – your transaction wrapper
extra_macros // (mutation) => void – called before the mutation runs
)The handler applies the supersede macro (so users can only supersede data they can read), authenticates, enforces permissions and ownership, then runs the mutation inside your transaction wrapper.
Authentication
import { make_token, authenticate } from 'biab/src/api/auth/auth'
// Issue a token
const token = await make_token(user_id, role_ids, jwt_secret)
// Verify a token (reads Authorization: Bearer <token> header)
const { user_id, role_ids } = await authenticate(req, jwt_secret)Role-Based Permissions
Define which role IDs are allowed to perform each CRUD operation on each table:
import { RoleHasPerms } from 'biab/src/api/auth/perms'
const role_has_perms: RoleHasPerms = {
posts: {
create: [1], // only role 1 can create
read: [1, 2], // roles 1 and 2 can read
update: [1],
delete: [1]
},
comments: {
create: [1, 2],
read: [1, 2],
update: [1, 2],
delete: [1]
}
}Google OAuth
Redirect flow:
import { google_login, google_login_callback } from 'biab/src/api/auth/auth_google'
// Redirect the user to Google
app.get('/auth/google', (req, res) => google_login(res, SERVER_ROOT_URI, GOOGLE_CLIENT_ID))
// Handle the callback
app.get('/auth/google/callback', async (req, res) => {
const { token, user_id } = await google_login_callback(
req.query.code as string,
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
SERVER_ROOT_URI,
ensure_user_exists, // (google_user) => Promise<{ id, user_has_roles }>
JWT_SECRET
)
res.json({ token, user_id })
})Headless (mobile) flow:
import { google_auth_headless } from 'biab/src/api/auth/auth_google'
app.post('/auth/google/headless', async (req, res) => {
const { token, user_id } = await google_auth_headless(
req.body, // { id_token, access_token }
ensure_user_exists,
JWT_SECRET
)
res.json({ token, user_id })
})Apple Sign-In
Headless (mobile) flow:
import { apple_auth_headless } from 'biab/src/api/auth/auth_apple'
app.post('/auth/apple/headless', async (req, res) => {
const { token, user_id } = await apple_auth_headless(
req.body, // { id_token, access_token }
ensure_apple_user_exists, // (apple_user) => Promise<{ id, user_has_roles }>
JWT_SECRET,
BUNDLE_ID
)
res.json({ token, user_id })
})Callback flow:
import { sign_in_with_apple } from 'biab/src/api/auth/auth_apple_callback'
app.get('/callbacks/sign_in_with_apple', async (req, res) => {
const result = await sign_in_with_apple(
req, SERVER_ROOT_URI, BUNDLE_ID, SERVICE_ID,
TEAM_ID, KEY_CONTENTS, KEY_ID
)
res.json(result)
})Database Utilities
Introspect
Generate an orma_schema file from your live PostgreSQL database:
import { introspect } from 'biab/src/config/orma'
await introspect('./src/orma_schema.ts', pool, byo_query_fn)Prepopulate
Idempotently seed tables – rows are created or updated but never deleted:
import { prepopulate } from 'biab/src/scripts/prepopulate'
await prepopulate(
{
roles: [{ id: 1, name: 'admin' }, { id: 2, name: 'user' }]
},
pool, orma_schema, byo_query_fn, trans, connection_edges
)Reset Migrations
Drop and re-apply all db-migrate migrations:
import { reset } from 'biab/src/scripts/reset'
await reset(db_migrate_config)Or via the CLI:
npm run resetScripts
| Script | Description |
|---|---|
| npm run dev | Start the server with hot-reload via nodemon |
| npm start | Start the server (production) |
| npm test | Run the Mocha test suite |
| npm run coverage | Run tests with NYC coverage report |
| npm run reset | Drop and re-apply all database migrations |
| npm run deploy | Publish the package to npm |
Environment Variables
| Variable | Description |
|---|---|
| JWT_SECRET | Secret used to sign and verify JWT tokens |
| DATABASE_URL | PostgreSQL connection string |
| GOOGLE_CLIENT_ID | Google OAuth client ID |
| GOOGLE_CLIENT_SECRET | Google OAuth client secret |
| NPM_TOKEN | npm publish token (used by npm run deploy) |
Create a .env file in the project root. The dev, test, and reset scripts load it automatically via dotenv.
License
ISC
