DB Native
The DB Native block gives you direct access to your database with JavaScript. Write a raw SQL query (or, for MongoDB, use the driver) for anything the other DB blocks can't do, and the value you return goes to the next block.
When to use it
- Write a query the standard blocks can't, like
GROUP BY, window functions, unions, or database-specific features. - Call a stored procedure, or run a one-off maintenance command.
- Don't use it for simple reads and writes. DB Get All, DB Insert and friends are safer, easier to read, and work on every database.
- Don't build the query text from user input. Always use placeholders, see below.
Inputs
| Field | Required | Default | What it does |
|---|---|---|---|
| Connection | Yes | none | The database integration to use. |
| JS | Yes | none | The code to run. It can call dbQuery(query, params?) (or use db on MongoDB) and must return the result. |
| Save output to variable | No | off | Store the result in outputs.<name>. |
- SQL (PostgreSQL, MySQL):
dbQuery(query, params?)runs the query and returns the result rows as a list. - MongoDB: use
dbandObjectIdinstead. See MongoDB below.
Outputs
| Handle | When it is used |
|---|---|
| Next (right) | The code finished. The next block receives whatever your code returns. |
Example
Count orders per status.
| Field | Value |
|---|---|
| Connection | main-db |
| JS | see below |
const rows = await dbQuery(
"SELECT status, COUNT(*) AS total FROM orders WHERE created_at > $1 GROUP BY status",
[getQueryParam("since")],
);
return rows;The next block receives:
[{ "status": "paid", "total": 12 }, { "status": "pending", "total": 3 }]How it behaves
- You must
returnthe result. Withoutreturn, the next block receives nothing. - Always
awaitthe call. Without it you get a pending promise instead of rows. - Errors fail the block. A bad query, a missing table or a lost connection goes to the Error Handler.
- Inside a transaction, the query is part of the DB Transaction it runs in. On MongoDB this covers every
db.collection(...)read and write. - Value types follow the other DB blocks. For example,
COUNT(*)is a number and aDECIMALis text. See Value types. - No safety net. The query runs exactly as written. A
DELETEwithoutWHEREdeletes everything. - It replaces the flowing data with what you return.
Passing values safely
Never put user input straight into the query text. Pass it in the params array and use a placeholder instead:
| Database | Placeholder |
|---|---|
| PostgreSQL | $1, $2, … |
| MySQL | $1, $2, … or ? |
$1 is the first value in params, $2 the second, and so on. The same number can appear more than once. The placeholders work the same on both databases. On MySQL, use either $1 or ? in one query, not both. A $1 inside quotes, like '$1' or the JSON path '$.name', is plain text and not a placeholder.
const search = getQueryParam("q");
const users = await dbQuery(
"SELECT id, username, email FROM users WHERE username = $1 LIMIT 20",
[search],
);
return users; // [{ id: 1, username: "john", email: "[email protected]" }]WARNING
Writing `... WHERE username = ${search}` puts the raw text into the SQL. A value like john:doe then breaks the query with a syntax error, and a crafted value can change what the query does (SQL injection). Placeholders avoid both.
MongoDB
On a MongoDB connection, the code gets two things instead of dbQuery:
| Name | What it is |
|---|---|
db | The database, already connected, from the official MongoDB driver. |
ObjectId | Builds document ids: new ObjectId("665f…"). |
const id = new ObjectId(getRouteParam("id"));
await db.collection("orders").updateOne({ _id: id }, { $set: { status: "paid" } });
return await db.collection("orders").findOne({ _id: id });- Inside a DB Transaction, every
db.collection(...)read and write joins the transaction on its own. If the transaction rolls back, the writes are undone. Commands ondbitself (likedb.command(...)) and index changes don't join it. dbQuery("...")with a query throws on MongoDB, with the message "dbQuery takes SQL; on MongoDB use db.collection(...)". Older code that callsawait dbQuery()with nothing still works: it returns the samedb.- The code editor knows the driver's types, so
db.suggests methods as you type. - Ids from other blocks are text. The other DB blocks give and take ids as text. Turn text into an id with
new ObjectId(text), and turn an id back into text withid.toHexString()before you return it. See MongoDB ids.
Joining collections
The other DB blocks can't join on MongoDB. Use MongoDB's $lookup here instead. This returns each order with its rider nested under rider:
return await db.collection("orders").aggregate([
{ $match: { status: "open" } },
{ $lookup: { from: "riders", localField: "riderId", foreignField: "_id", as: "rider" } },
{ $unwind: { path: "$rider", preserveNullAndEmptyArrays: true } },
]).toArray();- Leave out
preserveNullAndEmptyArraysto drop orders with no rider, like an inner join. With it, they stay with norider, like a left join. - Both fields must hold the same type. An id saved as text never matches an ObjectId. Store ids as ObjectIds, or convert one side with
$toObjectId. - The results keep MongoDB's own types, so ids come back as ObjectIds. Call
.toHexString()on one to get text.
Related blocks
- DB Get All and DB Get Single: the no-code way to read.
- DB Transaction: run native queries together with other changes.
- JS Runner: JavaScript without database access.
