Using the Database Layer
The db module works the same way across SQLite, PostgreSQL, DynamoDB, Elasticsearch, and Rqlite.
Multiple databases can be used at the same time in the application, all using the same DB layer.
Pick your backend with an environment variable, bkjs.conf defines these sextions for each database:
BKJS_ROLES=sqlite # default
BKJS_ROLES=postgres
BKJS_ROLES=dynamodb
Every method has two forms — use whichever you prefer:
// callback
db.get("users", { id: 123 }, (err, data, info) => { ... });
// async (add 'a' prefix)
const { err, data, info } = await db.aget("users", { id: 123 });
This guide uses the async form.
Define your table
Describe tables as plain objects. Each key is a column name:
const tables = {
users: {
id: { type: "uuid", primary: 1 },
email: { type: "email" },
name: { validate: { max: 100 } },
role: { value: "user" }, // default value
counter: { type: "counter", value: 0 },
tags: { type: "list" },
profile: { type: "json" },
ctime: { type: "now", read_only: true },
},
};
db.describeTables(tables);
Common column types:
uuid— auto-generated unique IDtext,int,real,bigint— basic typesjson,obj— stored as JSON, returned as objectslist,set— arrays (set = no duplicates)counter— a number you incrementnow— timestamp, auto-set on createemail— validated email format
Useful options:
primary: 1— part of the primary key (use 1, 2, 3 for composite keys)index: 1— create an indexvalue: "default"— default when reading nullvalidate: { max: 100, not_empty: true }— input validationread_only: true— can't be changed after creation
Create and update tables
Call this on startup — it creates missing tables and adds new columns to existing ones:
const { created, upgraded } = await db.acreateTables();
// created = ["users"] — tables that were created
// upgraded = [] — tables that got new columns
To add a column later, just add it to your definition and call acreateTables again:
db.tables.users.phone = { type: "text" };
await db.acreateTables();
// The phone column now exists
Insert records
// Simple insert
db.add("users", {
email: "alice@example.com",
name: "Alice",
tags: ["admin", "active"]
}, lib.log);
// async
await db.aadd("users", {
email: "alice@example.com",
name: "Alice",
tags: ["admin", "active"]
});
// Get the inserted record back
db.add("users", {
email: "bob@example.com",
name: "Bob"
}, { returning: "*", first: true }, lib.log);
// async
const { data: user } = await db.aadd("users", {
email: "bob@example.com",
name: "Bob"
}, { returning: "*", first: true });
console.log(user.id); // auto-generated UUID
console.log(user.ctime); // auto-set timestamp
console.log(user.role); // "user" (the default)
Inserting the same primary key twice returns an error:
const { err } = await db.aadd("users", { id: user.id, email: "dupe@example.com" });
if (err) console.log("Already exists!");
Read records
Get one by primary key:
const { data: user } = await db.aget("users", { id: "abc123" });
if (!user) {
console.log("Not found");
} else {
console.log(user.name);
console.log(user.profile); // JSON is already parsed
console.log(user.tags); // List is already an array
}
Get multiple by keys:
const { data: users } = await db.alist("users", [
{ id: "abc123" },
{ id: "def456" },
{ id: "xyz789" }
]);
// Returns only the ones that exist
Query records
Use aselect to find records matching conditions:
// Simple equality
const { data } = await db.aselect("users", { role: "admin" });
// Pick specific columns
const { data } = await db.aselect("users", { role: "admin" }, {
select: "id,name,email"
});
// Sort results
const { data } = await db.aselect("users", {}, {
sort: "ctime",
desc: true
});
Use different database:
If you need full text capability for example just use a different DB pool:
const { data } = await db.aselect("users", { role: "admin" }, { pool: "elasticsearch" });
Operators
By default, conditions use equality. Add operators with _$op in the column name:
// Greater than
const { data } = await db.aselect("users", { counter_$gt: 10 });
// In a list
const { data } = await db.aselect("users", { role_$in: ["admin", "moderator"] });
// Starts with
const { data } = await db.aselect("users", { name_$begins_with: "A" });
// Contains substring
const { data } = await db.aselect("users", { email_$includes: "@gmail.com" });
// Is null
const { data } = await db.aselect("users", { phone: null });
// Not null
const { data } = await db.aselect("users", { phone_$not_null: "" });
const { data } = await db.aselect("users", { phone_$ne: null });
// Range
const { data } = await db.aselect("users", { counter: [5, 10] }, {
ops: { counter: "between" }
});
All operators:
| What | Inline | Example |
|---|---|---|
| equals | col: val |
{ name: "Alice" } |
| not equals | col_$ne |
{ role_$ne: "guest" } |
| greater than | col_$gt |
{ age_$gt: 18 } |
| less than | col_$lt |
{ price_$lt: 100 } |
| greater or equal | col_$ge |
{ score_$ge: 50 } |
| less or equal | col_$le |
{ score_$le: 100 } |
| in list | col_$in |
{ status_$in: ["active", "pending"] } |
| not in list | col_$not_in |
{ status_$not_in: ["deleted"] } |
| between | col: [a,b] + ops |
{ age: [18, 65] }, { ops: { age: "between" } } |
| starts with | col_$begins_with |
{ name_$begins_with: "A" } |
| contains | col_$includes |
{ bio_$includes: "developer" } |
| ends with | col_$ends_with |
{ email_$ends_with: ".edu" } |
| is null | col: null |
{ deleted_at: null } |
| is not null | col_$not_null |
{ verified_$not_null: "" } |
| is not null | col_$ne: null |
{ verified_$ne: null } |
List/array operators
For list and set columns:
// Has this value
const { data } = await db.aselect("users", { tags_$contains: ["admin"] });
// Doesn't have this value
const { data } = await db.aselect("users", { tags_$not_contains: ["banned"] });
// Has ALL of these values
const { data } = await db.aselect("users", { tags_$all_in: ["admin", "verified"] });
Combine conditions
Use $or for alternatives:
// name is "Alice" OR role is "admin"
const { data } = await db.aselect("users", {
$or: { name: "Alice", role: "admin" }
});
// Multiple values for same column: id is 1 OR 2 OR 3
const { data } = await db.aselect("users", {
$or: { id: 1, id_$: 2, id_$$: 3 } // use _$ _$$ for duplicates
});
Update records
Replace values:
await db.aupdate("users", {
id: "abc123",
name: "Alice Smith",
email: "alice.smith@example.com"
});
Increment counters:
// Add 1 to counter
await db.aincr("users", { id: "abc123", counter: 1 });
// Subtract
await db.aincr("users", { id: "abc123", counter: -5 });
Modify lists:
// Add to list
await db.aupdate("users", { id: "abc123", tags_$add: ["premium"] });
// Remove from list
await db.aupdate("users", { id: "abc123", tags_$del: ["trial"] });
Set only if empty:
// Only sets role if it's currently null
await db.aupdate("users", { id: "abc123", role_$not_exists: "member" });
Get the updated record back:
const { data: user } = await db.aupdate("users", {
id: "abc123",
name: "New Name"
}, { returning: "*", first: true });
console.log(user.name); // "New Name"
Delete records
await db.adel("users", { id: "abc123" });
Pagination
Use count to limit results and next_token to get the next page:
// First page
let { data, info } = await db.aselect("users", {}, {
sort: "ctime",
count: 10
});
console.log(data); // first 10 users
console.log(info.next_token); // token for next page
// Next page
if (info.next_token) {
const page2 = await db.aselect("users", {}, {
sort: "ctime",
count: 10,
start: info.next_token
});
}
Loop through all pages:
let token = null;
let allUsers = [];
do {
const { data, info } = await db.aselect("users", { role: "admin" }, {
count: 100,
start: token
});
allUsers.push(...data);
token = info.next_token;
} while (token);
Streaming large datasets
For big tables, use ascan to process rows without loading everything:
// Process one at a time
await db.ascan("users", { role: "admin" }, { count: 100 }, (user, next) => {
console.log(user.email);
next(); // call next() to continue
});
// Process in batches
await db.ascan("users", {}, { count: 100, sync: true }, (batch) => {
console.log(`Got ${batch.length} users`);
});
Batch operations
Run multiple operations at once:
const ops = [
{ table: "users", op: "add", query: { email: "one@test.com", name: "One" } },
{ table: "users", op: "add", query: { email: "two@test.com", name: "Two" } },
{ table: "users", op: "update", query: { id: "abc", counter: 1 } },
];
await db.abulk(ops);
For all-or-nothing (rollback on any failure):
await db.atransaction(ops);
Caching
Cache reads to reduce database load:
// Enable caching for a table
db.cache.tables.push("users");
db.cache.ttl.users = 60000; // 60 seconds
// First read hits the database
const { data, info } = await db.aget("users", { id: "abc123" });
console.log(info.cached); // 0 = from database
// Second read comes from cache
const { data, info } = await db.aget("users", { id: "abc123" });
console.log(info.cached); // 1 = from cache
Table aliases
Use short names for tables:
db.aliases.u = "users";
// These are the same:
await db.aget("users", { id: "abc" });
await db.aget("u", { id: "abc" });
Field visibility (cleanup)
Control which fields are returned based on user roles:
db.describeTables({
users: {
id: { type: "uuid", primary: 1 },
email: { cleanup: false }, // always visible
password: { cleanup: true }, // never visible
ssn: { cleanup: { roles: ["admin"] } }, // only for admins
salary: { cleanup: { roles: ["hr", "admin"] } },
}
});
// Strip sensitive fields before sending to client
const safeUser = db.cleanupResult("users", user, {
user: { roles: ["member"] } // current user's roles
});
// safeUser has email, but not password, ssn, or salary
Raw SQL
When you need it (SQL backends only):
const { data } = await db.asql(
"SELECT * FROM users WHERE email LIKE $1 AND created_at > $2",
["%@gmail.com", "2024-01-01"]
);
Validation errors
Validation failures return a descriptive error:
// Name too long (max: 100)
const { err } = await db.aadd("users", { name: "A".repeat(200) });
if (err) console.log(err.message); // "...too large..."
// Required field missing
const { err } = await db.aadd("users", { name: "" }); // not_empty validation
if (err) console.log(err.message); // "...not be empty..."
Quick reference
Read:
db.aget(table, { key })— one record by primary keydb.alist(table, [{ key }, ...])— multiple by keysdb.aselect(table, { conditions }, options)— query with conditionsdb.ascan(table, { conditions }, options, callback)— stream large results
Write:
db.aadd(table, record)— insert (fails if exists)db.aput(table, record)— insert or replacedb.aupdate(table, { key, ...changes })— update fieldsdb.aincr(table, { key, counter: N })— increment counterdb.adel(table, { key })— delete
Batch:
db.abulk([{ table, op, query }, ...])— run multiple opsdb.atransaction([...])— all-or-nothing batch
Options:
{ returning: "*", first: true }— return the affected record{ select: "id,name" }— only these columns{ sort: "ctime", desc: true }— order results{ count: 10, start: token }— pagination{ ops: { col: "gt" } }— set operator via options instead of_$